Foundation of Dependency Injection
What is Dependency Injection (DI)?
Dependency Injection (DI) is an idea for developing maintainable code. ScenarioFlow is designed based on DI so that it is a highly extensible system. Deepening your understanding of DI, you will be able to bring out the potential of ScenarioFlow greatly.
In general, a software application development will not finish immediately after it is launched, but the developers will perform maintenance on the application, for example, they fix existing bugs, change existing functions, add new features, and so on. Then, we can reduce the cost of maintenance in the future by developing well-designed code with an assumption of the future modification.
A great way to improve code maintainability is to develop loosely coupled code. Loose coupling means that modules in a system depend on each other weakly. With loose coupling, the maintainability is enhanced due to the high independence of each module in the system.
One familiar example of loosely coupled design is USB. USB is one of the standards for connecting PCs to peripheral devices. As long as a device compiles with the USB standard, you can connect various devices such as keyboards, mice, and USB flash drives to a PC using the same port, which is a significant advantage. Then, a PC and a peripheral device are completely independent.
DI is a guideline for designing loosely coupled code. We can design code with weak dependencies by following the principle, “Program to an interface, not an implementation”. Code where DI is effectively applied enables “code that uses other codes” and “code that is used by other codes” to exist independently, and the latter can be replaced with another code that compiles with the same standard as well as the relationship between a PC and its peripheral devices which enhance their independence by the USB standard. Understanding programming based on DI, you will be able to develop code that is tolerant of change.
Tightly Coupled Code
Tightly coupled code is something like a keyboard on a laptop PC. Keyboards on laptop PCs are designed only for use of the laptop PCs, so they fit perfectly into their small spaces. However, such design has some disadvantages.
Firstly, its maintainability is low. If such a keyboard is broken, it is not easy to repair it. We need tools to take apart it and knowledge of the machine, then we need to take apart the computer, replace the broken keyboard with new one, and put the computer back together in practice. Needless to say, it takes time.
Secondly, it has low compatibility. For example, is it possible to replace a keyboard on a laptop with another keyboard on a different laptop? It is almost impossible.
Loosely Coupled Code
Loosely Coupled Code is something like connecting a keyboard to a PC with a USB plug.
In this case, we only have to unplug the old keyboard and plug new one when a keyboard is broken. Which manufacturer made the keyboard doesn’t matter. We can replace a keyboard with new one if it has a USB connector regardless of whether or not it has LED lighting, it has macro functions, or it has any other useful features.
The Difference between Tightly and Loosely Coupled Code
The most important feature that loosely coupled code has but tightly coupled code doesn’t is that “implementations can be easily replaced”. In the first example, since a keyboard is designed only for its laptop, it is difficult to change the keyboard. However, in the second example, it is easy to change keyboards thanks to the USB standard with which devices should compile.
In DI-applied code, interface plays the similar role to the USB standard. If a class requires an external function, we will not let that class depend on another class directly, but we will define an interface that provides the required function and let the class depend on it. Then, we will develop a new class that has the required function and make that new class implement the required interface. Finally, we will be able to choose a class to be used from classes that implement the same interface freely, and we will be able to change a class to be used again and again if necessary.
Hello, world! with DI
As the first step in understanding DI, let’s develop a “Hello, world!” program with DI which is often used in introduction to programming.
Codes
At first, we create the following 4 classes. Only the SampleManager
class is a MonoBehaviour
object.
Attach the SampleManager
script to an object, and run the code. “Hello, world!” will be shown on the console.
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
ITextWriter textWriter = new ConsoleTextWriter();
var someone = new Someone(textWriter);
someone.SayHello();
}
}
C#using System;
public class Someone
{
private readonly ITextWriter textWriter;
public Someone(ITextWriter textWriter)
{
if (textWriter == null)
throw new ArgumentNullException(nameof(textWriter));
this.textWriter = textWriter;
}
public void SayHello()
{
textWriter.WriteText("Hello, world!");
}
}
C#public interface ITextWriter
{
void WriteText(string text);
}
C#using UnityEngine;
public class ConsoleTextWriter : ITextWriter
{
public void WriteText(string text)
{
Debug.Log(text);
}
}
C#Features of DI
The created classes have the dependencies as shown in the figure above.
We are going to lean about benefits gained from DI, but at this point, let’s take a look at the following two features of the DI-applied code.
- The
Someone
class depends on theITextWriter
interface - The
Someone
class receives an implementation of theITextWriter
interface in its constructor
They are generalized as follows:
- A class depends on an interface
- An implementation of the interface is provided from outside the class
These points teach us what DI is.
As mentioned in earlier, the principle of DI is, “program to an interface, not an implementation”. In the sample code, the Someone
class doesn’t use the ConsoleTextWriter
class directly, but uses it through the ITextWriter
interface. In addition, the Someone
class doesn’t know the existence of the ConsoleTextWriter
class at all because an instance of the ConsoleTextWriter
class is given as an implementation of the ITextWriter
interface when the Someone
class receives the instance via its constructor. This is programming to an interface, not an implementation (in other words, class).
The point of DI is, a class has a dependency with an interface, and the dependency is provided from outside the class. In other words, we need to inject dependencies into classes.
Constructor injection
The Someone
class receives an implementation of the ITextWriter
interface through its constructor. Although we have other methods where dependencies are given in places other than constructors, the method of providing dependencies through a constructor is especially called constructor injection.
With constructor injection, a guard clause confirms that a given implementation is not null
as shown in the Someone.cs
script. Then, store the implementation in a read-only field so that methods in the class can use it. This field has to be read-only so that it will never modified later.
Constructor injection is the most popular in methods of injection, and it is the ideal choice. This method should be used unless we have special reason.
Benefits gained from DI
You might be unable to realize any benefits by DI feel like it made the code redundant. In fact, it is completely meaningless to apply DI to this example.
However, you will realize power of DI with real applications. We are going to take a look at benefits gained from DI with more practical examples from the next section.
Applying DI
We are going to learn about benefits gained from loosely coupled code with DI.
At first, we will assume a certain scenario, and develop tightly coupled code based on it. Next, we will consider what problems there are and why they are problems. Then, we apply DI to the code in order to rebuild it into loosely coupled code. Finally, we will consider what benefits we gained by applying DI.
Program for Distribution of Login Bonus
Let’s assume that we develop a program that distributes a login bonus to a player in a mobile game development. The required process is as follows:
- Retrieve the current date when a player logs in
- Provide the player one of the following items depending on the retrieved date
- 5th: 10 coins
- 15th: 1 diamond
- 25th: 20 coins and 3 diamonds
Tightly Coupled Code
Create the following codes and attach the SampleManager
script to an object in the scene. Then, run it. Dependencies between the classes are also shown.
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
LoginService loginService = new();
loginService.Login();
}
}
C#using UnityEngine;
public class LoginService
{
public void Login()
{
DateBasedLoginBonusProvider bonusProvider = new();
var loginBonus = bonusProvider.GetLoginBonus();
if (loginBonus.Any())
Debug.Log($"Hello! You got a login bonus: \n{loginBonus}");
else
Debug.Log("Hello!");
}
}
C#using System;
public class DateBasedLoginBonusProvider
{
public LoginBonus GetLoginBonus()
{
var day = DateTime.Now.Date.Day;
return day switch
{
5 => new LoginBonus { Coin = 10, Diamond = 0 },
15 => new LoginBonus { Coin = 0, Diamond = 1 },
25 => new LoginBonus { Coin = 20, Diamond = 3 },
_ => new LoginBonus { Coin = 0, Diamond = 0 },
};
}
}
C#public record LoginBonus()
{
public int Coin { get; set; }
public int Diamond { get; set; }
public bool Any() => Coin > 0 || Diamond > 0;
}
C#The result varies depending on the date, but will be as follows:
Problems of Tightly Coupled Code
Now, we will consider problems in the system we created in the previous section from several perspectives.
Non-determinism and Unit Test
Let’s assume that we want to perform an unit test for the DateBasedBonusProvider
class. In this test, we make sure that a proper bonus is given depending on the retrieved date.
However, it is the date on which the code is executed that is retrieved with the current code. It means that we can only run the test for the date on which the test is run.
In order to perform tests for all patterns of login bonus, we need to make a change to a part of the class every time a test is performed.
public LoginBonus GetLoginBonus()
{
// var day = DateTime.Now.Date.Day;
var day = 5;
return day switch
{
5 => new LoginBonus { Coin = 10, Diamond = 0 },
15 => new LoginBonus { Coin = 0, Diamond = 1 },
25 => new LoginBonus { Coin = 20, Diamond = 3 },
_ => new LoginBonus { Coin = 0, Diamond = 0 },
};
}
C#This way is extremely inefficient. We can’t automate an unit test for the DateBasedLoginBonusProvider
class, and we are forced to rewrite a little bit of the code for each test case every time we need to redo a test.
In summary, it becomes difficult to perform the unit test if tightly coupled code has non-deterministic logic such as retrieving the current time or a random number.
Difficulty of Parallel Development
Let’s assume that you develop this login bonus system in your team, and you are responsible for the development of LoginService
class. What if the development of the LoginService
class has been done but the development of the DateBasedLoginBonusProvider
class is being delayed?
The LoginService
class depends on the DateBasedLoginBonusProvider
class, therefore, any tests for the former can’t be performed until the development of the latter is completed. It means the difficulty of parallel development in a team.
DateBasedLoginBonusProvider bonusProvider = new();
var loginBonus = bonusProvider.GetLoginBonus();
C#The system we developed is so simple that you might not realize that it is a problem. Then, what if the two classes are responsible for more complex processes? For example, what if the LoginService
class is responsible for showing the provided item on the screen after distributing a login bonus, and what if the DateBasedLoginBonusProvider
class doesn’t retrieve the current time from the player’s device but retrieve it from a remote server to prevent cheating?
The thing is that in this case, it takes time to develop each class, and it can be delayed due to some unexpected troubles. The situation where the development of the dependency need to be finished to be able to perform the unit test of a class is extremely undesirable. The ideal scenario is to be able to develop each class completely independently.
Impact by Implementation Replacement
As of now, different login bonuses are given depending on the login date. Let’s assume that we want to change this behavior to provide a constant bonus temporary regardless of the date because a certain event is held.
To achieve that, firstly we need to develop a new class that distributes a constant bonus for all dates. Then, with tightly coupled code, we have to make a change to the class that depends on the old class. For example, we need to rewrite the LoginService
class if we create the EventLoginBonusProvider
class newly.
public class EventLoginBonusProvider
{
public LoginBonus GetLoginBonus()
{
return new LoginBonus { Coin = 30, Diamond = 5 };
}
}
C#using UnityEngine;
public class LoginService
{
public void Login()
{
//DateBasedLoginBonusProvider bonusProvider = new();
EventLoginBonusProvider bonusProvider = new();
var loginBonus = bonusProvider.GetLoginBonus();
if (loginBonus.Any())
Debug.Log($"Hello! You got a login bonus: \n{loginBonus}");
else
Debug.Log("Hello!");
}
}
C#The problem is that a higher-level module is swayed by a lower-level module. A higher-level module has to be rewritten every time a lower-level module changes, and in this case, re-compiling is also required.
In the example, the LoginService
class is in a higher-level module and the DateBasedLoginBonusProvider
class is in the lower-level module. Let’s assume that each module is developed in different projects, and the size of the project that includes the LoginService
class is huge. Then, we have to rewrite the LoginService
class every time we change the logic for providing a login bonus, and as a result, re-compiling a huge project and a large scale file replacement are required. Particularly, we should avoid situations like this with functions that is more likely to change such as logic for loigin bonus distribution.
Decreased Reusability
What if we want to provide an extra bonus in addition to the usual bonus? We can achieve that by creating a new class and rewrite the LoginService
class.
using System;
public class ExCoinLoginBonusProvider
{
public LoginBonus GetLoginBonus()
{
var day = DateTime.Now.Date.Day;
return day switch
{
5 => new LoginBonus { Coin = 20, Diamond = 0 },
15 => new LoginBonus { Coin = 10, Diamond = 1 },
25 => new LoginBonus { Coin = 30, Diamond = 3 },
_ => new LoginBonus { Coin = 10, Diamond = 0 },
};
}
}
C#The ExCoinLoginBonusProvider
class provides 10 coins regardless of the date in addition to the usual bonus based on the date.
The problem is that the logic of “the usual bonus distribution” is hard coded. That logic was already written in the DateBasedLoginBonusProvider
; nevertheless that logic appears in the other class again.
The contents of the usual bonus can be change in the future. With the current implementation, we need to make changes to the two classes every time the usual bonus changes in the future. If there are more classes that depend on the logic of the usual bonus distribution, we need to make changes to all of them.
In this way, it is likely to be difficult to reuse existing logic with tightly coupled code. Not to reuse existing logic means greater costs for change.
Loosely Coupled Code
We developed the login bonus system with tightly coupled code, and considered the disadvantages. Now, we are going to rewrite the system with loosely coupled code, then, consider the benefits gained from DI. In this section, we start with rewriting the code.
Let’s preare the following classes. The dependencies between these classes are also shown.
This program provides the same result as that of the tightly coupled code version.
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
LoginService loginService = new(
new DateBasedLoginBonusProvider(
new DateTimeProvider())
);
loginService.Login();
}
}
C#using System;
using UnityEngine;
public class LoginService
{
private readonly ILoginBonusProvider loginBonusProvider;
public LoginService(ILoginBonusProvider loginBonusProvider)
{
this.loginBonusProvider = loginBonusProvider ??
throw new ArgumentNullException(nameof(loginBonusProvider));
}
public void Login()
{
var loginBonus = loginBonusProvider.GetLoginBonus();
if (loginBonus.Any())
Debug.Log($"Hello! You got a login bonus: \n{loginBonus}");
else
Debug.Log("Hello!");
}
}
C#public interface ILoginBonusProvider
{
LoginBonus GetLoginBonus();
}
C#using System;
public class DateBasedLoginBonusProvider : ILoginBonusProvider
{
private readonly IDateTimeProvider dateTimeProvider;
public DateBasedLoginBonusProvider(IDateTimeProvider dateTimeProvider)
{
this.dateTimeProvider = dateTimeProvider ??
throw new ArgumentNullException(nameof(dateTimeProvider));
}
public LoginBonus GetLoginBonus()
{
var day = dateTimeProvider.GetCurrentDateTime().Day;
return day switch
{
5 => new LoginBonus { Coin = 10, Diamond = 0 },
15 => new LoginBonus { Coin = 0, Diamond = 1 },
25 => new LoginBonus { Coin = 20, Diamond = 3 },
_ => new LoginBonus { Coin = 0, Diamond = 0 },
};
}
}
C#using System;
public interface IDateTimeProvider
{
DateTime GetCurrentDateTime();
}
C#using System;
public class DateTimeProvider : IDateTimeProvider
{
public DateTime GetCurrentDateTime()
{
return DateTime.Now;
}
}
C#Solution to the Problems by Loosely Coupled Code
We rewritten the system with loosely coupled code already. It is time to consider how the problems with tightly coupled code were resolved.
Unit Test with Objects Only for Tests
With loosely coupled code, we can perform unit tests by creating objects only for tests, even if the target class has non-deterministic behavior due to retrieving the current time or a random number, or even if developments of lower-level modules are delayed.
For example, we can create a test code for the DateBasedLoginBonusProvider
class that determines the given login bonus based on the current date as follows. As a side note, the test code is created with the Unity Test Runner.
using NUnit.Framework;
using System;
using System.Linq;
public class DateBasedProviderTest
{
[Test]
public void Day5thPasses()
{
LoginBonus correctBonus = new LoginBonus { Coin = 10, Diamond = 0 };
DateTime dateTime = new DateTime(2024, 1, 5);
foreach (var i in Enumerable.Range(0, 36))
{
DateBasedLoginBonusProvider dateBasedLoginBonusProvider = new(
new DateTimeProviderStub(dateTime.AddMonths(i)));
var loginBonus = dateBasedLoginBonusProvider.GetLoginBonus();
Assert.That(loginBonus == correctBonus);
}
}
private class DateTimeProviderStub : IDateTimeProvider
{
private readonly DateTime dateTime;
public DateTimeProviderStub(DateTime dateTime)
{
this.dateTime = dateTime;
}
public DateTime GetCurrentDateTime()
{
return dateTime;
}
}
}
C#The reason why we can perform such an unit test is that it is not the DateTimeProvider
class but the IDateTimeProvider
interface that the DateBasedLoginBonusProvider
class depends on. Since an implementation of that interface is provided via the constructor, any class that implements that interface can be used.
The DateTimeProvider
class, which is originally intended to be passed, is an implementation that returns the date on which the program is executed. It means that it has non-deterministic behavior, therefore, it is difficult to automate the unit test. Then, in the example, we created the DateTimeProviderStub
class so that we can freely set any DateTime
object passed to the DateBasedLoginBonusProvider
class. Like this, we can perform an unit test for a class that depends on another class with non-deterministic behavior by creating an object that implements the required interface and is used only for the test .
Additionally, the important thing is that the independence of classes improves because the classes depend on each other through interfaces, which enables parallel development perfectly. This time we learned the example of the test for the DateBasedLoginBonusProvider
class, but we will do the same thing for a test for the LoginService
class. Any class that implements the ILoginBonusProvider
interface, not limited to the DateBasedLoginBonusProvider
class, can be passed to the LoginSerive
class because the LoginService
class depends on that interface. Therefore, we can perform an unit test for the LoginService
class by creating a new class only for the test regardless of the DateBasedLoginBonusProvider
class. In other words, the two classes can be developed independently because there is no direct relationship between the two classes.
In summary, with loosely coupled code based on DI, we can create convenient implementations used only for tests and pass them to the the target classes by connecting classes with interfaces. As a result, we can try any test cases freely regardless of non-deterministic logic of lower-level modules. Also, we can perform tests for higher-level modules independently regardless of progress of lower-level modules.
Stub
An object used only for tests, which returns a constant value like the object created in the example, is called stub. There are other objects for tests, for example, mock, spy, fake, and so on.
Test Double
An object that can be an alternative implementation for the test target and is used only for test purposes, like stub, is called test double.
Test double is useful if the test target class has non-deterministic behavior or the cost of the development for the dependency is really high.
Replace an Implementation with Another One That Implements the Same Interface
In tightly coupled code, we have the problem that a change in a lower-level module has an impact on higher-level modules that depend on the lower-level one. For example, if we want to create a new class to change the logic of providing a login bonus, that change affects the LoginService
class because it depends on the logic to be changed.
In loosely coupled code, classes are connected through interfaces. Therefore, a class can be freely replaced with another one without making any changes to higher-level modules if those classes implement the same interface and the higher-level modules depend on it.
For example, if we want to hold an event and distribute a constant login bonus every day temporary, we can achieve that by creating a new class that implements the required interface and making a change to the SampleManager
class which is the highest-level class. Although the DateBasedLoignBonusProvider
class was originally injected into the LoignService
class as an implementation of the ILoginBonusProvider
class, the EventLoginBonusProvider
class is injected instead of it.
The point is that we don’t have to make any changes to the LoginService
class when we change the implementation the LoginService
uses. It means that changes of low-level modules have no impact on higher-level modules.
public class EventLoginBonusProvider : ILoginBonusProvider
{
public LoginBonus GetLoginBonus()
{
return new LoginBonus { Coin = 30, Diamond = 5 };
}
}
C#public class SampleManager : MonoBehaviour
{
private void Start()
{
//LoginService loginService = new(
// new DateBasedLoginBonusProvider(
// new DateTimeProvider())
// );
LoginService loginService = new(
new EventLoginBonusProvider());
loginService.Login();
}
}
C#Composition root
As shown in the dependency diagram, the SampleManager
class plays a role in creating instances of all classes except for itself and connecting them. That process is performed in the Start
method, and in this context, such place is called composition root. Composition root is usually placed at the entry point of an application, which should be the Start
method in Unity, for example. In composition root, all classes that create a system are instantiated, and all dependencies between the classes are resolved by mainly giving necessary objects to the constructors.
In DI-applied loosely coupled code, only composition root is rewritten when an implementation is replaced. Any other classes are affected.
Reuse of Code by Design Pettern
In loosely coupled code based on DI, we can reuse existing code when adding new functions if the system is well-designed. Furthermore, we don’t have to modify that existing code.
What if we want to provide an extra bonus in addition to the usual login bonus when an event is held? With tightly coupled code, we had to create a new class that includes the logic of the existing class as shown in the EXCoinLoginBonusProvider class. On the other hand, with loosely coupled code, we can create a new class that doesn’t know the logic of the existing class to which the new logic is added when adding new logic to the system.
using System;
public class LoginBonusProviderExCoinDecorator : ILoginBonusProvider
{
private ILoginBonusProvider baseLoginBonusProvider;
public LoginBonusProviderExCoinDecorator(ILoginBonusProvider baseLoginBonusProvider)
{
this.baseLoginBonusProvider = baseLoginBonusProvider
?? throw new ArgumentNullException(nameof(baseLoginBonusProvider));
}
public LoginBonus GetLoginBonus()
{
var baseLoginBonus = baseLoginBonusProvider.GetLoginBonus();
return baseLoginBonus with { Coin = baseLoginBonus.Coin + 10 };
}
}
C#using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
//LoginService loginService = new(
// new DateBasedLoginBonusProvider(
// new DateTimeProvider())
// );
LoginService loginService = new(
new LoginBonusProviderExCoinDecorator(
new DateBasedLoginBonusProvider(
new DateTimeProvider())));
loginService.Login();
}
}
C#The results for each date are shown.
The above code is a class that provides 10 coins in addition to the usual bonus. Let’s compare this example and the first result.
There are two important points.
- The
LoginBonusProviderExCoinDecorator
class uses an implementation of theILoginBonusProvider
interface, and that class is also an implementation of theILoginBonusProvider
interface - Neither the
LoginService
nor theDateBasedLoginBonusProvider
class is rewritten
Firstly, the LoginBonusProviderExCoinDecorator
class retrieves a login bonus from an implementation of the interface stored as a field and add coins to the retrieved bonus. Then, it returns that value. This process realizes the logic that adds extra coins to the usual bonus. Furthermore, this class doesn’t know what the usual bonus is at all.
Next, neither the DateBasedLoginBonusProvider
class that has the logic about the usual bonus nor the LoginService
class that uses the usual bonus logic was rewritten. Only the composition root was rewritten.
In summary, with loosely coupled code, we can develop a new class that has the required function by reusing existing code when adding a new function.
As a side note, there are some design patterns used to improve re-usability of code by DI. In the example, we used decorator pattern.
Decorator pattern
We used a design pattern called decorator pattern in the example. In decorator pattern, we develop a decorator class that depends on a certain interface and simultaneously implement that interface. Decorators are used to add another process to an implementation of an interface.
We already developed the decorator that returns a processed value made from the return value from the decoratee. Let’s take a look at another example of decorator. The following decorator checks a bonus value before returning it, and if too much bonus is given, it assumes that some errors are happening and throws an exception. The process of this decorator, “throwing an exception due to an abnormal value”, is also realized independently of the decoratee.
using System;
public class LoginBonusProviderValueCheckDecorator : ILoginBonusProvider
{
private readonly ILoginBonusProvider loginBonusProvider;
public LoginBonusProviderValueCheckDecorator(ILoginBonusProvider loginBonusProvider)
{
this.loginBonusProvider = loginBonusProvider
?? throw new ArgumentNullException(nameof(loginBonusProvider));
}
public LoginBonus GetLoginBonus()
{
var loginBonus = loginBonusProvider.GetLoginBonus();
if (loginBonus.Coin > 10000 || loginBonus.Diamond > 100)
{
throw new Exception("Too much bonus!");
}
else
{
return loginBonus;
}
}
}
C#Decorators and decoratees are independent!
The LoginBonusProviderExCoinDecorator
class doesn’t depend on the DateBasedLoginBonusProvider
class but the ILoginBonusProvider
interface. It means that the LoginBonusProviderExCoinDecodor
provides 10 coins in addition to “the login bonus given by the decoratee” rather than “the usual bonus”.
Because any class that implements the ILoginBonusProvider
interface can be given, the logic that “a constant bonus is provided regardless of the loign date, and in addition to that, extra coins are provided” can be realized by passing the EventLoginBonusProvider
class, for example. This is also realized by rewriting only the composition root.
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
LoginService loginService = new(
new LoginBonusProviderExCoinDecorator(
new EventLoginBonusProvider()));
loginService.Login();
}
}
C#Also, an unit test is performed easily because we can prepare an object used only for the test.
using NUnit.Framework;
public class ExCoinDecoratorTest
{
[Test]
public void ExCoinDecoratorTestSimplePasses()
{
LoginBonus correctBonus = new LoginBonus { Coin = 25, Diamond = 3 };
LoginBonusProviderExCoinDecorator exCoinDecorator = new(
new LoginBonusProviderStub(new LoginBonus { Coin = 15, Diamond = 3 }));
LoginBonus loginBonus = exCoinDecorator.GetLoginBonus();
Assert.That(correctBonus == loginBonus);
}
private class LoginBonusProviderStub : ILoginBonusProvider
{
private readonly LoginBonus loginBonus;
public LoginBonusProviderStub(LoginBonus loginBonus)
{
this.loginBonus = loginBonus;
}
public LoginBonus GetLoginBonus()
{
return loginBonus;
}
}
}
C#Benefits from Loosely Coupled Code
If we develop loosely coupled code with DI, in general, we will gain the following 5 benefits form it.
Benefit | Summary |
---|---|
Late Binding | Implementations can be replaced without re-compiling |
Extensibility | Less costs for extending code |
Parallel Development | Independent codes can be developed by a number of people in parallel |
Maintainability | Each class has its clear responsibility, so that is is easy to maintain |
Testability | An unit test can be performed for each class |
Late Binding
This is the feature that we can change the composition of a system without re-compiling at run-time. In loosely coupled code, dependencies are resolved only in composition root. Hence it is possible to change the composition of dependencies by rewriting a configuration file read by the system at run-time.
For example, the composition of the system can be changed based on values from a configuration file at run-time as follows. Assume that the isEventGoing
value is retrieved from a text file such as JSON and XML. In this case, we can change the composition of the system without re-compiling by rewriting the value in the configuration file based on whether any event is held.
var isEventGoing = true; // Get this from a configuration file
ILoginBonusProvider loginBonusProvider = isEventGoing ?
new EventLoginBonusProvider() :
new DateBasedLoginBonusProvider(
new DateTimeProvider());
LoginService loginService = new(loginBonusProvider);
loginService.Login();
C#Note that this feature, late binding, is used with reflection in practice, so that we can switch the composition more flexibly.
Extensibility
This is the feature that we can efficiently add new features and extend existing features. As we learned in “implementation replacement” section and “use of design pettern” section, in well-designed loosely coupled code, we don’t need to make changes to existing code when extending a system. We only have to develop a new class and rewrite composition root.
Liskov substitution principle
Liskov Substitution Principle (LSP) states that “an implementation must be able to replace another one if they implement the same interface”.
As we learned in “implementation replacement” section, since a class depends on interfaces in loosely coupled code, any class that implements the required interface can be given to a class that depends on a certain interface. Taking LSP into consideration, this feature, “replaceable”, is exactly important in terms of DI, and it suggests that classes that depend on interfaces mustn’t be aware of any specific implementations.
Open/Closed principle
Open/Closed Principle (OCP) states that “classes should be open for extension but closed for modification”.
A decorator as we learned in “design pattern” section, which adds new features without any changes to existing classes, is exactly following OCP.
Parallel Development
This is the feature that we can efficiently develop code in parallel. As we learned in “unit test” section, in loosely coupled code, we can perform tests for higher-level modules using objects only for test, even if the required lower-level modules haven’t been developed.
Maintainability
This is the feature that we can perform maintenance of code easily. In loosely coupled code, we aim to design a system with classes, each of which has a clear and simple responsibility. Consequently, it become easy to find out what must be changed for extension, and it also become easy to find out what is wrong when some bugs are found.
Single responsibility principle
Single Responsibility Principle (SRP) states that “every class should have a single reason to change”. In other words, every class should has a single responsibility, which leads to better maintainability mentioned here.
In DI, constructor injection is usually employed to inject dependencies. Then, the number of dependencies a class has can be a good guideline to decide if SRP is violated. Specifically, a class that has more than three dependencies can indicate SRP violation. In our example, only one dependency is injected, by the way.
Testability
This is the feature that unit test is available. As we learned in “unit test with objects only for test” section, in loosely coupled code, we can perform an unit test by creating an object used only for the test called test double, even if the dependency of the target class has non-deterministic behavior or haven’t developed yet.
Inversion of Dependency
In the tightly coupled code, the LoginService
class uses the DateBasedBonusProvider
class, i.e., the former depends on the latter. In other words, the higher-level class depends on the lower-level class directly.
Let’s take a look at the difference between the tightly and loosely coupled code. A part of each dependency of the tightly and loosely coupled code is shown below.
The point is the dependency between the LoginService
class and the DateBasedLoginBonusProvider
class. What changed when we rewrote the tightly coupled code to the loosely coupled code.
In the loosely coupled code, the LoginService
class uses the ILoginBonusProvider
interface, and the DateBasedLoginBonusProvider
class implements that interface. That is, the both higher-level class and the lower-level class depend on the interface.
Eventually, the difference between tightly coupled code and loosely coupled code is whether classes depend on other specific classes or abstractions which means interfaces. In DI, classes doesn’t depend on each other directly but are connected indirectly through interfaces, which leads to better extensibility and reusability of code.
Dependency inversion principle
Dependency Inversion Principle (DIP) is what DI mainly aims to achieve. DIP states that “higher-level modules shouldn’t depend on lower-level modules, instead, they should depend on abstractions”.
In the example, the LoginService
class corresponds to higher-level module while the DateBasedLoginBonusProvider
corresponds to lower-level module. Also, the ILoginBonusProvider
interface corresponds to abstraction. In the tightly coupled code, the higher-level module depends on the lower-level module, where the higher-level module is affected by changes of the lower-level module. On the other hand, both the higher-level module and lower-level module depend on the interface in loosely coupled code, so that they can exist independently.
Furthermore, DIP suggests that “the control for abstraction should be owned by the module using it”. That is, it is said that higher-level modules should own the control for interfaces in the ideal case.
In the example, it is actually the LoginService
class side that stipulates the required behavior of the ILoginBonusProvider
interface. The DateBasedLoginBonusProvider
class was developed to satisfy that requirement. Here, compared to the situation where classes are connected directly, it can be said that the lower-level module depends on the higher-level module. In this context, lower-level modules depend on higher-level modules in DI-applied loosely coupled code, which implies that dependencies are inverted.
Note:
DIP suggests that interfaces should belong to higher-level modules in the ideal case, however, there are some cases in which an interface can’t avoid being owned by a lower-level module in practice. The thing is that each class shouldn’t depend on other classes directly but interfaces.
Interface or Class?
We already learned that the main idea of DI is to connect classes through interfaces.
Now, does every class always have to depend on interfaces? In other words, is it impossible for a class to depend on other classes directly in DI anyway?
The answer is, NO. It is possible that a class uses another class directly in some cases; otherwise abstraction by interfaces would never finish. We need to decide if we should replace a dependency on a class with dependency on an interface depending on the situation.
One guideline for the decision is shown below. If any of the following criteria is true, a dependency on a class should be replaced with a dependency on an interface.
- The requirements for the dependency are more likely to change
- The requirements for the dependency change depending on the run-time environment
- The dependency has non-deterministic behavior: e.g. retrieving the current time or a random number
If the requirements for a class are more likely to change or the different features are required depending on the run-time environment, the benefits of abstraction outweigh the costs of defining a new interface, as the costs to adapt to changing requirements are lower.
In fact, in the system we developed, the method for determining the content of the distributed login bonus is more likely to change, and the method that determines the login bonus based on the date includes the logic that retrieves the current time, which is non-deterministic. Therefore, we created two abstractions, the ILoginBonusProvider
interface and the IDateTimeProvider
interface, so that the system acquired torelance to change and testability. Also, one example of dealing with the situation where the requirements change depending on the run-time environment is abstraction for adapting to the difference between operation systems such as Windows and Mac or the difference between data base management systems such as MySQL and Microsoft SQL Server.
On the other hand, the System.DateTime
struct and the LoginBonus
record will never change their behavior. That is because in the first place they don’t have any behavior that can change. They are just used to transfer data from a class to another one.
Firstly consider if it has non-deterministic behavior, secondly consider if the requirements are more likely to change in the future or change depending on the run-time environment in order to decide which should be used as the dependency, a class or an interface.
Summary
In the end, we summarize what we have learned about DI, reiterating the codes and diagrams that have appeared so far.
The Purpose and Benefits of DI
The purpose of DI is to design loosely coupled code by “programming to an interface, not a class”. Applying DI to code, the following benefits are gained.
Benefit | Summary |
---|---|
Late Binding | Implementations can be replaced without re-compiling |
Extensibility | Less costs for extending code |
Parallel Development | Independent codes can be developed by a number of people in parallel |
Maintainability | Each class has its clear responsibility, so that is is easy to maintain |
Testability | An unit test can be performed for each class |
How to Apply DI
The guideline of applying DI to design loosely coupled code is to connect classes through interfaces. A class shouldn’t use another class directly, but it should use an interface and the interface should be implemented by another class.
Dependencies are usually resolved by constructor injection.
using System;
using UnityEngine;
public class LoginService
{
private readonly ILoginBonusProvider loginBonusProvider;
public LoginService(ILoginBonusProvider loginBonusProvider)
{
this.loginBonusProvider = loginBonusProvider ??
throw new ArgumentNullException(nameof(loginBonusProvider));
}
public void Login()
{
var loginBonus = loginBonusProvider.GetLoginBonus();
if (loginBonus.Any())
Debug.Log($"Hello! You got a login bonus: \n{loginBonus}");
else
Debug.Log("Hello!");
}
}
C#The place at which dependencies are resolved is called composition root. Composition root is usually placed at the entry point of an application. In Unity, the Start()
method can be composition root.
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
LoginService loginService = new(
new DateBasedLoginBonusProvider(
new DateTimeProvider())
);
loginService.Login();
}
}
C#The Criteria of Abstraction
- he requirements for the dependency is more likely to change in the future
- The requirements for the dependency changes depending on the run-time environment
- The dependency has non-deterministic behavior: e.g. retrieving the current time or a random number
If any of the above criteria is true, the dependency should be replaced with an interface. Note that all dependencies have to be replaced with interfaces, specifically, classes like the LoginBonus
record which are only used to transfer data from a class to another one are depended on as they are.
public record LoginBonus()
{
public int Coin { get; set; }
public int Diamond { get; set; }
public bool Any() => Coin > 0 || Diamond > 0;
}
C#If a dependency has non-deterministic behavior, its requirements are more likely to change in the future, or its requirements change depending on the run-time environment, it should be replaced with an interface.
Exercise
Abstraction of the Method of Showing a Retrieved Login Bonus
Abstract the method of showing a retrieved login bonus and modify the LoginService
class that was already created. You will create a new interface that requires an implementation to receive a LoginBonus
object and show it on the screen, and you will also create an implementation of that interface that shows the content on a TextMeshPro
component.
Prepare a stub to perform tests with an arbitrary date
You can perform tests with any date you like by creating a stub for the IDateTimeProvider
interface.
using System;
using TMPro;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
[SerializeField]
private TextMeshProUGUI textMeshPro;
private void Start()
{
LoginService loginService = new(
new DateBasedLoginBonusProvider(
new DateTimerProviderStub()),
new TMProLoginBonusWriter(textMeshPro));
loginService.Login();
}
private class DateTimerProviderStub : IDateTimeProvider
{
public DateTime GetCurrentDateTime()
{
return new DateTime(2024, 8, 25);
}
}
}
C#Decorator That Keep a Log of Retrieved Login Bonuses
Create a decorator that outputs the content of a retrieved login bonus on the debug console as it is. That is, the content of a login bonus is shown on the TMPro component after it is processed while it is shown on the console as it is.
This decorator implements the same interface that you developed in the previous question.
Sample Answers
Abstraction for the method of showing a retrieved login bonus
using TMPro;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
[SerializeField]
private TextMeshProUGUI textMeshPro;
private void Start()
{
LoginService loginService = new(
new DateBasedLoginBonusProvider(
new DateTimeProvider()),
new TMProLoginBonusWriter(textMeshPro));
loginService.Login();
}
}
C#using System;
public class LoginService
{
private readonly ILoginBonusProvider loginBonusProvider;
private readonly ILoginBonusWriter loginBonusWriter;
public LoginService(ILoginBonusProvider loginBonusProvider, ILoginBonusWriter loginBonusWriter)
{
this.loginBonusProvider = loginBonusProvider ??
throw new ArgumentNullException(nameof(loginBonusProvider));
this.loginBonusWriter = loginBonusWriter ??
throw new ArgumentNullException(nameof(loginBonusWriter));
}
public void Login()
{
var loginBonus = loginBonusProvider.GetLoginBonus();
loginBonusWriter.WriteLoginBonus(loginBonus);
}
}
C#public interface ILoginBonusWriter
{
void WriteLoginBonus(LoginBonus loginBonus);
}
C#using System;
using TMPro;
public class TMProLoginBonusWriter : ILoginBonusWriter
{
private readonly TextMeshProUGUI textMeshPro;
public TMProLoginBonusWriter(TextMeshProUGUI textMeshPro)
{
this.textMeshPro = textMeshPro ??
throw new ArgumentNullException(nameof(textMeshPro));
}
public void WriteLoginBonus(LoginBonus loginBonus)
{
textMeshPro.text = loginBonus.Any() ?
$"Hello! You got a login bonus: \n{loginBonus}" :
"Hello!";
}
}
C#Decorator that keep a log of retrieved login bonuses
using System;
using TMPro;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
[SerializeField]
TextMeshProUGUI textMeshPro;
private void Start()
{
LoginService loginService = new(
new DateBasedLoginBonusProvider(
new DateTimeProvider()),
new LoginBonusWriterLogDecorator(
new TMProLoginBonusWriter(textMeshPro)));
loginService.Login();
}
}
C#using System;
using UnityEngine;
public class LoginBonusWriterLogDecorator : ILoginBonusWriter
{
private readonly ILoginBonusWriter loginBonusWriter;
public LoginBonusWriterLogDecorator(ILoginBonusWriter loginBonusWriter)
{
this.loginBonusWriter = loginBonusWriter
?? throw new ArgumentNullException(nameof(loginBonusWriter));
}
public void WriteLoginBonus(LoginBonus loginBonus)
{
Debug.Log($"From decorator: {loginBonus}");
loginBonusWriter.WriteLoginBonus(loginBonus);
}
}
C#
Comments