1. Simple Factory (Fábrica Simples)
1.1. Definição
📘Simple Factory permite encapsular a criação de objetos em uma classe que representa uma fábrica. Ela não é de fato um padrão de projeto, podendo ser definida como um modelo para a criação de objetos.
1.2. Aplicabilidade
Sabemos que para instanciar objetos em Java e outras linguagens é utilizado o operador new
.
Apesar de podermos declarar uma variável com um tipo abstrato, ao instanciar um objeto para tal variável,
estamos definindo explicitamente qual tipo concreto será usado.
Se existir uma lógica que define qual tipo concreto será utilizado em um determinado momento e tivermos que
utilizar esta lógica em diversas partes do código quando precisarmos instanciar um objeto, podemos utilizar
a Simple Factory para encapsular a criação destes objetos. Com isto, evitamos de duplicar esta lógica em
vários locais do projeto.
Esta lógica normalmente é definida por um bloco de if()
que define as condições para a criação dos objetos.
Adicionalmente, considere que temos um tipo abstrato/super-tipo (classe abstrata ou interface) e várias sub-classes que podem ser instanciados.
Se utilizarmos new
para instanciar uma determinada sub-classe em diferentes locais do sistema e precisarmos
substituir estas instâncias por uma sub-classe diferente, teríamos que fazer isso manualmente alterando
os vários locais no código. Utilizando a Simple Factory, a criação de objetos de uma determinada sub-classe
fica encapsulada e centralizada em um método da Simple Factory.
1.3. Modelagem
A Simple Factory pode ser modelada como demonstrado no diagrama a seguir.
Teremos uma classe SimpleFactory
que você deve dar um nome apropriado.
Alguns exemplos incluem:
-
SimplePizzaFactory
ouPizzaFactory
para criar pizzas; -
SimpleProdutoFactory
ouProdutoFactory
para criar produtos.
Cliente
é uma classe qualquer que precisa utilizar objetos produzidos pela Factory.
Assim, podemos ter várias classes clientes.
O termo "cliente", amplamente utilizado quando falamos de padrões de projeto e POO, representa uma classe que usa os serviços de outra classe, como uma pessoa que usa os serviços de uma empresa. |
O TipoAbstrato
é um super tipo representado por uma interface ou classe abstrata.
Como ele é abstrato, a Factory criará objetos de algum dos sub-tipos disponíveis,
de acordo com uma possível lógica que você definir.
Os métodos newObject
da SimpleFactory
retornam um TipoAbstrato
, mas internamente
criam um objeto de algum dos sub-tipos existentes. Este método é normalmente definido como estático,
assim, não temos que instanciar a fábrica para depois podermos instanciar os objetos que queremos.
Observe que pode haver duas versões de tal método. A versão que não recebe parâmetros normalmente vai
sempre retornar um tipo específico de objeto. Ao usar tal método em várias partes do sistema,
se precisarmos trocar qual o tipo de objeto a ser instanciado, temos que apenas alterar este método.
Os locais onde ele estava sendo usado não sofrem nenhuma alteração.
A outra versão do método que possui parâmetros (cujo quantidade e tipos depende do seu problema)
normalmente vai permitir, ao desenvolvedor que for usar a Factory, informar qual objeto
a ser criado e possíveis dados a serem usados para criá-lo, sem especificar
qual classe será instanciada. Adicionalmente, se existir uma determinada lógica necessária
para criar qualquer dos tipos de objeto possíveis, tal lógica ficará encapsulada
dentro destes métodos newObject
. Qualquer alteração neste lógica precisará ser feita
em apenas um local.
Um projeto de exemplo para o diagrama apresentado está disponível aqui. Ele deve ser alterado para incluir as mudanças necessárias para o problema específico que você estiver resolvendo com o padrão.
1.4. Princípios utilizados
1.4.1. Open-Closed Principle
Com o Open/Closed Principle (OCP), separamos as partes que mudam, i.e. a lógica de criação de objetos, de dentro da classe que utiliza tais objetos. Isto torna a classe que utiliza a factory "aberta para extensão e fechada para modificação".
1.4.2. Programar para uma "interface" não uma implementação
Programar para uma "interface" não uma implementação (GoF) indica que devemos declar variáveis com tipos abstratos (TipoAbstrato
) e usar tipos concretos apenas na instanciação (SubTipo1
… SubTipoN
).
1.4.3. Dependency Inversion Principle (DIP)
Ao aplicar o Dependency Inversion Principle (DIP), a classe cliente que utiliza a factory não depende mais de tipos concretos, mas sim de tipos abstratos (interfaces ou classes abstratas). Se não utilizássemos a Simple Factory, a classe cliente seria responsável pela lógica de criação dos objetos, dependendo de cada um dos objetos que pode criar, como mostra o diagrama a seguir.
Se observarmos a Figura 1 na Seção 1.3, podemos ver que a classe Cliente
depende da SimpleFactory
, que depende do TipoAbstrato
.
Como Cliente
usará objetos do TipoAbstrato
, ele depende de tal tipo. Então, podemos esquecer a SimpleFactory
por um momento e visualizar esta dependência como no diagrama a seguir.
Comparando a Figura 2 com a 3, podemos ver que a relação de dependência foi invertida. A classe Cliente
deixou de depender de tipos concretos e passou a depender de um tipo abstrato.
1.5. Exemplos
Podemos utilizar a Simple Factory para definir qual estratégia de leitura de arquivo de retorno de boleto bancário será utilizada. Assim, não teremos que manualmente decidir qual estratégia instanciar cada vez que precisarmos ler arquivos de retorno. Baseado no projeto retorno-boleto-template-funcional, vamos implementar a Simple Factory.
Implemente o projeto seguindo a modelagem abaixo:
1.6. Detalhes de Implementação
A forma mais fácil de implementar a Simple Factory, como apresentado na Figura 1, é definir o método newObject
na classe SimpleFactory
como estático.
Assim, não é preciso instanciar uma fábrica para depois criar objetos, basta chamar o método diretamente da classe
fazendo SimpleFactory.newObject()
. Isto torna o uso da fábrica mais fácil.
Se a fábrica não armazena nenhum atributo, pode não haver uma razão específica para exigir que ela seja instanciada
para podermos chamar seus métodos. É apenas preciso ter em mente que ao declarar o método como estático,
ele não poderá ser modificado em possíveis sub-classes. Se não pretende fazer override do método, não há problema.
O método newObject
também pode receber parâmetros para definir qual e/ou como um objeto será criado.
O nome do método também deve ser definido adequadamente, como newPizza()
, newProduto()
, etc.
2. Padrões Relacionados
Padrões que possuem similaridades ou podem ser usados em conjunto:
3. Onde o padrão é usado no JDK
3.1. Calendar
A classe Calendar
no JDK é abstrata, logo, não pode ser instanciada.
Existem diferentes tipos de calendário que podem ser instanciados, de acordo
com a região geográfica definida para o sistema (Locale
).
O método getInstance()
então é responsável por definir qual subclasse de Calendar
será usada para instanciar um calendário.
Ele utiliza o método auxiliar createCalendar
para isto. Como falado
anteriormente, neste caso o método possui parâmetros para definir como criar o objeto.
Note que dentro do método ele define diferentes tipos de calendário, como gregorianos (o nosso), japonês e budista.
3.2. JCF no JDK 9+
A partir do JDK 9, as classes Collection introduziram os chamados "Factory Methods". Apesar do nome ser o de outro padrão que veremos à frente, tal padrão não é implementado aqui. O que temos é simplesmente uma Simple Fatory.
A expressão Factory Method expressão é confusa pois pode se referir à:
-
um padrão com este mesmo nome (não é o caso), que veremos a seguir;
-
ou simplesmente a um método fábrica em uma classe/interface responsável por instanciar objetos. Qualquer tipo de fábrica terá um método fábrica.
List<String> listaNomes = List.of("Minha", "Lista", "de", "Strings");
Map<String, Integer> mapaHabitantesCidade =
Map.of("Palmas", 217000, "Gurupi", 84000);
3.3. Falando de JDK, pula pro JDK 10
var listaNomes = List.of("Minha", "Lista", "de", "Strings");
var listaPares = List.of(2, 4, 6);
var mapaHabitantesCidade = Map.of("Palmas", 217000, "Gurupi", 84000);
4. Exercícios
4.1. Exportação de dados
Implemente um conjunto de classes que recebe uma lista de produtos e permite exportar tais produtos em diferentes formatos como Tabela HTML, CSV, tabela Markdown. Utilize a diagrama de classes abaixo como base para sua implementação. Tenha em mente que um diagrama desses pode apresentar os detalhes mais importantes como métodos e atributos que já se sabe previamente. No entanto, durante o desenvolvimento, pode-se perceber que são necessários mais métodos e/ou atributos. O diagrama apresenta apenas os elementos públicos e protegidos. Novos métodos que precisem ser incluídos, que serão usados pelos apresentados, normalmente são privados, exatamente por serem apenas métodos auxiliares que não fazem sentido serem chamados de fora das classes.
Observe que neste caso, não temos uma classe específica para a Simple Factory.
Temos apenas os métodos estáticos newInstance()
na interface ExportadorListaProdutos
que instanciam objetos da própria classe. Esta é a mesma modelagem utilizada pela classe Calendar
do JDK. Métodos estáticos em interfaces é um dos recursos do Java 8.
Existem inúmeras bibliotecas que realizam tal tarefa, mas o objetivo aqui é praticar. Se precisar realmente exportar dados em qualquer formato, tente utilizar uma biblioteca existente. Pode-se exportar dados em XML e JSON utilizando bibliotecas padrões do JDK. |
4.2. Tornando a exportação de dados genérica
O projeto anterior permite exportar apenas uma lista de produtos. Se precisarmos exportar outros dados como clientes ou vendas, teremos que criar um novo conjunto de classes para cada tipo de objeto que queremos exportar. Isto é bastante trabalhoso e repetitivo. Neste projeto vamos usar Reflection, um recurso avançado da linguagem Java, com o Simple Factory para conseguir exportar qualquer dado em qualquer formato implementado.