The word "factory" is certainly one of the most frequently used by programmers when discussing their (or others) programs. But the meaning to be invested in it is very different: it can be a class that generates objects (polymorphic or not); and a method that creates instances of any type (static or not); it happens, and even just any generating method (including
constructors ).
Of course, not everything, anything that generates copies of something, can be called the word "factory." Moreover, this word can hide two different generators of the Gang of Four arsenal - the
“factory method” and the
“abstract factory” , in details of which I would like to go a little deeper, paying particular attention to their classical understanding and realization.
And
Joshua Kerivski (head
of Industrial Logic ) inspired me to write this essay, or rather, his book
Refactoring to Patterns , which was published at the beginning of the century as part of a series of books founded by
Martin Fowler (eminent author of the modern programming classics - the book
“ Refactoring " ). If someone has not read or even heard of the first (and I know there are a lot of such), then be sure to add it to your list to read. This is a worthy “sequel” of both “Refactoring” and even more classic book -
“Object-oriented design techniques. Design patterns .
')
The book, among other things, contains several dozens of recipes for getting rid of various
"smells" in the code using
design patterns . Including three (at least) “recipes” on the topic under discussion.
Abstract Factory
Kerivski in his book cites two cases in which the use of this pattern will be useful.
The first is the
encapsulation of knowledge about specific classes connected by a common interface. In this case, only the type, which is a factory, will possess this knowledge. The public
API of the factory will consist of a set of methods (static or not) that return instances of the common interface type and have any “speaking” names (to understand which method you need to call for a particular purpose).
The second example is very similar to the first (and, in general, all the scenarios for using a pattern are more or less similar to each other). This is the case when instances of one or several types of one group are created in different places of the program. The factory in this case again encapsulates knowledge about the code that creates instances, but with a slightly different motivation. For example, this is especially true if the process of creating instances of these types is complex and is not limited to calling a constructor.
To be closer to the theme of development under
“iOS” , it is convenient to practice on subclasses of
UIViewController
. And indeed, this is exactly one of the most common types in the “iOS” development, almost always “inherited” before use, and a particular subclass is often not even important for client code.
I will try to keep code samples as close as possible to the classic implementation from the book “Gangs of Four”, but in real life, code is often simplified in some way or another. And only a sufficient understanding of the pattern opens the door for its more free use.
Detailed example
Suppose we trade vehicles in an application, and the display depends on the type of a particular vehicle: we will use different
UIViewController
subclasses for different vehicles. In addition, all vehicles differ in condition (new and used):
enum VehicleCondition{ case new case used } final class BicycleViewController: UIViewController { private let condition: VehicleCondition init(condition: VehicleCondition) { self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("BicycleViewController: init(coder:) has not been implemented.") } } final class ScooterViewController: UIViewController { private let condition: VehicleCondition init(condition: VehicleCondition) { self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("ScooterViewController: init(coder:) has not been implemented.") } }
Thus, we have a family of objects of the same group, instances of types of which are created in the same places depending on some condition (for example, the user clicked on the product in the list, and depending on whether it is a scooter or a bicycle, we create the appropriate controller). Controller designers have some parameters that must also be set each time. Do these two arguments indicate the creation of a “factory”, which alone will have knowledge of the logic of creating the desired controller?
Of course, the example is quite simple, and in a real project in a similar case, introducing a “factory” would be an obvious
“overengineering” . Nevertheless, if we imagine that the types of vehicles we have are not two, but the parameters of the designers are not one, then the advantages of the “factory” will become more obvious.
So, let's declare an interface that will play the role of an “abstract factory”:
protocol VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController func makeScooterViewController() -> UIViewController }
(A rather brief
“guideline” on designing “API” in the
“Swift” language recommends naming “factory-made” methods starting with the word “make”.)
(An example in the book of the gang of four is given in
"C ++" and is based on
inheritance and
"virtual" functions . Using "Swift" we are, of course, closer to the paradigm of protocol-oriented programming.)
The abstract factory interface contains only two methods: to create controllers for selling bicycles and scooters. Methods do not return instances of specific subclasses, but of a common base class. Thus, the area of ​​dissemination of knowledge about specific types is limited to the area in which it is really necessary.
As "concrete factories" we will use two implementations of the abstract factory interface:
struct NewVehicleViewControllerFactory: VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController(condition: .new) } func makeScooterViewController() -> UIViewController { return ScooterViewController(condition: .new) } } struct UsedVehicleViewControllerFactory: VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController(condition: .used) } func makeScooterViewController() -> UIViewController { return ScooterViewController(condition: .used) } }
In this case, as can be seen from the code, specific factories are responsible for vehicles of different states (new and used).
Creating the desired controller will now look something like this:
let factory: VehicleViewControllerFactory = NewVehicleViewControllerFactory() let vc = factory.makeBicycleViewController()
Encapsulating classes with a factory
Now let's briefly go over the usage examples that Kerivski offers in his book.
The first case is related to the
encapsulation of specific classes . For example, let's take the same controllers for displaying vehicle data:
final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { }
Suppose we are dealing with a separate module, for example, a plug-in library. In this case, the classes declared above remain (by default)
internal
, and a factory will act as a public “API” of the library, which in its methods returns the base classes of controllers, thus leaving knowledge of specific subclasses inside the library:
public struct VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController() } func makeScooterViewController() -> UIViewController { return ScooterViewController() } }
Moving knowledge about creating an object inside the factory
The second “case” describes a
complex object initialization , and Kerivski, as one of the ways to simplify the code and guard the encapsulation principles, suggests limiting the spread of knowledge about the initialization process beyond the factory.
Suppose we wanted to sell cars at the same time. And this is undoubtedly a more complex technique, possessing a larger number of characteristics. For example, we limit ourselves to the type of fuel used, the type of transmission and the size of the wheel:
enum Condition { case new case used } enum EngineType { case diesel case gas } struct Engine { let type: EngineType } enum TransmissionType { case automatic case manual } final class CarViewController: UIViewController { private let condition: Condition private let engine: Engine private let transmission: TransmissionType private let wheelDiameter: Int init(engine: Engine, transmission: TransmissionType, wheelDiameter: Int = 16, condition: Condition = .new) { self.engine = engine self.transmission = transmission self.wheelDiameter = wheelDiameter self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("CarViewController: init(coder:) has not been implemented.") } }
Example of initialization of the corresponding controller:
let engineType = EngineType.diesel let engine = Engine(type: engineType) let transmission = TransmissionType.automatic let wheelDiameter = 18 let vc = CarViewController(engine: engine, transmission: transmission, wheelDiameter: wheelDiameter)
We can put the responsibility for all these “trifles” on the “shoulders” of a specialized factory:
struct UsedCarViewControllerFactory { let engineType: EngineType let transmissionType: TransmissionType let wheelDiameter: Int func makeCarViewController() -> UIViewController { let engine = Engine(type: engineType) return CarViewController(engine: engine, transmission: transmissionType, wheelDiameter: wheelDiameter, condition: .used) } }</source : <source lang="swift">let factory = UsedCarViewControllerFactory(engineType: .gas, transmissionType: .manual, wheelDiameter: 17) let vc = factory.makeCarViewController()
Factory Method
The second “single root” pattern also encapsulates knowledge about specific types generated, but not by hiding this knowledge inside a specialized class, but by polymorphism. Kerivski in his book gives examples of
“Java” and suggests using
abstract classes , but the inhabitants of the “Swift” universe are not familiar with this concept. We have our own atmosphere ... and protocols.
The book "Gangs of Four" reports that the template is also known under the name "virtual designer", and this is not in vain. In "C ++" virtual is called a function that is redefined in derived classes. The ability to declare a virtual constructor language does not, and it is possible that it was an attempt to simulate the desired behavior led to the invention of this pattern.
Polymorphic object creation
As a classic example of the benefits of a pattern, consider the case where
different types in the hierarchy have identical implementations of the same method except for the object that is created and used in this method . As a solution, it is proposed to create this object in a separate method and implement it separately, and to raise the general method higher in the hierarchy. Thus, different types will use the general implementation of the method, and the object necessary for this method will be created polymorphically.
For example, back to our controllers for displaying vehicles:
final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { }
And suppose that a certain entity is used to display them, for example, a
coordinator who represents these controllers modally from another controller:
protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() }
At the same time, the
start()
method is always used in the same way, except that different controllers are created in it:
final class BicycleCoordinator: Coordinator { weak var presentingViewController: UIViewController? func start() { let vc = BicycleViewController() presentingViewController?.present(vc, animated: true) } } final class ScooterCoordinator: Coordinator { weak var presentingViewController: UIViewController? func start() { let vc = ScooterViewController() presentingViewController?.present(vc, animated: true) } }
The proposed solution is to put the creation of the used object into a separate method:
protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() func makeViewController() -> UIViewController }
And the main method is to provide the base implementation:
extension Coordinator { func start() { let vc = makeViewController() presentingViewController?.present(vc, animated: true) } }
Specific types in this case will take the form:
final class BicycleCoordinator: Coordinator { weak var presentingViewController: UIViewController? func makeViewController() -> UIViewController { return BicycleViewController() } } final class ScooterCoordinator: Coordinator { weak var presentingViewController: UIViewController? func makeViewController() -> UIViewController { return ScooterViewController() } }
Conclusion
I tried to cover this simple topic by combining three approaches:
- the classical declaration of the existence of admission, inspired by the book "Gangs of Four";
- use motivation, blatantly inspired by the book Kerivski;
- application on the example of a branch of programming close to me.
At the same time, I tried to be as close to the textbook template structure as possible, without destroying the principles of the modern approach to developing for the iOS system and using the capabilities of the Swift language (instead of the more common C ++ and Java).
As it turned out, to find detailed materials on the topic containing application examples is quite difficult. Most of the existing articles and manuals contain only superficial reviews and abbreviated examples, which are already quite truncated compared to the textbook versions of implementations.
I hope, at least in part, I managed to achieve my goals, and the reader, at least in part, was interested or at least curious to learn or refresh my knowledge on this topic.
My other materials on the topic of design patterns:And this is a link to my Twitter, where I post links to my essays, and a little more.