Artigo original: The 3 Types of Design Patterns All Developers Should Know (with code examples of each)

O que é um padrão de projeto?

Padrões de projeto são soluções em nível de design para problemas recorrentes que os engenheiros de software encontram com frequência. Não são código - repito, CÓDIGO. Eles são como uma descrição de como lidar com esses problemas e projetar uma solução.

Usar esses padrões é considerado uma boa prática, já que o projeto da solução foi testado muitas vezes e com sucesso, resultando em maior legibilidade do código final. Padrões de projetos são, com frequência, criados para linguagens de POO (Programação Orientada a Objetos), como o Java, e usadas por elas. Será em Java que escreveremos os exemplos que daremos daqui até o final do texto.

Tipos de padrões de projetos

Existem cerca de 26 padrões descobertos atualmente (não acho que tratarei de todos eles).

Esses 26 podem ser classificados em 3 tipos:

1. Criacionais: esses padrões foram criados para a instanciação de classes. Eles podem tanto ser padrões de criação de classes como de criação de objetos.

2. Estruturais: esses padrões foram criados levando em conta a estrutura de uma classe e sua composição. O objetivo principal da maioria desses padrões é aumentar a funcionalidade das classes envolvidas, sem alterar muito de sua composição.

3. Comportamentais: esses padrões foram criados levando em conta a forma como uma classe se comunica com as outras.

Neste artigo, examinarei um padrão de projeto básico para cada tipo classificado.

Tipo 1: Criacional – o padrão de projeto Singleton

O padrão de projeto Singleton é um padrão criacional, cujo objetivo é criar apenas uma instância de uma classe e fornecer apenas um ponto global de acesso àquele objeto. Um exemplo comumente usado dessa classe em Java é o calendário, onde você não pode fazer uma instância daquela classe. Ele também usa seu próprio método getInstance()para obter o objeto a ser usado.

Uma classe usando o padrão de projeto Singleton incluirá,

singleton-class-diagram
Diagrama da classe Singleton
  1. Uma variável estática privada, que contém a única instância da classe.
  2. Um construtor privado, de modo a não ser possível instanciá-la de qualquer outro lugar.
  3. Um método estático público, para retornar a instância única da classe.

Há muitas implementações diferentes do padrão Singleton. Hoje, examinarei as implementações:

1. Instanciação ávida

2. Instanciação preguiçosa

3. Instanciação thread-safe

A ávida (Eager)

public class EagerSingleton {
	// cria uma instância da classe.
	private static EagerSingleton instance = new EagerSingleton();

	// construtor privado, para que ela não possa ser instanciada fora da classe.
	private EagerSingleton() {  }

	// obtém a única instância criada do objeto.
	public static EagerSingleton getInstance() {
		return instance;
	}
}

Esse tipo de instanciação acontece durante o carregamento da classe, já que a instância da variável acontece fora de qualquer método. Isso impõe uma desvantagem substancial se essa classe não for usada pela aplicação do client. O plano de contingência, se essa classe não for usada, é a instanciação preguiçosa.

A preguiçosa (Lazy)

Não há muita diferença com relação à implementação acima. As principais diferenças estão na variável estática, que inicialmente é declarada como null (nula), e no fato de que ela é instanciada apenas dentro do método getInstance() se e somente se a variável de instância permanecer null (nula) no momento da verificação.

public class LazySingleton {
	// inicializa a instância como null.
	private static LazySingleton instance = null;

	// construtor privado, para que ela não possa ser instanciada fora da classe.
	private LazySingleton() {  }

	// verifica se a instância é null. Se estiver, o objeto é criado.
	public static LazySingleton getInstance() {
		if (instance == null) {
			instance = new LazySingleton();
		}
		return instance;
	}
}

Isso conserta um problema, mas ainda existe outro. E se dois clients diferentes acessarem a classe Singleton ao mesmo tempo? Bem, elas verificarão se a instância está null ao mesmo tempo, verão que isso é verdade e criarão duas instâncias da classe para cada solicitação feita pelos dois clients. Para resolver isso, a instanciação Thread Safe deve ser implementada.

A segurança (da thread) é fundamental

Em Java, a palavra-chave synchronized é usada nos métodos ou nos objetos para implementar a segurança das threads, de modo que apenas uma thread acesse um recurso específico em determinado momento. A instanciação da classe é colocada dentro de um bloco synchronized para que o método somente possa ser acessado por um client de cada vez.

public class ThreadSafeSingleton {
	// inicializa a instância como null.
	private static ThreadSafeSingleton instance = null;

	// construtor privado, para que ela não possa ser instanciada fora da classe.
	private ThreadSafeSingleton() {  }

	// verifica se a instância é null dentro de um bloco synchronized. Se for, o objeto é criado
	public static ThreadSafeSingleton getInstance() {
		synchronized (ThreadSafeSingleton.class) {
			if (instance == null) {
				instance = new ThreadSafeSingleton();
			}
		}
		return instance;
	}
}

A sobrecarga de um método synchronized é alta, reduzindo o desempenho de toda a operação.

Por exemplo, se a variável de instância já tiver sido instanciada, cada vez que algum client acessar o método getInstance(), o método synchronized é executado e há uma queda de desempenho. Isso acontece apenas para verificar se o valor da variável instance é null. Se descobrir que é, ele sai do método.

Para reduzir essa sobrecarga, é usado o double locking. A verificação é usada antes do método synchronized também. Somente se o valor for null o método synchronized é executado.

// double locking é usado para reduzir a sobrecarga do método synchronized
public static ThreadSafeSingleton getInstanceDoubleLocking() {
	if (instance == null) {
		synchronized (ThreadSafeSingleton.class) {
			if (instance == null) {
				instance = new ThreadSafeSingleton();
			}
		}
	}
	return instance;
}

Sigamos para a próxima classificação.

Tipo 2: estrutural - o padrão de projeto Decorator

Permitam-me apresentar para vocês um pequeno cenário para contextualizar melhor o motivo e as situações nas quais devemos usar o padrão Decorator.

Digamos que você tenha um café e, como qualquer iniciante, você começa com apenas dois tipos de café normal, o da casa e o escuro torrado. Em seu sistema de cobrança, havia uma classe para cada um dos tipos de café, os quais herdam da classe abstrata bebida. A clientela começa a aparecer e a tomar seu maravilhoso (ainda que um tanto amargo) café. Há, porém, aqueles novatos do café, que, Deus me livre!!!, querem tomar café com açúcar ou leite. Nem parece mais café!

Agora, você precisa desses dois adicionais, no menu e, infelizmente, no sistema de cobrança. Originalmente, seu funcionário da TI criará uma subclasse para os dois cafés, um para o café com açúcar e outro para o café com leite. Então, como o cliente tem sempre razão, alguma hora aparecerá um que diga aquelas palavras pavorosas:

"Poderia me dar um café com leite e açúcar, por favor?"

???

Lá vai seu sistema de cobrança rir da sua cara novamente. Bem, de volta à prancheta de desenhos…

O funcionário da TI adiciona café com leite e açúcar para cada classe pai de café. O resto do mês é tranquilo, as pessoas fazem fila para tomar o seu café, e você até está fazendo dinheiro. ??

Mas espere, tem mais!

O mundo vai contra você novamente. A concorrência abre um café do outro lado da rua, não apenas com 4 tipos de café, mas com 10 adicionais! ?

Você compra tudo isso e muito mais para poder vender um café melhor no seu estabelecimento, e é aí que você se lembra de que não atualizou o maldito sistema de cobrança. Possivelmente, você não conseguirá fazer o número infinito de subclasses para toda e qualquer combinação dos adicionais com os novos tipos de café também. Isso para não falar do tamanho final que o sistema terá.??

Está na hora de investir, de fato, em um sistema de cobrança adequado. Você encontra uma nova equipe de TI, que saiba de verdade o que está fazendo, e eles dizem:

"Bem, isso fica muito mais fácil e menor se usarmos o padrão Decorator."

Mas que diabos é isso?

O padrão de projetos Decorator está na categoria estrutural de uma classe, seja por herança, composição ou ambos. O objetivo desse padrão é modificar a funcionalidade de um objeto em tempo de execução. Esse é um dos vários padrões de projetos que usam classes abstratas e interfaces com composição para obter o resultado desejado.

Vamos dar uma chance à matemática (medo!) e colocar isso em perspectiva:

Imagine 4 tipos de café e 10 complementos. Se ficarmos na criação de subclasses para cada combinação diferente de todos os adicionais para um tipo de café, teremos:

(10–1)² = 9² = 81 subclasses

Subtraímos 1 de 10, já que não podemos combinar um adicional com outro do mesmo tipo (açúcar com açúcar parece estúpido). E isso para um único tipo de café. Multiplique esses 81 por 4 e você chega a incríveis 324 subclasses diferentes! Que tal codificar isso tudo?

Porém, com o padrão Decorator, serão necessárias apenas 16 classes para esse cenário. Quer apostar?

decorator-class-diagram
Diagrama de classe do padrão de projeto Decorator
decorator-coffee-class-diagram
Diagrama de classe de acordo com o cenário do café

Se mapearmos nosso cenário de acordo com o diagrama de classes acima, temos 4 classes para os quatro tipos de café, 10 para cada adicional, 1 para o componente abstrato e mais 1 para o Decorator abstrato. Viram? 16! Agora, me entreguem os $100 dólares da aposta (brincadeira, mas eu não recusaria se me oferecessem…)

Como você pode ver acima, da mesma forma que os tipos concretos de café são subclasses da classe abstrata Beverage (bebida), a classe abstrata AddOn (adicional) também herda seus métodos dela. Os adicionais, que são suas subclasses, por sua vez, herdam todos os novos métodos para acrescentar funcionalidade ao objeto base quando necessário.

Vamos olhar para o código para ver esse padrão em uso.

Primeiro, tornamos a classe Beverage abstrata. É dela que todos os tipos de café herdarão:

public abstract class Beverage {
	private String description;
    
	public Beverage(String description) {
		super();
		this.description = description;
	}
    
	public String getDescription() {
		return description;
	}
    
	public abstract double cost();
}

Em seguida, adicionamos as classes dos tipos de café concretos:

public class HouseBlend extends Beverage {
	public HouseBlend() {
		super(“House blend”);
	}

	@Override
	public double cost() {
		return 250;
	}
}

public class DarkRoast extends Beverage {
	public DarkRoast() {
		super(“Dark roast”);
	}

	@Override
	public double cost() {
		return 300;
	}
}

A classe abstrata AddOn também herdará da classe abstrata Beverage (mais sobre isso abaixo).

public abstract class AddOn extends Beverage {
	protected Beverage beverage;

	public AddOn(String description, Beverage bev) {
		super(description);
		this.beverage = bev;
	}

	public abstract String getDescription();
}

E agora, as implementações concretas dessa classe abstrata:

public class Sugar extends AddOn {
	public Sugar(Beverage bev) {
		super(“Sugar”, bev);
	}

	@Override
	public String getDescription() {
		return beverage.getDescription() + “ with Mocha”;
	}

	@Override
	public double cost() {
		return beverage.cost() + 50;
	}
}

public class Milk extends AddOn {
	public Milk(Beverage bev) {
		super(“Milk”, bev);
	}

	@Override
	public String getDescription() {
		return beverage.getDescription() + “ with Milk”;
	}

	@Override  public double cost() {
		return beverage.cost() + 100;
	}
}

Como você pode ver acima, podemos passar qualquer subclasse de Beverage para qualquer subclasse de AddOn, e obter o custo adicional, assim como a descrição atualizada. Como a classe AddOn é, essencialmente, do tipo Beverage, podemos passar um AddOn em outro AddOn. Dessa forma, podemos acrescentar o número de adicionais que quisermos a um tipo específico de café.

Vamos escrever o código para testar isso:

public class CoffeeShop {
	public static void main(String[] args) {
		HouseBlend houseblend = new HouseBlend();
		System.out.println(houseblend.getDescription() + “: “ + houseblend.cost());

		Milk milkAddOn = new Milk(houseblend);
		System.out.println(milkAddOn.getDescription() + “: “ + milkAddOn.cost());

		Sugar sugarAddOn = new Sugar(milkAddOn);
		System.out.println(sugarAddOn.getDescription() + “: “ + sugarAddOn.cost());
	}
}

O resultado final é:

decorator-final
Calma! Esse valor é em rúpias do Sri Lanka!

Funcionou! Conseguimos acrescentar mais de um adicional a um tipo de café e atualizamos com sucesso seu preço final e sua descrição. Vejam que não foi necessário fazer subclasses infinitas para cada combinação de adicional para todos os tipos de café.

Chegamos, finalmente, à última categoria.

Tipo 3: Comportamental - o padrão de projeto Command

O padrão de projeto comportamental tem como foco a maneira como as classes e objetos se comunicam uns com os outros. A ideia central do padrão Command é produzir um maior nível de acoplamento fraco entre as partes envolvidas (leia-se as classes).

o que é isso?

Acoplamento é a maneira pela qual duas classes (ou mais) interagem umas com as outras. O cenário ideal é quando essas classes interagem sem depender fortemente umas das outras. Esse é o acoplamento fraco. Então, uma definição melhor para o acoplamento fraco seria "classes interconectadas, mas fazendo o menor uso possível umas das outras".

A necessidade desse padrão surgiu quando solicitações precisavam ser enviadas sem que se soubesse conscientemente o que estava sendo solicitado ou quem era o receptor da solicitação.

Neste padrão, a classe que o chama é desacoplada da classe que, de fato, realiza a ação. A classe que chama tem apenas o método de execução para chamar, que executa o comando necessário quando o client o solicita.

Vamos usar um exemplo do mundo real, pedir um prato em um restaurante elegante. O fluxo é: você faz o pedido (comando) ao garçom (quem "invoca" o pedido), que o entregará ao chef (receptor do pedido), para que você possa receber o alimento. Pode parecer simples… mas é um pouco complicado de codificar.

chain-of-command-be-like-pop-snoke-im-going-to-27790631

A ideia, de fato, é simples. A codificação, no entanto, é um pouco mais difícil.

command-class-diagram
Diagrama de classes do padrão Command

O fluxo da operação do lado técnico é: você faz um comando concreto, que implementa a interface Command, pedindo que o receptor realize uma ação e envia o comando a quem invoca o pedido. Essa é a pessoa que sabe a quem dar esse comando. O chef é o único que sabe o que fazer com o comando/pedido específico. Assim, quando o método "execute" daquele que invoca o pedido é executado, ele, por sua vez, faz com que o método execute dos objetos de comando seja passado ao receptor, completando assim as ações necessárias.

O que precisamos implementar:

  1. Uma interface Command
  2. Uma classe Order (pedido) que implementa a interface Command
  3. Uma classe Waiter (Garçom, quem invoca o pedido)
  4. Uma classe Chef (o receptor)

Desse modo, o código terá a seguinte aparência:

Chef, o receptor

public class Chef {
	public void cookPasta() {
		System.out.println(“O Chef está fazendo massa ao molho Alfredo…”);
	}

	public void bakeCake() {
		System.out.println(“O Chef está fazendo um bolo de chocolate…”);
	}
}

Command, a interface

public interface Command {
	public abstract void execute();
}

Order (pedido), o comando concreto

public class Order implements Command {
	private Chef chef;
	private String food;

	public Order(Chef chef, String food) {
		this.chef = chef;
		this.food = food;
	}

	@Override
	public void execute() {
		if (this.food.equals(“Massa”)) {
			this.chef.cookPasta();
		} else {
			this.chef.bakeCake();
		}
	}
}

Waiter, (Garçom) quem invoca o pedido

public class Waiter {
	private Order order;

	public Waiter(Order ord) {
		this.order = ord;
	}

	public void execute() {
		this.order.execute();
	}
}

Você, o cliente

public class Client {
	public static void main(String[] args) {
		Chef chef = new Chef();
        
		Order order = new Order(chef, “Massa”);
		Waiter waiter = new Waiter(order);
		waiter.execute();

		order = new Order(chef, “Bolo”);
		waiter = new Waiter(order);
		waiter.execute();
	}
}

Como podemos ver acima, Client faz um Order (pedido) e define o Receiver (Receptor) como sendo o Chef. Order é enviado ao Waiter (Garçom), que saberá quem deve executar o pedido (ou seja, quando dar ao chefe o pedido para que ele o faça). Quando quem invoca o pedido é executado, o método execute de Order é executado no receptor (ou seja, o chef recebe o comando para cozinhar a massa ou para assar o bolo).

Recapitulando

Neste artigo, vimos:

  1. O que de fato é um padrão de projetos,
  2. Os diferentes tipos de padrão de projetos e o motivo de serem diferentes
  3. Um padrão de projetos básico ou comum de padrão de projetos para cada tipo

Espero que isso tenha sido útil.

Encontre o repositório do código para este artigo aqui.