Princípios SOLID

Separar razões para mudar, proteger contratos e escolher dependências que permitem evolução.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Acrescentar um inimigo não devia exigir alterar a leitura de teclas nem os testes de posições. Se essas alterações aparecem juntas, procura a decisão que atravessa os módulos. Os cinco princípios SOLID ajudam a separar responsabilidades e a manter os contratos dos clientes.

Os cinco princípios

PrincípioPergunta a fazer ao desenho
SRP, Single ResponsibilityEsta classe junta razões independentes para mudar?
OCP, Open/ClosedConsigo acrescentar esta alternativa sem reescrever os clientes estáveis?
LSP, Liskov SubstitutionO subtipo mantém todas as promessas que o cliente da base usa?
ISP, Interface SegregationO cliente depende de operações que não precisa?
DIP, Dependency InversionA regra da aplicação depende de detalhes externos ou de um contrato adequado?

Uma classe não precisa de ser pequena para ter uma responsabilidade coerente. E uma aplicação não precisa de uma interface para cada classe. Vamos justificar cada princípio através de uma mudança concreta.

Este vídeo percorre os cinco princípios em Java:

Abrir no YouTube

Para cada princípio, identifica uma alteração concreta que motivaria a separação. Distingue implementar uma interface de cumprir o seu contrato.

SRP: uma razão para mudar

Uma Pontuacao calcula pontos, escreve um ficheiro e desenha um texto no ecrã. As regras de pontuação, o formato do ficheiro e a apresentação podem mudar por motivos independentes. Separar essas decisões evita que mudar o formato altere a regra do jogo.

O modelo pode fornecer o valor e operações válidas. Um adaptador guarda-o. Uma vista apresenta-o. Métodos relacionados com a mesma regra podem continuar juntos.

Concretiza numa pontuação. Antes, tudo vive junto:

class Pontuacao {
    int pontos;
    void somar(int v) { pontos += v; }
    void guardarFicheiro() { /* formato + I/O aqui */ }
    void desenharEcra() { /* texto gráfico aqui */ }
}

Mudar o formato do ficheiro reedita a classe da regra do jogo. Depois, cada razão de mudança tem um dono:

class Pontos { /* regra: somar, limites */ }
class PontuacoesFicheiro { /* formato + escrita */ }
class PontosVista { /* apresentação */ }
antes:  Pontuacao --> Ficheiro, Ecra, Regra
depois: Jogo --> Pontos; PontuacoesFicheiro --> Pontos; PontosVista --> Pontos

A regra fica estável quando o formato muda. Dividir cada método numa classe sem separar uma decisão útil só cria ligações novas.

Se o designer muda a cor do texto, altera PontosVista; se a regra passa a atribuir dois pontos por inimigo, altera Pontos. São pedidos independentes, e a separação reflete-se nas dependências: o modelo não chama a vista para validar a soma.

OCP: escolher onde permitir extensão

Imagina um controlador que recebe a tecla e contém um switch com todos os comandos. Cada comando novo altera o mesmo método. Se as ações variam frequentemente, o controlador pode depender de uma interface Comando, cuja operação executar() cada ação implementa.

Acrescentar uma classe e registá-la ainda muda a configuração da aplicação. OCP protege o cliente que usa o contrato, não promete que nenhum ficheiro possa mudar. Também não obriga a prever todas as extensões futuras.

Um mapa de comandos tem sentido quando queremos trocar ações sem alterar o controlador. Um switch pequeno sobre uma enumeração fechada pode continuar a ser a solução mais clara.

LSP: substituição é um contrato

Suponhamos que Retangulo permite alterar largura sem alterar altura. O cliente escolhe largura 4 e altura 3, e espera área 12. Uma subclasse Quadrado que faz cada setter alterar ambas as dimensões produz área 9. O subtipo viola o contrato usado pelo cliente.

O mesmo cliente recebe duas implementações
class Retangulo {
    protected int largura;
    protected int altura;
    void setLargura(int valor) { largura = valor; }
    void setAltura(int valor) { altura = valor; }
    int area() { return largura * altura; }
}

class Quadrado extends Retangulo {
    @Override
    void setLargura(int valor) { largura = altura = valor; }
    @Override
    void setAltura(int valor) { largura = altura = valor; }
}

public class Main {
    static int preparar(Retangulo forma) {
        forma.setLargura(4);
        forma.setAltura(3);
        return forma.area();
    }

    public static void main(String[] args) {
        System.out.println(preparar(new Retangulo()));
        System.out.println(preparar(new Quadrado()));
    }

}
Resultado

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

A saída é 12 e 9. A relação matemática entre figuras não basta para justificar a herança de uma API mutável. Poderíamos definir uma interface imutável Forma com area(), sem prometer setters independentes, e deixar retângulos e quadrados implementá-la.

Em termos de contratos, um subtipo não deve exigir pré-condições mais fortes nem garantir pós-condições mais fracas. Deve também preservar os invariantes e as propriedades de evolução do estado que a base promete. Se a base aceita um valor positivo, o subtipo não pode passar a aceitar apenas valores acima de dez sem quebrar clientes válidos.

ISP: interfaces segundo os clientes

Uma interface Dispositivo com lerTecla, desenhar e tocarSom obriga um teclado a implementar operações irrelevantes. Métodos vazios ou UnsupportedOperationException podem revelar esse problema.

Separa Entrada, Vista e Audio quando os clientes precisam dessas capacidades em separado. Um objeto real pode implementar várias interfaces. O controlador que só lê teclas depende de Entrada, por isso uma alteração ao áudio não muda o seu contrato.

A divisão deve seguir usos coerentes. Uma interface com uma única operação não é automaticamente melhor que outra com várias operações relacionadas.

DIP: a regra define o contrato

Uma regra de fim de jogo que cria diretamente FicheiroPontuacoes passa a depender da pasta, do formato e das falhas do sistema de ficheiros. Em vez disso, define o que precisa, por exemplo Pontuacoes.guardar(int valor). O adaptador de ficheiro implementa esse contrato.

O código de alto nível depende da abstração. O detalhe também depende dessa abstração para a implementar. A escolha do adaptador concreto fica na montagem da aplicação, normalmente perto do main.

Este excerto mostra a fronteira:

interface Pontuacoes {
    void guardar(int valor);
}

class FimDeJogo {
    private final Pontuacoes pontuacoes;

    FimDeJogo(Pontuacoes pontuacoes) {
        this.pontuacoes = pontuacoes;
    }

    void concluir(int valor) {
        if (valor < 0) throw new IllegalArgumentException("pontos negativos");
        pontuacoes.guardar(valor);
    }
}

Injeção de dependências é passar o colaborador ao objeto, como no construtor. Inversão de dependências é a decisão sobre de que contratos os módulos dependem. Passar uma classe concreta pelo construtor é injeção, mas não demonstra por si só DIP.

A interface deve usar conceitos da regra, não obrigar o modelo a fornecer um caminho de ficheiro ou uma instrução SQL. Os testes podem passar uma implementação em memória ou um substituto controlado.

FimDeJogo --usa--> Pontuacoes <--implementa-- FicheiroPontuacoes
                            <--implementa-- PontuacoesMemoria

Coesão, acoplamento e pacotes

Coesão descreve como os elementos de um módulo colaboram numa responsabilidade. Acoplamento descreve quanto um módulo depende de outros e das suas decisões. Queremos dependências necessárias e explícitas, não eliminar toda a colaboração.

Classes que mudam juntas por uma razão comum podem pertencer ao mesmo pacote. As apresentações distinguem três critérios de agrupamento: reutilizar em conjunto, mudar em conjunto e não formar ciclos entre pacotes. Um ciclo entre pacotes torna mais difícil alterar ou reutilizar um deles isoladamente, porque o grafo de dependências deixa de ter uma ordem clara. Para o quebrar, localiza a dependência indevida e extrai um contrato no lado que precisa da capacidade.

Pacotes com muitos clientes devem ser difíceis de mudar por acidente e fáceis de estender por contrato; pacotes que ninguém reutiliza podem continuar concretos. Se modelo importa vista, uma regra do jogo passa a exigir a interface gráfica. Inverter essa dependência permite reutilizar o modelo numa vista de terminal e nos testes. A lição de MVC aplica esta separação a uma interação completa.

Justificar uma decisão

Antes de acrescentar uma abstração, escreve a mudança que ela deve suportar. Depois identifica o cliente que deve ficar estável e o contrato que as alternativas partilham. Finalmente compara a complexidade adicional com a dependência que removeste.

Por exemplo, trocar ficheiros por armazenamento em memória deve preservar FimDeJogo. O contrato Pontuacoes permite isso. Criar três interfaces entre duas funções puras sem qualquer alternativa concreta não resolve o mesmo problema.

Exercícios

Reconhecer a promessa que foi quebrada

A base promete que setLargura não altera a altura. Um Quadrado redefine o método para alterar ambas. Explica a violação sem usar apenas a afirmação matemática de que todo o quadrado é retângulo.

Pista
O cliente usa operações e promessas, não só a forma geométrica.
Mais uma pista
Escolhe primeiro altura 3 e depois largura 4, esperando área 12.
Solução

O subtipo quebra a pós-condição de setLargura. Percorre o cliente: setAltura(3), depois setLargura(4) que também muda a altura para 4, área 16 em vez de 12. Um cliente da base que preservava altura 3 passa a obter altura 4 e área 16. Um contrato imutável de Forma com area pode representar as duas figuras sem prometer setters independentes.

Confere a tua resposta:

  • Identifica a pós-condição de manter a altura.
  • Explica por que um cliente válido da base deixa de funcionar.
  • Propõe um contrato de área sem setters independentes ou outra separação adequada.

Erros frequentes

Uma relação entre conceitos não justifica automaticamente herança entre APIs mutáveis.

Voltar à explicação
Estender a ação sem editar o controlador

Um controlador consulta um Map&lt;String, Comando&gt; e chama executar() no comando da tecla. A montagem fornece o mapa. Acrescenta-se uma ação de pausa. Que partes mudam e qual pode ficar igual? Compara com um controlador que recebe o mesmo mapa mas usa instanceof para decidir como executar cada classe concreta.

Pista

Localiza a decisão que precisa de crescer quando chega uma ação nova.

Solução

Criamos o comando de pausa e registamo-lo na montagem. O controlador pode continuar a consultar o mapa e chamar executar(), desde que a alternativa cumpra o contrato. OCP protege esse cliente face a novas ações; não proíbe mudar a configuração. Na versão com testes de classes concretas, a execução está ainda centralizada no controlador: uma nova classe pode exigir outro ramo. Receber uma interface não basta se o cliente ignora o seu polimorfismo.

Confere a tua resposta:

  • Mantive o controlador dependente de Comando.executar().
  • Localizei a nova implementação e o registo na montagem.
  • Expliquei por que receber o mapa não chega se o controlador ainda testa classes concretas.
Voltar à explicação
Retirar os detalhes da regra

FimDeJogo cria um gravador que recebe caminho, separador CSV e SQL. Propõe um contrato que permita guardar a pontuação sem expor estes detalhes à regra.

Pista

A regra precisa de guardar um valor, não de escolher a sintaxe do armazenamento.

Mais uma pista

Um contrato Pontuacoes.guardar(valor) pode esconder o formato.

Solução

Desenha antes e depois: antes, FimDeJogo --> GravadorFicheiro(caminho, CSV, SQL); depois, FimDeJogo --> Pontuacoes <-- AdaptadorFicheiro. A regra depende de Pontuacoes com guardar(int valor), e o adaptador de ficheiro ou SQL implementa essa operação. Caminhos e formatos ficam no adaptador. A montagem constrói-o e passa-o a FimDeJogo.

Confere a tua resposta:

  • Define uma operação em termos de pontuação.
  • Coloca ficheiro/SQL no adaptador.
  • Escolhe e injeta o adaptador na montagem.

Erros frequentes

Passar o gravador concreto pelo construtor é injeção, mas não retira os detalhes do contrato.

Voltar à explicação
Separar clientes sem inventar capacidades

Dispositivo oferece lerTecla(), desenhar() e tocarSom(). O controlador só lê teclas e um teclado concreto lança UnsupportedOperationException nas outras operações. Propõe contratos para os clientes e explica a relação com ISP e SRP.

Pista
Começa pelas operações de que cada cliente precisa.
Mais uma pista

O controlador pode receber Entrada, mesmo que o dispositivo real ofereça outras capacidades.

Solução

O controlador depende de Entrada.lerTecla(). Clientes de desenho e som usam Vista e Audio. O teclado implementa apenas Entrada; um dispositivo que tenha as três capacidades pode implementar as três interfaces. ISP evita dependências em operações irrelevantes. SRP pergunta se uma classe junta razões independentes para mudar, por exemplo leitura de teclas e reprodução de áudio. Separar interfaces não prova, por si só, que as implementações tenham responsabilidades bem escolhidas.

Confere a tua resposta:

  • Define Entrada para o controlador e separa Vista e Audio quando usados independentemente.
  • Não obriga um teclado a fingir suporte a desenho ou som.
  • Distingue os contratos dos clientes das razões de mudança de uma classe.

Erros frequentes

Uma interface por método não é uma regra geral. Mantém juntas operações que formam uma capacidade coerente.

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.