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:

  • Encapsule o que varia;
  • Programa para interface, não para implementação;
  • Utilize baixo acoplamento (objetos levemente ligados);
  • As classes devem estar abertas à extensão, mas fechadas à modificação;
  • Dependa de abstrações. Não dependa de classes concretas.

Inscreva-se!

Inscreva seu nome e email na Programacao4Devs para receber atualizações de novos artigos, tutoriais e dicas.

Paulo Henrique de Morais

Mestre em Ciências da Computação pela Universidade Federal de Santa Catarina e atua no mercado de programação desde 2015.