This is a more basic article that I use when I do go language training for Java programmers.
Why do Dependency Inversion (DIP)?
dependency inversionThe first is a very important design principle for software development, or Dependency Inversion Programming (DIP). Many programmers have not understood the relevant knowledge, or only know the general idea from Java Spring. I want to use a short article today to make a simple example in Go language.
If you don’t know what it is yet, you can refer to the description in the wiki, or read theMartin Fowler’s article on DIP。
The principle of dependency inversion is going to address a common risk in software development: dependencies.
Try to remember:
- When you try to test by masking the underlying details through Mock, you find that the class you want to test references a large number of interfaces provided by the framework, causing you to need to mock a large number of underlying implementations.
- When you try to modify an old underlying class, but there are too many upper-level service classes that depend on it, and you’re worried about causing side effects while refactoring the upper-level code in all the dependent locations.
Let’s analyze these two scenarios:
In Scenario 1, the application class relies on the implementation provided by the framework, which makes it difficult to detach the application class from the framework. The industry approach to dealing with this problem is calledControl reversal(IoC, Inversion of Control). That is, the application class should not rely on the framework, but the framework to provide slots like the application class registered to the framework, the framework unified scheduling application, the execution of the corresponding methods.
In Scenario 2, the service class depends on the underlying class, making it increasingly difficult to modify the underlying. The solution isdependency injection(DI, Dependency Injection). That is, the upper class does not directly refer to the underlying class, but in the use of the upper class depends on the underlying class injected into the place.
Combining these two scenarios is the core of the principle of dependency inversion:
- Higher level modules should not depend on lower level modules, both should depend on abstract interfaces.
- Abstract interfaces should not depend on concrete implementations. Concrete implementations, on the other hand, should depend on abstract interfaces.
These two principles ensure high cohesion and low coupling of modules in the code, while creating conditions for Mock, iterative updating of modules.
Implementing it in Go
Suppose now we want to query user information from a UserService. There are two interfaces, UserRepository as the data layer is responsible for querying the database, UserService is responsible for the business logic, which relies on UserRepository. meanwhile, in order to facilitate the testing, we have to write a Mock data layer implementation. The whole structure is shown below.
Next, very easily, we implement the two interfaces and write their implementation classes. We also wrote a NewUserService in the UserService implementation class to inject the UserRepository implementation it depends on.
// 在 user_repository.go 中实现具体的接口
type UserRepository interface {
GetByID(id int) (*User, error)
Save(user *User) error
}
// ... 具体实现 UserRepository,略
// user_service.go 中实现
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(name string, age int) error
}
// ... 具体实现 UserService,略
func NewUserService(repo UserRepository) UserService {
return &UserServiceImpl{
repo: repo,
}
}
The question then arises whether it’s possible to directlyuser_service.go
What about referencing the repository directly in the Obviously not, because that would create a dependency between the two modules.
This is at the heart of dependency inversion, where instead of the upper module directly referencing the lower module, the executing class initializes the Service and injects the dependent lower service.
// 在main.go 中
func main() {
repo := &MySQLUserRepository{}
userService := NewUserService(repo)
}
This way, when writing test Mock code, you don’t need to change any code logic, and you can directly include theNewUserService
It is sufficient to replace the argument of the
// 在 user_service_test.go 中
func TestUserService() {
repo := &MockTestUserRepository{}
userService := NewUserService(repo)
}
Also, if the data layer modifies the implementation, or migrates to another database, you only need to modify two places: the data layer implementer and the dependency injector. For the callerUserService
Then it is not affected at all. Nor does the entire program create dependency traps.
summarize
Reliance on the two core principles of the inversion principle:
- Modules do not depend on other modules, but all depend on the abstract interface
- Abstract interfaces do not depend on implementations, while implementations depend on abstract interfaces
Implementing these two principles in Go is not a big deal, as long as the original caller-implementer is converted to registrar-caller-implementer. There are some libraries and frameworks that implement dependency inversion in Go, but the core idea is not different.