W3docs

Princípios SOLID em Java

Aplique os princípios SOLID — SRP, OCP, LSP, ISP, DIP — ao design Java.

SOLID é um conjunto de cinco princípios de design orientado a objetos — popularizados por Robert C. Martin — que mantêm o código Java fácil de modificar, testar e estender à medida que cresce. Não são regras de sintaxe impostas pelo compilador; são diretrizes para onde definir fronteiras entre classes de modo que uma alteração não se propague por toda a base de código. O acrônimo representa Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation e Dependency Inversion.

Esses princípios se baseiam nos fundamentos da orientação a objetos: você verá interfaces, herança e polimorfismo sendo usados ao longo do texto. Se esses conceitos parecerem frágeis, revise-os primeiro — SOLID é, em grande parte, bom julgamento sobre onde aplicá-los.

Os cinco princípios em resumo

Cada letra aborda um tipo específico de problema de design. Mantenha esta tabela por perto enquanto lê o restante do capítulo:

LetraPrincípioObjetivo em uma linha
SSingle ResponsibilityUma classe deve ter apenas um motivo para mudar
OOpen/ClosedAberta para extensão, fechada para modificação
LLiskov SubstitutionSubtipos devem ser usáveis onde quer que seu tipo base seja
IInterface SegregationMuitas interfaces pequenas valem mais do que uma grande
DDependency InversionDependa de abstrações, não de classes concretas

Os princípios se reforçam mutuamente. Em código bem estruturado, raramente se aplica apenas um — uma interface pequena (ISP) da qual código de alto nível depende (DIP) é exatamente o que permite adicionar uma nova implementação (OCP) sem tocar no chamador.

S — Princípio da Responsabilidade Única

Uma classe deve fazer uma coisa e ter um único motivo para mudar. Quando responsabilidades não relacionadas — regras de negócio e entrega de mensagens, por exemplo — compartilham uma classe, uma mudança em qualquer uma delas força a re-testar ambas. Separá-las isola as mudanças.

// Mixes WHEN to alert with HOW to deliver -- two reasons to change.
class BadAlertService {
  void raise(String user, int errors) {
    if (errors > 0) {
      // ...build an email, open an SMTP connection, send...
    }
  }
}

// One responsibility: deciding when to alert. Delivery lives elsewhere.
class AlertService {
  private final Notifier notifier;
  AlertService(Notifier notifier) { this.notifier = notifier; }
  void raise(String user, int errors) {
    if (errors > 0) notifier.send(user, errors + " error(s) detected");
  }
}

O — Princípio Aberto/Fechado

Entidades de software devem estar abertas para extensão, mas fechadas para modificação. Você deve ser capaz de adicionar novo comportamento escrevendo código novo, não editando — e arriscando — código que já funciona. Em Java, o mecanismo usual é uma interface estável mais novas implementações.

interface Notifier { void send(String to, String message); }

class EmailNotifier implements Notifier { /* ... */ }
class SmsNotifier   implements Notifier { /* ... */ } // new feature = new class

// AlertService never changes when a new channel appears.

Adicionar notificações push futuramente significa escrever PushNotifier implements NotifierAlertService permanece intacto, sem necessidade de revisão ou risco de regressão.

L — Princípio da Substituição de Liskov

Se S é um subtipo de T, então objetos do tipo T podem ser substituídos por objetos do tipo S sem quebrar o programa. Uma subclasse deve honrar o contrato de seu pai — mesmas expectativas, sem exceções surpreendentes, sem pré-condições mais restritivas.

abstract class Shape { abstract double area(); }
class Rectangle extends Shape { /* area() = w * h */ }
class Circle    extends Shape { /* area() = PI * r * r */ }

// Works for ANY Shape, present or future, without inspecting the concrete type.
double totalArea(List<Shape> shapes) {
  return shapes.stream().mapToDouble(Shape::area).sum();
}

A violação clássica é Square extends Rectangle: se definir a largura também muta a altura, o código escrito para um Rectangle quebra quando recebe um Square. A solução é modelá-los como irmãos sob Shape, não como um par pai-filho. (Consulte classes abstratas para a base Shape usada aqui.)

I — Princípio da Segregação de Interface

Clientes não devem ser forçados a depender de métodos que não utilizam. Prefira várias interfaces pequenas e focadas a uma grande — caso contrário, um implementador é obrigado a criar stubs de métodos que não pode honrar.

// Fat interface: a read-only source is forced to implement write().
interface Storage { String read(); void write(String data); }

// Segregated: implement only what you can honor.
interface Readable { String read(); }
interface Writable { void write(String data); }

class ConfigFile implements Readable {        // no empty write() stub
  public String read() { return "mode=prod"; }
}

D — Princípio da Inversão de Dependência

Módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações. Na prática: codifique contra interfaces e injete a implementação concreta (a injeção por construtor é a forma mais simples). Isso é o que faz os outros princípios se pagarem — e o que torna uma classe testável, pois você pode passar um objeto falso.

// AlertService depends on the Notifier interface, not EmailNotifier.
AlertService alerts = new AlertService(new EmailNotifier());
// In a test, inject a fake Notifier and assert on what it recorded.

Um exemplo completo: todos os cinco em um programa

Este programa conecta os princípios — um único AlertService (SRP) se comunica com um Notifier injetado (DIP), alterna entre EmailNotifier e SmsNotifier sem mudanças (OCP), lê um ConfigFile apenas Readable (ISP) e soma áreas de subtipos de Shape de forma uniforme (LSP). Ele verifica seus próprios resultados para que você possa ver cada princípio em ação.

java— editable, runs on the server

O que observar na execução:

  • email sent: [EMAIL -> alice: 3 error(s) detected] contém apenas uma entrada — bob tinha zero erros, então raise não enviou nada. AlertService tem a responsabilidade única de decidir quando alertar (SRP); nunca constrói o corpo da mensagem nem abre uma conexão.
  • A mesma classe AlertService acionou tanto um EmailNotifier quanto um SmsNotifier porque a dependência foi passada pelo construtor (DIP). A lógica de alertas de alto nível depende apenas da interface Notifier, nunca de um remetente concreto.
  • OCP check : ... unchanged = true confirma que ambos os objetos de alerta são da mesma classe AlertService: adicionar suporte a SMS significou escrever um novo SmsNotifier, sem nenhuma edição em AlertService — aberto para extensão, fechado para modificação.
  • ISP check : is Writable? false mostra que ConfigFile implementa apenas Readable. Como as interfaces são segregadas, a fonte somente de leitura nunca foi forçada a fornecer um stub write sem sentido.
  • LSP area : 9.142 é a soma de um retângulo 2×3 (6,0) e um círculo de raio 1 (≈3,142). totalArea percorreu referências de Shape e chamou area() sem verificar qual subtipo era — cada subtipo era substituível pelo seu base (LSP).

Prática

Prática
Uma classe chamada ReportGenerator tanto formata dados do relatório quanto os grava em disco, de modo que qualquer mudança nas regras de formatação ou no layout do arquivo obriga a modificar e re-testar a mesma classe. Qual princípio SOLID isso viola mais diretamente?
Uma classe chamada ReportGenerator tanto formata dados do relatório quanto os grava em disco, de modo que qualquer mudança nas regras de formatação ou no layout do arquivo obriga a modificar e re-testar a mesma classe. Qual princípio SOLID isso viola mais diretamente?
Was this page helpful?