Component-based architectures give us high level of isolation and independence of the underlying components, but at the same time exibits potential risk of duplication. How would a component-based architecture work for iOS apps, especially when persistance is the key. Let’s experiment…

Let me create a framerwork in which I will have seprate components. I want my components to be possibly independent from each other, but at the same time I want to limit duplication. And trying to push the idea to the extreme, if a component needs to store anything in CoreData (we will use CoreData in this example, but you can easily abstract it away and use different storage options), I let the component to have its own model and its own sqlite file - in other words rather than sharing one CoreData stack among all the components, each component maintains its own instance of the CoreData stack. I am not sure how robust this architecture would be, and how well will it scale in terms of perfomance, but the possibility of throwing a component away at any moment without worrying about other component is very appealing and may be a good option in some applications.

Every type that is supposed to be stored in CoreData has its domain level equivalent. CoreData’s managed objects, actually everything that’s related to CoreData can only occur in the CoreData stack itself and in a wrapper that encapsulates low-level CoreData operations and translates between managed objects and the corresponding domain types.

Above the CoreData wrapper, there is a data store that provides high level persistance interface and enforces data integrity.

The CoreData wrapper and the data store provide a uniform set of operations that can be reused among components. Each component will have its own instances of the CoreData stack, the CoreData wrapper, and a data store, but I want them all to share the same source code.

Domain objects

All our domain objects have to conform to the following protocol:

protocol EPQuantity {
    associatedtype EPQuantityType
    func getValue() -> EQQuantityType
}

The associatedtype is a placeholder for the actuall type that will be provided by the type conforming to the protocol. Our protocol requires that a type conforming to it must have a method getValue returing a value of a type that is specific to the given domain object. This makes the protocol a good candidate for many types as long they can provide a value.

Now, lets take two example domain level value types conforming to the EPQuantity protocol:

struct EPAcceleration: EPQuantity, CustomStringConvertible {
    let acceleration: Double

    var description: String {
        return "EPAcceleration(acceleration: \(acceleration))"
    }

    func getValue() -> Double {
        return acceleration
    }
}

struct EPSpeed: EPQuantity, CustomStringConvertible {
    let speed: Int

    var description: String {
        return "EPSpeed(speed: \(speed))"
    }

    func getValue() -> Int {
        return speed
    }
}

What is important to see is that both types represents quantities of different types: we have an Int for the speed, but Double for acceleration.

Data store

Now we want to be able to store the domain objects in a data store. We may be tempted to start with the following implementation:

class EPDataStore {
    func store(item: EPQuantity)
    func getAll() -> EPQuantity
}

The compiler, however, will give us the following compile-time error: Protocol ‘EPQuantity’ can only be used as a generic constraint because it has Self or associated type requirements.

To fix this problem we can either do:

class EPDataStore {
    func store<T: EPQuantity>(item: T) {

    }

    func getAll<T: EPQuantity>() -> [T]? {
        return nil
    }
}

or just:

class EPDataStore<T: EPQuantity> {

    func store(item: T) {

    }

    func getAll() -> [T]? {
        return nil
    }
}

The problem we may have with this implementation is that we may want to be slightly more specific about which types should be allowed to enter the store. For instance, EPSpeed and EPAcceleration being domain equivalent of some managed objects, will ultimately need to match the corresponding types of the attributes in the manged objects. We can immediatelly think of Int, Double, NSTimeInterval, String, NSDate, NSNumber and perhapes a couple of more options. For now, to support EPSpeed and EPAcceleration, we only need to support Int and Double. How can we make the type constraint in EPDataStore more specific?

One way is to create another protocol and require that an instance of (a type implementing)EPQuantity can be stored in our store if and only if the type returned by its getValue requirement conforms to this new protocol. Let’s call this protocol EPPersistable:

protocol EPPersistable {}

Now, let’s update EPDataStore with a more specific type constraint:

class EPDataStore<T: EPQuantity where T.EPQuantityValueType: EPPersistable> {

    func store(item: T) {

    }

    func getAll() -> [T]? {
        return nil
    }
}

Now, if you try to create an instance of DataStore for EPSpeed:

let dataStore = EPDataStore<EPSpeed>()

the compliler will complain with Type ‘Int’ does not conform to protocol ‘EPPersistable’. We can make the error go away by using an extenssion to add protocol conformance:

extension Int: EPPersistable {}

and if we want to persist instances of EPAcceleration, we would also need:

extension Double: EPPersistable {}

That’s nice, but let’s get back to our isolated components. Each is holding its own CoreData stack, a CoreData wrapper to translate between managed objects and domain objects, and a DataStore to handle data integrity or any other required logic. As a DataStore dependency, we want the CoreData wrapper to provide a mapping between objects conforming to the EPQuantity protocol and the corresponding managed objects that will land in our CoreData storage. As an example, EPSpeed would be matched by the following managed object:

@objc(EPSpeedCoreData)
class EPSpeedCoreData: NSManagedObject {

    // Insert code here to add functionality to your managed object subclass

}

extension EPSpeedCoreData {
    @NSManaged var speed: Int32
}

Then, in order to be able to handle different managed objects through a common interface, we could add the following protocol:

protocol EPCoreDataQuantity {
    associatedtype EPCoreDataQuantityValueType
    func getValue() -> EPCoreDataQuantityValueType
    func setValue(value: EPCoreDataQuantityValueType)
}

extension EPSpeedCoreData:EPCoreDataQuantity {
    func getValue() -> Int {
        return Int(speed)
    }

    func setValue(value: Int) {
        speed = Int32(value)
    }
}

And finally, the skeleton of our universal CoreData wrapper could be defined by the following set of requirements:

protocol EPPersistance {
    associatedtype EPQuantityType
    func write(item: EPQuantityType)
    func read() -> [EPQuantityType]?
}

and fullfilled by the following type:

class EPCoreDataWrapper<T: EPCoreDataQuantity, U: EPQuantity where T.EPCoreDataQuantityValueType: EPPersistable, U.EPQuantityValueType: EPPersistable>: EPPersistance {

    func write(item: U) {

    }

    func read() -> [U]? {
        return nil
    }
}

And obviously, our EPDataStore needs an instance of compatible CoreData wrapper, so obviously one could be tempted to try something like this:

class EPDataStore<T: EPQuantity where T.EPQuantityValueType: EPPersistable> {

    let persistance: EPPersistance

    init(persistance: EPPersistance) {
        self.persistance = persistance
    }

    func store(item: T) {
        persistance.write(item)    
    }

    func getAll() -> [T]? {
        return persistance.read()
    }
}

But we will get the familiar error - this time regarding EPPersistance: Protocol ‘EPPersiatance’ can only be used as a generic constraint because it has Self or associated type requirements.

The solution to this problem is commonly known as Type Erasure, a technique that I also describe shortly in my other post Around Type Erasure in Swift. We cannot refer to EPPersistance directly because it has an associated type. We need to hide this fact from the compiler by wrapping up the EPPersistance inside another type that will not exbit the limitation that EPPersistance has. To achieve that, we create a type that wraps an instance conforming to the EPPersistance protocol:

class EPAnyPersistance<T: EPQuantity where T.EPQuantityValueType: EPPersistable> : EPPersistance {

    let _write: T -> ()
    let _read: () -> [T]?

    init<BaseType: EPPersistance where BaseType.EPQuantityType == T>(base: BaseType) {
        _write = base.write
        _read = base.read
    }

    func write(item: T) {
        _write(item)
    }

    func read() -> [T]? {
        return _read()
    }
}

Now, it is safe to add a reference to the persistance object to our data store:

class EPDataStore<T: EPQuantity where T.EPQuantityValueType: EPPersistable> {

    let persistance: EPAnyPersistance<T>

    init(persistance: EPAnyPersistance<T>) {
      self.persistance = persistance
    }

    func store(item: T) {
        persistance.write(item)
    }

    func getAll() -> [T]? {
        return persistance.read()
    }
}

We can create an instance of EPDataStore for EPSpeed type as follows:

let dataStoreForSpeed = EPDataStore<EPSpeed>(persistance: EPAnyPersistance(base: EPCoreDataWrapper<EPSpeedCoreData, EPSpeed>()))

dataStoreForSpeed.store(EPSpeed(speed: 125))
dataStoreForSpeed.getAll()

Try it out in the playground: ExperimentingWithPersistanceInSwift.playground.