Pular para o conteúdo
Quiz
← Início

Padrões de projeto e SOLID

100 questões

Certas
0
Erradas
0
Não respondidas
100
Dificuldade
Número de questões

100 questões neste filtro

Resumo teórico

Imagine que você constrói coisas com LEGO. Depois de um tempo, percebe que resolve sempre os mesmos pequenos problemas: como fazer uma parede forte, como montar uma porta que abre, como juntar dois conjuntos que não encaixam. Construtores espertos deram nome às boas respostas, e assim dizem "use o truque da dobradiça" sem explicar peça por peça. Um padrão de projeto (design pattern) é exatamente isso: um nome para uma boa resposta a um problema que volta toda hora quando escrevemos programas.

Antes dos padrões existem cinco regras de bolso chamadas SOLID. Elas não são leis. São conselhos de como escrever classes (uma é como uma forma de biscoito, e cada biscoito é um objeto) para que o programa seja fácil de mudar depois. Programas sempre mudam: uma regra nova do chefe, uma nova empresa de pagamento, um novo tipo de cliente. Código fácil de mudar economiza dias de trabalho e evita bugs novos.

Você vai encontrar tudo isso na vida real: nos aplicativos que usa, nos frameworks que as pessoas programadoras baixam e em toda entrevista de emprego sobre software. No fim desta página você vai conhecer os cinco princípios SOLID, dez padrões famosos (Factory, Builder, Singleton, Adapter, Decorator, Facade, Strategy, Observer, Command e State), o Repository e a injeção de dependência usados em sistemas back-end, e os sinais de alerta de um projeto ruim. Vai saber também quando não usar um padrão, o que é tão importante quanto.

O quadro geral e o vocabulário

Software nunca está terminado. Sempre alguém pede mais uma coisa. Por isso, um bom projeto não é sobre deixar o código bonito. É sobre fazer a próxima mudança custar pouco: a mudança deve tocar poucos lugares e não pode quebrar o que já funciona.

Para falar de projeto (design) precisamos de algumas palavras. Aprenda agora, porque todas as seções as usam.

As sete palavras do projeto de software
PalavraO que significaImagem do dia a dia
CoesãoQuanto as partes dentro de uma classe combinam entre si. Coesão alta é bom.Um estojo que guarda só lápis e canetas
AcoplamentoQuanto uma classe depende de outras classes. Acoplamento baixo é bom.Brinquedos ligados por um barbante fino, não soldados
InterfaceA lista do que uma classe promete fazer, sem dizer como.Os botões de um controle remoto
ContratoA promessa completa de um método: o que quem chama deve dar, o que o método devolve e quais erros pode lançar.Um pedido de pizza: você dá o endereço, recebe a pizza
AbstraçãoUma visão simples que esconde os detalhes.Você dirige um carro sem conhecer o motor
PadrãoUma resposta com nome e reutilizável para um problema que aparece sempre.Uma receita com nome

Existem três famílias de padrões. Os criacionais tratam da criação de objetos. Os estruturais tratam de como os objetos se encaixam. Os comportamentais tratam de como os objetos conversam e dividem o trabalho. O livro famoso que listou 23 deles foi escrito por quatro autores, apelidados de "Gang of Four" (a gangue dos quatro).

Assista ao vídeo: Design Patterns in Object Oriented Programming (playlist de Christopher Okhravi, em inglês)↑ Voltar ao sumário

Princípio da responsabilidade única (SRP)

Pense numa mochila que também é lancheira, travesseiro e capacete de bicicleta. Quando uma parte quebra, você não consegue consertar sem mexer nas outras. O princípio da responsabilidade única diz que uma classe deve ter só um motivo para mudar.

"Motivo para mudar" é uma pessoa ou um grupo que pode pedir uma alteração. Veja a classe abaixo. O financeiro pode mudar a regra do imposto, o design pode mudar o texto do recibo e o time de banco de dados pode mudar como os dados são salvos. São três motivos para mudar num só lugar.

class Invoice {
	calculateTax(): number { /* regras de imposto */ }
	saveToDatabase(): void { /* SQL */ }
	sendByEmail(): void { /* SMTP */ }
}
Três assuntos numa classe só: viola o SRP

A cura é separar: uma classe para o cálculo do imposto, uma para salvar, uma para o e-mail. Agora cada mudança toca uma classe pequena, e os testes da regra de imposto não precisam de banco de dados.

Coesão e dois cheiros que se espelham

Uma classe tem métodos e campos que tratam de um único conceito. Uma dica para achar uma divisão que falta: se alguns métodos usam só alguns campos e outros métodos usam só os outros campos, provavelmente há duas classes escondidas numa só. Uma classe que ganha um bloco novo a cada caso novo (um cargo novo, um tipo de cliente novo) também é um sinal.

Os dois cheiros que são espelho um do outro
Cheiro (smell)O que você vêO que ele diz
Divergent ChangeUma classe é editada por muitos motivos diferentes (regra de imposto e layout de relatório)Falta de coesão: a classe tem várias responsabilidades
Shotgun SurgeryUma mudança obriga a fazer pequenas edições em muitas classesUma responsabilidade foi quebrada em pedaços demais
↑ Voltar ao sumário

Princípio aberto-fechado (OCP)

Um console de videogame tem entradas para novos controles. Você não abre o aparelho e solda fios para ligar mais um. O console é fechado para modificação (ninguém mexe por dentro) e aberto para extensão (controles novos só se encaixam). O princípio aberto-fechado pede o mesmo do código: um comportamento novo deve entrar como código novo, sem editar o código que já existe e já foi testado.

O jeito mais comum é uma . A classe conversa com uma interface, e cada comportamento novo é uma classe nova que a implementa. Isso se chama . O lugar onde a classe chama algo que não conhece em detalhe é o seu ponto de extensão.

interface DiscountRule {
	apply(total: number): number;
}

class PriceCalculator {
	constructor(private readonly rules: DiscountRule[]) {}

	total(base: number): number {
		return this.rules.reduce((value, rule) => rule.apply(value), base);
	}
}
Um desconto novo é uma classe nova que implementa DiscountRule, colocada na lista onde o programa é montado
ANTES: area() tem um if por forma
  area(forma) --> if circulo ... if quadrado ... (edite aqui!)

DEPOIS: cada forma sabe a própria área
  area(forma) --> forma.area()
                   |-- Circulo.area()
                   |-- Quadrado.area()
                   '-- (novo) Triangulo.area()
Uma cadeia de ifs sobre um tipo é fechada para extensão

Onde a mudança acontece

Nenhum programa é fechado contra toda mudança. Em algum lugar a classe nova precisa ser criada. Uma factory (uma função que decide qual classe criar) pode ter um switch para isso. Isso é aceitável: a variação fica confinada a um ponto, e o resto do código só conhece a abstração. O princípio não elimina a mudança. Ele escolhe onde ela acontece.

A mesma cadeia if (x instanceof ...) copiada num auxiliar, num relatório e numa tela é o verdadeiro sinal de alerta. Quando um tipo novo aparece, cada cópia precisa ser encontrada e editada, e uma cópia esquecida falha em silêncio.

↑ Voltar ao sumário

Princípio da substituição de Liskov (LSP)

Suponha que você peça "um bichinho de estimação" e receba um peixe dourado. Aí tenta levá-lo para passear. Não dá. Um peixe dourado é um bichinho, mas não um bichinho que anda. O princípio da substituição de Liskov diz que um subtipo (uma classe filha, ou qualquer classe que implementa uma interface) deve funcionar em qualquer lugar onde o tipo base é esperado. Se o código funciona com o pai, deve funcionar com o filho, sem saber qual filho recebeu.

class Bird {
	fly(): string { return 'flying'; }
}
class Penguin extends Bird {
	fly(): string { throw new Error('penguins cannot fly'); }
}
function release(birds: Bird[]): string[] {
	return birds.map((bird) => bird.fly());
}
Penguin não pode substituir Bird: release() estoura

Na biologia, o pinguim é uma ave. No código, o que decide a herança é o contrato dos métodos, não a frase "é um". A solução é mudar a hierarquia, por exemplo deixando fly só num tipo para aves que voam.

O contrato tem dois lados

Um método tem uma e uma . Tem também um jeito de avisar uma falha, como devolver null ou lançar um erro. O compilador só confere os tipos da assinatura. Ele não confere essas promessas, então os testes de contrato é que precisam conferir.

Exija menos, entregue mais
Parte do contratoO que um subtipo pode fazer
Pré-condição (o que ele exige)Manter ou enfraquecer (exigir menos). Nunca deixar mais rígida
Pós-condição (o que ele entrega)Manter ou fortalecer (entregar mais). Nunca enfraquecer
Como a falha é avisadaUsar o mesmo jeito. Se a base devolve null para um usuário que não existe, o subtipo não deve lançar erro

O exemplo clássico é Square extends Rectangle. Um Rectangle promete que mudar a largura não muda a altura. Um Square não consegue cumprir essa promessa, porque mudar um lado muda o outro. Outro caso: uma Account base promete que depois de um saque o saldo nunca fica negativo, e uma OverdraftAccount permite saldo negativo. Ela enfraqueceu uma pós-condição.

O sintoma mais forte de uma substituição quebrada é o instanceof no código cliente (if (account instanceof StudentAccount) ...). O cliente teve de aprender os subtipos, então cada subtipo novo exige mais um ramo. Aí uma violação de Liskov vira também uma violação do aberto-fechado.

↑ Voltar ao sumário

Princípio da segregação de interfaces (ISP)

Você pega emprestado um canivete suíço só para abrir uma carta. Ganha 30 ferramentas que não precisa, e se uma delas estiver quebrada você ainda se preocupa com ela. O princípio da segregação de interfaces diz que clientes não devem ser forçados a depender de métodos que não usam. Uma interface é um contrato visto do lado de quem usa. Se ela mistura métodos para usuários diferentes, todo mundo fica preso a tudo.

interface Machine {
	print(doc: string): void;
	scan(): string;
	fax(number: string): void;
}

class SimplePrinter implements Machine {
	print(doc: string): void { /* imprime */ }
	scan(): string { throw new Error('not supported'); }
	fax(number: string): void { throw new Error('not supported'); }
}
Uma interface gorda: a impressora precisa escrever métodos que não consegue cumprir

Um método escrito como "não suportado" é um sinal alto: a interface pede mais do que a classe tem. A solução é dividir por capacidade: Printer, Scanner, Fax. Agora o código que precisa escanear pede um Scanner, e uma impressora simples nunca chega lá. O sistema de tipos trabalha a seu favor.

O que uma interface gorda custa

  • Quem implementa precisa escrever métodos que não consegue cumprir, e eles lançam erros.
  • Os clientes dependem de métodos que nunca chamam, então uma mudança num método que não usam ainda pode obrigá-los a mudar.
  • Quando um décimo terceiro método, usado por um só cliente, entra numa interface com doze métodos e oito implementações, as oito classes precisam mudar, mesmo aquelas cujos clientes nunca o chamam.
  • Os test doubles (objetos falsos usados em testes) precisam ter todos os métodos. Um relatório que só lê precisa de um falso com um método, não quatro.

Um bom exemplo é um repositório de usuários. Um relatório só precisa ler. Dê a ele um UserReader com findAll e deixe save e delete em outra interface. A classe concreta implementa as duas, então continua igual. O relatório não consegue gravar nem apagar por engano, e o seu falso precisa de um único método.

ISP e SRP são duas visões da coesão. O SRP pergunta por que o próprio tipo muda. O ISP pergunta o que cada cliente precisa de um tipo. Um repositório pode ter uma responsabilidade (guardar usuários) e ainda assim precisar de interfaces separadas de leitura e de escrita.

↑ Voltar ao sumário

Princípio da inversão de dependência (DIP)

Uma luminária tem um plugue. Ela não tem um fio fixo na parede de uma casa só. A luminária (a coisa importante) define o formato do plugue, e qualquer tomada que encaixe serve. O princípio da inversão de dependência diz: módulos de alto nível e de baixo nível devem depender de abstrações. O código de não deve importar os detalhes de . Em vez disso, a regra diz o que precisa por meio de uma interface, e o detalhe a implementa.

class OrderService {
	private readonly database = new MySqlDatabase();

	place(order: Order): void {
		if (order.total() <= 0) throw new Error('empty order');
		this.database.insert('orders', order);
	}
}
A regra de negócio cria o próprio detalhe com new: ela muda quando o banco muda

O new dentro da classe é uma decisão dupla: a classe escolhe o que precisa e qual implementação recebe. A solução é declarar uma interface na linguagem do negócio (OrderStore) e recebê-la no construtor. Assim a regra pode ser testada com um falso em memória.

NORMAL                         INVERTIDO

OrderService                   OrderService --> OrderStore
     |                                              ^
     v                                              |
MySqlDatabase                  MySqlOrderStore -----'

seta: regra -> detalhe     detalhe -> interface
As setas do código-fonte apontam para a regra

Note a diferença entre o fluxo das chamadas e a dependência no código-fonte. Em tempo de execução, OrderService ainda chama o banco. No código, os dois conhecem só a interface. Note também quem é dono da interface: ela fica com o módulo de alto nível, e o adaptador do banco a implementa. Essa posse é o que "inversão" quer dizer. Colocar a interface ao lado do banco não inverteria nada.

DIP não é o mesmo que DI

Andam juntos, mas são coisas diferentes
TópicoDIP (inversão de dependência)DI (injeção de dependência)
O que éUma regra de projeto: depender de abstraçõesUma técnica: alguém de fora entrega a dependência
Pergunta que respondeDo que devo depender?Quem a cria e entrega para mim?
Pode existir sozinho?SimSim

Interfaces são menos arriscadas que classes concretas porque mudam menos: não carregam detalhes e muitas classes dependem delas. Mas a regra não é "ponha uma interface em tudo". O critério é a estabilidade. A classe de texto da biblioteca padrão (String) quase nunca muda, não faz entrada nem saída e se comporta igual toda vez, então pode ser usada direto. O que muda, conversa com o mundo de fora ou dá uma resposta diferente a cada chamada (banco, relógio, rede, números aleatórios) vai atrás de uma interface.

↑ Voltar ao sumário

Criacionais: Factory, Builder, Singleton

Criar um objeto envolve escolhas: qual classe, com quais partes, em que estado inicial. Se cada usuário do objeto repete essas escolhas, uma mudança precisa ser repetida em todo lugar. Os padrões criacionais tiram as escolhas de criação do código que usa o objeto, para que quem usa dependa só do tipo abstrato.

A Factory escolhe qual classe. O Builder monta um objeto em passos.
PadrãoO problemaA ideia
FactoryHá várias classes para o mesmo papel, e quem decide qual é usada está copiado por aíUma função ou classe decide qual classe criar
BuilderUma classe com muitos dados, alguns opcionais, numa lista longa de parâmetrosPassos com nome e um build final que confere tudo
SingletonQueremos exatamente uma instância de uma classeUma instância mais um jeito global de alcançá-la

Factory

function createNotifier(channel: string): Notifier {
	if (channel === 'email') return new EmailNotifier();
	if (channel === 'sms') return new SmsNotifier();
	throw new Error(`unknown channel: ${channel}`);
}
O código que usa um Notifier nunca escreve new EmailNotifier

O valor de uma factory está no que ela esconde. O cliente deve depender da interface do produto, não da classe concreta. Se o cliente faz um cast do resultado para a classe concreta, o benefício se perde.

Builder

// difícil de ler: o que significam null, 30, true e false?
new HttpRequest('POST', '/orders', null, null, 30, true, false, body);

// com um builder, cada valor tem nome
new RequestBuilder().method('POST').path('/orders').timeout(30).body(body).build();
Muitos parâmetros posicionais contra passos com nome

O método build é o lugar natural para regras que cruzam campos, como "um POST precisa ter corpo". O objeto só nasce se o conjunto todo fizer sentido. É também por isso que um construtor rico é melhor que um construtor vazio seguido de setters: com setters, entre o new e o último set o objeto existe num estado inválido. Outro cuidado: o build deve entregar ao produto uma cópia dos dados mutáveis, como uma lista de cabeçalhos. Se ele entrega o array do próprio builder, uma chamada posterior ao builder altera um produto que já foi entregue.

Singleton

O Singleton junta duas decisões: existe só uma instância e ela é alcançável de qualquer lugar. A primeira pode ser uma necessidade legítima. A segunda é uma variável global disfarçada, e é por isso que hoje se olha o padrão com desconfiança.

class InvoiceService {
	issue(order: Order): Invoice {
		const now = Clock.getInstance().now();
		const rate = TaxTable.getInstance().rateFor(order.state);
		return new Invoice(order, rate, now);
	}
}
A assinatura new InvoiceService() diz que não precisa de nada. Ela mente.

A assinatura de um construtor é a documentação mais honesta do que uma classe precisa. Um acesso global escondido faz essa documentação mentir. Pior: um Singleton com estado mutável liga todo o código que o usa. Nos testes isso aparece como dependência de ordem: o primeiro teste incrementa um contador, o segundo começa com o valor que sobrou e falha, embora cada um passe sozinho. Uma constante exportada de um módulo (export const config = loadConfig()) é a mesma coisa em JavaScript: quem importa fica preso àquela instância.

↑ Voltar ao sumário

Estruturais: Adapter, Decorator, Facade

Os padrões estruturais tratam de como juntar classes. Três deles se parecem, porque cada um põe uma classe na frente de outro código. O que muda é a intenção.

Um teste simples: o objeto envolvido poderia ser usado no lugar do que o envolve? No Decorator, sim. No Adapter, não.
PadrãoInterface que mostraIntenção
AdapterUma diferente: converte uma classe para a interface que o cliente já esperaFazer um desencontro encaixar
DecoratorA mesma interface do objeto que ele envolveAcrescentar comportamento (log, cache, retry)
FacadeUma nova e mais simples, para várias classesUma porta de entrada fácil para um subsistema complexo

Adapter

interface PaymentGateway {
	charge(cents: number): Receipt;
}

class AcmePayGateway implements PaymentGateway {
	constructor(private readonly sdk: AcmePaySdk) {}

	charge(cents: number): Receipt {
		const result = this.sdk.createTransaction({ amount: cents / 100, currency: 'BRL' });
		return { id: result.transactionId, approved: result.status === 'OK' };
	}
}
O adaptador traduz nomes, unidades (centavos contra moeda inteira) e respostas

O sistema é dono do contrato estável (PaymentGateway). Os tipos do fornecedor nunca saem do adaptador. Quando o fornecedor é trocado, só o adaptador muda e as regras de negócio ficam como estão. O custo de trocar de fornecedor vira o custo de uma classe.

Decorator

Um decorator implementa uma interface e recebe, no construtor, outro objeto da mesma interface. Como o formato é o mesmo, os decorators podem ser empilhados em qualquer combinação, como camadas de roupa numa boneca.

// Logged escreve cada chamada, Cached lembra as respostas
const a: Users = new Logged(new Cached(new SqlUsers()));
const b: Users = new Cached(new Logged(new SqlUsers()));
As mesmas peças, ordem diferente, comportamento diferente

A ordem faz parte do comportamento. Em a, o Logged está por fora, então toda chamada é registrada, até as respondidas pelo cache. Em b, o Cached está por fora, então o segundo find('42') é respondido pelo cache e nunca chega ao Logged: não é escrito no log. As duas são válidas e respondem perguntas diferentes.

Por que não herança? Com herança, cada combinação de recursos é uma subclasse. Com n recursos independentes são 2ⁿ − 1 classes (contando cada combinação com pelo menos um recurso, ignorando a ordem). Com decorators você escreve n classes e as combina quando o programa é montado.

Classes necessárias para os recursos de log, cache e retry
Herança, 3 recursos
7 classes
Decorators, 3 recursos
3 classes
Herança, 6 recursos
63 classes
Decorators, 6 recursos
6 classes

Um decorator é um subtipo do que ele envolve, então o princípio de Liskov se aplica. Um decorator de cache cujo save não limpa o produto guardado faz o find devolver dados velhos depois de uma escrita. Ele quebrou o contrato da interface. Um teste de contrato, executado contra o repositório simples e contra o decorado, pega o problema.

Facade

Um subsistema costuma ter muitas classes que precisam ser chamadas numa certa ordem. A facade guarda esse conhecimento num só lugar, e os clientes dependem de uma classe e não de dez. O subsistema pode mudar por dentro sem machucá-los. O limite é claro: a facade conhece a ordem das chamadas, não as regras.

↑ Voltar ao sumário

Comportamentais: Strategy, Observer, Command, State

Os padrões comportamentais tratam de como os objetos dividem o trabalho e conversam. Em cada um deles, uma classe deixa de decidir algo sozinha e passa a decisão para outro objeto que pode ser trocado.

Quatro jeitos de tirar uma decisão de dentro de uma classe
PadrãoIntençãoSinal no código
StrategyPôr algoritmos intercambiáveis atrás de uma interfaceUm if por tipo, cada ramo um cálculo independente
ObserverUm objeto avisa vários outros quando algo aconteceUm par de chamadas: subscribe e publish
CommandGuardar um pedido como objeto, com seus dadosFila, histórico, desfazer e refazer
StateMudar o comportamento quando o estado interno mudaO mesmo campo status testado em todo método

Strategy

Imagine ir à escola: de ônibus, de bicicleta ou a pé. O objetivo é o mesmo e o caminho muda. O Strategy separa o que varia (como calcular) do que fica (quem precisa do resultado). Cada tipo de frete ganha uma classe que pode ser testada sozinha, e um tipo novo não edita os outros.

type Pricing = (base: number) => number;

const regular: Pricing = (base) => base;
const blackFriday: Pricing = (base) => base * 0.7;

function checkout(base: number, pricing: Pricing): number {
	return pricing(base);
}
Nenhuma classe, e ainda assim é um Strategy

Os padrões descrevem papéis e relações, não sintaxe. Quando a estratégia tem um método e nenhum estado, um tipo função faz o papel da interface. Use uma classe quando ela precisar de dados ou de mais de uma operação.

Observer

Um canal do YouTube não sabe quem são seus inscritos. Ele só publica, e todos que se inscreveram são avisados. No Observer, quem publica não sabe quem escuta, então uma reação nova (somar pontos de fidelidade quando um pedido é pago) é só mais um subscribe na inicialização, e a classe do pedido não é editada. É o princípio aberto-fechado aplicado a eventos.

publish(event: string): void {
	for (const listener of [...this.listeners]) {
		try { listener(event); } catch (error) { this.errors.push(error); }
	}
}
Entrega segura: uma cópia da lista e cada ouvinte isolado
  • Percorra uma cópia. Se o ouvinte A se cancela durante a entrega, um laço comum move o B para o lugar dele e o pula. Publicar sobre um instantâneo (snapshot) significa: quem estava inscrito quando o evento começou o recebe.
  • Isole cada ouvinte. Se o B lança um erro e ninguém o captura, o C nunca é chamado e o erro chega a quem publicou, justamente o que o padrão queria desacoplar. Capture e registre o erro.
  • Cancele ao sair. O assunto (subject) guarda uma referência a cada observador. Uma tela que fecha sem cancelar continua na memória e continua sendo chamada. Esse vazamento é o "lapsed listener" (ouvinte esquecido). Um bom subscribe devolve a função que cancela.

Command

Uma chamada de método comum acontece e some. Um comando é a mesma intenção guardada como dado: qual ação, sobre quem, com quais argumentos. Isso permite colocá-lo numa fila, repeti-lo e desfazê-lo. Se cada comando sabe se reverter, um histórico com duas pilhas dá desfazer e refazer.

run(a) run(b) run(c)   histórico = [a, b, c]
undo()                 tira c  -> histórico = [a, b]
undo()                 tira b  -> histórico = [a]

Efeitos ainda aplicados: só o a
Executar um comando novo limpa a pilha de refazer
Desfazer vai na ordem inversa: o último a entrar é o primeiro a sair

State

Um pedido vai de pendente para pago e para enviado. Cada fase responde de um jeito à mesma operação: não se envia um pedido pendente. Com condicionais, todo método começa com um teste sobre status. Com o padrão State, cada fase é uma classe, e as transições permitidas ficam explícitas. A regra que recusa ship para um pedido pendente mora na classe do estado pendente.

Conte o que cresce. Com 4 estados e 5 operações escritas como métodos com condicionais, um quinto estado obriga a revisar os 5 métodos. Com State, é uma classe nova mais as transições que levam a ela. O contrário também vale: se as operações crescem mais que os estados, as condicionais podem ser a melhor escolha.

Mesma estrutura, intenção diferente
TópicoStrategyState
O que são as alternativasJeitos independentes de fazer a mesma tarefaFases de um ciclo de vida
Quem as trocaEscolhidas de foraTrocadas pelas próprias operações, como consequência
↑ Voltar ao sumário

Repository e injeção de dependência

Quando você pede um livro à bibliotecária, não entra no depósito para procurar nas prateleiras. Você diz o título e ela traz. Um Repository é essa bibliotecária para os seus dados. Ele dá às regras de negócio uma interface parecida com uma coleção (add, find, save) e esconde como os dados são guardados: SQL, arquivos ou memória.

interface OrderRepository {
	find(id: string): Order | null;
	save(order: Order): void;
}

class InMemoryOrders implements OrderRepository {
	private readonly items = new Map<string, Order>();
	find(id: string) { return this.items.get(id) ?? null; }
	save(order: Order) { this.items.set(order.id, order); }
}
A regra fala de pedidos. Só o repositório real conhece SQL.

Como a regra só conhece a interface, ela pode ser testada com um repositório em memória: rápido e igual toda vez. Um segundo grupo de testes, mais lento, confere o repositório real do banco. Um teste de contrato executado contra as duas implementações garante que elas se comportam do mesmo jeito.

Injeção de dependência

Injeção de dependência quer dizer que a classe deixa de criar ou buscar o que precisa e recebe de fora. A forma mais comum é a injeção pelo construtor: as dependências ficam visíveis na assinatura, são obrigatórias e podem ser somente leitura. Olhar o construtor mostra tudo o que a classe precisa.

class RegisterUser {
	constructor(
		private readonly users: UserRepository,
		private readonly mailer: Mailer,
	) {}

	execute(email: string): void {
		this.users.add(new User(email));
		this.mailer.send(email, 'Welcome');
	}
}
Injeção pelo construtor
main()  <-- raiz de composição: o único lugar com 'new'
  |  cria a conexão
  |  cria SqlOrderRepository(conexão)
  |  cria RegisterUser(repo, mailer)
  v
o resto do programa só conhece interfaces
Onde o grafo de objetos é montado

O lugar onde tudo é criado e ligado se chama raiz de composição (composition root), perto do ponto de entrada do programa. Escrever new SqlOrderRepository(connection) ali mantém o resto do sistema dependente só de abstrações.

Duas armadilhas

  • Limite da transação. Para salvar um pedido e baixar o estoque de forma atômica (tudo ou nada), a transação pertence ao caso de uso, que abre uma unidade de trabalho (unit of work) cobrindo os dois repositórios. Um repositório cuida de um tipo de objeto e não sabe de qual operação maior faz parte.
  • Dependência cativa (captive dependency). Um serviço criado uma vez na inicialização que recebe no construtor um objeto que deveria ser criado a cada requisição fica com o objeto da primeira requisição para sempre. Um objeto só pode depender de objetos que vivem tanto quanto ele ou mais. Dados por requisição chegam como argumento do método, ou por um provedor consultado a cada uso.
↑ Voltar ao sumário

Antipadrões e quando não usar um padrão

Um code smell (cheiro de código) é uma pista de que o projeto pode ter um problema, como um cheiro estranho na geladeira. Um antipadrão é uma resposta ruim que ficou popular. Os padrões também têm um preço: mais tipos, mais arquivos e mais um salto para entender o fluxo. Só compensam se o problema que resolvem realmente existe.

Seis cheiros e o seu custo
CheiroComo reconhecerPor que machuca
God ClassUma classe enorme que concentra boa parte da lógica e conhece quase todas as outrasBaixa coesão, difícil de testar, todo mundo edita e gera conflito
Shotgun SurgeryUma mudança obriga a fazer pequenas edições em muitas classesO que muda junto não está junto
Feature EnvyUm método lê vários getters de outro objeto para calcular algo sobre esse objetoO cálculo pertence ao outro objeto
Refused BequestUma subclasse sobrescreve métodos herdados para que não façam nada ou lancem erroQuebra o Liskov: ela queria o código, não o contrato
Modelo anêmicoClasses só com campos, getters e setters, e todas as regras nos serviçosCódigo procedural disfarçado: qualquer serviço pode deixar um objeto inválido
Generalidade especulativaInterface, factory e builder, cada um com uma implementação, "só por garantia"Todos pagam por uma flexibilidade que ninguém usou

Tell, Don't Ask e a Lei de Demeter

// pede tudo, depois decide
const city = order.getCustomer().getAddress().getCity().getName();

// diz ao pedido o que se quer
const city = order.shippingCity();
Uma cadeia de getters acopla quem chama a quatro classes

A Lei de Demeter diz que um método deve falar só com os vizinhos imediatos. Uma cadeia de getters atravessa vários objetos, então se o endereço muda de forma, toda cadeia espalhada pelo sistema quebra. Pedir o resultado ao primeiro objeto mantém o conhecimento da estrutura num só lugar. A mesma ideia, Tell, Don't Ask (diga, não pergunte), cura a Feature Envy: leve o comportamento para perto dos dados que ele usa.

Sobre o modelo anêmico: um projeto orientado a objetos mantém cada regra perto do dado que ela protege (encapsulamento). Se as classes de domínio são só sacos de campos, qualquer serviço pode deixar um pedido num estado inválido, e a mesma regra é reescrita em vários lugares.

Quando não aplicar um padrão

  • Com um só algoritmo e nenhum sinal de um segundo, o Strategy não se justifica.
  • Uma tela de cadastro com dois campos feita com uma interface, uma abstract factory e um Builder, cada um com uma implementação, é generalidade especulativa. Espere o segundo caso real: refatorar para um padrão quando a necessidade surge custa menos do que carregar a estrutura por anos.
  • Aplicar um padrão por precaução costuma produzir a abstração errada, e a flexibilidade errada atrapalha a mudança que realmente chega.
↑ Voltar ao sumário

Juntando tudo

Os princípios e os padrões estão ligados. Os padrões costumam ser o caminho para seguir um princípio. Quando vir um problema, pergunte qual dor você sente e escolha o princípio ou o padrão que a remove.

Da dor à cura
Dor que você sentePrincípio que a explicaPadrão ou técnica que ajuda
Uma classe muda por muitos motivosSRPExtrair cada assunto para sua própria classe
Todo caso novo edita uma cadeia de if antigaOCPStrategy, Factory, polimorfismo
Um subtipo lança erro ou surpreende seus clientesLSPMudar a hierarquia ou usar composição
Quem implementa escreve métodos "não suportado"ISPDividir a interface por papel
Regras de negócio importam banco ou e-mailDIPInterface que pertence à regra, DI, Adapter, Repository
Um construtor com muitos parâmetros(criação)Builder, construtor rico
Um global escondido que quebra testesDIPInjetar a instância única em vez de um Singleton
Comportamentos que se empilham em qualquer ordemOCP, composiçãoDecorator
Muitas classes para chamar em ordem(simplicidade)Facade
Reações a um eventoOCPObserver
Desfazer, fila, repetir(pedido como dado)Command
Status testado em todo métodoOCPState

Uma nota justa sobre custo: o código refatorado tem mais tipos e mais linhas que a primeira versão. Esse é o preço. Ele compensa onde mudanças assim realmente chegam, e é desperdício onde nunca chegam. A mudança não desaparece. Os projetos só escolhem onde ela cai.

  1. Escreva um código simples que funciona, com testes.
  2. Observe a dor: uma classe que muda por muitos motivos, uma cadeia de if que cresce, um teste que precisa de banco de dados.
  3. Dê nome à dor com um princípio ou um cheiro.
  4. Aplique o menor padrão que a remove, e nada além disso.
  5. Mantenha os testes verdes durante a refatoração.
↑ Voltar ao sumário