
Dependency Injection (DI) is a crucial design pattern in modern software development that promotes loose coupling and increased modularity. This blog will introduce you to the DI pattern using Go and explain why implementing it in Go is incredibly straightforward and powerful.
What Is Dependency Injection?
Dependency Injection is a design pattern where an object's dependencies are provided externally rather than being hard-coded within the object itself. This approach allows for greater flexibility and easier unit testing, as external dependencies can be swapped with mocks or stubs during testing.
DI in Go:
Go, with its simplicity and structural approach, facilitates DI in a seamless manner. Unlike languages that require special frameworks or complex configurations to manage dependencies, Go leverages its interface-based design to make DI implementation straightforward.
Let's use the following Go program as an example to see how DI works.
The below Go program manages product data and demonstrates dependency injection through its various layers: data storage, business logic, and the controller.
We define a `DataStore` interface for fetching product data and implement this interface in the `ProductDataStore` structure. `Product` is a simple DTO to represent products.
`ProductDataStore` struct is considered as implementing the interface `DataStore` because it implements two methods defined in the interface with the same name and signature. Unlike other languages such as Java where a `class` considered implementing an `interface` only by explictly declaring it as `implements` and implementing all the methods of the interface, Go requires only to implement the methods and is implicitly considered as implementing the interface.
Power of Go is both interface and struct are completely oblivious of each other’s existence and not explicitly knowing one is using another, so each can evolve independently in a loosely coupled way.
Methods `GetProductById` and `GetProductByName` are self-explanatory by getting product by either Id or Name respectively.
Logger Adapter and Interface
We define a `Logger` interface capable of logging messages. The `LoggerAdapter` is a function type that implements this interface, showcasing Go's flexibility in treating functions as first-class citizens.
Just like a struct, a `func` type can also be considered as implementing an interface when it has a method (Log) with same name and signature as the the method defined in that interface (Log). As a different style, LoggerAdapter can also be declared as an empty struct{} type producing same result. Either way, it follows interface and implementing type principle so that we can apply DI in a loosely couple way.
ProductLogic Layer
The `ProductLogic` structure uses DI by accepting a `Logger` and `DataStore` as dependencies. This decoupling means that both logging and data operations can be easily modified without altering the logic layer.
Note that `ProductLogic` is depending only on interface not concret type. So, it can be later injected with the required concret types. `Logic` interface is also declared and `ProductLogic` implementing its methods (GetProductsById and GetProductByName) below.
Factory function `NewProductLogic` is creating an instance of `ProductLogic` by injecting its dependencies. For this we must always follow an important Go idiom given below:
Accept Interfaces, Return concret types
The above two methods are again self-explanatory on getting products.
Controller Layer
Finally, the `Controller` leverages DI to manage HTTP requests, ensuring that core functionalities are abstracted through interfaces.
Note again that `Controller` struct only relies on interfaces and factory function `NewController` accepts interfaces and return concret type.
Above method encapsulates controller level concern by taking in a query string parameter from REST api and gets the product by either Id or Name.
Main Function
At the end, the main function wires up all dependencies using the New* constructor functions, thus achieving dependency injection without additional frameworks.
Keypoint to understand from above dependency wiring up is again following the Go Idiom `Accept interfaces, return concrete types`. Each statement is a factory function that create and return a concrete type instances and passed over to the next level factory function as dependencies, while all the parameter types receiving/accepting are interfaces.
As DI simply follows `Open-Close` from `SOLID Principles`, we can just extend this pattern for easy and effective unit testing as given below
How DI helps in unit testing
Unit testing is all about testing in isolation of one unit at a time. While one unit is being tested for its logic correctness, any dependent logic must be mocked and injected with correct expectations. In the above example, each higher level layer depends on next lower level entities.
Controller depends on business logic, Business logic depends on DataStore.
While testing controller logic, its dependent usiness instances is mocked and injected. While testing Business logic, Its dependent datastore is mocked and injected. Since each layer: Data, business, and controller accepts interfaces in its factory functions, we can simply implement mocked version of the dependent instances and inject for unit testing.
Mocking dependencies:
Following is the implementation of DataStore mocking to be used in business logic unit testing. Note that the mock implements `DataStore` interface.
For mocking, we leverage in built support from Go itself, while its preferable (avoiding any third party libraries), you can still try other thirdparty libraries like GoMock, Testability etc for mocking.
Following is the mock implementation of Logger interface.
Unit testing Product business logic:
Following code is the unit test case for `GetProductById`. First mock values are setup as expected and injected to business factory functions. Business logic is tested with mocked dependencies.
Following code is the unit test case for `GetProductByName`. First mock values are setup as expected and injected to business factory functions. Business logic is tested with mocked dependencies.
Conclusion
Dependency Injection is a powerful design pattern that enhances the modularity, testability, and extensibility of your design. When working with Go, you benefit from the language's simplicity and built-in capabilities to implement DI effectively without third-party dependencies. Use DI in your Go projects to keep your code clean, maintainable, and adaptable to changes.