Pular para conteúdo

Princípios SOLID

SOLID é um acrônimo para cinco princípios de design orientado a objetos apresentados por Robert C. Martin1. Eles foram formulados pela primeira vez em seu artigo de 2000, Design Principles and Design Patterns, e popularizados pelo livro Clean Code (2008). Esses princípios atuam no nível de classes e módulos — são os blocos de construção que a Clean Architecture e a Arquitetura Hexagonal impõem no nível do sistema.

Letra Princípio Proposto por
S Princípio da Responsabilidade Única (Single Responsibility) Robert C. Martin
O Princípio Aberto/Fechado (Open/Closed) Bertrand Meyer, 19882
L Princípio da Substituição de Liskov (Liskov Substitution) Barbara Liskov, 19873
I Princípio da Segregação de Interfaces (Interface Segregation) Robert C. Martin
D Princípio da Inversão de Dependência (Dependency Inversion) Robert C. Martin
flowchart LR
    S["S — Responsabilidade Única\nUm motivo para mudar"] -->|compõe| arch
    O["O — Aberto / Fechado\nEstenda, não modifique"] -->|compõe| arch
    L["L — Substituição de Liskov\nSubtipos respeitam contratos"] -->|compõe| arch
    I["I — Segregação de Interfaces\nSem dependências forçadas"] -->|compõe| arch
    D["D — Inversão de Dependência\nDependa de abstrações"] -->|compõe| arch
    arch["Arquitetura\nClean / Hexagonal"]:::highlight
    classDef highlight fill:#fcc

S — Princípio da Responsabilidade Única

"Uma classe deve ter um, e apenas um, motivo para mudar." — Robert C. Martin

Uma classe responde a um único ator. Um ator é um grupo de usuários ou stakeholders que se importam com a mesma preocupação. Se uma classe atende a dois atores, as mudanças pedidas por um deles correm o risco de quebrar funcionalidades do outro.

O que o SRP NÃO significa

O SRP não significa que uma classe deva fazer uma única coisa. Significa que uma classe deve ter um único motivo para mudar — um único dono. Uma classe de serviço pode orquestrar vários passos, mas, se todos esses passos atendem à mesma preocupação de negócio e ao mesmo stakeholder, a classe tem uma responsabilidade única.

AccountService.java — too many concerns
// VIOLATION: one class handles validation, persistence, email, and audit
// Every time any of these concerns changes, this class must be modified.
public class AccountService {

    private final DataSource dataSource;
    private final JavaMailSender mailSender;
    private final Logger logger = Logger.getLogger(AccountService.class.getName());

    public AccountService(DataSource dataSource, JavaMailSender mailSender) {
        this.dataSource = dataSource;
        this.mailSender = mailSender;
    }

    public void createAccount(String email, String password) {

        // 1. Validation — business concern
        if (email == null || !email.contains("@"))
            throw new IllegalArgumentException("Invalid email: " + email);
        if (password == null || password.length() < 8)
            throw new IllegalArgumentException("Password too short");

        // 2. Persistence — infrastructure concern
        try (Connection conn = dataSource.getConnection()) {
            PreparedStatement stmt = conn.prepareStatement(
                "INSERT INTO accounts (email, password_hash) VALUES (?, ?)");
            stmt.setString(1, email);
            stmt.setString(2, BCrypt.hashpw(password, BCrypt.gensalt()));
            stmt.execute();
        } catch (SQLException e) {
            throw new RuntimeException("Failed to persist account", e);
        }

        // 3. Email notification — messaging concern
        SimpleMailMessage msg = new SimpleMailMessage();
        msg.setTo(email);
        msg.setSubject("Welcome to the Platform!");
        msg.setText("Your account has been created. You can now log in.");
        mailSender.send(msg);

        // 4. Audit log — cross-cutting concern
        logger.info("Account created for email=" + email
            + " at=" + Instant.now());
    }
}

Esta classe tem quatro motivos para mudar: uma regra de validação diferente, uma mudança no schema do banco de dados, um novo template de e-mail ou um formato de auditoria diferente — todos exigem modificar o mesmo arquivo.

AccountService.java — focused on orchestration
// Single responsibility: orchestrate the account-creation use case.
// Validation, persistence, email, and audit are delegated to specialists.
public class AccountService {

    private final AccountRepository repository;
    private final EmailService emailService;
    private final AuditService auditService;

    public AccountService(AccountRepository repository,
                          EmailService emailService,
                          AuditService auditService) {
        this.repository   = repository;
        this.emailService = emailService;
        this.auditService = auditService;
    }

    public Account create(AccountIn in) {
        Account account = Account.of(in);           // validation inside domain object
        repository.save(account);                   // persistence
        emailService.sendWelcome(account.email());  // notification
        auditService.log("account.created", account.id()); // audit
        return account;
    }
}

Cada classe especialista muda apenas pelo seu próprio motivo. AccountService só muda quando o fluxo de criação de contas muda.

flowchart LR
    subgraph bad ["❌ Antes — uma classe, muitas preocupações"]
        as["AccountService\n• validar\n• persistir\n• enviar e-mail\n• auditar"]
    end
    subgraph good ["✅ Depois — uma classe por preocupação"]
        direction TB
        as2["AccountService\n(orquestrar)"]
        r["AccountRepository\n(persistir)"]
        e["EmailService\n(notificar)"]
        a["AuditService\n(registrar)"]
        as2 --> r & e & a
    end

O — Princípio Aberto/Fechado

"Entidades de software devem ser abertas para extensão, mas fechadas para modificação." — Bertrand Meyer

Um módulo é aberto se pode ser estendido com novos comportamentos. É fechado se o seu código-fonte é estável — os consumidores e testes existentes não quebram quando um novo comportamento é adicionado. O mecanismo é a abstração: defina uma interface da qual os chamadores dependem e, depois, adicione novas implementações sem tocar nos chamadores.

DiscountService.java — modified for every new type
// VIOLATION: every new customer type requires modifying this class.
// The class is open for modification but closed for extension.
public class DiscountService {

    public double calculate(Account account) {
        if ("VIP".equals(account.type())) {
            return account.orderTotal() * 0.20;
        } else if ("EMPLOYEE".equals(account.type())) {
            return account.orderTotal() * 0.30;
        } else if ("SEASONAL".equals(account.type())) {
            // Added later — had to reopen this class
            return account.orderTotal() * 0.15;
        } else {
            return account.orderTotal() * 0.05;
        }
        // Adding "PARTNER" requires another else-if and another class modification.
    }
}
DiscountPolicy.java — extension point
1
2
3
4
5
6
// Extension point: implement this interface to add a new discount rule
// without touching DiscountService.
public interface DiscountPolicy {
    boolean appliesTo(Account account);
    double calculate(Account account);
}
DiscountService.java — closed for modification
// Closed for modification: this class never changes when a new policy is added.
// Open for extension: inject a new DiscountPolicy implementation.
public class DiscountService {

    private final List<DiscountPolicy> policies;

    public DiscountService(List<DiscountPolicy> policies) {
        this.policies = policies;
    }

    public double calculate(Account account) {
        return policies.stream()
            .filter(p -> p.appliesTo(account))
            .mapToDouble(p -> p.calculate(account))
            .sum();
    }
}

// Each policy is a new class — DiscountService is never touched again.
class VipDiscountPolicy implements DiscountPolicy {
    public boolean appliesTo(Account a) { return "VIP".equals(a.type()); }
    public double calculate(Account a)  { return a.orderTotal() * 0.20; }
}

class EmployeeDiscountPolicy implements DiscountPolicy {
    public boolean appliesTo(Account a) { return "EMPLOYEE".equals(a.type()); }
    public double calculate(Account a)  { return a.orderTotal() * 0.30; }
}
classDiagram
    class DiscountService {
        +calculate(account) double
    }
    class DiscountPolicy {
        <<interface>>
        +appliesTo(account) bool
        +calculate(account) double
    }
    class VipDiscountPolicy
    class EmployeeDiscountPolicy
    class PartnerDiscountPolicy

    DiscountService --> DiscountPolicy
    VipDiscountPolicy ..|> DiscountPolicy
    EmployeeDiscountPolicy ..|> DiscountPolicy
    PartnerDiscountPolicy ..|> DiscountPolicy

Adicionar PartnerDiscountPolicy exige criar uma única classe nova — DiscountService nunca é tocado.


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 alterar nenhuma das propriedades desejáveis do programa." — Barbara Liskov, 1987

Todo subtipo deve respeitar o contrato do supertipo — não apenas as assinaturas dos métodos, mas o comportamento. Um chamador que funciona corretamente com T deve funcionar corretamente com qualquer S extends T, sem modificações.

Rectangle.java / Square.java — contract broken
public class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int w)  { this.width  = w; }
    public void setHeight(int h) { this.height = h; }
    public int area()            { return width * height; }
}

// VIOLATION: Square overrides setters in a way that breaks Rectangle's contract.
// Client code that sets width and height independently will get wrong results.
public class Square extends Rectangle {

    @Override
    public void setWidth(int w) {
        this.width  = w;
        this.height = w; // forces both dimensions to be equal
    }

    @Override
    public void setHeight(int h) {
        this.width  = h; // forces both dimensions to be equal
        this.height = h;
    }
}

// Client code expecting Rectangle behaviour:
// rect.setWidth(5);   rect.setHeight(3);
// assert rect.area() == 15;   ← FAILS for Square: area() returns 9
Shape.java — subtypes honour the contract
// Proper design: both share a common interface without inheritance between them.
// Each type upholds its own contract independently.
public interface Shape {
    int area();
}

public class Rectangle implements Shape {
    private final int width;
    private final int height;

    public Rectangle(int width, int height) {
        this.width  = width;
        this.height = height;
    }

    @Override
    public int area() { return width * height; }
}

public class Square implements Shape {
    private final int side;

    public Square(int side) { this.side = side; }

    @Override
    public int area() { return side * side; }
}

// Any code expecting a Shape can use Rectangle or Square interchangeably.
// No surprises — each type fulfils the Shape contract correctly.
classDiagram
    class Shape {
        <<interface>>
        +area() int
    }
    class Rectangle {
        -width int
        -height int
        +area() int
    }
    class Square {
        -side int
        +area() int
    }
    Rectangle ..|> Shape
    Square ..|> Shape

LSP e testes

Um teste prático: se substituir um subtipo faz falhar um teste escrito para o supertipo, o subtipo viola o LSP. É por isso que uma suíte de testes escrita contra uma interface (Shape) deve passar para todas as implementações.


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

"Clientes não devem ser forçados a depender de interfaces que não usam." — Robert C. Martin

Uma interface inchada obriga os clientes a declarar dependências de métodos que eles nunca chamam. Quando esses métodos mudam — por motivos que não têm relação com o cliente —, o cliente precisa ser recompilado e reimplantado. Interfaces menores e focadas minimizam o acoplamento.

AccountRepository.java — fat interface
// VIOLATION: a single fat interface mixes read, write, admin, and reporting.
// Clients that only read data are forced to depend on write and admin methods.
public interface AccountRepository {

    // Read operations
    Optional<Account> findById(String id);
    Optional<Account> findByEmail(String email);
    List<Account> findAll();

    // Write operations
    Account save(Account account);
    void delete(String id);

    // Admin operations — not needed by most clients
    List<Account> findByRole(String role);
    void bulkImport(List<Account> accounts);

    // Reporting — not needed by most clients
    Map<String, Long> countByRegion();
    List<Account> findInactive(Duration since);
}

// ReadOnlyAccountService is forced to implement (or stub) write and admin methods
// even though it never uses them — a clear ISP violation.
AccountReader / AccountWriter / AccountAdmin
// Segregated interfaces: each client depends only on what it actually uses.

public interface AccountReader {
    Optional<Account> findById(String id);
    Optional<Account> findByEmail(String email);
    List<Account> findAll();
}

public interface AccountWriter {
    Account save(Account account);
    void delete(String id);
}

public interface AccountAdmin extends AccountReader {
    List<Account> findByRole(String role);
    void bulkImport(List<Account> accounts);
}

// AccountService depends only on what it needs — no unused methods.
public class AccountService {
    private final AccountReader reader;
    private final AccountWriter writer;

    public AccountService(AccountReader reader, AccountWriter writer) {
        this.reader = reader;
        this.writer = writer;
    }

    public Account findById(String id) {
        return reader.findById(id).orElseThrow(AccountNotFoundException::new);
    }

    public Account save(Account account) {
        return writer.save(account);
    }
}

// The JPA adapter implements all interfaces — no behaviour is lost.
class JpaAccountAdapter implements AccountReader, AccountWriter, AccountAdmin {
    // single implementation, multiple interfaces
}
classDiagram
    class AccountReader {
        <<interface>>
        +findById(id)
        +findByEmail(email)
        +findAll()
    }
    class AccountWriter {
        <<interface>>
        +save(account)
        +delete(id)
    }
    class AccountAdmin {
        <<interface>>
        +findByRole(role)
        +bulkImport(accounts)
    }
    AccountAdmin --|> AccountReader
    class JpaAccountAdapter
    JpaAccountAdapter ..|> AccountReader
    JpaAccountAdapter ..|> AccountWriter
    JpaAccountAdapter ..|> AccountAdmin

    class AccountService {
        -reader AccountReader
        -writer AccountWriter
    }
    AccountService --> AccountReader
    AccountService --> AccountWriter

AccountService depende apenas de AccountReader e AccountWriter. Mudanças nos métodos de administração ou de relatórios nunca o afetam.


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. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações." — Robert C. Martin

Sem o DIP, a lógica de negócio de alto nível (casos de uso) conhece e instancia a infraestrutura de baixo nível (bancos de dados, clientes HTTP). Uma mudança no banco de dados obriga a reescrever a lógica de negócio. O DIP inverte isso: o módulo de alto nível declara uma interface (a abstração), e o módulo de baixo nível a implementa. Ambos dependem da interface — a seta da dependência aponta para a regra de negócio, não para a infraestrutura.

AccountService.java — tightly coupled to MySql
// VIOLATION: high-level module directly instantiates a low-level module.
// Swapping the persistence mechanism requires modifying AccountService.
public class AccountService {

    // Direct dependency on a concrete class — impossible to substitute in tests
    private final MySqlAccountRepository repository = new MySqlAccountRepository();

    public Account findById(String id) {
        return repository.findById(id);
    }

    public Account create(AccountIn in) {
        Account account = Account.of(in);
        repository.save(account);
        return account;
    }
}

// Concrete low-level module
class MySqlAccountRepository {
    public Account findById(String id) { /* SQL query */ return null; }
    public void save(Account account)  { /* SQL insert */ }
}
AccountService.java + AccountRepository + JpaAccountRepository
// Abstraction — defined by the high-level module, implemented by the low-level module.
public interface AccountRepository {
    Optional<Account> findById(String id);
    Account save(Account account);
}

// High-level module depends on the abstraction, not on a concrete class.
// The implementation is injected at runtime by the DI container (Spring).
public class AccountService {

    private final AccountRepository repository;

    public AccountService(AccountRepository repository) {
        this.repository = repository;
    }

    public Account findById(String id) {
        return repository.findById(id)
            .orElseThrow(AccountNotFoundException::new);
    }

    public Account create(AccountIn in) {
        return repository.save(Account.of(in));
    }
}

// Low-level module also depends on the abstraction.
@Repository
public class JpaAccountRepository implements AccountRepository {

    private final AccountJpaRepository jpa;

    public JpaAccountRepository(AccountJpaRepository jpa) {
        this.jpa = jpa;
    }

    @Override
    public Optional<Account> findById(String id) {
        return jpa.findById(id).map(AccountMapper::toDomain);
    }

    @Override
    public Account save(Account account) {
        return AccountMapper.toDomain(jpa.save(AccountMapper.toTable(account)));
    }
}
flowchart LR
    subgraph bad ["❌ Antes"]
        as_bad["AccountService"] -->|new| mysql["MySqlAccountRepository"]
    end
    subgraph good ["✅ Depois"]
        as_good["AccountService"] -->|depende de| iface["«interface»\nAccountRepository"]
        jpa["JpaAccountRepository"] -->|implementa| iface
        memory["InMemoryAccountRepository\n(teste)"] -->|implementa| iface
    end

O DIP é o princípio que torna possíveis a Clean Architecture e a Arquitetura Hexagonal: ao inverter as dependências em cada fronteira entre camadas, cada camada pode ser desenvolvida, testada e substituída de forma independente.


Resumo

Princípio Problema resolvido Mecanismo
SRP Classes que mudam por vários motivos quebram chamadores não relacionados Uma preocupação por classe
OCP Modificar código existente para adicionar funcionalidades quebra o comportamento existente Estender por meio de novas implementações de uma interface
LSP Subtipos que quebram os contratos do supertipo causam bugs sutis em tempo de execução Respeitar os contratos comportamentais, não só as assinaturas dos métodos
ISP Interfaces inchadas obrigam os chamadores a depender de métodos que não usam Dividir as interfaces conforme a necessidade de cada cliente
DIP Lógica de alto nível acoplada à infraestrutura de baixo nível Ambos dependem de uma abstração; os detalhes dependem das políticas


  1. MARTIN, R. C. Agile Software Development: Principles, Patterns, and Practices. Pearson, 2002. ↩

  2. MEYER, B. Object-Oriented Software Construction, 1st ed. Prentice Hall, 1988. ↩

  3. LISKOV, B.; WING, J. A Behavioral Notion of Subtyping. ACM TOPLAS, 1994. ↩

  4. MARTIN, R. C. The Single Responsibility Principle. The Clean Code Blog, 2014. ↩