Skip to main content
  1. Articles/

Practical SOLID in Golang: Dependency Inversion Principle

·2306 words·11 mins· loading · loading ·
Marko Milojevic
Author
Marko Milojevic
Software engineer and architect. Golang and LLM enthusiast. Awful chess player, gym rat, harmonica newbie and cat lover.
SOLID principles in Golang - This article is part of a series.
Part 5: This Article

Learning a new programming language is often a straightforward process. I often hear: “The first programming language you learn in a year. The second one in a month. The third one in a week, and then each next one in a day.” Saying that is an exaggeration, but it is not too distant from the truth in some cases. For example, jumping to a language relatively similar to the previous one, like Java and C#, can be a straightforward process. But sometimes, switching is tricky, even when we switch from one Object-Oriented language to another. Many features influence such transitions, like strong or weak types, if a language has interfaces, abstract classes, or classes at all. Some of those difficulties we experience immediately after switching, and we adopt a new approach. But some issues we experience later, during unit testing, for example. And then, we learn why The Dependency Inversion Principle is essential, especially in Go.

When we do not respect The Dependency Inversion
#

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Above is the definition of DIP as presented by Uncle Bob in his paper. There are also more details inside his blog. So, how can we understand this, especially in the context of Go? First, we should accept Abstraction as an object-oriented programming concept. We use this concept to expose essential behaviors and hide the details of their implementation.

Second, what are high and low-level modules? In the context of Go, high-level modules are software components used at the top of the application, such as code used for presentation. It can also be code close to the top level, like code for business logic or some use-case components. It is essential to understand it as a layer that provides real business value to our application. On the other hand, low-level software components are mostly small code pieces that support the higher level. They hide technical details about different infrastructural integrations. For example, this could be a struct that contains the logic for retrieving data from the database, sending an SQS message, fetching a value from Redis, or sending an HTTP request to an external API. So, what does it look like when we break The Dependency Inversion Principle, and our high-level component depends on one low-level component?

Let’s examine the following example:

The Infrastructure Layer

type UserRepository struct {
	db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
	return &UserRepository{
		db: db,
	}
}

func (r *UserRepository) GetByID(id uint) (*domain.User, error) {
	user := domain.User{}
	err := r.db.Where("id = ?", id).First(&user).Error
	if err != nil {
		return nil, err
	}

	return &user, nil
}

The Domain Layer

type User struct {
	ID uint `gorm:"primaryKey;column:id"`
	// some fields
}

The Application Layer

type EmailService struct {
	repository *infrastructure.UserRepository
	// some email sender
}

func NewEmailService(repository *infrastructure.UserRepository) *EmailService {
	return &EmailService{
		repository: repository,
	}
}

func (s *EmailService) SendRegistrationEmail(userID uint) error {
	user, err := s.repository.GetByID(userID)
	if err != nil {
		return err
	}
	// send email
	return nil
}

In the code snippet above, we defined a high-level component, EmailService. This struct belongs to the application layer and is responsible for sending an email to newly registered customers. The idea is to have a method, SendRegistrationEmail, which expects the ID of a User. In the background, it retrieves a User from UserRepository, and later (probably) it delivers it to some EmailSender service to execute email delivery. The part with EmailSender is currently out of our focus. Let’s concentrate on UserRepository instead. This struct represents a repository that communicates with a database, so it belongs to the infrastructure layer. It appears that our high-level component, EmailService, depends on the low-level component, UserRepository. In practice, without defining a connection to the database, we cannot initialize our use-case struct. Such an anti-pattern immediately impacts our unit testing in Go.

Let’s assume we want to test EmailService, as shown in the code snippet below:

Unit Tests for EmailService

import (
	"testing"
	// some dependencies
	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/assert"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func TestEmailService_SendRegistrationEmail(t *testing.T) {
	db, mock, err := sqlmock.New()
	assert.NoError(t, err)

	dialector := mysql.New(mysql.Config{
		DSN:        "dummy",
		DriverName: "mysql",
		Conn:       db,
	})
	finalDB, err := gorm.Open(dialector, &gorm.Config{})
	
	repository := infrastructure.NewUserRepository(finalDB)
	service := NewEmailService(repository)
	//
	// a lot of code to define mocked SQL queries
	//
	// and then actual test
}

In contrast to some languages, like PHP, we cannot simply mock whatever we would like in Go. Mocking in Go relies on the usage of interfaces, for which we can define a mocked implementation, but we cannot do the same for structs. Therefore, we cannot mock UserRepository, as it is a struct. In such a case, we need to create a mock on the lower level, in this case, on the Gorm connection object, which we can achieve using the SQLMock package.

However, even with this approach, it is neither reliable nor efficient for testing. We need to mock too many SQL queries and have extensive knowledge about the database schema. Any change inside the database requires us to adapt unit tests. Apart from unit testing issues, we face an even bigger problem. What will happen if we decide to switch the storage to something else, like Cassandra, especially if we plan to have a distributed storage system for customers in the future? In such a scenario, if we continue using this implementation of UserRepository, it will lead to numerous refactorings. Now, we can see the implications of a high-level component depending on a low-level one. But what about abstractions that rely on details?

Let’s check the code below:

UserRepository interface

type User struct {
	ID uint `gorm:"primaryKey;column:id"`
	// some fields
}

type UserRepository interface {
	GetByID(id uint) (*User, error)
}

To address the first issue with high and low-level components, we should start by defining some interfaces. In this case, we can define UserRepository as an interface on the domain layer. This step allows us to decouple EmailService from the database to some extent, but not entirely. Take a look at the User struct; it still contains a definition for mapping to the database. Even though such a struct resides in the domain layer, it retains infrastructural details. Our new interface UserRepository (abstraction) still depends on the User struct with the database schema (details), which means we are still breaking the Dependency Inversion Principle (DIP). Changing the database schema will inevitably lead to changes in our interface. This interface may still use the same User struct, but it will carry changes from a low-level layer.

In the end, with this refactoring, we haven’t achieved much. We are still in the wrong position, and this has several consequences:

  1. We cannot effectively test our business or application logic.
  2. Any change to the database engine or table structure affects our highest levels.
  3. We cannot easily switch to a different type of storage.
  4. Our model is strongly coupled to the storage layer.

So, once again, let’s refactor this piece of code.

How we do respect The Dependency Inversion
#

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Let’s revisit the original directive for The Dependency Inversion Principle and focus on the bold sentences. They provide us with some guidance for the refactoring process. We need to define an abstraction (an interface) that both of our components, EmailService and UserRepository, will depend on. This abstraction should not be tied to any technical details, such as the Gorm object.

Let’s take a look at the following code:

The Infrastructure Layer

type UserGorm struct {
	// some fields
}

func (g UserGorm) ToUser() *domain.User {
	return &domain.User{
		// some fields
	}
}

type UserDatabaseRepository struct {
	db *gorm.DB
}

var _ domain.UserRepository = &UserDatabaseRepository{}

func NewUserDatabaseRepository(db *gorm.DB) UserRepository {
	return &UserDatabaseRepository{
		db: db,
	}
}

func (r *UserDatabaseRepository) GetByID(id uint) (*domain.User, error) {
	user := UserGorm{}
	err := r.db.Where("id = ?", id).First(&user).Error
	if err != nil {
		return nil, err
	}

	return user.ToUser(), nil
}

In the new code structure, we observe the UserRepository interface as a component that relies on the User struct, both of which reside within the domain layer. The User struct no longer directly reflects the database schema; instead, we use the UserGorm struct for this purpose, which belongs to the infrastructure layer. The UserGorm struct provides a method called ToUser, which facilitates the mapping to the actual User struct.

The Domain Layer

type User struct {
	// some fields
}

type UserRepository interface {
	GetByID(id uint) (*User, error)
}

In this setup, UserGorm serves as part of the implementation details within UserDatabaseRepository, which acts as the concrete implementation for UserRepository. Within the domain and application layers, our dependencies are exclusively on the UserRepository interface and the User Entities, both originating from the domain layer. Within the infrastructure layer, we can define as many implementations for UserRepository as needed, such as UserFileRepository or UserCassandraRepository.

The Application Layer

type EmailService struct {
	repository domain.UserRepository
	// some email sender
}

func NewEmailService(repository domain.UserRepository) *EmailService {
	return &EmailService{
		repository: repository,
	}
}

func (s *EmailService) SendRegistrationEmail(userID uint) error {
	user, err := s.repository.GetByID(userID)
	if err != nil {
		return err
	}
	// send email
	return nil
}

The high-level component (EmailService) depends on an abstraction, as it contains a field with the type UserRepository. Now, let’s explore how the low-level component depends on this abstraction.

In Go, structs implicitly implement interfaces, so there’s no need to explicitly add code indicating that UserDatabaseRepository implements UserRepository. However, we can include a check with a blank identifier to ensure this relationship. This approach allows us to have better control over our dependencies. Our structs depend on interfaces, and if we ever need to change our dependencies, we can define different implementations and inject them. This technique aligns with the Dependency Injection pattern, a common practice in various frameworks.

In Go, several DI libraries are available, such as the one from Facebook, Wire, or Dingo.

Now, let’s examine how this refactoring affects our unit testing.

Unit Tests for EmailService

import (
	"errors"
	"testing"
)

type GetByIDFunc func(id uint) (*User, error)

func (f GetByIDFunc) GetByID(id uint) (*User, error) {
	return f(id)
}

func TestEmailService_SendRegistrationEmail(t *testing.T) {
	service := NewEmailService(GetByIDFunc(func(id uint) (*User, error) {
		return nil, errors.New("error")
	}))
	//
	// and just to call the service
}

Following this refactoring, we can easily create a straightforward mock using a new type, GetByIDFunc. This type defines a function signature that matches the GetByID method of the UserRepository interface. In Go, it’s a common practice to define a function type and assign a method to it in order to implement an interface. This approach greatly improves the elegance and efficiency of our testing process. We now have the flexibility to inject different UserRepository implementations for various use cases and precisely control the test outcomes.

Some more examples
#

Breaking the Dependency Inversion Principle (DIP) isn’t limited to structs alone; it can also occur with standalone, independent functions. For instance:

Breaking DIP in Functions

type User struct {
	// some fields
}

type UserJSON struct {
	// some fields
}

func (j UserJSON) ToUser() *User {
	return &User{
		// some fields
	}
}

func GetUser(id uint) (*User, error) {
	filename := fmt.Sprintf("user_%d.json", id)
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	
	var user UserJSON
	err = json.Unmarshal(data, &user)
	if err != nil {
		return nil, err
	}
	
	return user.ToUser(), nil
}

We aim to retrieve data for a User, and for this task, we utilize files in JSON format. The GetUser method reads from a file and converts the file content into a User object. However, this method is tightly coupled with the presence of these files, making it challenging to write effective tests. This is especially true when we introduce additional validation rules to the GetUser method at a later stage. Our code’s heavy reliance on specific details creates testing difficulties, emphasizing the need for abstractions:

Respecting DIP in Functions

type User struct {
// some fields
}

type UserJSON struct {
	// some fields
}

func (j UserJSON) ToUser() *User {
	return &User{
		// some fields
	}
}

func GetUserFile(id uint) (io.Reader, error) {
	filename := fmt.Sprintf("user_%d.json", id)
	file, err := os.Open(filename)
	if err != nil {
		return nil, err
	}

	return file, nil
}

func GetUserHTTP(id uint) (io.Reader, error) {
	uri := fmt.Sprintf("http://some-api.com/users/%d", id)
	resp, err := http.Get(uri)
	if err != nil {
		return nil, err
	}

	return resp.Body, nil
}

func GetDummyUser(userJSON UserJSON) (io.Reader, error) {
	data, err := json.Marshal(userJSON)
	if err != nil {
		return nil, err
	}

	return bytes.NewReader(data), nil
}

func GetUser(reader io.Reader) (*User, error) {
	data, err := ioutil.ReadAll(reader)
	if err != nil {
		return nil, err
	}

	var user UserJSON
	err = json.Unmarshal(data, &user)
	if err != nil {
		return nil, err
	}

	return user.ToUser(), nil
}

With this revised implementation, the GetUser method depends on an instance of the Reader interface. This interface is part of the Go core package, IO. Using this approach, we can define various methods that provide implementations for the Reader interface, such as GetUserFile, GetUserHTTP, or GetDummyUser (which is useful for testing the GetUser method). This strategy can be employed in various scenarios to address challenges related to unit testing or dependency cycles in Go. By introducing interfaces and multiple implementations, we can achieve effective decoupling.

Conclusion
#

The Dependency Inversion Principle is the last SOLID principle, represented by the letter D in the word SOLID. This principle asserts that high-level components should not rely on low-level components. Instead, all our components should be built on abstractions, specifically interfaces. These abstractions enable us to use our code with greater flexibility and to conduct thorough testing.

Useful Resources
#

SOLID principles in Golang - This article is part of a series.
Part 5: This Article

Related

Practical SOLID in Golang: Interface Segregation Principle

·1722 words·9 mins· loading · loading
When beginners embark on their programming journey, the initial focus is typically on algorithms and adapting to a new way of thinking. After some time, they delve into Object-Oriented Programming (OOP). If this transition is delayed, it can be challenging to shift from a functional programming mindset. However, eventually, they embrace the use of objects and incorporate them into their code where necessary, sometimes even where they’re not needed. As they learn about abstractions and strive to make their code more reusable, they may overgeneralize, resulting in abstractions applied everywhere, which can hinder future development. At some point, they come to realize the importance of setting boundaries for excessive generalization. Fortunately, The Interface Segregation Principle has already provided a guideline for this, representing the “I” in the word SOLID. When we do not respect The Interface Segregation # Maintain small interfaces to prevent users from relying on unnecessary features. Uncle Bob introduced this principle, and you can find more details about it on his blog. This principle clearly states its requirement, perhaps better than any other SOLID principle. Its straightforward advice to keep interfaces as small as possible should not be interpreted as merely advocating one-method interfaces. Instead, we should consider the cohesion of features that an interface encompasses. Let’s analyze the code below: User interface type User interface { AddToShoppingCart(product Product) IsLoggedIn() bool Pay(money Money) error HasPremium() bool HasDiscountFor(product Product) bool // // some additional methods // } Let’s assume we want to create an application for shopping. One approach is to define an interface User, as demonstrated in the code example. This interface includes various features that a user can have. On our platform, a User can add a Product to the ShoppingCart, make a purchase, and receive discounts on specific Products. However, the challenge is that only specific types of Users can perform all of these actions. Guest struct type Guest struct { cart ShoppingCart // // some additional fields // } func (g *Guest) AddToShoppingCart(product Product) { g.cart.Add(product) } func (g *Guest) IsLoggedIn() bool { return false } func (g *Guest) Pay(Money) error { return errors.New("user is not logged in") } func (g *Guest) HasPremium() bool { return false } func (g *Guest) HasDiscountFor(Product) bool { return false } We have implemented this interface with three structs. The first one is the Guest struct, representing a user who is not logged in but can still add a Product to the ShoppingCart. The second implementation is the NormalCustomer, which can do everything a Guest can, plus make a purchase. The third implementation is the PremiumCustomer, which can use all features of our system.

Practical SOLID in Golang: Open/Closed Principle

·1369 words·7 mins· loading · loading
Many different approaches and principles can lead to long-term improvements in our code. Some of them are well-known in the software development community, while others remain somewhat under the radar. In my opinion, this is the case with The Open/Closed Principle, represented by the letter O in the word SOLID. In my experience, only those genuinely interested in SOLID principles tend to understand what this principle means. We may have applied this principle without even realizing it in some instances, such as when working with the Strategy pattern. However, the Strategy pattern is just one application of the Open/Closed Principle. In this article, we will delve into the full purpose of this principle, with all examples provided in Go. When we do not respect the Open/Closed Principle # You should be able to extend the behavior of a system without having to modify that system. The requirement for the Open/Closed Principle, as seen above, was provided by Uncle Bob in his blog. I prefer this way of defining The Open/Closed Principle because it highlights its full brilliance. At first glance, it may seem like an absurd requirement. Seriously, how can we extend something without modifying it? I mean, is it possible to change something without changing it? By examining the code example below, we can see what it means for certain structures not to adhere to this principle and the potential consequences. The bad Authentication Service type AuthenticationService struct { // // some fields // } func (s *AuthenticationService) Authenticate(ctx *gin.Context) (*User, error) { switch ctx.GetString("authType") { case "bearer": return c.authenticateWithBearerToken(ctx.Request.Header) case "basic": return c.authenticateWithBasicAuth(ctx.Request.Header) case "applicationKey": return c.authenticateWithApplicationKey(ctx.Query("applicationKey")) } return nil, errors.New("unrecognized authentication type") } func (s *AuthenticationService) authenticateWithApplicationKey(key string) (*User, error) { // // authenticate User from Application Key // } func (s *AuthenticationService) authenticateWithBasicAuth(h http.Header) (*User, error) { // // authenticate User from Basic Auth // } func (s *AuthenticationService) authenticateWithBearerToken(h http.Header) (*User, error) { // // validate JWT token from the request header // } The example presents a single struct, AuthenticationService. Its purpose is to authenticate a User from the web application’s Context, supported by the Gin package. Here, we have the main method, Authenticate, which checks for specific authentication type associated with the data within the Context. How User is retrieved from the Context may vary based on whether the User uses a bearer JWT token, basic authentication, or an application key.

Practical SOLID in Golang: Single Responsibility Principle

·2013 words·10 mins· loading · loading
There aren’t too many opportunities for a breakthrough in software development. They usually arise from either rewiring our logic after initial misunderstandings or filling in gaps in our knowledge. I appreciate that feeling of deeper understanding. It can happen during a coding session, while reading a book or an online article, or even while sitting on a bus. An internal voice follows, saying, “Ah, yes, that’s how it works.” Suddenly, all past mistakes seem to have a logical reason, and future requirements take shape. I experienced such a breakthrough with the SOLID principles, which were first introduced in a document by Uncle Bob and later expounded upon in his book, “Clean Architecture.” In this article, I intend to embark on a journey through all the SOLID principles, providing examples in Go. The first principle on the list, representing the letter ‘S’ in SOLID, is the Single Responsibility Principle. When we do not respect Single Responsibility # The Single Responsibility Principle (SRP) asserts that each software module should serve a single, specific purpose that could lead to change. The sentence above comes directly from Uncle Bob himself. Initially, its application was linked to modules and the practice of segregating responsibilities based on the organization’s daily tasks. Nowadays, SRP has a broader scope, influencing various aspects of software development. We can apply its principles to classes, functions, modules, and naturally, in Go, even to structs. Some Frankenstein of EmailService type EmailService struct { db *gorm.DB smtpHost string smtpPassword string smtpPort int } func NewEmailService(db *gorm.DB, smtpHost string, smtpPassword string, smtpPort int) *EmailService { return &EmailService{ db: db, smtpHost: smtpHost, smtpPassword: smtpPassword, smtpPort: smtpPort, } } func (s *EmailService) Send(from string, to string, subject string, message string) error { email := EmailGorm{ From: from, To: to, Subject: subject, Message: message, } err := s.db.Create(&email).Error if err != nil { log.Println(err) return err } auth := smtp.PlainAuth("", from, s.smtpPassword, s.smtpHost) server := fmt.Sprintf("%s:%d", s.smtpHost, s.smtpPort) err = smtp.SendMail(server, auth, from, []string{to}, []byte(message)) if err != nil { log.Println(err) return err } return nil } Let’s analyze the code block above. In this code, we have a struct called EmailService, which contains only one method, Send. This service is intended for sending emails. Although it may seem okay at first glance, upon closer inspection, we realize that this code violates the Single Responsibility Principle (SRP) in several ways.