Dependency Injection Strategies in Swift

Simple address: decoupling strategy for dependency injection in Swift

Today we will delve into dependency injection in Swift, one of the most important techniques in software development and a frequently used concept in many programming languages. In particular, we’ll explore the policies/patterns that can be used, including the Service Locator pattern in Swift.

The intent behind dependency injection is to decouple by having one object provide the dependencies of another. It is useful for providing different configurations for modules and is especially useful for providing mock dependencies for (unit) test modules and/or applications. We will use the term dependency injection in this article only as a design pattern to describe how one object provides dependencies for other objects. Don’t confuse dependency injection with frameworks or libraries that help you inject dependencies.

Why should I use it?

Dependency injection helps us make our components less coupled and more reusable in different environments. In general, it is a form of separation of concerns because it uses the separation of dependencies from initialization and configuration. To achieve this goal, we can use different techniques to inject dependencies into our modules.

As mentioned above, a very important aspect of dependency injection is that it makes our code easier to test. We can inject mock instances for the class/module dependencies we want to test. This allows us to focus our tests on the unit test code in the module and ensure that this part works as expected without the ambiguous side effect of making the test failure unclear because one of the dependencies does not meet expectations. These dependencies should be tested on their own to make it easier to find true errors and speed up the development workflow.

We described our testing strategy in a previous article. If you want to learn more about our test setup, be sure to read the Testing Mobile Apps article.

In addition, dependency injection allows us to bypass one of the most common mistakes in software development: the abuse of singletons in the code base. If you want to learn more about why Singletons Are Bad, take a look at Are Singletons Bad or Singletons Are Evil.

Different strategies to do Dependency Injection in Swift

We use dependency injection in many ways in Swift, and most of the principles apply to other programming languages as well, even though in most other environments (especially in the Java community) people tend to use special dependency injection frameworks to do the heavy lifting for them.

Yes, there is also a Dependency Injection framework in Swift. The most popular is Swinject, with rich features and a large community. But today we’re going to show you some simple techniques for injecting dependencies without introducing another huge third-party framework.

To see how this technique can be used in practice, we can look at a short example of using a Service to fetch data using a Repository object.

class BasketService {
    private let repository: Repository<Article>

    init(repository: Repository<Article>) {
        self.repository = repository
    }

    func addAllArticles(to basket: Basket) {
        let allArticles = repository.getAll()
        basket.articles.append(contentsOf: allArticles)
    }
}

Copy the code

We’ve injected a repository into BasketService so that our service doesn’t need to know how to provide the goods it uses. They can come from a Repository that retrieves data from a local JSON file, retrieves it from a local database, or even retrieves it from a server.

This allows us to use our BasketService in different environments. If we want to write unit tests for this class, we can inject a repository of our simulations and make our tests more predictable by using the same test data all the time.


class BasketServiceTests: XCTestCase {
    func testAddAllArticles(a) {
        let expectedArticle = Article(title: "Article 1")
        let mockRepository = MockRepository<Article>(objects: [expectedArticle])
        let basketService = BasketService(repository: mockRepository)
        let basket = Basket()

        basketService.addAllArticles(to: basket)

        XCTAssertEqual(basket.articles.count.1)
        XCTAssertEqual(basket.articles[0], expectedArticle)
    }
}

Copy the code

Well, we can put mock goods into mock Repository, inject the Mock Repository into Service to test whether the service works as it does, and add the test goods to the shopping bag.

Property-based Dependency Injection

Ok, initializer-based dependency injection seems to be a good solution, but there are cases where it is not suitable, such as in ViewControllers, where using the initializer is not so easy, Especially if you use XIB or storyboard files.

We all know about this error message and the annoying solution that Xcode provides. But how do you use dependency injection without overriding all default initializers?

This is where property-based Dependency Injection comes into play. We assign module attributes after initialization.

Let’s take a look at our BasketViewController, which has our BasketService class as a dependency.

class BasketViewController: UIViewController {
    var basketService: BasketService! = nil
}

let basketViewController = BasketViewController()
basketViewController.basketService = BasketService(a)Copy the code

We were forced to force unpack an optional property here to ensure that the program crashed when the basketService property was not injected correctly earlier.

If we want to get rid of forced unpacking of the optional attribute, we can provide default values when declaring the attribute.

class BasketViewController: UIViewController {
    var basketService: BasketService = BasketService()}Copy the code

Property-based Dependency Injection also has some disadvantages: first, our class needs to handle dynamic changes to dependencies; Second, we need to make properties externally accessible and changeable, and we can no longer define them as private.

Factory Classes

Both solutions we’ve seen so far shift the responsibility for injecting dependencies to the class that creates the new module. This may be better than hard-coding dependencies into modules, but shifting this responsibility to your own type is usually a better solution. It also ensures that we don’t need to write duplicate code in our code for the initialization module.

These types handle the creation of a class and set all its dependencies. These so-called Factory classes also solve the problem of passing dependencies. We had to do this with all the other solutions before, and it can get messy if your class has a large number of dependencies, or if you have multiple dependency levels (such as the example above) : BasketViewController – > BasketService – > Repository.

Let’s take a look at Basket’s Factory.

protocol BasketFactory {
    func makeBasketService(a) -> BasketService
    func makeBasketViewController(a) -> BasketViewController
}

Copy the code

By making a factory a protocol, we can have multiple implementations, such as a special factory for test cases.

Factory-based Dependency Injection works closely with the solution we have seen before, allowing us to mix and match different technologies, but how do we keep the interface for creating instances of the class clear?

There’s no better way to explain it than to show you an example:

class DefaultBasketFactory: BasketFactory {

    func makeBasketService(a) -> BasketService {
        let repository = makeArticleRepository()
        return BasketService(repository: repository)
    }

    func makeBasketViewController(a) -> BasketViewController {
        let basketViewController = BasketViewController()
        basketViewController.basketService = makeBasketService()
        return basketViewController
    }

    // MARK: Private factory methods
    private func makeArticleRepository(a) -> Repository<Article> {
        return DatabaseRepository()}}Copy the code

Our DefaultBasketFactory implements the protocol defined above and has both public factory methods and private methods. Factory methods can and should use other factory methods in the class to create lower dependencies.

The above example is a good example of how we can combine initializer-based and property-based Dependency Injection with the advantage of an elegant and simple interface to create dependencies.

To initialize our Instance of BasketViewController, we write a single line of self-explanatory code.

let basketViewController = factory.makeBasketViewController()

Copy the code

The Service Locator Pattern

Based on the solutions we have seen so far, we will use the so-called Service Locator design pattern to build a more generic, flexible solution. Let’s start with the relevant entities that define Service Locator:

  • Container: Stores the configuration used to create instances of registered types.
  • Resolver: by useContainerThe configuration creates instances of classes that address the actual implementation of a type.
  • ServiceFactory: A generic factory for creating instances of generic types.

Resolver

We first define a Resolver protocol for the Service Locator Pattern. It is a simple protocol with only one method for creating instances that match the ServiceType type passed.

protocol Resolver {
    func resolve<ServiceType>(_ type: ServiceType.Type) -> ServiceType
}


Copy the code

We can use objects that conform to this protocol in the following ways:

let resolver: Resolver=...let instance = resolver.resolve(SomeProtocol.self)

Copy the code

ServiceFactory

Next, we define the ServiceFactory protocol using the association type ServiceType. Our factory will create type instances that conform to the ServiceType protocol.

protocol ServiceFactory {
    associatedtype ServiceType
    func resolve(_ resolver: Resolver) -> ServiceType
}


Copy the code

This looks very similar to the Resolver protocol we’ve seen before, but it introduces additional association types to add more type safety to our implementation.

Let’s define the first type that conforms to this protocol, BasicServiceFactory. This factory class uses the injected factory method to generate instances of classes/structures of type ServiceType. By passing the Resolver as a parameter to the factory closure, we can use it to create the lower-level dependencies needed to create instances of the type.

struct BasicServiceFactory<ServiceType> :ServiceFactory {
    private let factory: (Resolver) - >ServiceType

    init(_ type: ServiceType.Type, factory: @escaping (Resolver) - >ServiceType) {
        self.factory = factory
    }

    func resolve(_ resolver: Resolver) -> ServiceType {
        return factory(resolver)
    }
}

Copy the code

This BasicServiceFactory structure can be used in isolation and is more generic than the Factory class we saw above. But we’re not done yet. The last thing we need to implement the Service Locator Pattern in Swift is a Container.

Container

Before we start writing the Container class let’s repeat what it should do for us:

  • It should allow us to register new factories for a certain type
  • It should storeServiceFactoryThe instance
  • It should be used for any storage typeResolver

To be able to store instances of the ServiceFactory class in a type-safe manner, we need to be able to implement mutable parameterized generics in Swift. This is not yet possible in Swift, but it is part of the GenericsManifesto that will be added to Swift in a future release. In the meantime, we need to use a type erase version called AnyServiceFactory to eliminate generic types.

For the sake of simplicity, we won’t show you the implementation, but if you’re interested, check out the link below.

struct Container: Resolver {

    let factories: [AnyServiceFactory]

    init() {
        self.factories = []
    }

    private init(factories: [AnyServiceFactory]) {
        self.factories = factories
    }

    ...

Copy the code

We define a Container as a factory that acts as a structure for the Resolver parser and stores erased types. Next, we’ll add the code to register the new type in the factory.

// MARK: Register
    func register<T>(_ type: T.Type, instance: T) -> Container {
        return register(type) { _ in instance }
    }

    func register<ServiceType>(_ type: ServiceType.Type._ factory: @escaping (Resolver) -> ServiceType) - >Container {
        assert(! factories.contains(where: {$0.supports(type) }))

        let newFactory = BasicServiceFactory<ServiceType>(type, factory: { resolver in
            factory(resolver)
        })
        return .init(factories: factories + [AnyServiceFactory(newFactory)])
    }

    .

Copy the code

The first method allows us to register an instance of a class for ServiceTyp. This is especially useful for injecting Singleton (like) classes such as UserDefaults and bundles.

The second and even more important method is to create a new factory and return a new non-container, including the new factory.

The last missing piece is actually conforming to our Resolver protocol and using our stored factory resolution instance.

// MARK: Resolver
    func resolve<ServiceType>(_ type: ServiceType.Type) -> ServiceType {
        guard let factory = factories.first(where: {$0.supports(type) }) else {
            fatalError("No suitable factory found")}return factory.resolve(self)}Copy the code

We use a GUARD statement to check if it contains a factory that resolves dependencies, otherwise a FATAL error will be thrown. Finally, we return the first instance of factory creation that supports this type.

Usage of Service Locators

Let’s start with our Basket example before and define a container for all basket related classes:

let basketContainer = Container()
    .register(Bundle.self, instance: Bundle.main)
    .register(Repository<Article>.self) { _ in DatabaseRepository() }
    .register(BasketService.self) { resolver in
        let repository = resolver.resolve(Repository<Article>.self)
        return BasketService(repository: repository)
    }
    .register(BasketViewController.self) { resolver in
        let basketViewController = BasketViewController()
        basketViewController.basketService = resolver.resolve(BasketService.self)
        return basketViewController
    }


Copy the code

This shows the power and elegance of our super simple solution. We can use the chain register method to store all factories while mixing up all the different dependency injection techniques we saw earlier.

Last, but not least, the interface we use to create instances is kept simple and elegant.


let basketViewController = basketContainer.resolve(BasketViewController.self)
Copy the code

Conclusion

We have seen different techniques for dependency injection used in Swift. More importantly, we’ve seen that you don’t have to decide on a single solution. They can be mixed to get the combined advantage of each technology. To take everything to the next level, we have introduced the Factory class and the more general ServiceLocator pattern solution in Swift. This can be improved by adding additional support for multiple parameters or by adding more type safety when Swift introduces mutable parameter generics.

For the sake of simplicity, we ignore things like scopes, dynamic dependencies, and circular dependencies all of which are solvable but beyond the scope of this article. You can be in DependencyInjectionPlayground view all content in this show.

One last personal note

Object and TyphoonSwift have TyphoonSwift, Swinject, Secan, and Needle Modular development of iOS project Files using Object source code analysis of iOS component communication scheme Swinject source code analysis