Dependency Injection for dummies?

All you need to know about Dependency Injection. What is DI and how to achieve it?

brian
5 min readFeb 8, 2020
The idea comes from https://stackify.com/dependency-injection/

Once upon a time, Object-Oriented Programming (OOP) was found with a bunch of benefits. That was the huge change in the Software Engineering industry, giving ppl, who are software engineers, the different ways to think of how to structure the project and bring real-life objects into the software.

Then SOLID was found which is known as design principles for OOP intended to make software designs more understandable, flexible and maintainable. And till now SOLID is still applied in every project/software that is OOP.

“D” is one of the most popular ones when talking about SOLID. It is Dependency Inversion.

One should depend upon abstractions, not concretions.

Inversion of Control (IoC) is a design pattern used to represent Dependency Inversion in real software.

Then Dependency Injection becomes a key player, which is mentioned day by day, in all conferences, from dev to dev,… So what is it?

What is Dependency Injection?

Basically, Dependency Injection (DI) is one way to achieve the IoC. Or we can say, it is the real-life implementation of IoC. Instead of depending on a concrete implementation (class, struct), we depend on interfaces (protocol) and all dependencies should be given from outside. It achieves by decoupling the usage of an object from its creation.

// Not preferred
class DummyClass {
var dependency = Dependency()
}
let dummy = DummyClass()
// Preferred
class DummyClass {
var dependency: DependencyProtocol
init(dependency: DependencyProtocol) {
self.dependency = dependency
}
}
let dependency = Dependency()
let dummy = DummyClass(dependency: dependency)

The dependencies are broken down between higher and lower level classes. If you do that, both classes depend on the interface and no longer on each other.

Here is the saying from my own experience.

Dependency Injection is like when u need to perform something u don’t know exactly, just ask (inject) the person (dependency) who knows it better.

Dependency Injection also helps you follow Single Responsibility Principle

Roles in DI

  • Client: means your implementing component (class).
  • Service: means your dependency/dependencies (another class).
  • Interface: means the interface that is used to bridge between your Client and Service.

Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern. A client who wants to call some services should not have to know how to construct those services.

Types of dependencies

Basically, we have 3 types of DI, which are:

  • Initializer-based: injected via init methods
  • Property-based: injected via assigning instance for a property
  • Method-based: injected via method/func called

For instance, we’re now having DataRepository that needs LocalDataSource and RemoteDataSource to execute. Local and Remote data sources are dependencies of Repo. Accordingly, we also have concrete classes that conform to these protocols: LocalDataSourceImp , RemoteDataSourceImp and DataRepositoryImp

Oops!! I just realized the direction of pure black arrows should be reverted 😢

NOTE: Interface for these protocols can be any for demo purposes.

protocol LocalDataSource { 
// interface
...
}
protocol RemoteDataSource {
// interface
...
}
protocol DataRepository {
var localDataSource: LocalDataSource { get }
var remoteDataSource: RemoteDataSource { get }
// interface
...
}
/// Concrete class for LocalDataSource
class LocalDataSourceImp: LocalDataSource { // implementation }
/// Concrete class for RemoteDataSource
class RemoteDataSourceImp: RemoteDataSource { // implementation }
/// Concrete class for DataRepository
class DataRepositoryImp: DataRepository { // implementation }

Initializer-based

Dependencies are injected via init methods.

let localDataSource = LocalDataSourceImp()
let remoteDataSouce = RemoteDataSourceImp()
let repo = DataRepositoryImp(localDS: localDataSource,
remoteDS: remoteDataSouce)

Property-based

Dependencies are injected via property assignment.

let localDataSource = LocalDataSourceImp()
let remoteDataSouce = RemoteDataSourceImp()
let repo = DataRepositoryImp()
repo.localDataSource = localDataSource
repo.remoteDataSource = remoteDataSouce

Method-based

Inject dependencies via a method call. Then dependencies are only injected when it’s really needed.

let localDataSource = LocalDataSourceImp()
let repo = DataRepositoryImp()
repo.fetchData(from: localDataSource)

Why DI?

It is necessary to understand why we need DI?

From my experience, I love DI because of the ability to build the blueprint for my architecture. I can always prepare the architecture before implementation, think of separate components and make them totally single responsibility, how to connect them together, draw the diagram to visualize the architecture. From that, I can understand my project better and improve architecture accordingly.

Otherwise, DI comes along with a bunch of benefits, such as:

  • Make your code testable. To make sure your code stable, improve test coverage and protect your code from changes. YES!!! We can achieve that with DI. My favorite saying “The code that can’t be tested is the code easily to be broken”. So it’s easier to test your class with DI. Your class/component depend on interfaces/protocols so that you can easily mock the dependencies by writing mock class that conforms to interfaces. Then inject them to your class and unit test it.
class MockLocalDataSouce: LocalDataSource {
func fetchData() -> [Any] {
// return fake data
return ["haha", "hihi"]
}
}
let localDataSource = MockLocalDatSouce()
let repo = DataRepositoryImp()
repo.localDataSource = localDataSource
repo.fetchDataFromLocal()
class DataRepositoryImpTests: XCTestCase {
var sut: DataRepositoryImp!
var localDS: MockLocalDataSouce!

func setUp() {
localDS = MockLocalDataSouce()
sut = DataRepositoryImp()
sut.localDataSource = localDS
}
func tearDown() { // Don't forget to tear down. Reasonale:
sut = nil
localDS = nil
}
func testFetchDataFromLocal() {
let results = sut.fetchDataFromLocal()
XCTAssertEqual(results, ["haha", "hihi"])
}
}
  • Reduce coupling between your class with others. Now all dependencies are injected into your class. Each component/class does its own responsibility.
  • Then make the code easier to maintain in the future
  • And it makes your class very flexible. It only depends on protocol, so it’s easy to switch the implementation of dependency when requirements change or behavior/logic needs to be updated.
class NewLocalDataSourceImp: LocalDataSource { // implementation }let localDataSource = NewLocalDataSourceImp()
let repo = DataRepositoryImp()
repo.localDataSource = localDataSource
repo.fetchDataFromLocal()
  • Minimize the impact on a high-lever component when changing low-level components it depends on. (cuz it doesn’t depend on the low-level component itself but the interface). Then it makes your component more reusable.

DI enemy

We should know that the enemy of DI is Singleton which is the most favorite pattern in iOS.

So could we make Singleton DI?

The answer is YES :) Easy… make your Singleton conform to a protocol and inject it into your component. Then u know what to do :3

Wohoo! My first English blog ever ❤ ❤ ❤. Thanks for reading through itttt thoo

The next blog post will be more detailed on how we should manage our dependencies. How to inject multiple services into a client more efficiency.

--

--

brian
brian

Written by brian

a software engineer who does software engineering

Responses (2)