Separation of concerns using protocols in Swift

Separation of concerns is a core principle when it comes to designing architectures and systems that are easy to maintain. It's the idea that each object or type should only know enough about its surroundings to do its work, and no more.

However, even though it's a principle most programmers learn about early in their career, it's not always very easy to apply in practice. This week, let's take a look at how to more easily separate the concerns of various types in Swift using protocols.

Let's start with an example

Let's say we're building a ContactSearchViewController that lets our users search contacts that are locally saved in the app. The app is currently using Core Data for persistence, so our initial solution might be to simply inject the app's Core Data context (NSManagedObjectContext) into our new view controller as a dependency:

class ContactSearchViewController: UIViewController {
    private let coreDataContext: NSManagedObjectContext

    init(coreDataContext: NSManagedObjectContext) {
        self.coreDataContext = coreDataContext
        super.init(nibName: nil, bundle: nil)
    }
}

Above we are using initializer-based dependency injection. You can read more about that technique and other flavors of dependency injection in last week's post.

Doing something like the above is a very common solution, and is totally valid, however it does have two problems:

  1. Our view controller is aware that our app is using Core Data. While this can be very convenient, it also makes our code a lot less flexible (if we, for example, want to change our database solution to something else - like Realm). It also makes testing really difficult (even if we are using dependency injection), since mocking a system-class like NSManagedObjectContext is hard and often leads to unstable tests.
  2. Since our view controller gets complete access to our database, it can do whatever it wants with it. It can both read & write, which in this case is really not necessary - the view controller will only search through contacts, which only requires read access. It would be a lot nicer if we could decouple reading & writing to only provide a given view controller with the functionality it needs.

Another abstraction

When facing issues with testability and separation of concerns, what most often is needed is another form of abstraction. Like I mentioned in my talk "Everyone is an API designer", protocols are a great way to define such an abstraction.

Instead of giving our view controllers direct access to the concrete implementation of our database, we can create a protocol that defines all the APIs that our app needs for loading and saving objects, like this:

protocol Database {
    func loadObjects<T: Model>(matching query: Query) -> [T]
    func loadObject<T: Model>(withID id: String) -> T?
    func save<T: Model>(_ object: T)
}

We can now use our new Database protocol when initializing ContactSearchViewController, instead of injecting the Core Data context directly:

class ContactSearchViewController: UIViewController {
    private let database: Database

    init(database: Database) {
        self.database = database
        super.init(nibName: nil, bundle: nil)
    }
}

Doing the above solves problem number one - our code is now a lot more flexible and easier to test. In our tests we can now simply create mocks by implementing the Database protocol, and if we ever want to migrate to a new database solution (or even use a special one for things like UI tests), we can easily do so by adding new implementations of Database:

extension NSManagedObjectContext: Database {
    ...
}

extension Realm: Database {
    ...
}

extension MockedDatabase: Database {
    ...
}

extension UITestingDatabase: Database {
    ...
}

But what about issue number two - decoupling reads & writes? ๐Ÿค”

Protocol composition

Instead of using a single protocol for all database functionality, let's use protocol composition to split things up. In this case, we'll create one protocol for reading and one for writing. Same APIs, just divided into two protocols instead of one:

protocol ReadableDatabase {
    func loadObjects<T: Model>(matching query: Query) -> [T]
    func loadObject<T: Model>(withID id: String) -> T?
}

protocol WritableDatabase {
    func save<T: Model>(_ object: T)
}

What I love about protocol composition is that it's much easier to mix & match different functionality depending on what various types need. It also makes testing even easier, since you can create mocks by simply implementing a small number of methods instead of having to conform to a large protocol.

For something like a database, it also lets us have much more fine grained control over how data flows in our app. We can now limit database access for view controllers that only need to read from it, giving them the read only protocol instead of the full database:

class ContactSearchViewController: UIViewController {
    private let database: ReadableDatabase

    init(database: ReadableDatabase) {
        self.database = database
        super.init(nibName: nil, bundle: nil)
    }
}

The good thing is that we can still easily have a Database type for when full access is needed, by using Swift's protocol composition operator with a typealias:

typealias Database = ReadableDatabase & WritableDatabase

Conclusion

Protocols can be a great tool when you want to have more fine grained control over what functionality a given object gets access to. In general, the less concerns an object needs to have, the less the surface area for bugs is. By keeping your types simple, small and with as few concerns as possible, you usually end up with systems that are easier to test & maintain.

The above technique is also super useful when designing APIs for frameworks - either internal ones or ones you share as open source. Instead of having big protocols that contain lots of functionality, you can more easily choose what you share as part of the public API, and avoid leaking implementation details. I'll write more about framework API design in an upcoming post.

What do you think? Do you usually split your app's core functionality into separate protocols or is it something you'll try out? Let me know, along with any questions, comments or feedback you have - either in the comments section below or on Twitter @johnsundell.

Thanks for reading, and happy holidays! ๐Ÿš€

Wrapping up 2017 on Swift by Sundell

Different flavors of dependency injection in Swift