Hiding Implementation Details Using internal Properties

Swift comes with five access-level modifiers: open, public, internal, fileprivate and private. The internal modifier leads to entities being available for use only within their defining module. It’s a default modifier but it starts getting interesting only once we split our codebase into modules.

In this article, we’ll see how to provide an ability to inject a framework’s data structure into the framework’s classes, while at the same time keeping its internals hidden.

Problem Statement

Most apps these days need some kind of local persistence of data. There are many choices: Core Data, Realm, SQLite with or without wrappers, etc. No matter what we choose, once our project exceeds 10K, 50K, or 100K lines of code we’ll inevitably start thinking about splitting it up into modules.

One of the modules we can consider extracting from the main target would contain the database access code. Let’s call it PersistenceKit, following Apple’s naming convention. We can implement the module either as a dynamic framework or a static library.

Let’s assume that PersistenceKit will contain many repositories, such as: ArticleRepository, UserRepository, etc that we’ll use to fetch and store data. A repository can be implemented as follows:

public struct Article {
    public let id: ArticleID
    public let title: String
    public let content: String
}

public class ArticleRepository {
    public func article(for id: ArticleID) -> Article? {
        // finds a row in the database and maps it to a struct
        //
        // missing implementation
    }

    // other methods...
}

To be able to perform an actual database access in implementations of repositories’ methods, we need some kind of reference to the database, be it:

Being good engineers we strive to be, we want to:

  1. Avoid keeping any of these references in a singleton or as a shared global variable.
  2. Don’t let users of PersistenceKit know about its implementation details, i.e. we want the fact that we use NSManagedObjectContext or DatabasePool internally stay hidden.

Solution

I recently spent some time thinking about these two goals and came up with an approach that I’m happy with. It’s based on a mix of public and internal modifiers. Let’s introduce a new struct:

public struct Connection {
    let pool: DatabasePool
}

This way Connection struct is accessible outside of PersistenceKit but pool property isn’t. It’s not even possible to initialize this struct outside of PersistenceKit because its memberwise initializer is in this case internal.

Now, since users of our framework won’t be able to initialize Connection, we have to provide them with an instance. We can do that in an entry point to PersistenceKit:

public struct AppDatabase {
    public func setup(with path: URL) throws -> Connection {
        // performs the setup and returns a connection instance
    }
}

What’s left, is injecting Connection to our ArticleRepository, by changing its implementation to:

public class ArticleRepository {
    public func article(for id: ArticleID, connection: Connection) -> Article? {
        // we can access `pool` property here because it’s accessible in this module
        return connection.pool.read { (db) -> Article? in
            return Article.fetchOne(db, key: id)
        }
    }
}

Users can now set up PersistenceKit as follows:

class AppCoordinator {
    let connection: PersistenceKit.Connection
    ...

    init() throws {
        let database = AppDatabase()
        connection = try database.setup(with: path)
    }
}

Then, when they want to fetch something, they can simply pass connection to ArticleRepository’s methods:

let repository = ArticleRepository()
let article = repository.article(for: id, connection: connection)

Even if users of our framework wanted to access Connection.pool here directly, they couldn’t because it’s not accessible outside of PersistenceKit. We can be sure that database access code stays in PersistenceKit leading to a cleaner overall architecture.

Summary

Public types with internal properties are a powerful tool. We showed how to allow users to own and pass an object we need – as the framework’s authors – without exposing any of the internals. Are you aware of any other cool uses of public types with internal properties?