Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

1. Design Principles
Written by Massimo Carli

In this chapter, you’ll get motivated to use a Dependency Injection (DI) library like Dagger by learning all about the problem you need to solve: dependency. You’ll understand what dependencies are and why you need to control them to create successful apps.

Note: This first chapter describes all the main concepts about object-oriented programming using a dependency management focus. Go to the next chapter if you’re already familiar with this topic.

Dependency Injection is one of the most important patterns to use in the development of a modern professional app, and frameworks like Dagger and Hilt help implement it in Android.

When you thoroughly understand the object-oriented principles of this chapter, the learning curve of those frameworks, initially steep, becomes flatter and everything gets easier.

Note: You can find all the code for this chapter in the material section of this book. It’s a good idea to follow along by adding the code to the starter project with IntelliJ. You can also check out the complete code in the final project. The challenge project contains the solutions to the challenges at the end of the chapter.

What dependency means

Dependency is a fancy way of saying that one thing relies on another in order to do its job. You can use the term in different ways for different contexts: One person might depend on another. A project might depend on a budget. In math, given y = f(x), you can say that y depends on x.

But what does dependency mean? In the previous examples, it means that if the person you depend on is not available anymore, you need to change your way of life. If the dependency is economic, you have to reduce your expenses.

Similarly, if the business cuts the budget, you have to change the requirements for your project or cancel it.

This concept is obvious in the last example because if x changes, y changes as well.

But why are changes important? Because it takes effort to make changes.

The previous examples showed how dependencies can cause changes, and you can even prove this using physics. Consider the famous principle, Newton’s second law, shown in Figure 1.1:

Figure 1.1 - Newton’s second law
Figure 1.1 - Newton’s second law

In that formula, F is the force you need to apply if you want the equation to be true with a, which is acceleration. Acceleration is a change in speed that, again, is a measure of how the position changes in time.

This doesn’t mean that change is bad. It just says that if you want to change, you need to apply effort that’s as big or bigger than the mass m.

Hey, but this is a book about Dagger and Hilt! What does this have to do with your code?

In the context of coding, dependency is inevitable, and applying changes will always take some effort. But there are also ways to reduce the mass of your code, reducing the effort, even for big changes.

This is what you’re going to learn in the following paragraphs. Mastering this skill will allow you to use tools like Dagger and Hilt effectively and productively.

A formal definition of dependency

In the previous paragraph, you learned what dependency means in your real life, but in the context of computer science, you need a more formal definition:

Entity A depends on entity B if a change in B can imply a change in A.

Note the “can” because this is something that could, but shouldn’t necessarily, happen. In the previous examples, A can be you, your project or y. In the same examples, B can be the person you depend on, the budget of your project or, simply, x.

Figure 1.2 - Real life dependencies
Figure 1.2 - Real life dependencies

It’s a relationship that, in the object-oriented (OO) context, you can represent using the following Unified Modeling Language (UML) diagram:

Figure 1.3 - Dependency relation in UML
Figure 1.3 - Dependency relation in UML

In UML, you represent a dependency between entity A and entity B by showing an open arrow with a dotted line from A to B. This is how you indicate that a change in B can result in a change of A.

In these diagrams, A and B can be different things like objects, classes or even packages or modules. This is a model that reflects what happens in software development, where many components interact with many others in different ways.

If a change in a component triggers a change in all its dependents, you end up changing a lot of code — which increases the probability of introducing new bugs. Additionally, you need to rewrite and run the tests. This takes time, which translates into money.

On the other hand, it’s not possible to create an app without dependencies. If you tried, you’d have the opposite problem of a monolithic app: All the code would be in a single point, making writing code in large teams difficult and testing almost impossible.

As a developer, one possible solution is to use patterns and practices that allow the adoption of benign types of dependencies, which is the topic of the following paragraphs.

Types of dependencies

Figure 1.3 above depicts a generic type of dependency, where the arrow simply indicates that A depends on B without going into detail. It doesn’t show what A and B are, or how the dependency looks in code.

Using object-oriented language, you can define relationships more precisely, and you can do it in different ways for different types of dependencies. In the following paragraphs you’ll learn about:

  1. Implementation Inheritance
  2. Composition
  3. Aggregation
  4. Interface Inheritance

You’ll also learn how abstraction can limit the impact of dependency.

Implementation inheritance

Implementation Inheritance is the strongest type of dependency. You describe it using the UML diagram in Figure 1.4 below:

Figure 1.4 - Implementation Inheritance; the strongest level of dependency
Figure 1.4 - Implementation Inheritance; the strongest level of dependency

You represent this relationship by using a continuous arrow with the tip closed and empty. Read the previous diagram by saying that Student IS-A Person. This means that a student has all the characteristics of a person and does all the things a person does.

The Student class depends on the Person class because a change of the former has, as an obvious consequence, a change in the latter — which is the very definition of dependency.

For example, if you change the Person class by adding eat function, the Student class now also has the ability to eat.

A Student differs from a generic Person because they study a particular subject. You need both Person and Student classes due to two fundamental object-oriented concepts: The first is that not all people study. The second, more important, concept is that the fact that some people study may not interest you at all.

You have a Person class so you can generalize people of different types when the only thing that interests you is that they are people.

For this reason, the statement Student IS-A Person is not the most correct way of phrasing it. To be more accurate, you’d say: Person is a generalization or abstraction of Student instead.

This app probably started with Student, then the developers added Person to limit dependency. As you’ll see in the following paragraphs, they introduced a level of abstraction to limit the dependency relationship.

Implementation inheritance in code

To define that Student depends on Person through an implementation inheritance relationship, simply use the following code:

open class Person(val name: String) {
  fun think() {
    println("$name is thinking...")
  }
}

class Student(name: String) : Person(name) {
  fun study(topic: String) {
    println("$name is studying $topic")
  }
}

Here, you can see that Person describes objects with a name and that they can think(). A Student IS-A Person and so they can think() but they also study some topics. All students are persons but not all persons are students.

Abstraction reduces dependency

The discussion of when to use implementation inheritance, although interesting, is beyond the scope of this book. It’s important to say that merely introducing Person-type abstraction is a step toward reducing dependency. You can easily prove it with a story.

The first implementation

Suppose you’re starting a new project from scratch and you want to print the names of a list of students for a university. After some analysis, you write the code for Person like this:

class Student(val name: String) {
  fun study(topic: String) {
    println("$name is studying $topic")
  }

  fun think() {
    println("$name is thinking...")
  }
}

A Student has a name, can think and studies a topic. In Figure 1.5 below, you have its UML representation.

Figure 1.5 - Initial implementation for the Student class
Figure 1.5 - Initial implementation for the Student class

Your program wants to print all the names of the students; you end up with the following code:

fun printStudent(students: List<Student>) = students.forEach { println(it.name) }

You can then test printStudent() with the following code:

fun main() {
  val students = listOf<Student>(
    Student("Mickey Mouse"),
    Student("Donald Duck"),
    Student("Minnie"),
    Student("Amelia")
  )
  printStudent(students)
}

Build and run main() and you get this output:

Mickey Mouse
Donald Duck
Minnie
Amelia

Your program works and everybody is happy… for now. But something is going to change.

Handling change

Everything looks fine, but the university decided to hire some musicians and create a band. They now need a program that prints the names of all the musicians in the band.

You’re an expert now and you know how to model this new item, so you create the Musician class like this:

class Musician(val name: String) {
  fun think() {
    println("$name is thinking...")
  }

  fun play(instrument: String) {
    println("$name is playing $instrument")
  }
}

Musicians have a name, they think and play a musical instrument. The UML diagram is now this:

Figure 1.6 - Initial implementation for the Musician class
Figure 1.6 - Initial implementation for the Musician class

You also write the printMusician() function like this:

fun printMusician(musicians: List<Musician>) = musicians.forEach { println(it.name) }

Then you can test it with the following code:

fun main() {
  val musicians = listOf<Musician>(
    Musician("Mozart"),
    Musician("Andrew Lloyd Webber"),
    Musician("Toscanini"),
    Musician("Puccini"),
    Musician("Verdi")
  )
  printMusician(musicians)
}

Build and run main() and you’ll get this output:

Mozart
Andrew Lloyd Webber
Toscanini
Puccini
Verdi

Everything looks fine and everybody is still happy. A good engineer should smell that something is not ideal, though, because you copy-pasted most of the code. There’s a lot of repetition, which violates the Don’t Repeat Yourself (DRY) principle.

Keeping up with additional changes

The university is happy with the system you created and decides to ask you to do the same thing for the teachers, then for the teacher assistants, and so on.

Following the same approach, you ended up creating N different classes with N different methods for printing N different lists of names.

Now, you’ve been asked to do a “simple” task. Instead of just printing the name, the University asked you to add a Name: prefix. In code, instead of using:

println(it.name)

they asked you to use:

println("Name: $it.name")

Note: It’s curious how the customer has a different perception of what’s simple and what isn’t.

Because of this request, you have to change N printing functions and the related tests. You need to apply the same change in different places. You might miss some and misspell others.

The probability of introducing bugs increases with the number of changes you need to make.

If something bad happens, you need to spend a lot of time fixing the problem. And even after that, you’re still not sure everything’s fine.

Using your superpower: abstraction

If you end up in the situation described above, you should immediately stop coding and start thinking. Making the same change in many different places is a signal that something is wrong.

Note: There’s a joke about a consultant who wanted to make their employer dependent on them. To do that, they only needed to implement the same feature in many different ways and in many different places. This is no bueno!

The solution, and the main weapon in your possession, is abstraction.

To print names, you’re not interested in whether you have Student or Musician. You don’t care if they study a topic or play an instrument. The only thing that interests you is that they have a name.

You need a way to be able to see all the entries as if they were the same type, containing the only thing that interests you: the name.

Here, the need to remove the superfluous leads you to the definition of the following abstraction, which you call Person:

abstract class Person(val name: String) {
  fun think() {
    println("$name is thinking...")
  }
}

Abstraction means considering only the aspects that interest you by eliminating everything superfluous. Abstraction is synonymous with reduction.

Knowing how to abstract, therefore, means knowing how to eliminate those aspects that don’t interest you and, therefore, you don’t want to depend upon.

Creating Person means that you’re interested in the fact that a person can think and you don’t care whether this person can study.

This is an abstract class. It allows you to define the Person type as an abstraction only, thus preventing you from having to create an instance of it.

think() is present in both classes, which makes it part of the abstraction. As defined, every person is able to think, so it’s a logical choice.

Now, Student and Musician become the following:

class Student(name: String) : Person(name) {
  fun study(topic: String) {
    println("$name is studying $topic")
  }
}

class Musician(name: String) : Person(name) {
  fun play(instrument: String) {
    println("$name is playing $instrument")
  }
}

Now that you’ve put in the effort, you can reap the benefits of simplifying the method of displaying names, which becomes:

fun printNames(persons: List<Person>) = persons.forEach { println(it.name) }

The advantage lies in the fact that you can print the names of all the objects that can be considered Person and, therefore, include both Student and Musician.

Because of that, you can run the following code:

fun main() {
  val persons = listOf(
    Student("Topolino"),
    Musician("Bach"),
    Student("Minnie"),
    Musician("Paganini")
  )
  printNames(persons)
}

And that’s not all. Returning to the concept of dependency, you can see that adding a further specialization of Person does not imply any change in the printing function. That’s because the only thing this depends on is the generalization described by Person.

With this, you’ve shown how the definition of an abstraction can lead to a reduction of dependency and, therefore, to changes having a smaller impact on the existing code.

Abstraction & UML

Explain the level of dependency using the following UML diagram, Figure 1.7:

Figure 1.7 - The abstract Person class
Figure 1.7 - The abstract Person class

Here, you can see many important things:

  1. printNames() now depends on the Person abstraction. Even if you add a new Person specialization, you won’t need to change printNames().
  2. Person is abstract. It’s now the description of an abstraction and not of a specific object. In UML, you represent this using a stereotype which is the abstract word between « ». Alternatively, you can use an italic font.
  3. Student, Musician and Teacher are some of the realizations of the Person abstract class. These are concrete classes that you can actually instantiate. AnyOtherItem is an example of a concrete class you can add without impacting printNames() in any way.

Finding the right level of abstraction

Reading the previous code, you’ll notice there are still some problems. That’s because what printNames() really needs, or depends on, are objects with a name. Right now, however, you’re forcing it to care that the name belongs to a person.

But what if you want to print the names for a list of objects for whom the IS-A relation with Person is not true? What if you want to name cats, vehicles or food? A cat is not a person, nor is food.

The current implementation of printNames() still has an unnecessary dependency on the Person class. How can you remove that dependency? You already know the answer: abstraction.

So now, define the following Named interface:

interface Named {
  val name: String
}

and change Person to:

abstract class Person(override val name: String) : Named {
  fun think() {
    println("$name is thinking...")
  }
}

Now, each person implements the Named interface. So do the Student, Musician, Teacher and other realizations of the Person abstract class.

Now, change printNames() to:

fun printNames(named: List<Named>) = named.forEach { println(it.name) }

The good news is that now you can create Cat like this:

class Cat(override val name: String) : Named {
  fun meow() {
    println("$name is meowing...")
  }
}

and successfully run the following code:

fun main() {
  val persons = listOf(
    Student("Topolino"),
    Musician("Bach"),
    Student("Minnie"),
    Musician("Paganini"),
    Cat("Silvestro")
  )
  printNames(persons)
}

getting this as output:

Topolino
Bach
Minnie
Paganini
Silvestro

In the context of printing names, all the objects are exactly the same because they all provide a name through a name property defined by the Named interface they implement.

This is the first example of dependency on what a specific object DOES and not on what the same component IS. You’ll learn about this in detail in the following paragraphs.

The named interface in UML

It’s interesting to see how you represent the solution of the previous paragraph in UML:

Figure 1.8 - The Named interface
Figure 1.8 - The Named interface

Here you can see that:

  1. printNames() now depends only on the Named interface.
  2. Person implements the Named interface and you can now use each of its realizations in printNames().
  3. Cat implements the Named interface, printNames() can use it and it has nothing to do with the Person class.

Now you can say that Cat as well as Student, Musician, Teacher and any other realization of Person IS-A Named and printNames() can use them all.

What’s described here is an example of Open Closed Principle. It’s one of the SOLID principles and it states: software entities should be open for extension, but closed for modification.

This means that if you want to implement a new feature, you should add the new thing without changing the existing code.

In the previous example, to add a new object compatible with printNames(), you just need to make a new class that implements the Named interface. None of the existing code needs to be changed.

Composition over (implementation) inheritance

In the previous paragraph, you saw how you can remove the dependency between printNames() and realizations for the Person abstract class by introducing the Named interface.

This change is actually a big thing, because it’s your first example of dependency on what an object DOES and not on what the same object IS.

In the printNames() example, this further reduced the dependency on the Person abstraction.

This is a very important principle you should always consider in your app: Program to an interface, not an implementation.

This principle is also true in real life. If you need a plumber, you don’t usually care who that plumber is. What’s important is what they do. You want to hire the plumber who can fix the pipes in your house.

Because of this, you can change who you use as your plumber if you have to. If you need a specific plumber because of who they are, you need to consider a course of action if they aren’t available anymore.

Composition

A classic example of dependency on what an object does is persistence management.

Suppose you have a server that receives requests from clients, collects information then stores it within a repository.

In this case, it would be completely wrong to say the Repository IS-A Server or the Server IS-A Repository.

So if you were to represent the relationship between Server and Repository, you could say that the former uses the latter, as the UML diagram in Figure 1.9 shows:

Figure 1.9 - The Server uses a Repository
Figure 1.9 - The Server uses a Repository

This diagram just says that Server uses Repository, but it doesn’t say how.

How do different entities communicate? In this case, Server must have a reference to Repository and then invoke one or more methods on it. Here, you can suppose it invokes save(Data) with a parameter of type Data.

You can represent the previous description with the following code:

data class Data(val value: Int)

class Repository {
    fun save(data: Data) {
        // Save data
    }
}

class Server {
    private val repository = Repository()

    fun receive(data: Data) {
        repository.save(data)
    }
}

Note: Data is not important; it simply represents the information Server receives and saves into Repository without any transformation using save().

Everything looks perfect, but a problem arises as soon as you represent the previous relationship through the UML diagram in Figure 1.10:

Figure 1.10 - Composition
Figure 1.10 - Composition

The dependency between Server and Repository is a composition, which has a UML representation of an arrow starting with a full diamond.

You can say that Server composes Repository. As you can see in the previous code, Server has a private local variable of Repository. It initializes with an instance of the Repository class itself.

This means that:

  • Server knows exactly what the implementation of Repository is.
  • Repository and Server have the same lifecycle. The Repository instance is created at the same time as the Server instance. Repository dies when Server does.
  • A particular instance of Repository belongs to one and only one instance of Server. Therefore, it cannot be shared.

In terms of dependency, if you wanted to modify the Repository implementation, you’d have to modify all the classes, like Server, that use it in this way. Ring a bell? Once again, you’re duplicating a lot of work — and running the risk of introducing bugs.

You now understand that if a change of Repository leads to a change of Server, then there’s a dependency between these two entities. How can you reduce that? A different kind of dependency will help.

Aggregation

For a better solution to this problem, you can use a different type of dependency: aggregation. You represent it as in the UML diagram in Figure 1.11:

Figure 1.11 - Aggregation
Figure 1.11 - Aggregation

This leads to the following code, where the Server, Repository and Data classes remain the same.

class Server(val repository: Repository) {

  fun receive(data: Data) {
    repository.save(data)
  }
}

In this case, you pass the reference to a Repository instance in the constructor of the Server class.

This means that Server is no longer responsible for creating the particular Repository. This (apparently simple) change greatly improves your dependency management.

You can now make Server use different Repository implementations simply by passing a different instance in the constructor.

Now:

  • Server doesn’t know exactly which implementation of the Repository it’s going to use.
  • Repository and Server may have different lifecycles. You can create the Repository instance before the Server. Repository doesn’t necessarily die when Server does.
  • You can use a particular instance of Repository in many different instances of Server or similar classes; therefore, it can be shared.

Using a composition or aggregation doesn’t change the fact that Server depends on Repository. The difference is in the type of dependency.

Using composition, Server depends on what Repository IS. With aggregation, Server depends on what Repository DOES.

In the latter case, Repository can be an abstraction. This isn’t possible with composition because you need to create an instance.

Note: You might ask why you need to support different Repository implementations. As you’ll see in the following chapters, any abstraction always has at least one additional implementation: the one you use in testing.

Interface inheritance

In the previous paragraphs, you learned the importance of abstractions and, specifically, of using interfaces. In the Server and Repository example, you use an aggregation relationship to make Repository an abstraction instead of using a concrete class. This is possible because what the Server really needs is not an instance of Repository but something that allows it to save some Data.

Note: The dependency is based on what the Repository DOES and not on what the Repository IS.

For this reason, you can implement the following solution, where the Repository is now an interface that might have different implementations, including the one you can describe using the following RepositoryImpl class:

interface Repository {
  fun save(data: Data)
}

class RepositoryImpl : Repository {
  override fun save(data: Data) {
    // Save data
  }
}

You don’t need to change Server — it continues to depend on Repository, which is now an interface. Now, you can pass any implementation of Repository to Server and, therefore, any class capable of making a Data object persistent.

You usually refer to this kind of dependency between Server and RepositoryImpl as loosely coupled and describe it with the UML diagram in Figure 1.12:

Figure 1.12 - Loosely Coupled
Figure 1.12 - Loosely Coupled

This is an ideal situation in which the Server class does not depend on the particular Repository implementation, but rather on the abstraction that the interface itself describes.

This describes a fundamental principle: the Dependency Inversion Principle, which says that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions like the Repository interface.

  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

But why inversion? What, exactly, are you inverting?

In Figure 1.10, Server had a dependency on Repository, which contains implementation details. Instead, Server should have a dependency on the Repository interface. Now, all the Repository implementations depend on the same interface — and the dependency is reversed, as you can see in Figure 1.12.

Why abstraction is important

What you’ve learned in the previous paragraphs looks theoretical, but it has important practical consequences.

Note: In theory, there’s no difference between theory and practice, but in practice, there is :]

You might ask, why do you need to manage different implementations for the Repository abstraction? This is usually an interface to a database, the part of a project that changes less frequently compared to the UI. As mentioned in a previous note, you should always have at least one implementation of any abstraction for testing. In Figure 1.12, this is called RepositoryMock.

How would you test the Server class you wrote for the composition case described in Figure 1.10?

class Server {
    private val repository = Repository() 

    fun receive(data: Data) {
        repository.save(data)
    }
}

If you want to test this class, you need to change it. If you change it, then this is not the same class anymore. Consider, then, interface inheritance and the following implementation:

class Server(val repository: Repository) {
  fun receive(data: Data) {
    repository.save(data)
  }
}

Now, in your test, you just have to pass the mock implementation of the Repository instance. If you use Mockito, this looks something like the following:

class ServerTest {
  @Test
  fun `When Data Received Save Method is Invoked On Repository`() {
    // 1
    val repository = mock<Repository>()
    // 2
    val server = Server(repository)
    // 3
    val dataToBeSaved = Data(10)
    server.receive(dataToBeSaved)
    // 4
    verify(repository).save(dataToBeSaved)
  }
}

Here you:

  1. Use Mockito to create an instance of the mock implementation of the Repository interface to use for testing.
  2. Instantiate the Server, passing the previously-created mocked instance of Repository as a parameter for the primary constructor.
  3. Create Data(), which you pass as a parameter for receive().
  4. Verify that you’ve invoked save() on the Repository mock with the expected parameter.

Note: The previous code uses the Mockito testing framework, which is outside the scope of this book. If you want to learn more about testing, read the Android Test-Driven Development by Tutorials book. You can implement the same test without Mockito, as you’ll see in the Challenge 3 for this chapter.

This is a typical example of how thinking in terms of abstraction can help in the creation, modification and testing of your code.

Challenges

Now that you’ve learned some important theory, it’s time for a few quick challenges.

Challenge 1: What type of dependency?

Have a look at this UML diagram in Figure 1.13. What type of dependency is described in each part?

Figure 1.13 - Challenge 1 - Type of dependency
Figure 1.13 - Challenge 1 - Type of dependency

Challenge 2: Dependency in code

How would you represent the dependencies in Figure 1.13 in code? Write a Kotlin snippet for each one.

Challenge 3: Testing without Mockito

Earlier in this chapter, you learned that RepositoryMock is an implementation of Repository to use for testing. How would you implement it to test Server without the Mockito framework?

Challenge solutions

Challenge solution 1: What type of dependency?

In Figure 1.13, you have different types of dependency. Specifically:

  1. This is an implementation inheritance. Class B is a realization of the abstract class A.
  2. This is also an implementation inheritance between two concrete classes, A and B. If using Kotlin, class A might also have an «open» stereotype. UML is extensible, so nobody can prevent you from using the stereotype you need as soon as it has a simple and intuitive meaning.
  3. The third dependency is an interface inheritance. This shows class B implementing interface A.
  4. The last dependency is a loosely coupled dependency. Class C depends on abstraction A. Class B is a realization of A.

Challenge solution 2: Dependency in code

If you did the previous challenge, this should be a piece of cake. The code for each case is:

Case 1

abstract class A

class B : A()

Case 2

open class A

class B : A()

Case 3

interface A

class B : A

Case 4

interface A

class B : A

class C(val a: A)

fun main() {
  val a: A = B()
  val c = C(a)
}

Challenge solution 3: Testing without Mockito

The testing code in the Why abstraction is important paragraph is:

class ServerTest {
  @Test
  fun `When Data Received Save Method is Invoked On Repository`() {
    // 1
    val repository = mock<Repository>()
    // 2
    val server = Server(repository)
    // 3
    val dataToBeSaved = Data(10)
    server.receive(dataToBeSaved)
    // 4
    verify(repository).save(dataToBeSaved)
  }
}

If the Mockito framework is not available, you need to define a mock implementation for the Repository interface, then create RepositoryMock. A possible solution is:

// 1
class RepositoryMock : Repository {
  // 2
  var receivedData: Data? = null

  override fun save(data: Data) {
    // 3
    receivedData = data
  }
}

Here you:

  1. Create RepositoryMock, implementing the Repository interface.
  2. Define the public receivedData variable.
  3. Implement save(), saving the received parameter data to the local variable receivedData.

Now, the testing code can be:

fun main() {
  val repository = RepositoryMock()
  val server = Server(repository)
  val dataToBeSaved = Data(10)
  server.receive(dataToBeSaved)
  assert(repository.receivedData == dataToBeSaved) // HERE
}

The test is successful if the value of the receivedData equals the value passed to the receive() function of the server. Of course, using a framework like JUnit or Mockito makes the testing experience better from the engineering tools perspective, but the point doesn’t change.

Note: In this case, you’re not actually testing interaction but state, so the proper name for RepositoryMock should be RepositoryFake.

Key points

  • Dependency is everywhere — you can’t avoid it altogether.
  • In computer science, you need to control dependency using the proper patterns.
  • Implementation inheritance is the strongest type of dependency.
  • Program to an interface, not an implementation.
  • Interface inheritance is a healthy type of dependency.
  • It’s better to depend on what an entity DOES rather than what an entity IS.

Where to go from here?

In this first chapter, you learned what dependency means and how you can limit its impact. You’ve seen that a good level of abstraction allows you to write better code. Good code is easy to change.

If you want to learn more about the concepts and libraries of this chapter, take a look at:

Now, it’s time for some code!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.