> -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