Padrões de desenho

Os dez padrões do programa, escolhidos pelo problema e pelas suas consequências.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

O inimigo mantém posição e vida, mas pode patrulhar ou perseguir o herói. Separar o algoritmo numa interface permite trocar o movimento sem reescrever Inimigo. Strategy dá nome a essa colaboração. Cada padrão descreve um problema, os participantes que o resolvem e as consequências da escolha.

Não é uma classe pronta a copiar. Para aplicar um padrão, identifica o cliente, a decisão que varia e o contrato que permite separar essa decisão.

Strategy: trocar o algoritmo

Strategy encapsula algoritmos alternativos com a mesma interface. O contexto guarda uma estratégia e delega-lhe a operação. O cliente ou a configuração escolhe a alternativa.

No exemplo de composição, Inimigo é o contexto, Movimento a estratégia e ParaDireita e Parado as alternativas. passo() não testa o tipo do movimento. Para acrescentar uma perseguição, criamos outra implementação do contrato.

A estratégia precisa dos dados suficientes para calcular o resultado, não de acesso a todo o estado interno. A escolha concreta continua a existir em algum lado, normalmente na construção ou configuração do inimigo. Se o algoritmo é fixo e pequeno, separar várias classes pode não ajudar.

State: reagir conforme o estado

State delega o comportamento num objeto que representa o estado atual. As operações podem mudar esse estado. O foco é o ciclo de vida do contexto, em vez da seleção de um algoritmo pelo cliente.

Este programa completo mostra um jogo que começa ativo. Guarda-o em Main.java para correr localmente:

interface Estado {
    void tick(Jogo jogo);
    void alternar(Jogo jogo);
}

class Ativo implements Estado {
    public void tick(Jogo jogo) { jogo.avancar(); }
    public void alternar(Jogo jogo) { jogo.mudar(new Pausa()); }
}

class Pausa implements Estado {
    public void tick(Jogo jogo) { }
    public void alternar(Jogo jogo) { jogo.mudar(new Ativo()); }
}

class Jogo {
    private Estado estado = new Ativo();
    private int passos;
    void tick() { estado.tick(this); }
    void alternar() { estado.alternar(this); }
    void mudar(Estado novo) { estado = novo; }
    void avancar() { passos++; }
    int getPassos() { return passos; }
}

public class Main {
    public static void main(String[] args) {
        Jogo jogo = new Jogo();
        jogo.tick();
        jogo.alternar();
        jogo.tick();
        jogo.alternar();
        jogo.tick();
        System.out.println(jogo.getPassos());
    }
}

A saída é 2. Segue o estado e o contador, que são dados diferentes:

PedidoAntesDepoisPassos
tickAtivoAtivo1
alternarAtivoPausa1
tickPausaPausa1
alternarPausaAtivo1
tickAtivoAtivo2

O tick em Pausa não altera o contador. Repara que são dois valores diferentes a evoluir: o estado atual e o número de passos. Cada estado decide o que cada pedido faz e para que estado o jogo segue. Se as transições vivessem todas no contexto, continuava a ser preciso um responsável claro por cada reação; aqui esse responsável é o objeto de estado.

State e Strategy podem ter estruturas parecidas. A intenção distingue-os: trocar perseguição por patrulha é Strategy; reagir de outra forma depois de entrar em Pausa é State. Um pequeno conjunto de estados também pode ser representado por uma enumeração e condições sem perder clareza.

Command: guardar um pedido

Command representa um pedido como objeto. O invocador não precisa de conhecer os detalhes da operação; o comando conhece o recetor e os dados necessários. Podemos guardar comandos numa fila ou num histórico.

Para desfazer um movimento, precisamos do estado anterior. Fazer o movimento inverso nem sempre recupera esse estado, por exemplo quando colisões ou limites impediram parte da ação.

Executar e desfazer um comando
interface Comando {
    void executar();
    void desfazer();
}

class Heroi {
    private int x;
    Heroi(int x) { this.x = x; }
    int getX() { return x; }
    void colocar(int x) { this.x = x; }
}

class Mover implements Comando {
    private final Heroi heroi;
    private final int deslocamento;
    private int anterior;
    private boolean executado;

    Mover(Heroi heroi, int deslocamento) {
        this.heroi = heroi;
        this.deslocamento = deslocamento;
    }

    public void executar() {
        if (executado) throw new IllegalStateException("ja executado");
        anterior = heroi.getX();
        heroi.colocar(anterior + deslocamento);
        executado = true;
    }

    public void desfazer() {
        if (!executado) throw new IllegalStateException("nao executado");
        heroi.colocar(anterior);
        executado = false;
    }

}

public class Main {
    public static void main(String[] args) {
        Heroi heroi = new Heroi(2);
        Comando comando = new Mover(heroi, 3);
        comando.executar();
        System.out.println(heroi.getX());
        comando.desfazer();
        System.out.println(heroi.getX());
    }
}
Resultado

Executa o código para ver a saída e os gráficos.

Obtemos 5 e 2: Main é o invocador, Mover o comando concreto e Heroi o recetor. Este exemplo supõe que nada altera o herói entre executar e desfazer; num histórico, desfaz os comandos por ordem inversa. Um pedido com efeitos externos, como enviar uma mensagem, pode não ter inversão exata, pois o padrão não garante undo automaticamente.

Observer: notificar interessados

Observer permite que um sujeito notifique vários observadores através de um contrato. O sujeito não conhece as classes concretas que reagem. Cada interessado subscreve e deve poder deixar de subscrever.

Começa pelo caso simples, um sujeito com lista de inscritos e notificação síncrona: o cliente subscreve dois observadores, altera o valor e cada um recebe mudou(pontos); sair da lista termina as notificações futuras.

Na pontuação, um observador atualiza o texto e outro verifica uma conquista, seguindo a sequência alterar o valor, obter o novo estado válido e notificar os inscritos; o observador pode receber o valor (modelo push) ou pedir o estado ao sujeito (modelo pull).

Segue os dois tipos de seta. A seta contínua indica a chamada feita pelo sujeito; as tracejadas indicam que os destinatários implementam o contrato. Os objetos inscritos recebem o novo valor, mas cada um decide a sua reação.

Este excerto mostra um sujeito com notificação síncrona:

interface Observador {
    void mudou(int pontos);
}

class Pontuacao {
    private int pontos;
    private final java.util.List<Observador> observadores =
        new java.util.ArrayList<Observador>();

    void subscrever(Observador observador) { observadores.add(observador); }
    void remover(Observador observador) { observadores.remove(observador); }

    void somar(int valor) {
        pontos += valor;
        java.util.List<Observador> copia =
            new java.util.ArrayList<Observador>(observadores);
        for (Observador observador : copia) observador.mudou(pontos);
    }
}

A cópia estabiliza os destinatários desta notificação se um callback alterar a lista. O exemplo pressupõe uma única thread e callbacks que não voltam a alterar a pontuação. Num sistema real, decide também o que acontece se um observador lança uma exceção e se a remoção vale logo para a notificação atual ou apenas para as seguintes.

Uma referência esquecida na lista pode manter um observador vivo e continuar a enviar-lhe eventos. Por isso o ciclo de vida da subscrição faz parte do contrato.

Este vídeo mostra o padrão Observer com um exemplo próprio:

Abrir no YouTube

Identifica quem mantém as subscrições, quem provoca a notificação e como um observador deixa de a receber. Relaciona essa colaboração com os eventos de uma interface.

Composite: tratar uma árvore pela mesma interface

Composite usa uma interface comum para folhas e grupos. O grupo guarda filhos dessa interface, que podem ser folhas ou outros grupos. O cliente pede a operação sem testar qual é o caso.

Imagina um grupo de inimigos com força total. Uma folha devolve a sua força. Um grupo soma a força dos filhos; se um filho for outro grupo, a mesma chamada continua recursivamente. Desenha a árvore antes de somar:

Grupo(exterior)
+- Soldado(4)
+- Grupo(interior)
   +- Soldado(2)
   +- Soldado(7)

O interior dá 2 + 7 = 9; o exterior dá 4 + 9 = 13. Cada chamada usa a mesma interface forca(), sem testar o tipo do filho.

O programa seguinte constrói essa mesma árvore. Guarda-o em Main.java para o executar localmente:

import java.util.Arrays;
import java.util.List;

interface Unidade {
    int forca();
}

class Soldado implements Unidade {
    private final int valor;
    Soldado(int valor) { this.valor = valor; }
    public int forca() { return valor; }
}

class Grupo implements Unidade {
    private final List<Unidade> filhos;
    Grupo(Unidade... filhos) { this.filhos = Arrays.asList(filhos); }
    public int forca() {
        int total = 0;
        for (Unidade filho : filhos) total += filho.forca();
        return total;
    }
}

public class Main {
    public static void main(String[] args) {
        Unidade grupo = new Grupo(new Soldado(4),
            new Grupo(new Soldado(2), new Soldado(7)));
        System.out.println(grupo.forca());
    }
}

A saída é 13, como na árvore anterior. Um new Grupo() sem filhos devolve zero porque o ciclo não soma nenhuma parcela. O parâmetro Unidade... filhos aceita zero ou mais argumentos, reunidos num array dentro do construtor. Arrays.asList permite percorrer esse array como lista; não faz uma cópia dos seus elementos.

Unidade é o componente, Soldado a folha e Grupo o composto. Nem todas as operações têm de pertencer à interface comum: acrescentar um filho pode ficar só em Grupo, para não obrigar uma folha a fingir que suporta filhos.

Composite não é o mesmo que composição UML. O padrão trata uma árvore uniformemente; a relação UML descreve posse. Se permitires partilhar filhos ou criar ciclos, precisas de regras explícitas para evitar contagens duplicadas ou recursão sem fim.

Factory Method: subclasses escolhem o produto

Factory Method deixa uma operação de criação ser redefinida por subclasses do criador. A lógica que utiliza o produto depende da interface desse produto.

Este programa completo separa a criação da arma da lógica de ataque:

interface Arma {
    int dano();
}

class Espada implements Arma {
    public int dano() { return 4; }
}

class Feitico implements Arma {
    public int dano() { return 7; }
}

abstract class Combatente {
    protected abstract Arma criarArma();
    int atacar() { return criarArma().dano(); }
}

class Guerreiro extends Combatente {
    protected Arma criarArma() { return new Espada(); }
}

class Mago extends Combatente {
    protected Arma criarArma() { return new Feitico(); }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(new Guerreiro().atacar());
        System.out.println(new Mago().atacar());
    }
}

A saída é 4 e 7. Combatente é o criador; criarArma o factory method; Guerreiro e Mago os criadores concretos. atacar não escolhe classes de armas. Neste exemplo cria uma arma em cada ataque; guardar ou reutilizar o produto é uma decisão separada.

Evita chamar operações redefiníveis no construtor da base: o construtor da subclasse ainda pode não ter inicializado o estado de que elas dependem. Um método estático que contém um switch e devolve produtos pode ser uma fábrica simples, mas não demonstra esta colaboração por subclasses.

Abstract Factory: escolher uma família

Abstract Factory resolve o passo seguinte ao Factory Method: quando o cliente precisa de uma família coerente de produtos, como inimigo e arma do mesmo tema, recebe a fábrica e usa as interfaces dos produtos. A decisão passa a ser que fábrica recebe o cliente. A fábrica concreta garante que as variantes pertencem à mesma família.

Uma arena pode ter uma fábrica de tema com criarInimigo() e criarArma(). A fábrica Medieval cria um cavaleiro e uma espada; a fábrica Espacial cria um robô e um laser. Trocar a fábrica troca a família sem editar o cliente que monta a arena. O exemplo seguinte junta contrato, produtos, fábricas e cliente num programa completo; guarda-o como Main.java e corre-o localmente:

interface Inimigo {
    String descricao();
}

interface Arma {
    int dano();
}

interface FabricaTema {
    Inimigo criarInimigo();
    Arma criarArma();
}

class Cavaleiro implements Inimigo {
    public String descricao() { return "cavaleiro"; }
}

class Espada implements Arma {
    public int dano() { return 4; }
}

class Robo implements Inimigo {
    public String descricao() { return "robo"; }
}

class Laser implements Arma {
    public int dano() { return 7; }
}

class FabricaMedieval implements FabricaTema {
    public Inimigo criarInimigo() {
        return new Cavaleiro();
    }
    public Arma criarArma() { return new Espada(); }
}

class FabricaEspacial implements FabricaTema {
    public Inimigo criarInimigo() { return new Robo(); }
    public Arma criarArma() { return new Laser(); }
}

class ArenaJogo {
    private final Inimigo inimigo;
    private final Arma arma;
    ArenaJogo(FabricaTema fabrica) {
        inimigo = fabrica.criarInimigo();
        arma = fabrica.criarArma();
    }
    String descrever() { return inimigo.descricao() + ":" + arma.dano(); }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(new ArenaJogo(new FabricaMedieval()).descrever());
        System.out.println(new ArenaJogo(new FabricaEspacial()).descrever());
    }
}

A saída é cavaleiro:4 e robo:7. O cliente ArenaJogo só conhece FabricaTema, Inimigo e Arma; trocar a fábrica troca a família sem editar o cliente. Acrescentar outra família é simples: outra fábrica e produtos. Acrescentar um novo tipo de produto, como criarEscudo(), exige mudar o contrato e todas as fábricas. Esta é uma consequência que deve pesar na escolha.

Factory Method resolve a decisão de um produto numa hierarquia de criadores. Abstract Factory fornece uma família através de um objeto fábrica. Uma Abstract Factory pode implementar os seus métodos de criação usando Factory Methods, mas as intenções continuam distintas.

Adapter e Decorator

Adapter traduz uma interface existente para o contrato de que o cliente precisa. Decorator envolve um objeto, conserva a interface e acrescenta comportamento. Ambos usam composição, mas não resolvem o mesmo problema.

Neste exemplo, o cliente precisa de um preço em cêntimos. A biblioteca antiga fornece euros inteiros, por isso o adapter converte unidades. Um decorator acrescenta a taxa de entrega.

Adaptar a unidade e acrescentar uma taxa
interface Preco {
    int centimos();
}

class PrecoAntigo {
    int euros() { return 12; }
}

class AdapterPreco implements Preco {
    private final PrecoAntigo antigo;
    AdapterPreco(PrecoAntigo antigo) { this.antigo = antigo; }
    public int centimos() { return antigo.euros() * 100; }
}

class ComEntrega implements Preco {
    private final Preco base;
    ComEntrega(Preco base) { this.base = base; }
    public int centimos() { return base.centimos() + 250; }
}

public class Main {
    public static void main(String[] args) {
        Preco base = new AdapterPreco(new PrecoAntigo());
        Preco total = new ComEntrega(base);
        System.out.println(base.centimos());
        System.out.println(total.centimos());
    }
}
Resultado

Executa o código para ver a saída e os gráficos.

Obtemos 1200 e 1450. O adapter permite usar a classe antiga sem a alterar. O decorator aceita qualquer Preco, incluindo outro decorator. Se aplicares duas entregas, cobras duas taxas; a composição não valida sozinha se essa combinação tem sentido.

Num adapter real, define também erros, unidades e conversões que podem perder informação. Num decorator, a ordem pode mudar o resultado: aplicar 10% de desconto antes ou depois de acrescentar 250 cêntimos não produz sempre o mesmo total. A interface comum não torna as operações comutativas.

Delegação e preço atual

Guardar uma referência ao objeto envolvido é diferente de guardar o resultado de uma consulta. Se ComEntrega.centimos() chama base.centimos() em cada pedido, uma mudança no preço base aparece no próximo pedido. Calcular o total só no construtor guardaria um snapshot e poderia quebrar esse contrato.

No programa seguinte, a entrega acrescenta 200 cêntimos e o desconto reduz em 10% o total que recebe. Os preços usados dão resultados inteiros exatos; para outros valores, a divisão inteira arredondaria para baixo e essa política precisaria de fazer parte da regra.

Consultar novamente o objeto envolvido
interface Preco {
    int centimos();
}

final class Base implements Preco {
    private int valor;
    Base(int valor) { mudar(valor); }
    void mudar(int novo) {
        if (novo < 0 || novo > 10000)
            throw new IllegalArgumentException("preco invalido");
        valor = novo;
    }
    public int centimos() { return valor; }
}

final class ComEntrega implements Preco {
    private final Preco base;
    ComEntrega(Preco base) { this.base = base; }
    public int centimos() { return base.centimos() + 200; }
}

final class ComDesconto implements Preco {
    private final Preco base;
    ComDesconto(Preco base) { this.base = base; }
    public int centimos() { return base.centimos() * 9 / 10; }
}

public class Main {
    public static void main(String[] args) {
        Base base = new Base(1000);
        Preco total = new ComDesconto(new ComEntrega(base));
        System.out.println(total.centimos());
        base.mudar(600);
        System.out.println(total.centimos());
        Preco outraOrdem = new ComEntrega(new ComDesconto(base));
        System.out.println(outraOrdem.centimos());
    }
}
Resultado

Executa o código para ver a saída e os gráficos.

As saídas são 1080, 720 e 740. Primeiro, o total com entrega e depois desconto é (1000+200)×0,9=1080(1000+200)\times0{,}9=1080. Ao mudar a mesma base para 600, a cadeia existente passa a dar (600+200)×0,9=720(600+200)\times0{,}9=720. Na última linha, o desconto fica por dentro da entrega: 600×0,9+200=740600\times0{,}9+200=740.

Não reconstruímos os decorators para obter 720: continuam a consultar o objeto a que estavam ligados. Também não notificámos observadores; a alteração aparece porque houve uma nova consulta. Se a interface incluir operações de alteração, define quais são delegadas e quais pertencem ao wrapper. O padrão não decide sozinho entre estado partilhado e cópia.

Singleton: uma instância com acesso global

Singleton impõe uma instância e um ponto global de acesso. Um exemplo Java com criação antecipada é:

final class Configuracao {
    private static final Configuracao INSTANCIA = new Configuracao();
    private Configuracao() { }
    static Configuracao instancia() { return INSTANCIA; }
}

A inicialização da classe protege a criação da instância segundo as regras de Java. Isso não torna automaticamente thread-safe qualquer estado mutável acrescentado à classe. Uma implementação lazy sem sincronização pode criar mais de uma instância em acessos concorrentes.

O acesso global esconde dependências e pode fazer testes partilharem estado. Se a aplicação precisa de uma configuração única, muitas vezes podes criá-la uma vez na montagem e passá-la aos clientes. Ter uma instância durante uma execução é diferente de proibir outras instâncias pelo tipo.

Escolher pelo problema

Quando vês classes com a mesma estrutura, pergunta quem decide variar e o que o cliente precisa de ignorar. Strategy varia um algoritmo; State varia a reação conforme o estado; Command guarda um pedido; Observer distribui notificações. Composite trata folhas e grupos; Adapter traduz; Decorator acrescenta responsabilidades. As fábricas separam criação; Singleton restringe instâncias e acrescenta acesso global.

Depois explica o custo. Um padrão pode acrescentar objetos, interfaces, gestão de ciclo de vida ou restrições de teste. A solução deve tornar uma mudança concreta mais simples e preservar um contrato compreensível.

Exercícios

Percorrer uma árvore e distinguir partilha

Uma folha devolve a sua força; um grupo soma forca() de cada filho. O grupo exterior contém uma folha de força 4 e um grupo com folhas de forças 2 e 7. Calcula a força e explica o percurso. Quanto devolve um grupo vazio? Se a mesma folha de força 2 aparecesse duas vezes na lista do grupo interior, o algoritmo contaria essa força uma ou duas vezes? Não há ciclos.

Pista

A implementação percorre entradas da lista, não um conjunto de identidades.

Solução

O interior devolve 2 + 7 = 9 e o exterior 4 + 9 = 13. Cada filho responde pela interface Unidade, incluindo grupos que repetem a mesma operação nos seus filhos. Um grupo vazio devolve zero, o valor inicial do acumulador. Duas entradas para a mesma folha chamam forca() duas vezes: o interior passaria a 2 + 2 + 7 = 11 e o exterior a 15. Se queremos contar unidades únicas, temos de proibir a partilha ou definir outra política; Composite não garante essa regra.

Confere a tua resposta:

  • Calculei 13 e justifiquei a soma recursiva pela interface comum.
  • Obtive zero para um grupo vazio.
  • Distingui somar ocorrências de contar objetos únicos num filho partilhado.
Voltar à explicação
Política de movimento e ciclo de estados

Um inimigo recebe na construção um algoritmo de patrulha ou perseguição. O jogo, por sua vez, começa Ativo: tick() avança, alternar() entra em Pausa, e tick() em Pausa não avança. Ambos os desenhos guardam um objeto através de uma interface e delegam-lhe operações. Escolhe o padrão de cada caso e justifica pela intenção. O diagrama de classes, sozinho, decide a resposta?

Pista

Pergunta quem escolhe a alternativa e se a mudança faz parte do ciclo de vida.

Solução

Patrulha/perseguição são estratégias intercambiáveis escolhidas para o inimigo: Strategy. Ativo/Pausa determinam reações e transições do próprio jogo: State. As estruturas podem parecer iguais, por isso uma seta para uma interface não decide a intenção. Precisamos de conhecer os pedidos, as transições e quem seleciona o objeto delegado.

Confere a tua resposta:

  • Associei patrulha/perseguição a Strategy pela escolha de algoritmo.
  • Associei Ativo/Pausa a State pelas reações e transições.
  • Expliquei que uma interface e delegação não bastam para distinguir os padrões.
Voltar à explicação
A ordem dos decorators

O preço base é 1000 cêntimos. Um decorator dá 10% de desconto e outro soma 200 cêntimos de entrega. Aplica primeiro o desconto à base e depois acrescenta entrega. Qual é o total?

Pista
O desconto afeta apenas o valor que já está dentro dele.
Mais uma pista
Dez por cento de mil são cem; a entrega entra depois.
Solução

Calcula por passos: desconto dá 1000 × 0,9 = 900, entrega soma 200, total 1100. Na ordem inversa seria (1000 + 200) × 0,9 = 1080. A interface comum permite composição, mas não garante que as operações comutem.

Erros frequentes

Não apliques o desconto à entrega quando o enunciado a acrescenta depois.

Voltar à explicação
Desfazer um movimento limitado

Uma arena limita a posição ao intervalo de 0 a 4. O herói começa em 3. Um comando pede deslocamento +3, mas a regra coloca-o em 4. Fazer -3 para desfazer deixa-o em 1. Propõe os dados que o comando deve guardar e o estado depois de um undo correto. Supõe que nada mais altera o herói entretanto.

Pista
O pedido e a deslocação efetiva são diferentes.
Mais uma pista

O comando pode conservar um snapshot do estado que deve recuperar.

Solução

Guarda a posição 3 antes de executar. Um undo repõe 3 através de uma operação válida do recetor. Inverter +3 usa o pedido, embora a deslocação aceite tenha sido apenas +1. Num histórico, desfaz em ordem inversa e define o que acontece se outro participante alterar o estado. Command representa o pedido; não cria sozinho uma inversão correta.

Confere a tua resposta:

  • Guarda a posição anterior 3.
  • Repõe 3 em vez de aplicar o deslocamento inverso.
  • Reconhece a hipótese de ausência de alterações intervenientes e a ordem inversa num histórico.

Erros frequentes

Não deduzas a posição anterior só a partir do pedido quando há limites, colisões ou outras regras que o alteram.

Voltar à explicação
Remover um observador durante a notificação

No sujeito da lição, A e B estão inscritos nessa ordem e a notificação percorre uma cópia da lista. Durante o seu callback, A remove B da lista original. Nenhum callback altera a pontuação, volta a inscrever B ou lança exceções. B recebe a notificação atual? E a seguinte? Justifica pela política do exemplo.

Pista

A lista percorrida e a lista de subscrições já não são o mesmo objeto.

Mais uma pista

Cada chamada a somar cria uma nova cópia das subscrições desse momento.

Solução

B recebe a notificação atual, pois a cópia ainda contém A e B. Não recebe a seguinte, cuja cópia só contém A. Esta é a política do exemplo, não uma propriedade obrigatória de Observer. Se o contrato exigir remoção imediata, o sujeito precisa de outra forma de decidir quem ainda pode receber cada callback.

Confere a tua resposta:

  • B recebe a notificação atual porque está na cópia.
  • B não recebe a seguinte porque já não está inscrito na lista original.
  • Reconhece que a política de remoção tem de fazer parte do contrato.

Erros frequentes

Copiar a lista evita mudar os destinatários dessa passagem, mas não garante segurança entre threads nem tratamento de exceções.

Voltar à explicação
Acrescentar uma família ou um produto

FabricaTema cria inimigos e armas nas famílias Medieval e Espacial. Compara acrescentar uma família Submarina com acrescentar um novo tipo de produto, escudos, a todas as famílias. Diz onde mudam os contratos. Como se distingue este desenho de Combatente.criarArma() redefinido por Guerreiro e Mago?

Pista

Separa variar as implementações de variar as operações que todos devem oferecer.

Mais uma pista

criarEscudo ainda não existe no contrato de FabricaTema.

Solução

A fábrica Submarina implementa criarInimigo() e criarArma() com novos produtos; o contrato existente pode ficar igual. Acrescentar escudos exige a interface do produto, criarEscudo() no contrato e implementação nas fábricas Medieval, Espacial e Submarina. A escolha de uma família de produtos por um objeto fábrica é Abstract Factory. Redefinir criarArma() em subclasses do criador é Factory Method. Um pode usar o outro, mas resolvem decisões distintas.

Confere a tua resposta:

  • A nova família implementa os métodos já existentes e acrescenta os seus produtos.
  • Escudos exigem uma nova operação na interface e em todas as fábricas.
  • Distingue Abstract Factory por objeto de família de Factory Method por redefinição no criador.

Erros frequentes

O nome fábrica e a presença de new não bastam para identificar o padrão. Acrescentar um produto não tem o mesmo custo de acrescentar uma família.

Voltar à explicação
Uma instância não protege o estado

Uma configuração Singleton é criada na inicialização da classe e contém um campo mutável volume. Um teste altera-o para zero; outro espera o volume inicial. Duas threads também podem mudar o campo. Explica o que a criação antecipada garante e o que continua por resolver. Propõe uma alternativa para clientes que só precisam de partilhar a configuração durante uma execução.

Pista

A inicialização ocorre uma vez, mas as mudanças ao volume acontecem depois.

Mais uma pista

Pergunta quem controla o estado inicial de cada teste.

Solução

A inicialização da classe publica a instância com segurança. Não torna todas as leituras e escritas posteriores ao volume sincronizadas. Os testes podem depender do estado global que outro deixou; a ordem passa a influenciar o resultado. Podemos criar uma configuração na montagem e passá-la aos clientes. Cada teste recebe a sua configuração. Uma configuração imutável elimina alterações ao volume; se precisa de mudanças concorrentes, define uma disciplina de sincronização adequada.

Confere a tua resposta:

  • Distingue criação segura da instância de sincronização dos campos mutáveis.
  • Identifica estado global partilhado entre testes.
  • Propõe construir uma configuração na montagem e passá-la aos clientes.

Erros frequentes

Guardar a referência num campo static final não torna imutável o objeto referido nem assegura exclusão entre operações.

Voltar à explicação
A mesma cadeia depois de mudar a base

Um preço base B começa em 1000 cêntimos. Um decorator E acrescenta 200 cêntimos ao consultar o objeto envolvido; outro D devolve 90% do preço que consulta. Usamos somente os valores dados, que não exigem arredondamento, e uma única thread.

Construímos total = D(E(B)) e consultamos. Depois mudamos o mesmo B para 600 e voltamos a consultar a cadeia já construída. Calcula os dois valores e compara o segundo com E(D(B)). Uma implementação de E guarda o total calculado no construtor e nunca volta a consultar B: cumpre o contrato dado? A alteração de B, por si só, avisa uma vista que já mostrou o preço?

Solução

O primeiro total é (1000 + 200) × 0,9 = 1080. Depois, a mesma cadeia consulta B novamente e dá (600 + 200) × 0,9 = 720. Na ordem E(D(B)), dá 600 × 0,9 + 200 = 740.

Se E guarda apenas 1200 no construtor, D continuará a ler esse valor e dará 1080 depois da alteração, quebrando o contrato de preço atual. E deve delegar a consulta quando centimos() é chamado. Isto não é uma regra de que todos os decorators tenham base mutável: é o comportamento exigido aqui.

A alteração de B não avisa a vista por si só. O novo valor aparece numa consulta posterior; uma atualização automática da vista precisa de uma colaboração de eventos ou notificações adequada. Partilhar uma referência e receber uma notificação são mecanismos diferentes.

Confere a tua resposta:

  • Calculei 1080 antes da alteração e 720 depois, na cadeia existente.
  • Obtive 740 para desconto antes da entrega.
  • Distingui delegação em cada consulta de snapshot calculado no construtor e de notificação automática.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.