A better approach would be to introduce an abstraction or interface,UserRepository, to encapsulate the database operations. TheUserServiceclass would then depend on this interface instead of the concreteConcreteUserRepository class.
interface UserRepository {
fun findUserBy(username: String): User?
...
}
class ConcreteUserRepository: UserRepository {
override fun findUserBy(username: String): User? {
...
}
...
}
class UserService(private val userRepository: UserRepository) {
fun authenticateUser(username: String, password: String): Boolean {
// Logic to authenticate user using the DatabaseService
val existingUser = userRepository.findUserBy(username)
if(existingUser != null) {
// authenticate
...
}
}
...
}
By introducing theUserRepositoryinterface, I adhere to theDependency Inversion Principle. Now, theUserServiceclass depends on an abstraction, allowing for easy substitution or extension of the database implementation without modifying theUserServiceclass itself. TheUserService does not need to know any details from the concrete repository implementation, just that it fulfils the contract of the abstraction and returns a user if available.
With this change the testing for theUserServiceis a lot easier. Because theUserRepositoryabstraction is injected to theUserServiceby constructor I can replace it in the test by a fake implementation. So with this it is no longer necessary to use a real database for it. This makes the tests a lot faster and also more predictable, because the risk of failure of external components is removed (e.g. database not available, constraint violations, …).
class FakeUserRepository: UserRepository{
override fun findUserBy(username: String): User? {
return User(username)
}
}
class UserTest{
val fakeUserRepository = FakeUserRepository()
val userService = UserService(fakeUserRepository)
@Test
fun `authenticateUser returns true for existing username`(){
// given
val username = "existingUser"
// when
val actual = userService.authenticateUser(username)
// then
assertThat(actual).isTrue()
}
}
Conclusion
TheDependency Inversion Principle is a powerful concept that promotes loose coupling and abstraction in software development. By applyingDIPin projects, I can achieve flexible, testable, and maintainable code. Through the practical example, I’ve seen how to invert dependencies by introducing interfaces and relying on abstractions. Remember, violating theDependency Inversion Principle can lead to tightly coupled code, making it difficult to change or extend the system.