[swift-evolution] [Proposal] Factory Initializers

Brent Royal-Gordon brent at architechies.com
Tue Mar 22 18:02:36 CDT 2016


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



More information about the swift-evolution mailing list