Soluções — Padrões de Projeto: Vocabulário, Imutabilidade e a Gênese Segura do Objeto

Aula 11 — Programação Orientada a Objetos

Autor

Marcos M. Raimundo — Instituto de Computação, UNICAMP

Aula Exercícios

NotaTeste 1 — O Vocabulário e a Natureza dos Padrões de Projeto
  • □ Uma equipe que copia o código-fonte de um projeto anterior para reaproveitar uma solução de design, sem adaptar as classes ao novo domínio, está aplicando corretamente um Padrão de Projeto no sentido em que o GoF definiu o termo.
  • □ Se um engenheiro descrever uma solução dizendo apenas “implemente um Observer aqui” para um colega que também domina o vocabulário GoF, essa frase provavelmente transmite mais informação estrutural precisa do que uma explicação equivalente sem o nome do padrão.
  • □ Um sistema construído inteiramente com funções globais e variáveis compartilhadas, sem nenhuma classe ou encapsulamento, se beneficiaria da mesma forma ao aplicar a estrutura de um padrão GoF que um sistema já orientado a objetos se beneficiaria.
  • □ Um algoritmo de ordenação otimizado para grandes volumes de dados, por mais sofisticado e reutilizável que seja, não se qualifica como um Padrão de Projeto no sentido do catálogo GoF, ainda que resolva um problema recorrente.
Dica(Resposta) Teste 1 — O Vocabulário e a Natureza dos Padrões de Projeto
  • ✗ Falso — Um Padrão de Projeto é um gabarito conceitual — a topologia de classes, responsabilidades e mensagens —, não um trecho de código para copiar e colar. Reaproveitar código-fonte literal de outro projeto, sem adaptar as classes ao novo domínio, é exatamente o antipadrão de confundir “padrão” com “biblioteca pronta”.
  • ✔ Verdadeiro — É exatamente o argumento da “linguagem ubíqua”: entre dois engenheiros que compartilham o vocabulário, uma única palavra (“Observer”, “Strategy”) condensa toda uma topologia de classes, responsabilidades e protocolo de comunicação que, sem o nome, exigiria um parágrafo inteiro de explicação — e ainda assim correria risco de ambiguidade.
  • ✗ Falso — Padrões de Projeto pressupõem que encapsulamento e troca de mensagens já estejam estabelecidos — eles não curam código procedural. Aplicar a estrutura de classes de um padrão sobre um sistema de variáveis globais e funções soltas apenas mascara o problema de fundo (a ausência de encapsulamento), sem resolvê-lo, ao contrário do que aconteceria num sistema já orientado a objetos.
  • ✔ Verdadeiro — Padrões de Projeto resolvem problemas de acoplamento e coesão entre classes/objetos, não problemas algorítmicos de ordenação, criptografia ou busca — mesmo que ambos sejam soluções recorrentes e reutilizáveis, a natureza do problema resolvido é categoricamente diferente.
NotaTeste 2 — As Três Famílias de Padrões
  • □ Um padrão cujo objetivo é permitir que um objeto adicione responsabilidades novas dinamicamente, sem alterar sua classe original nem depender de herança estática, se encaixa melhor na família Estrutural do que na família de Criação.
  • □ Um sistema de checkout que usa duas soluções ao mesmo tempo — uma para decidir qual meio de pagamento processar (Strategy) e outra para decidir qual classe concreta de repositório instanciar (Factory Method) — está misturando indevidamente duas famílias de padrões incompatíveis entre si.
  • □ Um padrão cujo foco declarado é “isolar a complexidade do operador new e gerenciar o ciclo de vida na Heap” pertence, por definição, à família de Criação, independentemente de também usar composição internamente.
  • □ Classificar o Strategy como um padrão Comportamental (e não Estrutural) depende do fato de ele envolver uma interface implementada por múltiplas classes, e não do fato de seu propósito ser distribuir a decisão de “como fazer” no tempo de execução.
Dica(Resposta) Teste 2 — As Três Famílias de Padrões
  • ✔ Verdadeiro — O padrão descrito (Decorator, citado em aula como exemplo de Estrutural) tem como foco a topologia — como compor objetos para formar uma estrutura maior e flexível —, não a gênese do objeto (Criação, que trata de como ele nasce) nem a comunicação no tempo (Comportamento).
  • ✗ Falso — As três famílias descrevem o propósito arquitetural predominante de cada padrão isolado, não uma exclusividade mútua entre padrões de famílias diferentes num mesmo sistema. Strategy (Comportamental) e Factory Method (Criação) resolvem problemas ortogonais — um trata de comportamento intercambiável, o outro de gênese segura — e sua combinação no mesmo sistema é comum e desejável, não uma mistura indevida.
  • ✔ Verdadeiro — A definição de família de Criação é exatamente essa — isolar a gênese e o ciclo de vida do new. O fato de quase todos os 23 padrões GoF (de qualquer família) se apoiarem em composição e polimorfismo como mecânica interna não muda a classificação por propósito: o critério de família é “para que serve”, não “com que mecanismo é construído”.
  • ✗ Falso — É o oposto: a classificação por família depende do propósito (comunicação e distribuição de obrigações no tempo, no caso do Strategy), não do mecanismo estrutural usado (uma interface com múltiplas implementações). Esse mesmo mecanismo — interface com várias implementações — também aparece em padrões Estruturais e de Criação; o que decide a família é para que a variação existe, não como ela é implementada em Java.
NotaTeste 3 — Aliasing e a Fragilidade do Estado Mutável
  • □ Em Retangulo r2 = r1;, se Retangulo for uma classe (não um primitivo), a variável r2 recebe uma cópia independente dos valores de largura e altura de r1 no momento da atribuição.
  • □ Um sistema que compartilha uma mesma instância de uma classe Configuracao mutável entre dois módulos independentes está estruturalmente exposto ao mesmo padrão de bug de aliasing descrito para o Retangulo, mesmo que o domínio seja diferente.
  • □ Aplicar uma “Cópia Defensiva” antes de cada compartilhamento de uma referência mutável elimina o risco de aliasing sem nenhum custo adicional de memória ou processamento sobre o sistema.
  • □ Se um método de uma classe imutável precisasse, para calcular seu retorno, ler o estado de outro objeto imutável passado como parâmetro, o risco de aliasing bug sobre esse parâmetro seria estruturalmente idêntico ao risco existente ao ler um parâmetro mutável.
Dica(Resposta) Teste 3 — Aliasing e a Fragilidade do Estado Mutável
  • ✗ Falso — Em Java, atribuir uma variável de tipo referência a outra copia o endereço de memória, nunca os dados. r2 passa a apontar para o mesmo objeto Retangulo na Heap que r1 já apontava — não existe uma segunda cópia independente de largura/altura.
  • ✔ Verdadeiro — O mecanismo do aliasing não depende do domínio (retângulos, configurações, ou qualquer outra classe) — depende apenas de duas condições: mutabilidade e uma referência compartilhada. Qualquer classe mutável compartilhada entre módulos está exposta ao mesmo risco estrutural.
  • ✗ Falso — A Cópia Defensiva elimina o risco de aliasing naquele ponto específico, mas tem custo real: sobrecarrega o Garbage Collector com objetos descartáveis extras e polui o código com lógica de clonagem repetida em cada fronteira de compartilhamento — não é uma solução “de graça”.
  • ✗ Falso — O risco de aliasing bug nasce especificamente da possibilidade de mutação por uma referência compartilhada. Se o objeto lido é imutável, nenhuma leitura pode alterá-lo “pelas costas” de quem mais o referencia — o cenário de corrupção silenciosa simplesmente não existe para um parâmetro imutável, ao contrário de um mutável.
NotaTeste 4 — Entidade vs. Value Object
  • □ Um sistema bancário que precisa diferenciar duas contas correntes com saldo, titular e agência idênticos (por serem contas efetivamente distintas, uma de cada cliente) exige que ContaCorrente seja modelada como Entidade, não como Value Object.
  • □ Se dois objetos Dinheiro representarem quantias e moedas idênticas, mas tiverem sido instanciados em pontos completamente diferentes do código, equals() entre eles deve retornar false, pois cada instância ocupa um endereço de memória próprio na Heap.
  • □ Uma classe Endereco (rua, número, cidade, CEP) usada apenas para exibir a localização de entrega de um Pedido, sem que o sistema precise rastrear “este endereço específico” ao longo do tempo, é uma candidata natural a Value Object.
  • □ Modelar Aluno como Entidade (identidade por RA) não impede que um dos seus atributos, como Endereco, seja internamente representado por um Value Object imutável.
Dica(Resposta) Teste 4 — Entidade vs. Value Object
  • ✔ Verdadeiro — A necessidade de distinguir dois objetos com atributos de valor idênticos é exatamente o critério que exige identidade persistente (um número de conta, por exemplo) — a marca registrada de uma Entidade. Um Value Object, cuja igualdade é por conteúdo, tornaria as duas contas indistinguíveis.
  • ✗ Falso — É exatamente o oposto do que define um Value Object: equals() compara valores (quantia e moeda), não endereços de memória. Dois Dinheiro com o mesmo conteúdo devem ser equals() independentemente de onde ou quando foram instanciados — essa é a substituibilidade que motiva o padrão.
  • ✔ Verdadeiro — Não há necessidade de identidade persistente aqui — o que importa é o conteúdo do endereço (rua, número, cidade, CEP), não “qual instância específica” foi usada. Dois Enderecos com os mesmos dados são intercambiáveis para todos os efeitos práticos, exatamente o critério de um Value Object.
  • ✔ Verdadeiro — Entidade e Value Object não são mutuamente exclusivos dentro do mesmo grafo de objetos — uma Entidade tipicamente contém Value Objects como parte do seu estado mutável (o RA de Aluno continua sendo a identidade persistente, mas o valor do campo endereco pode ser trocado por uma nova instância imutável de Endereco sem que isso mude a identidade do Aluno).
NotaTeste 5 — Obsessão por Primitivos e Anemia Semântica
  • □ Representar uma idade humana como um int sem nenhuma validação adicional permite, do ponto de vista do compilador Java, que o valor -500 seja atribuído sem qualquer erro de compilação.
  • □ Se duas partes distintas do sistema validarem o formato de um CPF de forma ligeiramente diferente (uma aceitando pontuação, outra não), isso é uma consequência esperada e inofensiva de representar CPF como String em vez de como um Value Object dedicado.
  • □ Substituir um parâmetro double taxaDeJuros por um Value Object TaxaDeJuros que valida, no construtor, que o valor está entre 0 e 1, move um erro de negócio que antes só apareceria em produção para o momento da criação do objeto.
  • □ A “Anemia Semântica” ocorre quando o tipo de um dado (ex.: String) descreve completamente o que esse dado representa para o negócio, sem nenhuma perda de informação em relação a um tipo de domínio dedicado.
Dica(Resposta) Teste 5 — Obsessão por Primitivos e Anemia Semântica
  • ✔ Verdadeiro — O tipo int em Java aceita qualquer valor dentro do seu intervalo numérico, sem nenhuma noção de domínio de negócio — o compilador não tem como saber que uma idade nunca deveria ser negativa. É exatamente o risco de erros que deveriam ser de compilação (ou ao menos de construção Fail-Fast) tornarem-se, na prática, erros de runtime ou nem serem detectados.
  • ✗ Falso — Essa divergência de validação é exatamente o sintoma da “lógica dispersa” que a Obsessão por Primitivos causa — longe de inofensiva, ela gera inconsistência real: um CPF aceito por uma parte do sistema pode ser rejeitado por outra, produzindo comportamento imprevisível. Um Value Object CPF centralizaria essa validação num único lugar (o construtor), eliminando a divergência por definição.
  • ✔ Verdadeiro — É exatamente o mecanismo Fail-Fast do Value Object aplicado a um domínio diferente do Email/Dinheiro vistos em aula: a validação migra do ponto de uso (potencialmente distante, potencialmente em produção) para o momento da construção — se o objeto TaxaDeJuros existe, ele já é garantidamente válido em qualquer lugar do sistema.
  • ✗ Falso — É a definição invertida: a Anemia Semântica ocorre exatamente quando o tipo primitivo não descreve o que o dado significa para o negócio — String diz “isto é uma sequência de caracteres”, nunca “isto é um e-mail válido” ou “isto é um CPF”. A perda de informação semântica é o próprio problema, não sua ausência.
NotaTeste 6 — O Padrão Builder e a Interface Fluente
  • □ No PedidoBuilder, o fato de cada método de configuração retornar this é o que torna sintaticamente possível encadear .paraCliente(...).comItem(...).comDesconto(...) numa única expressão.
  • □ Se a validação “desconto não pode ser maior que o total” fosse movida do método build() para dentro de comDesconto(), ela continuaria funcionando corretamente mesmo que comItem() fosse chamado depois de comDesconto() na cadeia de chamadas.
  • □ Um PedidoBuilder cujo construtor da classe Pedido alvo fosse public (em vez de private) ainda impediria, com a mesma eficácia, que código externo criasse um Pedido sem passar pelas validações do build().
  • □ A imutabilidade do objeto Pedido produzido no final não exige que o próprio PedidoBuilder, enquanto acumula dados, também seja imutável — os dois objetos podem ter regras de mutabilidade diferentes por servirem a propósitos diferentes.
Dica(Resposta) Teste 6 — O Padrão Builder e a Interface Fluente
  • ✔ Verdadeiro — Encadear chamadas de método em Java (objeto.metodo1().metodo2()) só é possível porque cada chamada intermediária devolve uma referência sobre a qual o próximo método pode ser invocado — no Builder, essa referência é a própria instância do Builder (return this), o que faz do encadeamento fluente possível.
  • ✗ Falso — Se a validação estivesse dentro de comDesconto(), ela só teria acesso ao valor de total acumulado até aquele ponto da cadeia — se comItem() (que incrementa total) for chamado depois, a validação já executada em comDesconto() estaria comparando o desconto contra um total ainda incompleto. É exatamente por isso que build(), executado só ao final, é o único lugar correto para validações cross-field que dependem do estado final e completo do objeto.
  • ✗ Falso — Se o construtor de Pedido fosse público, qualquer código externo poderia chamar new Pedido(...) diretamente, contornando inteiramente o PedidoBuilder e suas validações cross-field. A blindagem depende especificamente do construtor ser private, forçando toda instanciação a passar pelo portão único do build().
  • ✔ Verdadeiro — É exatamente o design discutido em aula: o PedidoBuilder é deliberadamente mutável (seus campos são reatribuídos a cada método de configuração) porque sua função é acumular estado temporário; o Pedido final é imutável porque sua função é representar um estado de negócio estável e protegido. As duas classes têm papéis diferentes e, por isso, regras de mutabilidade diferentes.
NotaTeste 7 — Factory Method e Desacoplamento
  • □ Se NotificacaoPedidoFactory.criar(String meio) retornasse o tipo concreto NotificacaoPedidoEmail em vez da interface NotificacaoPedido quando o meio fosse “EMAIL”, o código cliente que só espera a interface ainda compilaria sem nenhuma alteração.
  • □ Adicionar uma nova implementação NotificacaoPedidoWhatsApp exige, necessariamente, alterar tanto a fábrica quanto todos os pontos do sistema que hoje chamam NotificacaoPedidoFactory.criar(...).
  • □ Um sistema de geração de convites de evento que decide, a partir de uma preferência do organizador, se instancia um ConviteDigital ou um ConviteImpresso, centralizando essa decisão numa única classe fábrica, replica a mesma lógica estrutural do Factory Method do e-commerce.
  • □ Se a fábrica de notificações precisar de credenciais de um gateway de SMS para construir NotificacaoPedidoSMS, essas credenciais podem ficar inteiramente confinadas à fábrica, sem que o código cliente que chama criar(...) precise conhecê-las.
Dica(Resposta) Teste 7 — Factory Method e Desacoplamento
  • ✗ Falso — Embora NotificacaoPedidoEmail seja um subtipo de NotificacaoPedido (então a atribuição a uma variável do tipo interface ainda compilaria por polimorfismo), o problema real é de assinatura declarada: se o método criar declarasse formalmente o retorno como NotificacaoPedidoEmail, o case "SMS" do mesmo método (que devolve NotificacaoPedidoSMS) deixaria de compilar, pois NotificacaoPedidoSMS não é um NotificacaoPedidoEmail. Só declarar o retorno como a interface comum permite que a fábrica devolva qualquer uma das implementações.
  • ✗ Falso — É exatamente o benefício central do Factory Method: a nova classe mais um novo caso no switch/if da fábrica bastam. Nenhum dos pontos de chamada precisa mudar, porque todos eles já dependem apenas da interface NotificacaoPedido, nunca de uma classe concreta específica.
  • ✔ Verdadeiro — Mesma topologia — uma fábrica decide, com base num critério de negócio, qual classe concreta instanciar, devolvendo sempre a abstração comum (Convite) — aplicada a um domínio diferente do e-commerce.
  • ✔ Verdadeiro — É exatamente o benefício de “encapsulamento de parâmetros de infraestrutura”: a fábrica pode gerenciar dependências específicas de cada implementação concreta (credenciais de SMTP, chaves de API de gateway) sem que o cliente que apenas pede uma NotificacaoPedido precise conhecer nenhum desses detalhes.
NotaTeste 8 — Inversão de Dependência na Criação
  • □ Um ServicoDePedidos que instancia new RepositorioPedidosMySQL() diretamente dentro de seu próprio construtor pode ser substituído, em um teste automatizado, por uma versão que usa um repositório falso (mock) sem nenhuma alteração no código de ServicoDePedidos.
  • □ Injetar uma RepositorioPedidosFactory no construtor de ServicoDePedidos, em vez de instanciar diretamente uma classe concreta, empurra o conhecimento sobre “qual banco de dados usar” para a periferia do sistema, mantendo o núcleo de negócio isolado dessa decisão.
  • □ Um ServicoDePedidos que depende apenas da interface RepositorioPedidos ainda estaria violando o Princípio de Inversão de Dependência se essa interface fosse implementada por uma única classe concreta em todo o sistema.
  • □ A Inversão de Dependência na criação é estruturalmente análoga à injeção da interface Pagavel no Pedido feita na Aula 10 — em ambos os casos, um módulo de alto nível passa a depender de uma abstração em vez de uma implementação concreta.
Dica(Resposta) Teste 8 — Inversão de Dependência na Criação
  • ✗ Falso — É exatamente o problema do acoplamento rígido: se ServicoDePedidos instancia RepositorioPedidosMySQL diretamente, não existe nenhum ponto de substituição para um mock sem editar o próprio código-fonte de ServicoDePedidos — a dependência está fundida na implementação, não injetada de fora.
  • ✔ Verdadeiro — É a definição de DIP aplicada à criação: o núcleo de negócio (ServicoDePedidos) passa a depender só do contrato RepositorioPedidos, e a decisão concreta (“qual banco”, “qual fornecedor”) é tomada no ponto de entrada da aplicação, que injeta a fábrica apropriada — exatamente o mesmo raciocínio de fronteira aplicado a Pagavel na Aula 10.
  • ✗ Falso — O DIP é sobre a direção da dependência (módulos de alto nível dependendo de abstrações, não de implementações concretas), não sobre quantas implementações concretas existem no momento. Mesmo com uma única implementação hoje, ServicoDePedidos já está corretamente desacoplado — pronto para uma segunda implementação (ou um mock de teste) sem nenhuma mudança em sua própria lógica.
  • ✔ Verdadeiro — É exatamente o paralelo traçado ao final do bloco: assim como Pedido conhece só Pagavel (nunca Pix/Cartao/Boleto diretamente) para o comportamento de pagamento, ServicoDePedidos conhece só RepositorioPedidos (nunca RepositorioPedidosMySQL diretamente) para a persistência — o mesmo princípio de fronteira, aplicado uma vez ao comportamento (Strategy) e outra vez à criação (Factory Method + DIP).