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:
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.
It’s a relationship that, in the object-oriented (OO) context, you can represent using the following Unified Modeling Language (UML) diagram:
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:
- Implementation Inheritance
- Composition
- Aggregation
- 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:
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.
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:
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:
Here, you can see many important things:
-
printNames()
now depends on thePerson
abstraction. Even if you add a newPerson
specialization, you won’t need to changeprintNames()
. -
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. -
Student
,Musician
andTeacher
are some of the realizations of thePerson
abstract class. These are concrete classes that you can actually instantiate.AnyOtherItem
is an example of a concrete class you can add without impactingprintNames()
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:
Here you can see that:
-
printNames()
now depends only on theNamed
interface. -
Person
implements theNamed
interface and you can now use each of its realizations inprintNames()
. -
Cat
implements theNamed
interface,printNames()
can use it and it has nothing to do with thePerson
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:
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 informationServer
receives and saves intoRepository
without any transformation usingsave()
.
Everything looks perfect, but a problem arises as soon as you represent the previous relationship through the UML diagram in Figure 1.10:
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 ofRepository
is. -
Repository
andServer
have the same lifecycle. TheRepository
instance is created at the same time as theServer
instance.Repository
dies whenServer
does. - A particular instance of
Repository
belongs to one and only one instance ofServer
. 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:
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 theRepository
it’s going to use. -
Repository
andServer
may have different lifecycles. You can create theRepository
instance before theServer
.Repository
doesn’t necessarily die whenServer
does. - You can use a particular instance of
Repository
in many different instances ofServer
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 theRepository
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:
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:
- Use Mockito to create an instance of the mock implementation of the
Repository
interface to use for testing. - Instantiate the
Server
, passing the previously-created mocked instance ofRepository
as a parameter for the primary constructor. - Create
Data()
, which you pass as a parameter forreceive()
. - Verify that you’ve invoked
save()
on theRepository
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?
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:
- This is an implementation inheritance. Class B is a realization of the abstract class A.
- 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.
- The third dependency is an interface inheritance. This shows class B implementing interface A.
- 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:
- Create
RepositoryMock
, implementing theRepository
interface. - Define the public
receivedData
variable. - Implement
save()
, saving the received parameterdata
to the local variablereceivedData
.
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 beRepositoryFake
.
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:
- The SOLID Principles and, in particular, the Dependency Inversion Principle
- Android Test-Driven Development By Tutorials
- [Advanced Android App Architecture] (https://www.raywenderlich.com/books/advanced-android-app-architecture)
- Mockito Testing Framework
Now, it’s time for some code!