> -0.5 for factory initializers on protocols.
> 
> I believe  there is a strong pairing between Dependency Inversion (from SOLID 
> principals, that you should depend on abstractions like protocols instead of 
> concretions like a particular class) and dependency injection (that your 
> implementation should be given the instances of the abstraction you need 
> rather than creating concrete classes on its own)
> 
> By having your code depend on a factory initializer at runtime to get its 
> abstractions, you are limited in your ability to adapt the code to other 
> scenarios such as testing. You may for instance need to put your factory 
> initializer on your protocol into a ‘testing mode’ in order to perform unit 
> testing on your code.
> 
> Or in other words, while its already possible to have factory methods and 
> factory functions, I worry that factory initializers will result in APIs 
> being unknowingly designed toward a higher degree of coupling in their code. 
> Having factory initializers provides a greater degree of “blessing” in API 
> design to (what I at least consider to be) an anti-pattern.

Testability and dependency injection are red herrings; factory initializers are 
no better or worse for those than any other mechanism in the language.

For instance, suppose you have an Image protocol with a factory initializer on 
its data:

        protocol Image {
                init(data: NSData)
                var data: NSData { get }
                
                var size: CGSize
                func draw(at point: CGPoint, in context: CGContext)
        }
        
        extension Image {
                factory init(data: NSData) {
                        if isJPEG(data: data) {
                                self = JPEGImage(data: data)
                        }
                        else if isPNG(data: data) {
                                self = PNGImage(data: data)
                        }
                        else {
                                self = BitmapImage(data: data)
                        }
                }
        }

Certainly if your code says `Image(data:)` directly, this violates dependency 
injection:

        class ImageDownloader: Downloader {
                var completion: (Image?, Error?) -> Void
                
                func didComplete(data: NSData) {
                        let image = Image(data: data)
                        completion(image, nil)
                }
        }

But the same would be true if we didn't have factory inits and instead had a 
static method or function that did the same thing. The solution is not to ban 
factory methods; it's to keep using dependency injection.

        class ImageDownloader: Downloader {
                var completion: (Image?, Error?) -> Void

                // Defaulted for convenience in normal use, but a test can 
change it.
                var makeImage: NSData -> Image = Image.init(data:)
                
                func didComplete(data: NSData) {
                        let image = makeImage(data)
                        completion(image, nil)
                }
        }

Similarly, if you think the factory init is not testable enough, you are not 
complaining about it being a factory init; you are complaining about it being 
poorly factored. The solution is to improve its factoring:

        func imageType(for data: NSData) -> Image.Type {
                if isJPEG(data: data) {
                        return JPEGImage.self
                }
                else if isPNG(data: data) {
                        return PNGImage.self
                }
                else {
                        return BitmapImage.self
                }
        }
        
        extension Image {
                // Defaulted for convenience in normal use
                factory init(data: NSData, decideImageType: NSData -> 
Image.Type = imageType(for:)) {
                        let type = decideImageType(data)
                        self = type.init(data: data)
                }
        }

With this in place, you can separately test that:

* `imageType(for:)` correctly detects image types.
* `Image.init` constructs an image of the type returned by `decideImageType`.

Which lets you test this design without forcing you to construct any unwanted 
concrete types.

So in short, I think factory inits are no less testable than any other code, 
and if you're running into trouble because you're not injecting dependencies, 
the solution is, quite simply, to inject dependencies.

-- 
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to