PkL728
PkL728

Reputation: 955

Is it possible to pass generic protocols into a constructor for proper Dependency Injection in Swift 3?

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

Answers (2)

Arseniy
Arseniy

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

Anton3
Anton3

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

Related Questions