Abstract Model in Swift

Abstract Model in Swift

Using protocols as a base for models can help to better architect iOS applications.
Jason Nam
Jason Nam
April 29, 2019
iOS Mobile

Protocol-first models acting as a glue between modules. Illustration done by Nimble design team.

Here comes an iOS developer. He sits down at his desk with a cup of coffee ☕️. Recently, the team shipped a web app for taking notes. It is now his turn to build an iOS client for the service. After a few sips, he jots down his plan on a sticky note:

  1. Create a Note model

  2. Create a local cache service using Realm

  3. Create a network layer

  4. Create a view controller to display the content of a note

Shouting ‘Perfect!’ silently in his mind, he begins to type on Xcode…

class Note {}

It is common to create a model using aclass or struct.

Next, to make the model useful and work with Realm, he adds a few lines of code on top of it.

class Note: Object {

    @objc dynamic var id = UUID().uuidString
    @objc dynamic var content = ""

    override static func primaryKey() -> String? {
        return "id"
    }
}

It conforms to Realm’s Object. id and content were added and marked as @objc and dynamic. Also, the model now has a static function returning the name of the primary key used in the database.

Since he finished the first step, he takes another sip of coffee ☕️ and starts to tackle the second task at hand. It is now time to create a service to save and fetch notes from a local Realm database.

class NoteCachingService {

    let realm = try! Realm()

    func save(_ note: Note) {
        try! realm.write {
            realm.add(note)
        }
    }

    func fetchNotes() -> [Note] {
        return Array(realm.objects(Note.self))
    }
}

Moving on to the network layer, he decides to use Codable to decode and encode data from the API. Because the API uses different keys for id, CodingKeys enum is needed.

class Note: Object, Codable {

    @objc dynamic var id = UUID().uuidString
    @objc dynamic var content = ""

    override static func primaryKey() -> String? {
        return "id"
    }

    enum CodingKeys: String, CodingKey {
        case id = "note_id"
        case content
    }
}

Finally, after pushing NoteViewController, he can now go to have lunch.

class NoteViewController: UIViewController {

    @IBOutlet weak var label: UILabel!

    func showNote(_ note: Note) {
        label.text = note.content
    }
}

Problems

The class Note breaks the encapsulation of each layer

A Model is a unit of data representation. This means that all of the application’s layers must share the same models to work together. So, if we put each layer-specific code in models, the code is shared through the whole app unwillingly.

NoteCachingService only cares about Realm but it has access to CodingKeys. ForNoteCachingService, CodingKeys is totally meaningless.

Also, every Note object has reference to Realm database managing it. So, the access to Realm in view controller or network layers cannot be prevented.

At first, this might seem much of a problem if we take enough precautions. But not every property of aNote object is so obvious. For example, Realm Object inherits NSObjectwhich means Notehas properties and methods like:

note.superclass
note.copy()
note.mutableCopy()

These are properties from NSObject and we might use them unintentionally in other classes. What if we ditch Realm and the Note class does not inherit NSObject anymore?

The class Note is getting bigger and has multiple responsibilities

Here, in our example Note class is supporting Realm and Codable. They need different kinds of properties and methods for configuration. If we keep adding new features this way, we will end up having a giant Note class with spaghetti code.

Also, what if we need to make the model Notework with two different database systems like Realm and CoreData? In that case, we would need to make Note conform to bothObject and NSManagedObject which is not possible because they are both of type class. If we keep using class for models, we would actually need to introduce a new model hierarchy to work with CoreData.

Turning Models into Protocols

These problems are signaling us that we need to abstract models. In general, abstracting help us to break down code into smaller pieces and to hide the internal implementation details.

Let’s convert Note to a protocol.

protocol Note {

    var id: String { get }
    var content: String { get set }
}

The concrete class for the caching service is now named RealmNote.

class RealmNote: Object, Note {

    @objc dynamic var id = UUID().uuidString
    @objc dynamic var content = ""

    override static func primaryKey() -> String? {
        return "id"
    }
}

AndCodableNote for the network layer.

class CodableNote: Note, Codable {

    let id: String
    var content: String

    enum CodingKeys: String, CodingKey {
        case id = "note_id"
        case content
    }
}

We don’t need to change anything on NoteViewController as it works perfectly with both RealmNote and CodableNote. It makes sense because the presentation layer doesn’t need to care about where the note comes from as long as it is aNote!

Conclusion

Protocol-first Model

To have the habit of creating model always with a protocol-first approach can help to structure large projects. Because a protocol-first model layer is truly acting as a glue between modules rather than the bridge leaking implementation unnecessarily.

Abstracting models is not a new concept

It has been told as part of abstraction design patterns like the widely-known Abstract Factory pattern. However, in the iOS community, protocol-based models are not commonly used. While using protocol as a base for models might not seem the first architecture choice to come to mind, this approach has great benefits and I hope this article serves as a reminder.

Happy coding.

If this is the kind of challenges you wanna tackle, Nimble is hiring awesome web and mobile developers to join our team in Bangkok, Thailand, Ho Chi Minh City, Vietnam, and Da Nang, Vietnam✌️