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?