Os padrões de projetos ajudam estruturar programas de maneiras mais flexíveis, fáceis de entender e manter. Através deles sistemas são construídos com boas qualidades de design orientado à objetos(OO). Eles nos fornecem soluções gerais para problemas de projeto. Esse artigo explica o padrão factory method mostrando seu uso e estrutura aplicada no contexto de uma loja de bolo.
Uma fábrica tem a responsabilidade de criar objetos, o padrão método de fábrica (factory method) oferece uma maneira de encapsular a criação de objetos.
Vamos analisar uma implementação para fazer o pedido de um bolo:
Cake orderCake(String type) {
Cake cake;
if (type.equals("chocolate")) {
cake = new ChocolateCake();
} else if (type.equals("carrot")) {
cake = new CarrotCake();
}
cake.prepare();
cake.bake();
cake.cut();
cake.box();
return cake;
}
A implementação acima se baseia no tipo de bolo para instanciar uma determinada classe concreta e atribuí à variável de instância de bolo. Observe que cada bolo tem que implementar a interface Cake. Nos padrões de projetos, quando se fala "implementar uma interface" nem sempre significa "escrever uma classe que implemente uma interface Java usando a palavra-chave" 'implements' na declaração de classe". No uso geral da frase, uma classe concreta que implementa um método a partir de um supertipo (que poderia ser uma classe ou uma interface) ainda é considerada "implementando a interface" desse supertipo.
Quando tivermos um Cake, é realizada a preparação (misturar os ingredientes para fazer a massa, colocar a cobertura), depois o bolo é assado, cortado e encaixotado.
Depois de um tempo, é necessário adicionar dois novos tipos de bolo no menu: bolo de limão (LemonCake) e coco (CoconuteCake). Então, o código precisa ser alterado para ter esses itens novos:
Cake orderCake(String type) {
Cake cake;
if (type.equals("chocolate")) {
cake = new ChocolateCake();
} else if (type.equals("carrot")) {
cake = new CarrotCake();
} else if (type.equals("lemon")) {
cake = new LemonCake();
} else if (type.equals("coconute")) {
cake = new CoconuteCake();
}
cake.prepare();
cake.bake();
cake.cut();
cake.box();
return cake;
}
Observe que esta classe NÃO está fechada para modificações. Se a fábrica de bolo alterar suas ofertas de bolo, é preciso entrar nesse código e modificá-lo.
Podemos perceber também que existe uma parte do código que pode variar. Nesse exemplo, é a seleção de bolos que muda com o tempo e precisará ser modificada muitas vezes.
Em contrapartida, preparar, assar, cortar e encaixotar um bolo é a mesma coisa há anos. Dessa forma, não é esperado que essa parte de código mude, apenas os bolos nos quais ele opera.
É evidente que essa implementação não está flexível para realizar alteração, pois não aplica o princípio de Orientaçõa à Objetos: as classes devem estar abertas para extensão, mas fechadas para a modificação.
Ao conhecer o que varia e o que não, podemos refatorar o código e encapsular a parte que varia. Em nosso caso de uso, vamos encapsular a criação de objetos através do padrão método de fábrica.
O diagrama de classes do padrão Factory Method é mostrado a seguir:
Aplicando o padrão Factory Method na loja de bolo:
Na fábrica de bolos o produto produzido é o bolo. Os produtos concretos fabricados pela loja de bolo são: ChocolateCake, CarrotCake, LemonCake e CoconuteCake.
Cake Store é a classe criador abstrata. Ela define um método de fábrica abstrato que a subclasse (DeliciousCakes) implementa para fabricar produtos. O método createCake() é nosso método fábrica.
O método de fábrica oferece um framework fornecendo um método orderCake(), que é combinado com um método de fábrica:
public Cake orderCake(String type) {
Cake cake;
cake = createCake(type);
cake.prepare();
...
}
Outra maneira de ver esse padrão como um framework está no modo como ele encapsula o conhecimento sobre o produto em cada criador.
Se compararmos as dependências de objetos na implementação que não encapsula a criação de objetos com a implementação após a refatoração utilizando o padrão método fábrica, temos o seguinte diagrama:
Na versão muito dependente, CakeStore depende dos objetos: ChocolateCake, CarrotCake, LemonCake e CoconuteCake, pois está criando os diretamente. Enquanto que na versão aplicando o método Factory, tanto CakeStore (componente de alto nível) quanto os componentes de baixo nível (ChocolateCake, CarrotCake, LemonCake e CoconuteCake) dependem apenas da classe abstrata, Cake.
Para reduzir as dependências a classes concretas, o padrão método fábrica utiliza o Princípio da Inversão de Dependência. Esse princípio nos traz o seguinte conhecimento: depende de abstrações, não depende de classes concretas. Ele sugere que nossos componentes de alto nível não dependam de nossos componentes de nível inferior; os dois devem depender de abstrações.
Um componente de "alto nível" é uma classe com comportamento definido em termos de outros componentes de "baixo nível".
Código da classe CakeStore:
import com.programacao4devs.factorymethod.product.Cake;
public abstract class CakeStore {
protected abstract Cake createCake(String type);
public Cake orderCake(String type) {
Cake cake;
cake = createCake(type);
cake.prepare();
cake.bake();
cake.cut();
cake.box();
return cake;
}
}
Código da classe DeliciousCakes:
public class DeliciousCakes extends CakeStore {
@Override
protected Cake createCake(String type) {
if (type.equals("chocolate")) {
return new ChocolateCake();
} else if (type.equals("carrot")) {
return new CarrotCake();
} else if (type.equals("lemon")) {
return new LemonCake();
} else if (type.equals("coconut")) {
return new CoconutCake();
} else {
return null;
}
}
}
Código da classe Cake:
public abstract class Cake {
protected String name;
protected String dough;
protected ArrayList toppings = new ArrayList();
public void prepare() {
System.out.println("Preparing " + name);
System.out.println("Tossing dough...");
System.out.println("Adding toppings: ");
for (int i = 0; i < toppings.size(); i++) {
System.out.println(" " + toppings.get(i));
}
}
public void bake() {
System.out.println("Bake for 40 minutes at 180");
}
public void cut() {
System.out.println("Cutting the cake into square slices");
}
public void box() {
System.out.println("Place cake in official CakeStore box");
}
public String getName() {
return name;
}
}
Código da classe ChocolateCake:
import com.programacao4devs.factorymethod.product.Cake;
public class ChocolateCake extends Cake {
public ChocolateCake() {
name = "Cake of Chocolate";
dough = "Chocolate Dough";
toppings.add("Chocolate Topping");
}
}
Código da classe ApplicationCakeStore para executar o teste:
public class ApplicationCakeStore {
public static void main(String[] args) {
CakeStore cakeStore = new DeliciousCakes();
Cake cake = cakeStore.orderCake("chocolate");
System.out.println("Order placed for a " + cake.getName() );
}
}
Resultado mostrado no Console:
Preparing Cake of Chocolate
Tossing dough...
Adding toppings:
Chocolate Topping
Bake for 40 minutes at 180
Cutting the cake into square slices
Place cake in official CakeStore box
Order placed for a Cake of Chocolate
Download do código completo em: https://github.com/programacao4devs/padrao-factory-method
Os princípios OO utilizados no padrão factory method:
Inscreva seu nome e email na Programacao4Devs para receber atualizações de novos artigos, tutoriais e dicas.
Sua inscrição foi registrada. Obrigado!