Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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:

  • Cannot specialize non-generic type 'RepositoryProtocol'

I get the following error in my ZombieService:

  • Cannot specialize non-generic type 'RepositoryProtocol'
  • Member 'insert' cannot be used on value of protocol type 'RepositoryProtocol'; use a generic constraint instead

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?

like image 906
PkL728 Avatar asked Jul 27 '16 17:07

PkL728


People also ask

What is @injected in Swift?

Dependency Injection is a software design pattern in which an object receives other instances that it depends on. It's a commonly used technique that allows reusing code, insert mocked data, and simplify testing.

What is DI container Swift?

A Dependency Injection Container is an object that is responsible for recording, deciding, and settling all the dependencies. Responsibilities of the deciding and settling mean that the DI container needs to know about the constructor arguments and the relationships between the objects.

Which of the following is an iOS or dependency injection?

Initializer-Based Injection There are three common types of dependency injection: setter-, interface-, and constructor-based. From those three, the constructor-based is the preferable one. In the iOS world, it could be called the initializer-based injection.


1 Answers

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!
like image 155
Arseniy Avatar answered Sep 28 '22 03:09

Arseniy