Dependency Injection with the Cake Pattern in Swift

Dec 6, 2015 | 10 min read

Dependency injection can be a useful tool for creating modular, loosely coupled, and easily testable code. At its core, it’s a simple concept. Objects should have their dependencies passed to them instead of creating their own internally. Unfortunately, simple practices like constructor or property injection can become more difficult and error prone as applications grow larger.

Anyone who has done any enterprise development in Java is probably familiar with dependency injection frameworks like Spring or Guice. These frameworks are fantastic tools for building even the largest of applications, but that comes at the cost of a high learning curve.

There is a middle ground. Scala developers, for example, often favor using language constructs instead of external frameworks where possible. The Cake Pattern is a common solution for dependency injection. Swift isn’t as powerful or expressive as Scala, but protocols and protocol extensions offer enough flexibility to get most of the benefits of the Cake Pattern.

Declaring the Services

For this example, suppose that we are building an application that needs to be able to authenticate users by username and password. To implement this functionality we will break the task into two pieces: a UserRepository which is responsible for persisting and retrieving user information from the data store, and a UserService which performs the business logic with the data veded from the repository. These will be the two services that we want to make available to the rest of the application.

public struct User {
    public let username: String
    public let password: String
}

public protocol UserRepository {
    func findByUsername(username: String) -> User?
}

public protocol UserService {
    func authenticate(username: String, password: String) -> User?
}

The Repository design pattern might not be well known by iOS developers but it is very common in enterprise software development. By splitting the responsibility between the repository and the service, each piece can be developed, tested, and updated independently.

Because the functionality is exposed only through public protocols, the actual implementations can change without impacting the rest of the application. For example, you could start off implementing the data persistence with Core Data but then switch to using Realm without making any other changes to the application.

Adding the Components

The Cake Pattern doesn’t use the services directly. Instead it wraps them in components. These components will then be used to instantiate the actual dependencies later.

public protocol UserRepositoryComponent {
    static var userRepository: UserRepository { get }
    static func createUserRepository() -> UserRepository
}

public protocol UserServiceComponent {
    static var userService: UserService { get }
    static func createUserService() -> UserService
}

Each component is a protocol that requires its implementing type to include a property to access its associated service as well as a helper method to create and configure the concrete dependency. It’s important to note that we still haven’t referenced any concrete types. The userRepository and userService properties can be any type that satisfies the contract of its respective protocol.

Building the Layers

Now that the protocols for the dependencies and components have been declared, it’s time to start building an actual implementation. For testing we might create stub implementations to test other features in isolation. Early in development we might create a simple implementation to jumpstart development or prototype a new technology. By programming to the protocol rather than a concrete type we gain maximum flexibility to explore and change implementations as the application develops.

Let’s start with creating a user repository. The protocol only requires a single method to look up a user by username. Depending on the requirements of the application, this could be satisfied by an in-memory store, a plist file, NSUserDefaults entries, a SQLite database, Core Data, alternate databases like Realm, or even REST service API. It doesn’t matter as long as the protocol is implemented.

private struct StaticUserRepository: UserRepository {
    private static let users = [
        "admin": User(username: "admin", password: "admin"),
        "alice": User(username: "alice", password: "123")
    ]
  
    private func findByUsername(username: String) -> User? {
        return StaticUserRepository.users[username]
    }
}

public protocol StaticUserRepositoryComponent: UserRepositoryComponent {}

extension StaticUserRepositoryComponent {
    public static func createUserRepository() -> UserRepository {
        return StaticUserRepository()
    }
}

This implementation of UserRepository vends users from a static dictionary of username to user mappings. This is a great way to get started building users into the application early on without having to wait for a full user management system to be developed. Even after the final repository is created, this simple version will be useful for writing tests that can bypass expensive databases or network calls.

Next we need to implement a UserRepositoryComponent to vend the new repository. The first step is to create a new protocol that inherits from UserRepositoryComponent but doesn’t add any additional requirements. We can then use a protocol extension on the new protocol to implement the required service method.

Notice that our StaticUserRepository isn’t public. Only the protocol extension needs to know its actual type to create it. The rest of the application can only access it through the public UserRepository protocol. This increases the robustness of the app and prevents “cheating” by making it impossible to access implementation details.

Now that the UserRepository has a concrete implentation, we can move on to implementing the UserService protocol. Because the repository implementation has taken care of all of the storage details for the user objects, the implementation of the service will be fairly straightforward. In fact, it’s possible that this first implementation might almost be good enough for the final app.

private struct DefaultUserService: UserService {
    private let userRepository: UserRepository
    
    private init(userRepository: UserRepository) {
        self.userRepository = userRepository
    }
  
    private func authenticate(username: String, password: String) -> User? {
        guard let user = userRepository.findByUsername(username) else {
            print("No user found with username: \(username)")
            return nil
        }
        guard user.password == password else {
            print("Username and password do not match for username: \(username)")
            return nil
        }
        return user
    }
}

public protocol DefaultUserServiceComponent: UserServiceComponent {}

extension DefaultUserServiceComponent where Self: UserRepositoryComponent {
    public static func createUserService() -> UserService {
        return DefaultUserService(userRepository: userRepository)
    }
}

The implementation of our UserService is similar to the implementation of UserRepository. A concrete type is created for the service, the component protocol is specialized, and then a protocol extension is created to instantiate the service. However, this time it’s a little more complicated.

The complication arises because DefaultUserService has a dependency on a UserRepository implementation to query the data store for users. This is easy to solve: just use standard constructor inject to pass the user repository on construction. However, this is a private struct that can only be instantiated by its component, and the component’s createUserService method doesn’t take any parameters.

This predicament is solved with Self type constraints on the DefaultUserService protocol. The WWDC session on Protocol Oriented Programming showed how to use Self type constraints to limit the applicability of a protocol extension method to types that satisfied specific conditions. This is different. Here we are using a Self type constraint to require any type that implements DefaultUserServiceComponent to also implement UserRepositoryComponent. And since UserRepositoryComponent requires any type that implements it to provide a userRepository property. That means that the component can simply access its implementations userRepository property and use it to construct the service.

Mixing the Cake

It took a while, but we have now created a set of protocols to define the public API for a user repository and service, components to create those objects, and concrete implmentations. Now all that’s left is to mix the pieces together and bake our dependency cake.

public enum AppContext: StaticUserRepositoryComponent, DefaultUserServiceComponent {
    public static let userRepository = AppContext.createUserRepository()
    public static let userService = AppContext.createUserService()
}

Here we used an enum without any cases to bring the pieces together. This was just a convenient way to create a concrete type to collect the dependencies that can’t be instantiated itself.

In the application code, you will want to use the component’s create method as we have done here to properly construct and configure the service object. For testing, however, you might find it more convenient to ignore the helper method and instead set a mock or stub object directly.

By declaring the dependencies as static constants, the Swift compiler will ensure that the properties are created lazily and in a thread safe way when they are first accessed. The lazy instantiation is particularly important because it allows all of the application dependencies to be gathered together without imposing an unnecessary delay at application launch.

The magic of the Cake pattern is that the Swift’s type safety will ensure that all dependencies are properly created.

  • If either userRepository or userService isn’t created, the compiler will flag the missing property requirement of the protocol.
  • If a base UserRepositoryComponent or UserServiceComponent is mixed in, the compiler will flag the missing implementation of the corresponding create function.
  • If the context implements DefaultUserServiceComponent but doesn’t also implement a UserRepositoryComponent, the compiler will flag the missing Self type requirement.

If compilation succeeds, then the application dependencies are available and ready for use.

if let user = AppContext.userService.authenticate("alice", password: "123") {
  print("Thank you for logging in, \(user.username)!")
} else {
  print("Please check your username and password")
}

The AppContext that was just created resembles the global context that a dependency injection framework like Spring would build, but we were able to build it using only standard Swift features.

More than Just One Slice

But wait, you might object. Isn’t this just a singleton and aren’t singletons bad?

Yes, the AppContext that we created is effectively a singleton, but that’s not necessarily a bad thing. A traditional singleton can be hard to test and harder to customize. A context built with the Cake Pattern is composed of regular objects that can be fully unit tested in isolation. More importantly, however, different implementations can be mixed together to provide just the functionality you need.

If you’re not convinced and would still prefer to have an object that you can pass around your application, that’s easy too. One approach would be to modify the component protocols to require an instance property instead of a static one. This approach, however, will soon run up against some of the limitations of Swift’s memory safety requirements. For example, you could make the context class immutable, but then all of the dependencies would have to be constructed at initialization. Alternatively, you could make all of the dependencies lazy, but this introduces performance costs and Swift does not guarantee that lazy instance properties are generated in a thread safe way.

A better solution would be to continue to mix global context types, but then create a new concrete class that delegates to the appropriate singleton context.

public typealias ContextType = protocol<UserRepositoryComponent, UserServiceComponent>

public struct Context {
    private let type: ContextType.Type
    
    public var userRepository: UserRepository {
        return type.userRepository
    }
    
    public var userService: UserService {
        return type.userService
    }
    
    public init(type: ContextType.Type) {
        self.type = type
    }
}

The typealias used here just gathers up all of the components we want the global context to implement. In this example we use all of them. However, you could also pick out multiple, possibly even overlapping, subsets of components to build different context objects that only exposed certain services. This could provide additional security by hiding services from parts of the app that don’t need to interact with them.

The context object itself is very simple. Just construct it with the type of “cake” it is proxying, and it will forward requests to that type.

let context = Context(type: AppContext.self)

if let user = context.userService.authenticate("alice", password: "123") {
    print("Thank you for logging in, \(user.username)!")
} else {
    print("Please check your username and password")
}

Conclusion

The Cake Pattern fully embraces Swift’s Protocol Oriented Programming model using only standard Swift language constructs. It doesn’t add any requirements of its own, so it should be just as easy to work with third party code as code written specifically for the application.

Another benefit of the Cake Pattern could be reducing the impulse to create the singletons that seem to overrun application code. By providing an easy way to collect dependencies together, it makes it a little bit harder to justify writing that quick sharedInstance property.

The compomnent abstraction does add some additional boilerplate and glue code. However, that extra code can help provide additional isolation, type safety, and points of extension that will lead to more robust, modular code.

References