Método de fábrica e fábrica abstrata no universo Swift e iOS

A palavra "fábrica" ​​é de longe uma das mais frequentemente usadas pelos programadores ao discutir seus programas (ou outros). Mas o significado embutido nela pode ser muito diferente: pode ser uma classe que gera objetos (polimórficos ou não); e um método que cria instâncias de qualquer tipo (estático ou não); isso acontece e até mesmo qualquer método de geração (incluindo construtores ).

Obviamente, nem tudo que gera instâncias de algo pode ser chamado de "fábrica". Além disso, sob essa palavra, dois padrões geradores diferentes do arsenal da Gangue dos Quatro podem ser ocultados - o “método de fábrica” e a “fábrica abstrata” , cujos detalhes gostaria de me aprofundar um pouco, prestando atenção especial à sua compreensão e implementação clássicas.

E fui inspirado a escrever este ensaio por Joshua Kerivsky (chefe da “Industrial Logic” ), ou melhor, seu livro “Refatoração para padrões” , publicado no início do século como parte de uma série de livros fundados por Martin Fowler (um famoso autor de clássicos da programação modernos - o livro “ Refatoração " ). Se alguém não leu ou ouviu falar do primeiro (e eu conheço muitos deles), adicione-o à sua lista de leitura. Esta é uma sequência digna de refatoração e um livro ainda mais clássico, Objective Design Techniques. Padrões de design " .

O livro, entre outras coisas, contém dezenas de receitas para se livrar de vários "cheiros" no código usando padrões de design . Incluindo três (pelo menos) “receitas” no tópico em discussão.

Fábrica abstrata


Kerivsky em seu livro apresenta dois casos em que o uso desse modelo será útil.

O primeiro é o encapsulamento de conhecimento sobre classes específicas conectadas por uma interface comum. Nesse caso, apenas o tipo que é a fábrica terá esse conhecimento. A API pública da fábrica consistirá em um conjunto de métodos (estáticos ou não) que retornam instâncias de um tipo de interface comum e têm alguns nomes "falantes" (para entender qual método precisa ser chamado para uma finalidade específica).

O segundo exemplo é muito semelhante ao primeiro (e, em geral, todos os cenários para usar o padrão são mais ou menos semelhantes entre si). É o caso de instâncias de um ou mais tipos do mesmo grupo em locais diferentes do programa. Nesse caso, a fábrica novamente encapsula o conhecimento sobre o código que cria as instâncias, mas com uma motivação um pouco diferente. Por exemplo, isso é especialmente verdadeiro se o processo de criação de instâncias desses tipos for complexo e não se limitar a chamar o construtor.

Para estar mais próximo do tópico de desenvolvimento em "iOS" , é conveniente praticar subclasses do UIViewController . De fato, esse é definitivamente um dos tipos mais comuns no desenvolvimento "iOS", quase sempre é "herdado" antes do uso, e uma subclasse específica nem sempre é importante para o código do cliente.
Tentarei manter os exemplos de código o mais próximo possível da implementação clássica do livro Gang of Four, mas na vida real o código é frequentemente simplificado de uma maneira ou de outra. E apenas uma compreensão suficiente do modelo abre as portas para seu uso mais livre.

Exemplo detalhado


Suponha que estamos trocando veículos em um aplicativo e o mapeamento depende do tipo de veículo específico: usaremos subclasses diferentes do UIViewController para veículos diferentes. Além disso, todos os veículos diferem em estado (novos e 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.") } } 

Assim, temos uma família de objetos de um grupo, instâncias de tipos criadas nos mesmos locais, dependendo de alguma condição (por exemplo, o usuário clicou em um produto da lista e, dependendo se é uma scooter ou uma bicicleta, nós crie o controlador apropriado). Os construtores de controladores têm alguns parâmetros que também precisam ser definidos a cada vez. Esses dois argumentos são a favor da criação de uma "fábrica" ​​que sozinha terá conhecimento da lógica para criar o controlador correto?

Obviamente, o exemplo é bastante simples e, em um projeto real em um caso semelhante, a introdução de uma "fábrica" ​​será explícita "superengenharia" . No entanto, se imaginarmos que não temos dois tipos de veículos e os projetistas tiverem mais de um parâmetro, as vantagens da “fábrica” se tornarão mais óbvias.

Então, vamos declarar uma interface que desempenhará o papel de uma "fábrica abstrata":

 protocol VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController func makeScooterViewController() -> UIViewController } 

(Uma "diretriz" bastante curta para projetar uma "API" no Swift recomenda chamar métodos de fábrica que começam com a palavra make.)

(Um exemplo no livro de quadrilhas de quatro é dado em "C ++" e é baseado em funções de herança e "virtuais" . Usando "Swift", é claro, o paradigma de programação orientado a protocolo está mais próximo de nós.)

A interface abstrata de fábrica contém apenas dois métodos: criar controladores para vender bicicletas e scooters. Os métodos retornam instâncias não de subclasses específicas, mas de uma classe base comum. Assim, o escopo do conhecimento sobre tipos específicos é limitado à área em que é realmente necessário.

Como "fábricas de concreto", usaremos duas implementações da interface abstrata de fábrica:

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

Nesse caso, como pode ser visto no código, fábricas específicas são responsáveis ​​por veículos de diferentes condições (novas e usadas).

Criar o controlador correto agora será algo como isto:

 let factory: VehicleViewControllerFactory = NewVehicleViewControllerFactory() let vc = factory.makeBicycleViewController() 

Classes de encapsulamento de fábrica


Agora repare brevemente os casos de uso que Kerivsky oferece em seu livro.

O primeiro caso está relacionado ao encapsulamento de classes específicas . Por exemplo, use os mesmos controladores para exibir dados sobre veículos:

 final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { } 

Suponha que estamos lidando com um módulo separado, por exemplo, uma biblioteca de plug-ins. Nesse caso, as classes declaradas acima permanecem (por padrão) internal , e a fábrica internal como a "API" pública da biblioteca, que em seus métodos retorna as classes base dos controladores, deixando conhecimento sobre subclasses específicas dentro da biblioteca:

 public struct VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController() } func makeScooterViewController() -> UIViewController { return ScooterViewController() } } 

Movendo o conhecimento sobre a criação de um objeto dentro de uma fábrica


O segundo "caso" descreve a inicialização complexa do objeto , e Kerivsky, como uma das maneiras de simplificar o código e proteger os princípios do encapsulamento, sugere restringir a disseminação do conhecimento sobre o processo de inicialização fora da fábrica.

Suponha que desejássemos vender carros ao mesmo tempo. E essa é sem dúvida uma técnica mais complexa, com maior número de características. Por exemplo, nos restringimos ao tipo de combustível usado, ao tipo de transmissão e ao tamanho da jante:

 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.") } } 

Um exemplo de inicialização do controlador correspondente:

 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 colocar a responsabilidade por todas essas "pequenas coisas" nos ombros de uma 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) } } 

E crie o controlador desta maneira:

 let factory = UsedCarViewControllerFactory(engineType: .gas, transmissionType: .manual, wheelDiameter: 17) let vc = factory.makeCarViewController() 

Método de fábrica


O segundo modelo de "raiz única" também engloba o conhecimento sobre tipos gerados específicos, mas não ocultando esse conhecimento em uma classe especializada, mas por polimorfismo. Kerivsky em seu livro dá exemplos em Java e sugere o uso de classes abstratas , mas os habitantes do universo Swift não estão familiarizados com esse conceito. Temos nossa própria atmosfera aqui ... e protocolos.
O livro "Gangs of Four" relata que o modelo também é conhecido como "construtor virtual", e isso não é em vão. Em "C ++", virtual é uma função redefinida em classes derivadas. A linguagem não dá ao designer a oportunidade de declarar virtual, e é possível que tenha sido uma tentativa de imitar o comportamento desejado que levou à invenção desse padrão.

Criação de objeto polimórfico


Como um exemplo clássico da utilidade do modelo, consideramos o caso em que na hierarquia diferentes tipos têm a implementação idêntica de um método, exceto o objeto que é criado e usado nesse método . Como solução, propõe-se criar esse objeto em um método separado, implementá-lo separadamente e elevar o método geral mais alto na hierarquia. Assim, tipos diferentes usarão a implementação geral do método, e o objeto necessário para esse método será criado polimorficamente.

Por exemplo, voltemos aos nossos controladores para exibir veículos:

 final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { } 

E suponha que uma determinada entidade seja usada para exibi-los, por exemplo, um coordenador , que representa esses controladores modalmente de outro controlador:

 protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() } 

O método start() é sempre usado da mesma maneira, exceto que ele cria controladores diferentes:

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

A solução proposta é fazer a criação do objeto usado em um método separado:

 protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() func makeViewController() -> UIViewController } 

E o método principal é fornecer a implementação básica:

 extension Coordinator { func start() { let vc = makeViewController() presentingViewController?.present(vc, animated: true) } } 

Tipos específicos nesse caso terão o formato:

 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() } } 

Conclusão


Tentei cobrir esse tópico simples combinando três abordagens:

  • a clássica declaração da existência da recepção, inspirada no livro “Gangs of Four”;
  • motivação para o uso, inspirada abertamente no livro de Kerivsky;
  • aplicação aplicada como um exemplo de uma indústria de programação perto de mim.

Ao mesmo tempo, tentei me aproximar o máximo possível da estrutura dos livros didáticos dos modelos, sem destruir os princípios da abordagem moderna de desenvolvimento para o sistema iOS e usar os recursos da linguagem Swift (em vez de C ++ e Java mais comuns).

Como se viu, é bastante difícil encontrar materiais detalhados sobre o tópico que contém exemplos aplicados. A maioria dos artigos e manuais existentes contém apenas revisões superficiais e exemplos resumidos, que já são bastante truncados em comparação com as versões de implementações de livros didáticos.

Espero que pelo menos parcialmente tenha sido capaz de atingir meus objetivos e que o leitor - pelo menos parcialmente esteja interessado ou pelo menos curioso em aprender ou atualizar meu conhecimento sobre esse tópico.

Meus outros materiais sobre padrões de design:


E este é um link para o meu “Twitter”, onde publico links para meus ensaios e um pouco mais do que isso.

Source: https://habr.com/ru/post/pt451324/


All Articles