Dependency Inversion Principle (Swift) - Applicable also without polymorphism? (Abstraction: constrained generics)

softwareengineering.stackexchange https://softwareengineering.stackexchange.com/questions/363985

質問

There exist a number of articles/blogs explaining the Dependency Inversion Principle (DIP) using Swift; to name a few (top Google hits):

Now, Swift is widely described (even by Apple) as a protocol-oriented programming language, rather that an OOP language; so naturally these articles realize the abstractions in DIP using protocols rather than inheritance, however specifically heterogeneous protocols to allow the policy layer to use polymorphism to invoke lower layers without needing to know any implementation details. E.g.:

// Example A
protocol Service {
    func work()
}

final class Policy {
    // Use heterogeneous protocol polymorphically.
    private let service: Service

    init(service: Service) { self.service = service }

    func doWork() { service.work() }
}

final class SpecificService: Service {
    func work() { /* ... */ print("Specific work ...") }
}

let policy = Policy(service: SpecificService())

// Resolves to doWork() of SpecificService at runtime
policy.doWork() // Specific work ...

I realize polymorphism is one of the key concepts in DIP (unless I'm mistaken), but from a Swift implementation perspective, I'd rather see DIP applied using protocol-constrained generics rather than runtime polymorphism. E.g.:

// Example B
protocol Service {
    func work()
}

final class Policy<PolicyService: Service> {
    private let service: PolicyService

    init(service: PolicyService) { self.service = service }

    func doWork() { service.work() }
}

final class SpecificService: Service {
    func work() { /* ... */ print("Specific work ...") }
}

let policy = Policy(service: SpecificService())

// policy.service specialized as SpecificService instance at compile time
policy.doWork() // Specific work ...

I haven't seen anyone use generics in the context of DIP and Swift, so I'm probably the one in the dark here, hence this question.

As a design principle, I believe B above achieves the same goal as A, w.r.t. DIP; and when applied in a statically typed protocol-oriented programming language that generally prefers composition over inheritance, specifically (afaik) protocols and generics over protocols and polymorphism, I would prefer using B. This is naturally under the constraint that we only ever use a single specialized Policy at a time, and fall back on DIP to ease changes in the low-level details by decoupling/dependency inversion.


Question: Would Example B above be considered a valid application of DIP, even if a specialized Policy "knows" about the concrete Service at compile time (due to generics; still de-coupled by abstractions applied as constraints to the generic placeholder)?

役に立ちましたか?

解決

Disclaimer: I don't know Swift. With that out of the way - if Swift generics are anything like C# generics, and it looks like they are quite a bit, I wouldn't say that Policy "knows" about the concrete service, because you can't actually call any methods specific to the concrete service - you have to work with what's exposed through the Service protocol. The fact that the compiler knows how to generate a version that can work with the concrete type doesn't really affect the coupling of your code. You can still change the implementation details of any concrete service without affecting the Policy, as long as it adheres to the Service protocol. So this does conform to the DIP - it's just that the mechanisms involved are slightly different.

That said, I don't think you gain much here (unless there's something specific to Swift that I'm unaware of). From the perspective of client code, the two variants are pretty much the same. From the perspective of code maintenance, it feels slightly confusing (IMO). And you won't necessarily get better performance, because, as far as I can tell, the compiler will still probably generate vtables based on the generic constraints, and use dynamic dispatch to polymorphically call the correct method on the correct type (generics don't work the same way as C++ templates do). Sure, the compiler may be able to optimize some of that away in certain situations, but even so, presuming that it will would amount to premature optimization on your part (and, besides, you should do measurements to determine if there are any actual performance gains, rather then assume that there will be).

P.S. The DIP does not necessarily have to involve the language mechanisms commonly used to employ it, like inheritance-based polymorphism. It states that a high-level component should not be directly dependent on a low-level component, but that both should depend on an abstraction. This abstraction should be "owned" (defined by, or prescribed by) the higher-level component. It conceptually forms part of the higher-level component, if you will. The lower level component should then adhere to the "contract" imposed by this abstraction, and the inversion comes about through having the low-level component be dependent on the the other two. So, while the role of the abstraction is often realized by a protocol or a base class, you could also do something fairly weird, such as having the high-level component write custom commands to a file at a known location, that would then be read and interpreted by the low-level component. Here, the abstraction is the combination of the communication mechanism, the file location and the custom command set, and again, this is all taken to be defined and owned by the high-level component, and the low level component must adhere to these requirements. It's unusual, but it still confirms to the DIP. It's not about the exact implementation, it's about how the various elements interact to control the coupling between different parts of the system.

ライセンス: CC-BY-SA帰属
所属していません softwareengineering.stackexchange
scroll top