Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Hands-On Dependency Injection in Go

You're reading from   Hands-On Dependency Injection in Go Develop clean Go code that is easier to read, maintain, and test

Arrow left icon
Product type Paperback
Published in Nov 2018
Publisher Packt
ISBN-13 9781789132762
Length 346 pages
Edition 1st Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Corey Scott Corey Scott
Author Profile Icon Corey Scott
Corey Scott
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

Preface 1. Never Stop Aiming for Better 2. SOLID Design Principles for Go FREE CHAPTER 3. Coding for User Experience 4. Introduction to the ACME Registration Service 5. Dependency Injection with Monkey Patching 6. Dependency Injection with Constructor Injection 7. Dependency Injection with Method Injection 8. Dependency Injection by Config 9. Just-in-Time Dependency Injection 10. Off-the-Shelf Injection 11. Curb Your Enthusiasm 12. Reviewing Our Progress 13. Assessment 14. Other Books You May Enjoy

Why does DI matter?

As professionals, we should never stop learning. Learning is the one true way to ensure we stay in demand and continue delivering value to our customers. Doctors, lawyers, and scientists are all highly respected professionals and all focus on continuously learning. Why should programmers be different?

In this book, we will take a journey that will start with some code that gets the job done and, by selectively applying various DI methods available in Go, together, we will transform it into something a hell of a lot easier to maintain, test, and extend.

Not everything in this book is traditional or perhaps even idiomatic, but I would ask you to try it before you deny it. If you like it, fantastic. If not, at least you learned what you don't want to do.

So, how do I define DI?

DI is coding in such a way that those resources (that is, functions or structs) that we depend on are abstractions. Because these dependencies are abstract, changes to them do not necessitate changes to our code. The fancy word for this is decoupling.

The use of the word abstraction here may be a little misleading. I do not mean an abstract class like you find in Java; Go does not have that. Go does, however, have interfaces and function literals (also known as closures).

Consider the following example of an interface and the SavePerson() function that uses it:

// Saver persists the supplied bytes
type Saver interface {
Save(data []byte) error
}

// SavePerson will validate and persist the supplied person
func SavePerson(person *Person, saver Saver) error {
// validate the inputs
err := person.validate()
if err != nil {
return err
}

// encode person to bytes
bytes, err := person.encode()
if err != nil {
return err
}

// save the person and return the result
return saver.Save(bytes)
}

// Person data object
type Person struct {
Name string
Phone string
}

// validate the person object
func (p *Person) validate() error {
if p.Name == "" {
return errors.New("name missing")
}

if p.Phone == "" {
return errors.New("phone missing")
}

return nil
}

// convert the person into bytes
func (p *Person) encode() ([]byte, error) {
return json.Marshal(p)
}

In the preceding example, what does Saver do? It saves some bytes somewhere. How does it do this? We don't know and, while working on the SavePerson function, we don't care.

Let's look at another example that uses a function literal:

// LoadPerson will load the requested person by ID.
// Errors include: invalid ID, missing person and failure to load
// or decode.
func LoadPerson(ID int, decodePerson func(data []byte) *Person) (*Person, error) {
// validate the input
if ID <= 0 {
return nil, fmt.Errorf("invalid ID '%d' supplied", ID)
}

// load from storage
bytes, err := loadPerson(ID)
if err != nil {
return nil, err
}

// decode bytes and return
return decodePerson(bytes), nil
}

What does decodePerson do? It converts the bytes into a person. How? We don't need to know to right now.

This is the first advantage of DI that I would highlight to you:

DI reduces the knowledge required when working on a piece of code, by expressing dependencies in an abstract or generic manner

Now, let's say that the preceding code came from a system that stored data in a Network File Share (NFS). How would we write unit tests for that? Having access to an NFS at all times would be a pain. Any such tests would also fail more often than they should due to entirely unrelated issues, such as network connectivity.

On the other hand, by relying on an abstraction, we could swap out the code that saves to the NFS with fake code. This way, we are only testing our code in isolation from the NFS, as shown in the following code:

func TestSavePerson_happyPath(t *testing.T) {
// input
in := &Person{
Name: "Sophia",
Phone: "0123456789",
}

// mock the NFS
mockNFS := &mockSaver{}
mockNFS.On("Save", mock.Anything).Return(nil).Once()

// Call Save
resultErr := SavePerson(in, mockNFS)

// validate result
assert.NoError(t, resultErr)
assert.True(t, mockNFS.AssertExpectations(t))
}

Don't worry if the preceding code looks unfamiliar; we will examine all of the parts in depth later in this book.

Which brings us to the second advantage of DI:

DI enables us to test our code in isolation of our dependencies

Considering the earlier example, how could we test our error-handling code? We could shut down the NFS through some external script every time we run the tests, but this would likely be slow and would definitely annoy anyone else that depended on it.

On the other hand, we could quickly make a fake Saver that always failed, as shown in the following code:

func TestSavePerson_nfsAlwaysFails(t *testing.T) {
// input
in := &Person{
Name: "Sophia",
Phone: "0123456789",
}

// mock the NFS
mockNFS := &mockSaver{}
mockNFS.On("Save", mock.Anything).Return(errors.New("save failed")).Once()

// Call Save
resultErr := SavePerson(in, mockNFS)

// validate result
assert.Error(t, resultErr)
assert.True(t, mockNFS.AssertExpectations(t))
}

The above test is fast, predictable, and reliable. Everything we could want from tests!

This gives us the third advantage of DI:

DI enables us to quickly and reliably test situations that are otherwise difficult or impossible

Let's not forget about the traditional sales pitch for DI. Tomorrow, if we decided to save to a NoSQL database instead of our NFS, how would our SavePerson code have to change?  Not one bit. We would only need to write a new Saver implementation, giving us the fourth advantage of DI:

DI reduces the impact of extensions or changes

At the end of the day, DI is a tool—a handy tool, but no magic bullet. It's a tool that can make code easier to understand, test, extend, and reuse—a tool that can also help reduce the likelihood of circular dependency issues that commonly plague new Go developers.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image