Pular para o conteúdo

Revisão Geral de Design Patterns

Este tópico reúne uma revisão geral do que foi estudado até aqui em Design Patterns. A ideia não é substituir os materiais individuais de cada padrão, mas consolidar a visão do conjunto para facilitar revisão, comparação e escolha consciente de soluções de projeto.

Ao longo do conteúdo, retomaremos:

  • o que são Design Patterns
  • a categorização GoF
  • os padrões criacionais estudados
  • os padrões estruturais estudados
  • uma síntese comparativa por categoria

O foco aqui é ajudar a responder perguntas como:

  • qual problema cada padrão tenta resolver?
  • em que contexto ele costuma ser útil?
  • qual estrutura básica de implementação ele sugere?
  • quais ganhos e custos aparecem ao adotá-lo?

Design Patterns são soluções recorrentes e reutilizáveis para problemas comuns de projeto de software. Eles não são trechos prontos de código para copiar e colar, mas sim modelos de organização que ajudam a estruturar classes, objetos e responsabilidades.

Em termos práticos, padrões de projeto ajudam a:

  • reduzir acoplamento desnecessário
  • organizar melhor criação, composição e colaboração entre objetos
  • tornar o código mais legível e extensível
  • criar um vocabulário comum entre desenvolvedores

Ao dizer que um trecho de código usa Adapter, Factory Method ou Decorator, por exemplo, o time consegue comunicar com rapidez a intenção do design adotado.

Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, a Gang of Four (GoF), organizaram os padrões clássicos em três grandes categorias. Essa categorização não existe por formalidade; ela ajuda a entender qual dimensão do problema cada padrão ataca.

Os padrões criacionais tratam do processo de criação de objetos. Eles buscam responder perguntas como:

  • quem deve criar o objeto?
  • quando o objeto deve ser criado?
  • como evitar dependência direta de classes concretas?
  • como representar construções complexas ou famílias de objetos?

Em geral, são úteis quando o problema principal está no instanciamento, na configuração inicial ou no controle da criação.

Os padrões estruturais tratam da composição de classes e objetos. Eles ajudam a organizar relações entre partes do sistema para que estruturas maiores permaneçam compreensíveis, flexíveis e reutilizáveis.

Em geral, respondem perguntas como:

  • como integrar interfaces incompatíveis?
  • como compor objetos sem explodir a hierarquia de classes?
  • como simplificar o acesso a subsistemas?
  • como compartilhar estrutura ou comportamento sem duplicação excessiva?

Os padrões comportamentais focam na interação entre objetos e na distribuição de responsabilidades. Eles procuram organizar fluxos, protocolos de comunicação e variações de comportamento.

Embora esta categoria ainda não tenha sido aprofundada em tópicos específicos até aqui, ela completa a visão GoF por lidar com perguntas como:

  • como objetos colaboram?
  • como encapsular algoritmos, comandos ou estados?
  • como notificar mudanças e coordenar ações?

Até este ponto da disciplina, foram abordados:

  • fundamentos introdutórios sobre Design Patterns
  • princípios SOLID como apoio ao bom design
  • 5 padrões criacionais
  • 7 padrões estruturais

Isso significa que já existe uma base suficiente para perceber um ponto importante: padrões não são soluções isoladas; eles formam um repertório complementar. Em muitos sistemas reais, um mesmo módulo combina mais de um padrão ao mesmo tempo.

Os padrões criacionais vistos até aqui foram:

  • Singleton
  • Builder
  • Factory Method
  • Abstract Factory
  • Prototype

O Singleton garante que uma classe possua apenas uma instância e oferece um ponto global de acesso a ela.

Use quando:

  • um recurso realmente deve ser único na aplicação
  • faz sentido centralizar acesso a configuração, log ou cache compartilhado
  • a unicidade faz parte do requisito, e não apenas da conveniência
classDiagram
    class Singleton {
        -instancia: Singleton
        -Singleton()
        +getInstancia() Singleton
        +operacao()
    }
  • garante instância única
  • centraliza acesso a recurso compartilhado
  • pode adiar criação até o primeiro uso
  • cria dependência global
  • dificulta testes e substituição por mocks
  • pode esconder acoplamento excessivo

O Builder separa a construção de um objeto complexo da sua representação, permitindo montar a solução passo a passo.

Use quando:

  • o objeto possui muitos parâmetros ou combinações possíveis
  • a criação precisa acontecer em etapas
  • você quer melhorar legibilidade e evitar construtores telescópicos
classDiagram
    class Cliente
    class Director {
        +construir()
    }
    class Builder {
        <<interface>>
        +reset()
        +construirParteA()
        +construirParteB()
        +obterResultado()
    }
    class ConcreteBuilder {
        +construirParteA()
        +construirParteB()
        +obterResultado()
    }
    class Produto

    Cliente --> Director
    Director --> Builder
    ConcreteBuilder ..|> Builder
    ConcreteBuilder --> Produto
  • melhora legibilidade da criação
  • facilita variações de montagem
  • reduz objetos inconsistentes durante construção
  • adiciona mais classes e passos
  • pode ser exagero para objetos simples
  • director nem sempre compensa em cenários pequenos

O Factory Method define um método de criação e delega às subclasses a decisão sobre qual produto concreto instanciar.

Use quando:

  • o código cliente deve depender de abstrações, não de concretos
  • subclasses precisam escolher diferentes implementações
  • a criação varia, mas o fluxo principal permanece semelhante
classDiagram
    class Product {
        <<interface>>
        +operacao()
    }
    class ConcreteProductA
    class ConcreteProductB
    class Creator {
        +algoritmoPrincipal()
        #factoryMethod() Product
    }
    class ConcreteCreatorA {
        +factoryMethod() Product
    }
    class ConcreteCreatorB {
        +factoryMethod() Product
    }

    ConcreteProductA ..|> Product
    ConcreteProductB ..|> Product
    ConcreteCreatorA --|> Creator
    ConcreteCreatorB --|> Creator
    Creator --> Product
  • reduz acoplamento com classes concretas
  • facilita extensão por novas subclasses
  • mantém o fluxo principal reutilizável
  • aumenta a hierarquia de classes
  • pode espalhar a lógica de criação em subclasses
  • é menos direto do que uma instanciação simples

O Abstract Factory cria famílias de objetos relacionados sem expor diretamente suas classes concretas.

Use quando:

  • há famílias coerentes de produtos
  • o sistema precisa evitar mistura entre variantes incompatíveis
  • o cliente deve trocar a família inteira sem reescrever o código
classDiagram
    class AbstractFactory {
        <<interface>>
        +criarProdutoA() ProdutoA
        +criarProdutoB() ProdutoB
    }
    class ConcreteFactory1
    class ConcreteFactory2
    class ProdutoA {
        <<interface>>
    }
    class ProdutoB {
        <<interface>>
    }
    class ProdutoA1
    class ProdutoA2
    class ProdutoB1
    class ProdutoB2
    class Cliente

    ConcreteFactory1 ..|> AbstractFactory
    ConcreteFactory2 ..|> AbstractFactory
    ProdutoA1 ..|> ProdutoA
    ProdutoA2 ..|> ProdutoA
    ProdutoB1 ..|> ProdutoB
    ProdutoB2 ..|> ProdutoB
    Cliente --> AbstractFactory
  • garante coerência entre produtos da mesma família
  • reduz dependência do cliente em relação às classes concretas
  • facilita troca de plataforma, tema ou ambiente
  • adicionar novo tipo de produto é custoso
  • aumenta o número de interfaces e fábricas
  • pode ser pesado para cenários com poucas variações

O Prototype cria novos objetos por cópia de instâncias existentes, reduzindo dependência do cliente em relação ao processo de criação detalhado.

Use quando:

  • objetos semelhantes são criados repetidamente
  • a configuração inicial é trabalhosa ou cara
  • faz sentido manter modelos prontos para clonagem
classDiagram
    class Prototype {
        <<interface>>
        +clonar() Prototype
    }
    class ConcretePrototype {
        +clonar() Prototype
    }
    class Cliente

    ConcretePrototype ..|> Prototype
    Cliente --> Prototype
  • evita reconstrução repetitiva de objetos complexos
  • reduz acoplamento com classes concretas
  • facilita criar variações a partir de modelos base
  • exige cuidado com cópia rasa e profunda
  • pode compartilhar estado mutável sem intenção
  • a lógica de clonagem pode ficar delicada

Os padrões criacionais estudados resolvem problemas diferentes, mas giram em torno da mesma pergunta: como criar objetos de forma mais controlada, flexível e compreensível?

  • Singleton controla unicidade
  • Builder organiza construção passo a passo
  • Factory Method desloca a decisão de criação para subclasses
  • Abstract Factory cria famílias coerentes
  • Prototype reutiliza objetos-base por cópia
PadrãoIdeia centralAplicabilidadeEstrutura básicaPrósContras
SingletonGarantir uma única instânciaConfiguração global, log, recursos únicosClasse com construtor privado e acesso estáticoControle central e unicidadeEstado global e testes mais difíceis
BuilderConstruir objeto em etapasObjetos complexos ou com muitos opcionaisCliente/Director, Builder, ProdutoLegibilidade e flexibilidade de montagemMais classes e cerimônia
Factory MethodDelegar criação a subclassesVariação de produtos com fluxo comumCreator, ConcreteCreator, ProductMenor acoplamento com concretosHierarquia maior
Abstract FactoryCriar famílias de objetos relacionadosTemas, plataformas, ambientes compatíveisFábrica abstrata, fábricas concretas, produtos abstratosCoerência entre famíliasDifícil adicionar novo produto
PrototypeClonar objetos existentesModelos pré-configurados e criação caraProtótipo com método de clonagemReuso de configuração baseComplexidade de cópia

Os padrões estruturais vistos até aqui foram:

  • Adapter
  • Bridge
  • Composite
  • Decorator
  • Facade
  • Proxy
  • Flyweight

O Adapter traduz uma interface incompatível para outra esperada pelo cliente.

Use quando:

  • você precisa integrar código legado ou biblioteca de terceiros
  • o cliente já depende de um contrato que não deve mudar
  • a incompatibilidade está na interface ou no formato de uso
classDiagram
    class Cliente
    class Target {
        <<interface>>
        +requisicao()
    }
    class Adapter {
        -adaptee: Adaptee
        +requisicao()
    }
    class Adaptee {
        +requisicaoEspecifica()
    }

    Adapter ..|> Target
    Adapter --> Adaptee
    Cliente --> Target
  • reaproveita código incompatível
  • isola conversões em um ponto claro
  • evita mudar cliente e serviço legado ao mesmo tempo
  • pode esconder modelagem ruim interna
  • adiciona mais uma camada de indireção
  • pode crescer demais se acumular lógica extra

O Bridge separa abstração e implementação em hierarquias independentes para evitar explosão de subclasses.

Use quando:

  • há duas dimensões independentes de variação
  • a herança geraria muitas combinações
  • abstração e implementação precisam evoluir separadamente
classDiagram
    class Abstraction {
        -implementor: Implementor
        +operacao()
    }
    class RefinedAbstraction
    class Implementor {
        <<interface>>
        +operacaoImpl()
    }
    class ConcreteImplementorA
    class ConcreteImplementorB

    RefinedAbstraction --|> Abstraction
    Abstraction --> Implementor
    ConcreteImplementorA ..|> Implementor
    ConcreteImplementorB ..|> Implementor
  • evita explosão combinatória de subclasses
  • permite trocar implementação em tempo de execução
  • reduz acoplamento entre dois eixos de variação
  • pode parecer abstrato demais no início
  • adiciona mais camadas conceituais
  • não vale a pena quando não há duas dimensões reais

O Composite permite tratar objetos simples e compostos de forma uniforme em estruturas de árvore.

Use quando:

  • o domínio possui hierarquia parte-todo
  • itens e grupos devem responder ao mesmo contrato
  • operações recursivas são frequentes
classDiagram
    class Component {
        <<interface>>
        +operacao()
    }
    class Leaf {
        +operacao()
    }
    class Composite {
        -filhos: List~Component~
        +adicionar(c: Component)
        +remover(c: Component)
        +operacao()
    }

    Leaf ..|> Component
    Composite ..|> Component
    Composite --> Component
  • simplifica cliente em estruturas hierárquicas
  • favorece recursão e composição uniforme
  • facilita expansão da árvore
  • interface comum pode ficar genérica demais
  • folhas podem herdar operações pouco úteis
  • complica cenários sem hierarquia real

O Decorator adiciona responsabilidades dinamicamente a um objeto por meio de envoltórios com a mesma interface.

Use quando:

  • funcionalidades extras são opcionais e combináveis
  • herança criaria muitas subclasses
  • o comportamento deve ser montado em camadas
classDiagram
    class Component {
        <<interface>>
        +operacao()
    }
    class ConcreteComponent {
        +operacao()
    }
    class Decorator {
        -wrappee: Component
        +operacao()
    }
    class ConcreteDecoratorA {
        +operacao()
    }
    class ConcreteDecoratorB {
        +operacao()
    }

    ConcreteComponent ..|> Component
    Decorator ..|> Component
    Decorator --> Component
    ConcreteDecoratorA --|> Decorator
    ConcreteDecoratorB --|> Decorator
  • combina comportamentos sem explodir subclasses
  • mantém o mesmo contrato para o cliente
  • favorece extensão incremental
  • cadeias longas dificultam depuração
  • a ordem das camadas pode importar
  • pode ficar mais difícil entender o objeto final

O Facade oferece uma interface simples para um subsistema mais complexo.

Use quando:

  • o cliente só precisa de operações de alto nível
  • há muitas classes técnicas ou etapas repetidas
  • você quer reduzir acoplamento com um subsistema
classDiagram
    class Cliente
    class Facade {
        +operacaoDeAltoNivel()
    }
    class SubsistemaA
    class SubsistemaB
    class SubsistemaC

    Cliente --> Facade
    Facade --> SubsistemaA
    Facade --> SubsistemaB
    Facade --> SubsistemaC
  • simplifica uso de subsistemas complexos
  • reduz atrito e acoplamento para o cliente
  • centraliza fluxos técnicos repetidos
  • pode virar classe inchada
  • pode esconder detalhes que às vezes importam
  • não substitui bom desenho interno do subsistema

O Proxy controla o acesso a outro objeto por meio de um substituto com a mesma interface.

Use quando:

  • o objeto real é caro, remoto ou sensível
  • é necessário adicionar cache, log, lazy loading ou autorização
  • o cliente deve continuar vendo o mesmo contrato
classDiagram
    class Subject {
        <<interface>>
        +requisicao()
    }
    class RealSubject {
        +requisicao()
    }
    class Proxy {
        -realSubject: RealSubject
        +requisicao()
    }
    class Cliente

    RealSubject ..|> Subject
    Proxy ..|> Subject
    Proxy --> RealSubject
    Cliente --> Subject
  • controla acesso sem alterar o cliente
  • permite cache, proteção e inicialização tardia
  • concentra políticas transversais em um ponto só
  • adiciona indireção extra
  • pode esconder custos reais de acesso remoto
  • pode concentrar responsabilidades demais

O Flyweight compartilha estado intrínseco entre muitos objetos semelhantes para reduzir consumo de memória.

Use quando:

  • há muitos objetos parecidos em memória
  • boa parte do estado se repete
  • o ganho de compartilhamento compensa a complexidade extra
classDiagram
    class Cliente
    class Flyweight {
        +operacao(estadoExtrinseco)
    }
    class ConcreteFlyweight
    class FlyweightFactory {
        -cache: Map
        +obter(chave) Flyweight
    }
    class Contexto {
        -estadoExtrinseco
    }

    ConcreteFlyweight ..|> Flyweight
    FlyweightFactory --> Flyweight
    Cliente --> FlyweightFactory
    Cliente --> Contexto
    Contexto --> Flyweight
  • reduz uso de memória em coleções grandes
  • evita duplicação de estado repetido
  • favorece reuso de objetos compartilhados
  • exige separação cuidadosa entre estados
  • aumenta a complexidade do modelo
  • pode ser otimização prematura se não houver volume real

Os padrões estruturais estudados mostram várias estratégias para lidar com composição e organização:

  • Adapter traduz interfaces
  • Bridge separa duas dimensões de variação
  • Composite organiza hierarquias parte-todo
  • Decorator empilha responsabilidades
  • Facade simplifica acesso a subsistemas
  • Proxy controla acesso a objetos
  • Flyweight compartilha estado repetido

Apesar de todos serem estruturais, cada um responde a um tipo diferente de acoplamento ou composição. Essa distinção é importante porque vários deles têm diagramas parecidos, mas intenções bem diferentes.

PadrãoIdeia centralAplicabilidadeEstrutura básicaPrósContras
AdapterTraduzir interface incompatívelIntegração com legado ou terceirosCliente, Target, Adapter, AdapteeReuso de componentes existentesCamada extra e possível mascaramento de problema interno
BridgeSeparar abstração e implementaçãoDuas dimensões independentes de variaçãoAbstraction, Implementor e concretosEvita explosão de subclassesMais abstrações para entender
CompositeTratar folha e grupo igualmenteÁrvores e composições parte-todoComponent, Leaf, CompositeCliente uniforme em hierarquiasInterface pode ficar genérica demais
DecoratorAdicionar comportamento por camadasFuncionalidades opcionais e combináveisComponent, Decorator, concretosCombinação flexível sem herança excessivaCadeias longas dificultam leitura
FacadeSimplificar subsistema complexoAPIs extensas e fluxos técnicos repetidosCliente, Facade, subsistemasMenor acoplamento para o clienteFachada pode inchar
ProxyControlar acesso mantendo a mesma interfaceCache, proteção, lazy loading, remotoSubject, Proxy, RealSubjectInterceptação transparente ao clienteIndireção e risco de esconder custos
FlyweightCompartilhar estado comumMuitos objetos semelhantes em memóriaFlyweight, Factory, ContextoEconomia de memóriaSeparação de estados mais complexa

Uma forma simples de comparar as categorias vistas até aqui é observar a pergunta dominante de cada uma:

CategoriaPergunta principalExemplos estudados
CriacionalComo criar objetos com menos acoplamento e mais controle?Singleton, Builder, Factory Method, Abstract Factory, Prototype
EstruturalComo organizar classes e objetos para colaborar melhor?Adapter, Bridge, Composite, Decorator, Facade, Proxy, Flyweight
ComportamentalComo distribuir responsabilidades e interações?Categoria apresentada, mas ainda não aprofundada em tópicos próprios

Até aqui, a disciplina já mostrou que Design Patterns não devem ser entendidos como receita fixa, e sim como ferramentas de modelagem. Cada padrão resolve um tipo de tensão recorrente:

  • excesso de dependência de classes concretas
  • criação complexa ou repetitiva
  • incompatibilidade entre interfaces
  • necessidade de simplificar subsistemas
  • controle de acesso ou compartilhamento eficiente de objetos

O principal ganho de estudar os padrões em conjunto é desenvolver repertório para decidir melhor. Em vez de decorar nomes, o objetivo é reconhecer sinais no problema:

  • há variações na criação?
  • há objetos demais com estado repetido?
  • há camadas extras de comportamento?
  • o cliente está conhecendo detalhes demais?

Quando essas perguntas começam a ficar claras, os padrões deixam de parecer catálogo teórico e passam a funcionar como apoio real no design de software.