Home Best Practice iOS Part3 - Dependency injection
Post
Cancel

Best Practice iOS Part3 - Dependency injection

Why we need dependency injection

1. Dependency

Dependency is the connection between modules, take BookList module for example, it needs lots of services, such as:

  • Network for data retrieving
  • Track for event tracking
  • UserDefault for simple configuration
  • Notification Center for notification

let’s take Track as an example, maybe we are now using AppCenter as our tracking platform (it can be newrelic or any other platforms), and have codes in our project like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public struct AppCenterTracker {
    public func logEvent(_ name: String, info: [String: String] = [:]) {
        //
    }
}

// used in other module such as BookList
struct BookListViewModel {
    private let tracker = AppCenterTracker()

    func loadBookList() async throws {
        tracker.logEvent("Load book list")
    }
}

The weakness of concreting tracker with specific implementation is obviously and it will be painfull if we change our tracking platform from AppCenter to Newrelic. so usually we use protocol oriented programming in Swift, here is the example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public protocol TrackingAPI {
    func logEvent(_ name: String, info: [String: String])
}

public struct AppCenterTracker: TrackingAPI {
    public func logEvent(_ name: String, info: [String: String] = [:]) {
        //
    }
}

struct BookListViewModel {
    private let tracker: TrackingAPI

    init(_ tracker: TrackingAPI) {
        self.tracker = tracker
    }

    func loadBookList() async throws {
        tracker.logEvent("Load book list", info: [:])
    }
}

There are two types of dependency injection:

  • initializer based
  • factory based

If there are only couple of dependencies needed, just like the tracker above, we can use the initializer based injection, and use factory based injection in more dependencies case.

2. Benefits of dependency injection

Let’s recap the benefits of dependency injection here:

1. De-coupling

Using protocol instead of concrete implementation to de-couple the service user from service provider, will result in a flexibility of potential change

2. Testable

Mocked service provider can be easly used in unit test

How to create a dependency injection factory

Let’s create a simple dependency injection factory step by step.

1. Interface

first we need to figure out the interface, and it is obviously simple.

1
2
3
4
public protocol ServiceAPI {
    func registerService<T>(_ type: T.Type, _ builder: @escaping () -> T)
    func getService<T>(_ type: T.Type) -> T
}

2. Factory or singleton

There can be two types of service provider we can use, Factory or Singleton. The servcie we get from a Singleton provoder is always the same instance while we get a new instance of service everytime from a Factory provider.

It’s usually a Singleton provider we needed, and we can easily extend our service by adding a new parameter in registerService like this.

1
2
3
4
enum ServiceType {
    case factory, singleton
}
func registerService<T>(_ type: T.Type, _ service: ServiceType = .singleton, _ builder: @escaping () -> T)

Now we just create a singleton service.

3. Public and private

Sometimes we need to share some open services among modules and hide the services only used within a module internally, just like this.

DI Tree

Other modules such Module2 and Module3 can only get access the public services rather than private services provided by Module1.

While within Module1, as a up-forward looking up, both private services and public services can be accessed.

4. Practice

  1. Create Swift Package and design interface
  1. How to save key and closure
  1. Tree
This post is licensed under CC BY 4.0 by the author.