La palabra "fábrica" es, con mucho, una de las más utilizadas por los programadores cuando discuten sus (u otros) programas. Pero el significado incrustado en él puede ser muy diferente: puede ser una clase que genera objetos (polimórficos o no); y un método que crea instancias de cualquier tipo (estáticas o no); sucede, e incluso cualquier método de generación (incluidos los
constructores ).
Por supuesto, no todo lo que genera instancias de algo puede llamarse la palabra "fábrica". Además, bajo esta palabra se pueden ocultar dos patrones generativos diferentes del arsenal Gang of Four: el
"método de fábrica" y la
"fábrica abstracta" , cuyos detalles me gustaría profundizar un poco, prestando especial atención a su comprensión e implementación clásicas.
Y me inspiró para escribir este ensayo de
Joshua Kerivsky (jefe de
"Lógica industrial" ), o mejor dicho, su libro
"Refactoring to Patterns" , que se publicó a principios de siglo como parte de una serie de libros fundados por
Martin Fowler (un famoso autor del clásico de programación moderna: el libro
" Refactorización " ). Si alguien no ha leído o incluso no ha oído hablar del primero (y sé muchos de estos), asegúrese de agregarlo a su lista de lectura. Esta es una secuela digna de Refactorización y un libro aún más clásico,
Objective Design Techniques. Patrones de diseño " .
El libro, entre otras cosas, contiene docenas de recetas para deshacerse de varios
"olores" en el código utilizando
patrones de diseño . Incluyendo tres (al menos) "recetas" sobre el tema en discusión.
Fábrica abstracta
Kerivsky en su libro da dos casos en los que el uso de esta plantilla será útil.
El primero es la
encapsulación del conocimiento sobre clases específicas conectadas por una interfaz común. En este caso, solo el tipo que es la fábrica tendrá este conocimiento.
La API pública de la fábrica constará de un conjunto de métodos (estáticos o no) que devuelven instancias de un tipo de interfaz común y tienen algunos nombres "parlantes" (para comprender qué método debe llamarse para un propósito particular).
El segundo ejemplo es muy similar al primero (y, en general, todos los escenarios para usar el patrón son más o menos similares entre sí). Este es el caso cuando se crean instancias de uno o más tipos del mismo grupo en diferentes lugares del programa. En este caso, la fábrica vuelve a resumir el conocimiento sobre el código que crea las instancias, pero con una motivación ligeramente diferente. Por ejemplo, esto es especialmente cierto si el proceso de creación de instancias de estos tipos es complejo y no se limita a llamar al constructor.
Para estar más cerca del tema de desarrollo en
"iOS" , es conveniente practicar subclases de
UIViewController
. De hecho, este es definitivamente uno de los tipos más comunes en el desarrollo "iOS", casi siempre se "hereda" antes de su uso, y una subclase particular a menudo ni siquiera es importante para el código del cliente.
Intentaré mantener los ejemplos de código lo más cerca posible de la implementación clásica del libro Gang of Four, pero en la vida real el código a menudo se simplifica de una forma u otra. Y solo una comprensión suficiente de la plantilla abre la puerta a su uso más gratuito.
Ejemplo detallado
Supongamos que estamos intercambiando vehículos en una aplicación, y el mapeo depende del tipo de vehículo específico: utilizaremos diferentes subclases de
UIViewController
para diferentes vehículos. Además, todos los vehículos difieren en estado (nuevos y usados):
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.") } }
Por lo tanto, tenemos una familia de objetos del mismo grupo, instancias de tipos de los cuales se crean en los mismos lugares dependiendo de alguna condición (por ejemplo, el usuario hizo clic en un producto de la lista, y dependiendo de si es un scooter o una bicicleta, nosotros crear el controlador apropiado). Los constructores de controladores tienen algunos parámetros que también deben establecerse cada vez. ¿Son estos dos argumentos a favor de crear una "fábrica" que solo tenga conocimiento de la lógica para crear el controlador correcto?
Por supuesto, el ejemplo es bastante simple, y en un proyecto real en un caso similar, la introducción de una "fábrica" será explícitamente
"sobre ingeniería" . Sin embargo, si imaginamos que no tenemos dos tipos de vehículos y los diseñadores tienen más de un parámetro, las ventajas de la "fábrica" serán más evidentes.
Entonces, declaremos una interfaz que desempeñará el papel de una "fábrica abstracta":
protocol VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController func makeScooterViewController() -> UIViewController }
(Una
"guía de diseño" bastante corta
para API en
Swift recomienda llamar a los métodos de fábrica que comienzan con la palabra make).
(Un ejemplo en el libro de las pandillas de cuatro se da en
"C ++" y se basa en la
herencia y
las funciones "virtuales" . Utilizando "Swift", por supuesto, estamos más cerca del paradigma de programación orientado al protocolo).
La interfaz de fábrica abstracta contiene solo dos métodos: crear controladores para vender bicicletas y scooters. Los métodos devuelven instancias no de subclases específicas, sino de una clase base común. Por lo tanto, el alcance del conocimiento sobre tipos específicos se limita al área en la que es realmente necesario.
Como "fábricas concretas" utilizaremos dos implementaciones de la interfaz de fábrica abstracta:
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) } }
En este caso, como se puede ver en el código, las fábricas específicas son responsables de los vehículos de diferentes condiciones (nuevos y usados).
Crear el controlador correcto ahora se verá más o menos así:
let factory: VehicleViewControllerFactory = NewVehicleViewControllerFactory() let vc = factory.makeBicycleViewController()
Clases de encapsulación de fábrica
Ahora repase brevemente los casos de uso que Kerivsky ofrece en su libro.
El primer caso está relacionado con la
encapsulación de clases específicas . Por ejemplo, tome los mismos controladores para mostrar datos sobre vehículos:
final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { }
Supongamos que estamos tratando con un módulo separado, por ejemplo, una biblioteca de complementos. En este caso, las clases declaradas anteriormente permanecen (por defecto)
internal
, y la fábrica
internal
como la "API" pública de la biblioteca, que en sus métodos devuelve las clases base de los controladores, dejando así conocimiento sobre subclases específicas dentro de la biblioteca:
public struct VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController() } func makeScooterViewController() -> UIViewController { return ScooterViewController() } }
Mover conocimiento sobre la creación de un objeto dentro de una fábrica
El segundo "caso" describe la
compleja inicialización del objeto , y Kerivsky, como una de las formas de simplificar el código y proteger los principios de encapsulación, sugiere restringir la difusión del conocimiento sobre el proceso de inicialización fuera de la fábrica.
Supongamos que quisiéramos vender autos al mismo tiempo. Y esta es, sin duda, una técnica más compleja, con un mayor número de características. Por ejemplo, nos restringimos al tipo de combustible utilizado, el tipo de transmisión y el tamaño de la llanta:
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.") } }
Un ejemplo de inicialización del controlador correspondiente:
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)
Podemos poner la responsabilidad de todas estas "pequeñas cosas" en los "hombros" de una fábrica especializada:
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) } }
Y cree el controlador de esta manera:
let factory = UsedCarViewControllerFactory(engineType: .gas, transmissionType: .manual, wheelDiameter: 17) let vc = factory.makeCarViewController()
Método de fábrica
La segunda plantilla de "raíz única" también encapsula el conocimiento sobre tipos específicos generados, pero no ocultando este conocimiento dentro de una clase especializada, sino por polimorfismo. Kerivsky en su libro da ejemplos en
Java y sugiere usar
clases abstractas , pero los habitantes del universo Swift no están familiarizados con este concepto. Tenemos nuestra propia atmósfera aquí ... y protocolos.
El libro "Gangs of Four" informa que la plantilla también se conoce como el "constructor virtual", y esto no es en vano. En "C ++", virtual es una función que se redefine en clases derivadas. El lenguaje no le da al diseñador la oportunidad de declarar virtual, y es posible que haya sido un intento de imitar el comportamiento deseado que llevó a la invención de este patrón.
Creación de objetos polimórficos
Como un ejemplo clásico de la utilidad de la plantilla, consideramos el caso cuando
en la jerarquía diferentes tipos tienen la implementación idéntica de un método con la excepción del objeto que se crea y utiliza en este método . Como solución, se propone crear este objeto en un método separado e implementarlo por separado, y elevar el método general más arriba en la jerarquía. Por lo tanto, diferentes tipos utilizarán la implementación general del método, y el objeto necesario para este método se creará polimórficamente.
Por ejemplo, volvamos a nuestros controladores para mostrar vehículos:
final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { }
Y suponga que se usa una determinada entidad para mostrarlos, por ejemplo, un
coordinador , que representa estos controladores de manera modal desde otro controlador:
protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() }
El método
start()
siempre se usa de la misma manera, excepto que crea diferentes controladores:
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) } }
La solución propuesta es hacer la creación del objeto usado en un método separado:
protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() func makeViewController() -> UIViewController }
Y el método principal es proporcionar la implementación básica:
extension Coordinator { func start() { let vc = makeViewController() presentingViewController?.present(vc, animated: true) } }
Los tipos específicos en este caso tomarán la forma:
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() } }
Conclusión
Traté de cubrir este tema simple combinando tres enfoques:
- la declaración clásica de la existencia de la recepción, inspirada en el libro "Gangs of Four";
- motivación para el uso, abiertamente inspirada en el libro de Kerivsky;
- aplicación aplicada como ejemplo de una industria de programación cercana a mí.
Al mismo tiempo, intenté estar lo más cerca posible de la estructura de los libros de texto de las plantillas, en la medida de lo posible, sin destruir los principios del enfoque moderno del desarrollo para el sistema iOS y utilizando las capacidades del lenguaje Swift (en lugar de los más comunes C ++ y Java).
Al final resultó que, es bastante difícil encontrar materiales detallados sobre el tema que contiene ejemplos aplicados. La mayoría de los artículos y manuales existentes contienen solo revisiones superficiales y ejemplos resumidos, que ya están bastante truncados en comparación con las versiones de libros de texto de implementaciones.
Espero que al menos parcialmente logré mis objetivos, y el lector, al menos parcialmente, estaba interesado o al menos curioso por aprender o actualizar mis conocimientos sobre este tema.
Mis otros materiales sobre patrones de diseño:Y este es un enlace a mi "Twitter", donde publico enlaces a mis ensayos y un poco más que eso.