Vamos combinar que, não é porque o desenvolvimento de software é complexo que ele precisa ser algo macarrônico, beleza? Ou seja, como bons desenvolvedores, nós devemos adotar boas práticas, padrões e convenções, não apenas como uma escolha técnica, mas como algo crucial, principalmente em projetos grandes e que evoluem com frequência. E se tratando da linguagem Java, onde a complexidade dos projetos pode ser muitas vezes bem significativa, a compreensão e aplicação de padrões torna-se uma habilidade fundamental. Neste artigo, apresentamos 5 Design Patterns que todo desenvolvedor Java deve conhecer e começar a pensar em integrar em seus projetos.
O que são Design Patterns?
Quando falamos de “Design Patterns”, não estamos falando de código pronto, framework ou nada parecido. O termo traduzido significa Padrões de Design e trata-se de modelos de soluções consolidadas para problemas recorrentes no desenvolvimento de software, que encapsulam estratégias para resolver questões específicas e comuns. Imagine-os como um conjunto de receitas confiáveis que os desenvolvedores podem seguir para criar códigos eficientes, limpos, modulares e de fácil manutenção.
Continue a leitura e veja alguns exemplos simples, mas bem interessantes de como podemos aplicar alguns deles em nosso dia a dia como devs Java.
1. Factory Method: Criando Objetos de Forma Flexível
O padrão Factory Method define uma interface para criar um objeto, mas deixa as subclasses alterarem os tipos de objetos que serão criados, permitindo que uma classe delegue a responsabilidade de instanciar seus objetos para suas subclasses.
Vamos deixar mais claro simulando um sistema de transporte com diferentes tipos de veículos.
// Interface que define o método de entrega
public interface Transport {
void deliver();
}
// Implementação concreta da interface para caminhões
public class Truck implements Transport {
@Override
public void deliver() {
System.out.println("Entregando via caminhão.");
}
}
// Implementação concreta da interface para navios
public class Ship implements Transport {
@Override
public void deliver() {
System.out.println("Entregando via navio.");
}
}
// Fábrica abstrata que contém o método Factory Method
public abstract class LogisticsCompany {
// Factory Method
protected abstract Transport createTransport();
// Método de negócio que utiliza o Factory Method
public void planDelivery() {
Transport transport = createTransport();
System.out.println("Planejando a entrega.");
transport.deliver();
}
}
// Fábrica concreta que estende a fábrica abstrata
public class RoadLogistics extends LogisticsCompany {
// Implementação do Factory Method para criar um caminhão
@Override
protected Transport createTransport() {
return new Truck();
}
}
// Outra fábrica concreta que estende a fábrica abstrata
public class SeaLogistics extends LogisticsCompany {
// Implementação do Factory Method para criar um navio
@Override
protected Transport createTransport() {
return new Ship();
}
}
- A interface
Transport
define o métododeliver
, que será implementado pelas classes concretas (Truck
eShip
). - A classe abstrata
LogisticsCompany
contém o Factory MethodcreateTransport
e um método de negócioplanDelivery
, que utiliza o objeto criado pelo Factory Method. - As subclasses concretas (
RoadLogistics
eSeaLogistics
) estendem a classe abstrata e implementam o Factory Method, decidindo assim qual tipo de veículo (caminhão ou navio) será usado para a entrega.
Agora, vamos testar!
public class LogisticsExample {
public static void main(String[] args) {
// Usando RoadLogistics para entregar via caminhão
LogisticsCompany roadLogistics = new RoadLogistics();
roadLogistics.planDelivery();
// Usando SeaLogistics para entregar via navio
LogisticsCompany seaLogistics = new SeaLogistics();
seaLogistics.planDelivery();
}
}
A classe cliente (LogisticsExample
) apenas instancia os tipos de logística que deseja usar e invoca o método planDelivery
, que, por sua vez, chama o método createTransport
na instância adequada de LogisticsCompany
. Isso permite que cada subclasse (RoadLogistics
e SeaLogistics
) decida internamente qual tipo de transporte utilizar sem que a classe cliente precise saber ou se preocupar com os detalhes de implementação.
Essa abstração facilita a extensibilidade do código, pois novas implementações de LogisticsCompany
podem ser adicionadas sem alterar a classe cliente, seguindo o princípio de aberto/fechado dos princípios SOLID.
2. Strategy: Alternando Comportamentos Dinamicamente
O Strategy é um padrão de design comportamental que define um grupo de algoritmos e os torna intercambiáveis em tempo de execução, e assim o cliente pode escolher o algoritmo a ser utilizado dinamicamente, sem alterar a classe que o utiliza.
Simulamos um sistema de processamento de pagamentos, onde diferentes formas de pagamento podem ser escolhidas. Cada estratégia representa um método de pagamento diferente (por exemplo, cartão de crédito, PayPal, transferência bancária).
// Interface que define a estratégia de pagamento
public interface PaymentStrategy {
void pay(int amount);
}
// Implementação concreta da estratégia para pagamento com cartão de crédito
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String name;
public CreditCardPayment(String cardNumber, String name) {
this.cardNumber = cardNumber;
this.name = name;
}
@Override
public void pay(int amount) {
System.out.println("Pagamento de $" + amount + " com cartão de crédito " + cardNumber);
}
}
// Implementação concreta da estratégia para pagamento com PayPal
public class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println("Pagamento de $" + amount + " via PayPal para " + email);
}
}
// Contexto que utiliza a estratégia de pagamento
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
- A interface
PaymentStrategy
define o métodopay
, que será implementado pelas estratégias concretas (CreditCardPayment
ePayPalPayment
). - As classes
CreditCardPayment
ePayPalPayment
são implementações concretas das estratégias de pagamento. - A classe
ShoppingCart
é o contexto que possui uma referência para a estratégia de pagamento. O métodocheckout
utiliza a estratégia para efetuar o pagamento.
Hora de testar!
public class PaymentExample {
public static void main(String[] args) {
// Criando um carrinho de compras
ShoppingCart shoppingCart = new ShoppingCart();
// Escolhendo a estratégia de pagamento com cartão de crédito
PaymentStrategy creditCardPayment = new CreditCardPayment("1234-5678-9012-3456", "John Doe");
shoppingCart.setPaymentStrategy(creditCardPayment);
// Realizando o pagamento
shoppingCart.checkout(100);
// Mudando para a estratégia de pagamento via PayPal
PaymentStrategy payPalPayment = new PayPalPayment("john.doe@example.com");
shoppingCart.setPaymentStrategy(payPalPayment);
// Realizando o pagamento com a nova estratégia
shoppingCart.checkout(50);
}
}
Este exemplo simula um sistema de compras onde diferentes estratégias de pagamento podem ser escolhidas dinamicamente. O padrão Strategy permite que o cliente (no caso, a classe ShoppingCart
) altere a estratégia de pagamento sem modificar seu código interno.
3. Command: Encapsulando Solicitações como Objetos
O Comand também é um padrão de design comportamental que encapsula uma solicitação como um objeto, permitindo que o cliente escolha, enfileire e reverta operações de maneira eficiente.
Vamos criar um exemplo que simula um sistema de controle remoto, onde diferentes dispositivos (como luzes e portas) podem ser controlados usando o padrão Command.
// Interface que define o comando
public interface Command {
void execute();
}
// Classes concretas que implementam o comando para ligar e desligar a luz
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
}
// Classe de luz que representa o receptor
public class Light {
public void turnOn() {
System.out.println("Luz ligada");
}
public void turnOff() {
System.out.println("Luz desligada");
}
}
// Controle remoto que usa comandos para controlar dispositivos
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
- A interface
Command
define o métodoexecute
, que será implementado pelos comandos concretos (LightOnCommand
eLightOffCommand
). - As classes
LightOnCommand
eLightOffCommand
são implementações concretas dos comandos para ligar e desligar a luz. - A classe
Light
representa o receptor, que é o objeto que realmente realiza a ação (ligar ou desligar a luz). - A classe
RemoteControl
possui um campo para armazenar o comando atual e um métodopressButton
para executar o comando.
Bora ver se essa coisa funcina mesmo?
public class CommandExample {
public static void main(String[] args) {
// Criando um controle remoto
RemoteControl remoteControl = new RemoteControl();
// Criando uma luz e comandos para ligar e desligar
Light livingRoomLight = new Light();
Command livingRoomLightOn = new LightOnCommand(livingRoomLight);
Command livingRoomLightOff = new LightOffCommand(livingRoomLight);
// Configurando o controle remoto com os comandos
remoteControl.setCommand(livingRoomLightOn);
// Pressionando o botão para ligar a luz
remoteControl.pressButton();
// Configurando o controle remoto com outro comando
remoteControl.setCommand(livingRoomLightOff);
// Pressionando o botão para desligar a luz
remoteControl.pressButton();
}
}
4. Observer: Notificando Mudanças de Estado
Mais uma vez temos um padrão de design comportamental, o Observer, que define uma dependência um-para-muitos entre objetos, de modo que quando um objeto muda de estado, todos os seus dependentes são notificados e atualizados automaticamente.
Para ficar mais claro, vamos simular um sistema de assinaturas, onde vários assinantes (observadores) são notificados quando um novo artigo é publicado (evento).
import java.util.ArrayList;
import java.util.List;
// Interface que define os métodos para observadores
interface Observer {
void update(String article);
}
// Classe que representa o editor (sujeito observado)
class Publisher {
private List<Observer> observers = new ArrayList<>();
private String latestArticle;
// Método para adicionar observadores
void addObserver(Observer observer) {
observers.add(observer);
}
// Método para remover observadores
void removeObserver(Observer observer) {
observers.remove(observer);
}
// Método para notificar observadores sobre uma nova publicação
void publishArticle(String article) {
this.latestArticle = article;
notifyObservers();
}
// Método privado para notificar observadores
private void notifyObservers() {
for (Observer observer : observers) {
observer.update(latestArticle);
}
}
}
// Classe que representa um assinante (observador)
class Subscriber implements Observer {
private String name;
Subscriber(String name) {
this.name = name;
}
@Override
public void update(String article) {
System.out.println(name + " recebeu a notificação: Novo artigo publicado - " + article);
}
}
- A interface
Observer
define o métodoupdate
, que será implementado pelos observadores (assinantes). - A classe
Publisher
representa o sujeito observado, que mantém uma lista de observadores e notifica-os quando ocorre um evento (publicação de um novo artigo). - A classe
Subscriber
representa um observador, que implementa o métodoupdate
para reagir às notificações.
Hora da verdade!!!
public class ObserverExample {
public static void main(String[] args) {
// Criando um editor (sujeito observado)
Publisher publisher = new Publisher();
// Criando assinantes (observadores)
Subscriber subscriber1 = new Subscriber("Assinante 1");
Subscriber subscriber2 = new Subscriber("Assinante 2");
// Adicionando assinantes ao editor
publisher.addObserver(subscriber1);
publisher.addObserver(subscriber2);
// Publicando um novo artigo (evento)
publisher.publishArticle("Padrão Observer em ação!");
// Removendo um assinante
publisher.removeObserver(subscriber1);
// Publicando outro artigo
publisher.publishArticle("Segundo artigo publicado!");
}
}
Bacana, né? Neste exemplo os observadores são notificados quando ocorre um novo evento de plublicação. O padrão Observer permite que objetos se comuniquem de forma flexível, desacoplada e extensível, facilitando a adição de novos observadores sem modificar o código do elemento observado.
5. Decorator: Estendendo Funcionalidades de Forma Flexível
O Decorator é um padrão de design estrutural que permite adicionar novos comportamentos a objetos existentes sem alterar suas estruturas. Ele é útil quando você precisa estender as funcionalidades de uma classe de forma flexível e independente.
Vamos demonstrar de forma bem simples com um sistema de café, onde diferentes ingredientes podem ser adicionados ao café base, usando o padrão Decorator.
// Interface que define o componente base (Café)
interface Coffee {
double cost(); // Método para obter o custo do café
String description(); // Método para obter a descrição do café
}
// Implementação concreta do componente base (Café simples)
class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 2.0; // Custo base do café simples
}
@Override
public String description() {
return "Café simples";
}
}
// Decorator abstrato que estende o componente base
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public double cost() {
return decoratedCoffee.cost();
}
@Override
public String description() {
return decoratedCoffee.description();
}
}
// Implementações concretas de decoradores (Adicionando Leite)
class MilkDecorator extends CoffeeDecorator {
MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 0.5; // Custo adicional para o leite
}
@Override
public String description() {
return super.description() + " com Leite";
}
}
// Outra implementação concreta de decorador (Adicionando Canela)
class CinnamonDecorator extends CoffeeDecorator {
CinnamonDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 0.3; // Custo adicional para a canela
}
@Override
public String description() {
return super.description() + " com Canela";
}
}
- A interface
Coffee
define o componente base, que é um café simples. - A classe
SimpleCoffee
é uma implementação concreta do componente base. - O
CoffeeDecorator
é um decorador abstrato que estende o componente base. Ele contém uma referência ao componente base e implementa os métodoscost()
edescription()
repassando a chamada para o componente base. - As classes
MilkDecorator
eCinnamonDecorator
são implementações concretas de decoradores que adicionam funcionalidades extras (leite e canela) ao café.
Bora ver se sai café com leite mesmo! Hehe
public class DecoratorExample {
public static void main(String[] args) {
// Criando um café simples
Coffee simpleCoffee = new SimpleCoffee();
System.out.println("Custo: $" + simpleCoffee.cost() + ", Descrição: " + simpleCoffee.description());
// Adicionando leite ao café
Coffee coffeeWithMilk = new MilkDecorator(simpleCoffee);
System.out.println("Custo: $" + coffeeWithMilk.cost() + ", Descrição: " + coffeeWithMilk.description());
// Adicionando canela ao café com leite
Coffee coffeeWithMilkAndCinnamon = new CinnamonDecorator(coffeeWithMilk);
System.out.println("Custo: $" + coffeeWithMilkAndCinnamon.cost() + ", Descrição: " + coffeeWithMilkAndCinnamon.description());
}
}
Conclusão
Design Patterns, Design Patterns, … muita coisa para absorver, mas foi bem legal, né?
Cada padrão aborda desafios específicos que são solucionados de forma elegante, e integrá-los em projetos reais não apenas melhora a qualidade do código, mas também facilita a manutenção e evolução contínua.
Estes e outros padrões são comumente utilizados por desenvolvedores experientes para criar sistemas robustos e de fácil manutenção, então estude mais sobre o assunto e comece a praticar, pois com este conhecimento, você se posiciona como um profissional mais apto a enfrentar desafios do desenvolvimento de maneira eficiente.
Quer saber mais sobre padrões de design e outros assuntos relacionados ao desenvolvimento de software? Acesse a nossa página de artigos da categoria Desenvolvimento.
Não deixe de visitar a nossa página de recomendações de livros se quer ir mais longe neste assunto.
Até logo!