Reputation: 955
I am trying to accomplish protocol oriented programming in Swift 3 using generics. Is this not fully supported yet? I'm going to show you what I would LIKE to do below but will not compile. Am I missing something here? My goal is to be able to use protocol oriented programming to perform dependency injection with the intent of easily mocking these structures in my unit tests.
protocol ZombieServiceProtocol {
func fetchZombies()
var zombieRepository: RepositoryProtocol<Zombie> { get set }
}
struct ZombieService: ZombieServiceProtocol {
var zombieRepository: RepositoryProtocol<Zombie>
init(zombieRepository: RepositoryProtocol<Zombie>) {
self.zombieRepository = zombieRepository
}
func fetchZombies() {
self.zombieRepository.deleteAll()
self.createFakeZombies()
}
private func createFakeZombies() {
for index in 1...100 {
let zombie = Zombie(id: index, name: "Zombie \(index)")
self.zombieRepository.insert(zombie)
}
}
}
The Zombie class looks like this:
public struct Zombie: Persistable {
var id: Int
let name: String?
init(id: Int, name: String?) {
self.id = id
self.name =name
}
}
Its Persistable protocol looks like this:
protocol Persistable {
var id: Int { get set }
}
And my Repository code looks something like this:
protocol RepositoryProtocol: class {
associatedtype Object: Persistable
//...
func insert(_ object: Object) -> Void
func deleteAll(_ predicate: (Object) throws -> Bool) -> Void
}
class Repository<Object: Persistable>: RepositoryProtocol {
var items = Array<Object>()
//...
func insert(_ object: Object) {
self.items.append(object)
}
func deleteAll() {
self.items.removeAll()
}
}
I get the following error in my ZombieServiceProtocol:
I get the following error in my ZombieService:
And to highlight exactly what I'm trying to accomplish, here is what a simple test would look like in which I create a Mock repository and attempt to use that instead of the real one in my ZombieService:
@testable import ZombieInjection
class ZombieServiceTests: XCTestCase {
private var zombieRepository: RepositoryProtocol<Zombie>!
private var zombieService: ZombieServiceProtocol
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
self.zombieRepository = RepositoryMock<Zombie>()
self.zombieService = ZombieService(zombieRepository: self.zombieRepository)
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// Arrange
// Act
self.zombieService.fetchZombies()
// Assert
XCTAssert(self.zombieRepository.count() > 0)
}
}
This code also does not compile presently with the same errors as above.
I have been looking at the associatedTypes and typeAlias tags as well as the Generics Manifesto. While looking at the Manifesto, I believe this falls into the "Generic Protocols" section which is currently marked as Unlikely (which is bumming me out). If I can't accomplish something like I'm trying to do above, what would be the next best solution?
Upvotes: 5
Views: 1396
Reputation: 304
The answer to your question is yes it is definitely possible, just that it currently requires some PAT-related "magic". With Swift3 and Xcode 8.0 beta 4, you should be able to run the following in a playground:
protocol Persistable {
var id: Int { get set }
}
protocol RepositoryProtocol: class {
associatedtype Object: Persistable
func insert(_ object: Object) -> Void
func deleteAll()
}
protocol ZombieServiceProtocol {
associatedtype RepositoryType: RepositoryProtocol
var zombieRepository: RepositoryType { get set }
func fetchZombies()
}
public struct Zombie: Persistable {
var id: Int
let name: String?
}
// Mocks
class RepositoryMock<Object: Persistable>: RepositoryProtocol {
func insert(_ object: Object) { print("look, there's another one!")}
func deleteAll() { print("it's safe out there, all zombies have been deleted") }
}
struct ZombieServiceMock<RepositoryType: RepositoryProtocol
where RepositoryType.Object == Zombie>: ZombieServiceProtocol {
var zombieRepository: RepositoryType
init(zombieRepository: RepositoryType) {
self.zombieRepository = zombieRepository
}
func fetchZombies() {
self.zombieRepository.deleteAll()
self.createMockZombies()
}
private func createMockZombies() {
for index in 1...5 {
let zombie = Zombie(id: index, name: "Zombie \(index)")
self.zombieRepository.insert(zombie)
}
}
}
// Tests
class ZombieServiceTests<RepositoryType: RepositoryProtocol,
ServiceType: ZombieServiceProtocol
where ServiceType.RepositoryType == RepositoryType> {
private var zombieRepository: RepositoryType
private var zombieService: ServiceType
init(repository: RepositoryType, service: ServiceType) {
zombieRepository = repository
zombieService = service
}
func testExample() {
self.zombieService.fetchZombies()
}
}
let repositoryMock = RepositoryMock<Zombie>()
let repositoryService = ZombieServiceMock(zombieRepository: repositoryMock)
let zombieTest = ZombieServiceTests(repository: repositoryMock, service: repositoryService)
zombieTest.testExample()
// Prints:
// it's safe out there, all zombies have been deleted
// look, there's another one!
// look, there's another one!
// look, there's another one!
// look, there's another one!
// look, there's another one!
Upvotes: 5
Reputation: 586
One thing you can do in current Swift is to replace (non-working) generic protocols with generic non-final classes. Example:
class RepositoryBase<Object: Persistable> {
func insert(_ object: Object) -> Void {
fatalError()
}
func deleteAll(_ predicate: (Object) throws -> Bool) -> Void {
fatalError()
}
}
struct ZombieService: ZombieServiceProtocol {
var zombieRepository: RepositoryBase<Zombie>
// ...
}
Such code is likely to be non-idiomatic in Swift (stub methods in base class), but it works.
Upvotes: 0