Padrões de Design GoF e Aplicação em Sistemas Orientados a Objetos

  1. Conceitos de Padrões de Design

Padrões de design representam soluções reutilizáveis para problemas recorrentes no desenvolvimento de software, consolidadas a partir de experiências práticas. Assim como estratégias militares clássicas, esses padrões oferecem abordagens validadas para desafios específicos em arquitetura de software.

Os padrões facilitam a reutilização de designs bem-sucedidos e tornam as soluções mais compreensíveis para outros desenvolvedores. Eles formam a base para software orientado a objetos reutilizável, ajudando a tomar decisões que preservam a manutenibilidade e extensibilidade do sistema.

Os padrões amplamente adotados são conhecidos como GoF (Gang of Four), referência ao livro Design Patterns: Elements of Reusable Object-Oriented Software de Gamma, Helm, Johnson e Vissides, que cataloga 23 padrões clássicos.

Esses padrões são classificados de duas formas principais:

  1. Por finalidade:
    • Criacionais: focam na criação de objetos, desacoplando a instanciação do uso.
    • Estruturais: lidam com a composição de classes ou objetos em estruturas maiores.
    • Comportamentais: definem interações e responsabilidades entre objetos para completar tarefas.
  2. Por escopo:
    • Padrões de classe: baseados em herança, relações estáticas definidas em tempo de compilação.
    • Padrões de objeto: baseados em composição ou agregação, relações dinâmicas em tempo de execução.

Observação: O padrão Adaptador pode ocorrer em ambas as formas (classe e objeto).

  1. Princípios de Design Orientado a Objetos

Softwares frequentemente sofrem mudanças de requisitos. Um design inadequado pode dificultar modificações ou exigir reeescrita total. Dependências entre classes aumentam a complexidade; alterações em um componente podem afetar todos os que o utilizam.

Por exemplo, considere uma camada de serviço que depende de uma camada de acesso a dados:

// Entidade Artigo
public class Artigo {
    private String titulo;
    private String conteudo;
    public Artigo() {}
    public Artigo(String titulo, String conteudo) {
        this.titulo = titulo;
        this.conteudo = conteudo;
    }
    // Getters e setters omitidos por brevidade
    @Override
    public String toString() {
        return "Artigo{titulo='" + titulo + "', conteudo='" + conteudo + "'}";
    }
}

// Interface DAO para Artigo
public interface ArtigoDao {
    void salvar(Artigo artigo);
}

// Implementação concreta do DAO
public class ArtigoDaoJdbc implements ArtigoDao {
    private Logger logger = Logger.getLogger(ArtigoDaoJdbc.class);
    @Override
    public void salvar(Artigo artigo) {
        logger.debug("Persistindo artigo no banco de dados");
        String sql = "INSERT INTO artigos VALUES(?,?)";
        Object[] params = {artigo.getTitulo(), artigo.getConteudo()};
        int affected = executarUpdate(sql, params);
        if (affected > 0) {
            System.out.println("Artigo salvo com sucesso");
        }
    }
}

// Interface de serviço para Artigo
public interface ArtigoServico {
    void adicionar(Artigo artigo);
}

// Implementação concreta do serviço
public class ArtigoServicoImpl implements ArtigoServico {
    private ArtigoDao dao = FabricaDaoSimples.obterInstancia();
    @Override
    public void adicionar(Artigo artigo) {
        dao.salvar(artigo);
    }
}

Nesse cenário, ArtigoServicoImpl está fortemente acoplado a ArtigoDaoJdbc. Se a implementação do DAO precisar mudar, a classe de serviço também deve ser alterada, dificultando a reutilização e manutenção.

Para construir sistemas flexíveis, princípios de design orientado a objetos são essenciais:

  • Princípio da Responsabilidade Única (SRP): Uma classe deve ter apenas um motivo para mudar, encapsulando uma única responsabilidade.
  • Princípio Aberto/Fechado (OCP): Entidades de software devem estar abertas para extensão, mas fechadas para modificação. Novas funcionalidades devem ser adicionadas via extensão, não alteração de código existente.
  • Princípio de Substituição de Liskov (LSP): Objetos de uma classe derivada devem ser substituíveis por objetos da classe base sem comprometer o comportamento do programa.
  • Princípio de Inversão de Dependência (DIP): Módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações. Injeção de dependência (via construtor ou setter) é uma técnica comum.
  • Princípio de Segregação de Interface (ISP): Interfaces devem ser granulares e específicas, evitando que clientes dependam de métodos que não utilizam.
  • Princípio de Demeter (LoD): Entidades devem minimizar o conhecimento sobre outras, reduzindo acoplamento através de encapsulamento.
  • Princípio de Composição sobre Herança: Prefira composição (relações has-a) à herança (relações is-a) para reutilziação, pois oferece mais flexibilidade em tempo de execução.
  1. Padrão Método de Fábrica

O acoplamento direto entre ArtigoServicoImpl e ArtigoDaoJdbc pode ser resolvido delegando a criação do DAO para uma fábrica. Assim, a classe de serviço depende apenas da interface abstrata.

Uma abordagem simples é uma fábrica estática:

public class FabricaDaoSimples {
    public static ArtigoDao obterInstancia() {
        return new ArtigoDaoJdbc();
    }
}

Para maior flexibilidade, a fábrica pode aceitar parâmetros:

public class FabricaDaoSimples {
    public static ArtigoDao obterInstancia(String tipo) {
        switch (tipo) {
            case "jdbc": return new ArtigoDaoJdbc();
            case "hibernate": return new ArtigoDaoHibernate();
            case "mongodb": return new ArtigoDaoMongo();
            default: throw new IllegalArgumentException("Tipo de DAO desconhecido: " + tipo);
        }
    }
}

No código do serviço, a dependência é injetada via setter:

public class ArtigoServicoImpl implements ArtigoServico {
    private ArtigoDao dao;
    public void setDao(ArtigoDao dao) {
        this.dao = dao;
    }
    @Override
    public void adicionar(Artigo artigo) {
        dao.salvar(artigo);
    }
}

// Exemplo de uso em teste
public class ArtigoServicoTeste {
    @Test
    public void testarAdicao() {
        ArtigoDao dao = FabricaDaoSimples.obterInstancia("jdbc");
        ArtigoServicoImpl servico = new ArtigoServicoImpl();
        servico.setDao(dao);
        Artigo artigo = new Artigo("Título de teste", "Conteúdo de teste");
        servico.adicionar(artigo);
    }
}

A fábrica simples funciona para casos com poucos produtos, mas viola o OCP quando novos tipos são adicionados. O padrão Método de Fábrica resolve isso introduzindo uma interface de fábrica abstrata:

// Interface de fábrica abstrata
public interface FabricaDao {
    ArtigoDao criarDao();
}

// Fábrica concreta para JDBC
public class FabricaDaoJdbc implements FabricaDao {
    @Override
    public ArtigoDao criarDao() {
        return new ArtigoDaoJdbc();
    }
}

// Fábrica concreta para Hibernate
public class FabricaDaoHibernate implements FabricaDao {
    @Override
    public ArtigoDao criarDao() {
        return new ArtigoDaoHibernate();
    }
}

A classe de serviço agora depende da interface FabricaDao:

public class ArtigoServicoImpl implements ArtigoServico {
    private FabricaDao fabrica;
    public ArtigoServicoImpl(FabricaDao fabrica) {
        this.fabrica = fabrica;
    }
    @Override
    public void adicionar(Artigo artigo) {
        ArtigoDao dao = fabrica.criarDao();
        dao.salvar(artigo);
    }
}

Este padrão permite adicionar novos produtos e fábricas sem alterar código existente, respeitando o OCP. Seu inconveniente é o aumento no número de classes.

  1. Padrão Proxy

O padrão Proxy fornece um substituto ou representante para outro objeto, controlando o acesso a ele. Exemplos incluem intermediários em transações imobiliárias ou serviços de brokerage.

Considere uma interface Comprador com um método analisar():

// Interface assunto
public interface Comprador {
    String analisar();
}

// Implementação real
public class CompradorConcreto implements Comprador {
    private Logger logger = Logger.getLogger(CompradorConcreto.class);
    @Override
    public String analisar() {
        logger.debug("Análise in loco do imóvel");
        return "Observações do comprador";
    }
}

// Proxy estático que adiciona lógica antes e depois
public class ProxyImobiliaria implements Comprador {
    private Logger logger = Logger.getLogger(ProxyImobiliaria.class);
    private Comprador compradorAlvo;
    public ProxyImobiliaria(Comprador alvo) {
        this.compradorAlvo = alvo;
    }
    @Override
    public String analisar() {
        preparar();
        String feedback = compradorAlvo.analisar();
        finalizar();
        return "Relatório: Feedback do comprador: " + feedback;
    }
    private void preparar() {
        logger.debug("Agendamento de visitas");
        logger.debug("Busca de imóveis disponíveis");
    }
    private void finalizar() {
        logger.debug("Acompanhamento pós-visita");
        logger.debug("Negociação com proprietário");
    }
}

Para testar:

public class CompradorTeste {
    @Test
    public void testarAnalise() {
        Comprador comprador = new ProxyImobiliaria(new CompradorConcreto());
        String resultado = comprador.analisar();
        System.out.println(resultado);
    }
}

Proxies estáticos requerem código manual para cada método alvo, violando o OCP. Proxies dinâmicos resolvem isso usando reflexão.

Usando a API de Proxy Dinâmico do JDK:

// Invocador para proxy dinâmico
public class InvocadorImobiliaria implements InvocationHandler {
    private Logger logger = Logger.getLogger(InvocadorImobiliaria.class);
    private Object alvo;
    public InvocadorImobiliaria(Object alvo) {
        this.alvo = alvo;
    }
    @Override
    public Object invoke(Object proxy, Method metodo, Object[] argumentos) throws Throwable {
        preparar();
        Object resultado = metodo.invoke(alvo, argumentos);
        finalizar();
        return "Relatório: Feedback do comprador: " + resultado;
    }
    // Métodos preparar() e finalizar() omitidos por brevidade
}

// Fábrica de proxy
public class FabricaProxyComprador {
    public static Comprador criar(Comprador alvo) {
        return (Comprador) Proxy.newProxyInstance(
            alvo.getClass().getClassLoader(),
            alvo.getClass().getInterfaces(),
            new InvocadorImobiliaria(alvo)
        );
    }
}

O teste:

public class CompradorTeste {
    @Test
    public void testarProxyDinamico() {
        Comprador comprador = FabricaProxyComprador.criar(new CompradorConcreto());
        String resultado = comprador.analisar();
        System.out.println(resultado);
    }
}

O proxy dinâmico do JDK requer que o objeto alvo implemente interfaces. Para classes não baseadas em interfaces, frameworks como CGLIB podem ser utilizados.

Tags: Padrões de Design GoF método de fábrica padrão proxy java

Publicado em 7-4 19:52