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:
- Core Data: NSManagedObjectContext
- Realm: Realm object
- SQLite in C: sqlite3 pointer
- GRDB.swift: DatabasePool
Being good engineers we strive to be, we want to:
- Avoid keeping any of these references in a singleton or as a shared global variable.
- Don’t let users of PersistenceKit know about its implementation details, i.e. we want the fact that we use
NSManagedObjectContext
orDatabasePool
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?