How to implement generic base classes that conform to protocols having type constraints and implement a factory for those generic types?

Viewed 129

Im trying to implement the repository pattern in my project. I'd like the design to be flexible enough so that I can swap out the underlying implementation should I need to in the future. For example, I'd like to be able to support Realm but be able to swap it out with Core Data if need be. I'd also like to implement fakes for testing.

I'm starting the implementation with fakes and what I'd like to do is implement a FakeRepository base class with generic constraints that conforms to the Repository protocol.

Note that I've omitted some of the implementation details to keep the post as short as possible.

protocol Repository {
    associatedtype Item

    var count: Int { get }

    func item(at index: Int) -> Item
}

class FakeRepository<Model>: Repository where Model: Identifiable {
    private var items: [Model] = []

    var count: Int { items.count }

    func item(at index: Int) -> Model {
        return items[index]
    }
}

I'd also like to define a protocol for each of the models that I intend on supporting. For example, a UserRepository.

struct User: Identifiable {
    let id: Int
    let name: String
}

protocol UserRepository: Repository where Item == User { }

Now all I have to do is define a concrete FakeUserRepository and I don't need to implement anything because all the work is already done.

class FakeUserRepository: FakeRepository<User>, UserRepository { }

The trouble I have is when I get to the factory pattern implementation. What I'd like to do is something like this, but the factory protocol doesn't compile because UserRepository has associated type requirements.

protocol UserRepositoryFactory {
    func makeRepository() -> UserRepository // DOES NOT COMPILE
}

struct FakeUserRepositoryFactory: UserRepositoryFactory {
    func makeRepository() -> UserRepository {
        return FakeUserRepository()
    }
}

struct CoreDataUserRepositoryFactory: UserRepositoryFactory {
    func makeRepository() -> UserRepository {
        return CoreDataUserRepository()
    }
}

And then I'd like to pass around a dependency container with all of my factories.

struct DependencyContainer {
    let userRepositoryFactory: UserRepositoryFactory
}

Another solution I tried was like this:

protocol Repository2 {
    associatedtype Item
    func item(at index: Int) -> Item
}

class Repository2Base<Model>: Repository2 {
    func item(at index: Int) -> Model {
        fatalError()
    }
}

class FakeRepository2<Model>: Repository2Base<Model> {
    var items: [Model] = []
    override func item(at index: Int) -> Model {
        return items[index]
    }
}

protocol UserRepositoryFactory2 {
    func makeRepository() -> Repository2Base<User>
}

class FakeUserRepositoryFactory2: UserRepositoryFactory2 {
    func makeRepository() -> Repository2Base<User> {
        return FakeRepository2<User>()
    }
}

Now this compiles and works well but I don't like how I have to invoke fatalError() just to compile. It seems like a hack. Is there an elegant way to achieve my goal?

1 Answers

I don't like to mix inheritance and generics, and I'd say it's the reason why you have to invoke fatalError()

Try this :

  1. first the Repository protocol
protocol Repository {
    associatedtype Item: Identifiable
    func item(at index: Int) -> Item
    init() 
}

class FakeRepository<Item: Identifiable>: Repository {
    var items: [Item]
    func item(at index: Int) -> Item {
        return items[index]
    }
    
    required init() { // notice that it it required
        items = []
    }
}

I added init() here because I want FakeRepositoryFactory to create any repository

  1. Then the factory :

public class SomeFakeRepositoryFactory {
    public init() { }
    public func makeRepository<Repo: Repository>() -> Repo {
        return Repo()
    }
}

You can use it like that :


struct User: Identifiable {
    let id: Int
    let name: String
}

struct Car: Identifiable {
    let id: Int
    let name: String
}


let user = User(id: 1, name: "Robert")
let factory = SomeFakeRepositoryFactory()
var userRepository: FakeRepository<User> = factory.makeRepository()
userRepository.items = [user]
userRepository.items.forEach { print($0.name) } // prints "Robert"

let car = Car(id: 1, name: "Delorean")
var carRepository = factory.makeRepository() as FakeRepository<Car>
carRepository.items = [car]
carRepository.items.forEach { print($0.name) } // prints Delorean

Related