Back-endDesenvolvimento

Design Patterns no desenvolvimento Java

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étodo deliver, que será implementado pelas classes concretas (Truck e Ship).
  • A classe abstrata LogisticsCompany contém o Factory Method createTransport e um método de negócio planDelivery, que utiliza o objeto criado pelo Factory Method.
  • As subclasses concretas (RoadLogistics e SeaLogistics) 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étodo pay, que será implementado pelas estratégias concretas (CreditCardPayment e PayPalPayment).
  • As classes CreditCardPayment e PayPalPayment 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étodo checkout 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étodo execute, que será implementado pelos comandos concretos (LightOnCommand e LightOffCommand).
  • As classes LightOnCommand e LightOffCommand 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étodo pressButton 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étodo update, 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étodo update 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étodos cost() e description() repassando a chamada para o componente base.
  • As classes MilkDecorator e CinnamonDecorator 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!


Givanilson Pereira

Sou graduado em Análise e Desenvolvimento de Sistemas e no momento estou no setor privado como Engenheiro de Software. Sou casado e pai de uma menina, aprecio uma boa comida, gosto de filmes e séries de ficção científica e um bom e velho Rock ’n’ Roll.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *