Implications of SE-0335 when implementing dependency injection

Hi :wave:

I would like to get some feedback/guidelines on how to implement dependency injection now that SE-0335 has been accepted.

I think the most widely accepted way of implementing dependency injection + tests does make use of protocols:

// In the main module

protocol MyServiceProtocol: AnyObject {
    func makeRequest(completion: @escaping () -> Void)
}

final class MyService: MyServiceProtocol {
    func makeRequest(completion: @escaping () -> Void) {
        print("do something")
        completion()
    }
}

final class MyRepository {
    private let myService: MyServiceProtocol

    /// Init used for tests
    init(myService: MyServiceProtocol) {
        self.myService = myService
    }

    /// Init used in production
    convenience init() {
        self.init(
            myService: MyService()
        )
    }
    
    func doSomething() {
        myService.makeRequest {
            print("Done")
        }
    }
}

// In test module, when writing tests for MyRepository

final class MyServiceMock: MyServiceProtocol {
    func makeRequest(completion: @escaping () -> Void) {
        print("mock triggered")
        completion()
    }
}

func testSomething() {
    let myMockedService = MyServiceMock()
    let sut = MyRepository(myService: myMockedService)
    ...
}

After reading SE-0335, my understanding is that the core team wants to discourage the usage of existential types. If that is the case, what other better alternatives are there to implement dependency injection?

I am aware that above code can be written with generics, but such approach seems to me like a poor alternative: the longer the list of dependencies, the longer the list of generic requirements, incrementing verbosity compared to above implementation (which is already pretty verbose)

I could not find any resources online discussing the implications of SE-0335 in dependency injection. Some links would be also appreciated.

Thanks! :pray:

3 Likes

Core Team doesn't want to discourage the usage of existential types. Current problem is that existentials are very easy to use, but current syntax doesn't highlight the difference between concrete and existential types. So currently they are overused.
As it said in proposal: The cost of using existential types should not be hidden, and programmers should explicitly opt into these semantics.

Dependency Injection is the case where using of existentials is reasonable.

3 Likes

It's not that existential types should be discouraged – but that it should be clear when using an existential versus using a generic type that a choice has been made. Use of existentials can have far-reaching consequences – both for performance, and for expressivity. But their lack of a marker makes them look like the "natural" thing to do in all circumstances – which misleads developers who might be better off using generics instead, depending on the circumstances.

In your case, any MyServiceProtocol is quite likely the right choice to make. The alternative is to make MyRepository generic over the service type. This preserves type information – but has lots of other knock-on consequences. Which choice is better really depends on your code.

There are indeed other alternatives. If MyServiceProtocol literally only exists to serve up a single method, you could consider just using a closure rather than requiring a protocol and type hierarchy. You could also consider switching out the implementation using #if SOME_TEST_FLAG instead, maybe using a type alias and ad-hoc polymorphism. This has a lot of appeal as it means you pay no performance penalty at runtime, unlike both the type-erased and generic solutions.

4 Likes

Thanks for the clarifications :pray:

If MyServiceProtocol literally only exists to serve up a single method, you could consider just using a closure rather than requiring a protocol and type hierarchy.

Above was a simple example. These protocols serve as interfaces and usually contain multiple functions.

You could also consider switching out the implementation using #if SOME_TEST_FLAG instead, maybe using a type alias and ad-hoc polymorphism

Thanks for that, did not think about it. My initial impression of this solution is that using macros seems to be not a good idea when implemented at scale (app with hundreds of modules), because in order to make sure that both production and test versions of the implementations compile, two different builds will be required: one with SOME_TEST_FLAG ON and one with SOME_TEST_FLAG OFF .

Based on the answers, seems like for a codebase that relies heavily on injection (and thus in protocols), the better approach is just to add any.

I would like to add below some thoughts about the implication of SE-0335 when using dependency injection, base on the codebase I am working on.

  • Consider a codebase organised in layers (viewControllers / viewModels / interactors / repositories / services), where the components in each layer are tested in isolation, and the connections between layers relies on injection (using protocols, like shown above). Under such scenario the vast majority of usages for protocols is injection. This means thousands of places where any has to be added, versus a limited amount of places where protocols are used for something else that is not injection.
  • Under above scenario, which I believe is the most common way of maintaining a testable codebase, I struggle to see the usefulness of SE-0335. Seems like the proposal is making the injection pattern (a very common one) more verbose.

Do any of you have a different opinion? I do not try to undermine the proposal, just trying to understand: how is SE-0335 helping a codebase that heavily relies on injection, if for the vast majority of cases the developers will add any and move on?

Thanks again :pray:

After several months I keep struggling to understand the benefit of the proposal for a codebase that uses the injection pattern. Would very much appreciate feedback on the latest comment:

how is SE-0335 helping a codebase that heavily relies on injection, if for the vast majority of cases the developers will add any and move on?

The benefit of adding the any keyword is clarifying in your code that you do not have the full API surface promised by the protocol constraint. There also may be places where you can change the any to some instead, e.g. in function parameters. There's more information on when to use some vs any here:

SE-0335 will have more code changes for specific programming patterns, including some forms of dependency injection and the delegate pattern, compared to emergent programming patterns that preserve more static type information. It's okay to disagree that this language change is ultimately "worth it", because every programmer has different opinions and constraints. At the end of the day, if you want to write any everywhere it was previously inferred, the code change will amount to applying all fix-its from the compiler.

3 Likes