MÓDULO 00 · CONCEITO 01 DE 5

Clean Code & SOLID na prática

Não como dogma, mas como ferramenta. Saber quando não aplicar é tão importante quanto aplicar.

Tempo de leitura ~25 min Pré-requisito noção básica de OO Próximo Coesão & Acoplamento

Existe um vício pedagógico no ensino de SOLID: ele costuma ser apresentado como uma lista de cinco princípios sagrados que todo código deveria seguir. Quem aprende assim acaba escrevendo seis classes para uma função de quinze linhas, três interfaces para garantir flexibilidade que ninguém vai usar, e um IFactoryFactory que vira meme antes de virar produção. SOLID não é checklist. SOLID é um conjunto de heurísticas para gerenciar mudança: onde mudança é esperada, eles ajudam; onde não é, eles cobram um preço sem entregar valor.

O ponto de partida para usar bem esses princípios é entender que cada um deles existe por causa de uma dor real, observada empiricamente em código que envelhece mal. Robert C. Martin não inventou os cinco do nada — ele cristalizou padrões que já existiam no folclore da engenharia, dando nomes e siglas que pegaram. Conhecer a dor por trás de cada princípio é o que permite saber quando aplicá-lo, quando relaxá-lo, e quando ignorá-lo de propósito. A pergunta de um sênior nunca é "isso está seguindo SOLID?". É "essa estrutura facilita o tipo de mudança que provavelmente vou precisar fazer?".

Os cinco princípios — o que cada um realmente significa

SRP — Single Responsibility Principle

A formulação popular ("uma classe deve ter apenas uma responsabilidade") é tão vaga que se torna inútil — qualquer coisa pode ser argumentada como "uma" responsabilidade se você apertar os olhos. A formulação que importa é a do próprio Martin, mais precisa: uma classe deve ter apenas uma razão para mudar. E "razão para mudar" significa, na prática, "um stakeholder ou um eixo de negócio que pode pedir alteração".

Considere uma classe Employee que tem três métodos: calculatePay(), reportHours() e save(). Parece coesa, certo? Tudo é sobre empregado. Mas calculatePay é mexido pelo financeiro, reportHours pelo RH, e save pela equipe de plataforma. Três stakeholders diferentes, três cadências de mudança diferentes, três razões para mudar a mesma classe. Quando o financeiro muda a regra de cálculo, há risco de quebrar relatórios de RH. Isso é uma violação de SRP — não porque a classe faz três coisas, mas porque essas três coisas respondem a três entidades diferentes.

A correção idiomática separa a regra de cálculo, o relatório e a persistência em módulos distintos, possivelmente coordenados por um serviço fino. Cada módulo pode evoluir no ritmo do seu stakeholder sem afetar os outros. Esse é o ganho real — não "código mais limpo" no abstrato, mas mudança isolada em concreto.

armadilha comum

SRP é frequentemente usado como justificativa para criar microfunções de duas linhas e classes anêmicas com um único método. Isso é coesão baixa disfarçada de SRP. Uma classe pode ter cinco métodos relacionados ao mesmo propósito de negócio e estar perfeitamente dentro do princípio. O critério é razões para mudar, não tamanho.

OCP — Open/Closed Principle

"Aberto para extensão, fechado para modificação." A formulação original de Bertrand Meyer (1988) era sobre herança; a reformulação de Martin é sobre polimorfismo. O espírito é o mesmo: você deveria conseguir adicionar comportamento novo sem modificar código antigo já testado. Se cada nova feature exige editar um switch gigante de quinze cláusulas, você está pagando o custo da modificação repetidamente e introduzindo risco a cada toque.

O exemplo canônico é o sistema de descontos de um e-commerce. A primeira versão tem um if para cliente VIP. Aí entra cupom de Black Friday, mais um if. Depois frete grátis acima de R$200, mais um. Em seis meses, há um método de duzentas linhas com vinte cláusulas, e cada nova promoção é uma cirurgia de risco. A versão polimórfica tem uma interface DiscountRule e cada regra é uma classe — adicionar uma nova é adicionar um arquivo, não editar lógica existente.

Mas — e aqui é onde os juniores tropeçam — OCP não significa abstrair preventivamente todo possível ponto de variação. Isso é especulação, e especulação tende a errar. A heurística sólida é a regra dos três: na primeira ocorrência, você implementa direto. Na segunda ocorrência muito parecida, você nota o padrão e tolera a duplicação. Na terceira, aí sim você abstrai — porque agora tem três pontos de dados sobre a forma da variação, e a abstração tem boa chance de acertar.

LSP — Liskov Substitution Principle

Formulado por Barbara Liskov em 1987, é o mais sutil dos cinco. A versão informal: se S é subtipo de T, qualquer instância de T no programa deveria poder ser substituída por uma de S sem quebrar o comportamento. A versão formal envolve pré-condições, pós-condições e invariantes — herda-se o contrato, e o subtipo só pode aliviar pré-condições e endurecer pós-condições. Nunca o contrário.

O contraexemplo clássico é o Quadrado vs Retângulo. Geometricamente, quadrado é retângulo. Em código, herdar Square de Rectangle quebra LSP: o setter de largura num retângulo só altera a largura; num quadrado, tem que alterar também a altura para manter a invariante. Qualquer cliente que esperava o comportamento de retângulo é violado. A lição: ser-um na vida real não implica ser-um no design de tipos. Tipos são contratos comportamentais, não taxonomias da natureza.

Outra violação muito comum no mundo real: lançar NotSupportedException ou UnsupportedOperationException num método herdado. Se uma ReadOnlyList herda de List e lança exceção em add(), ela não pode substituir uma List — clientes que esperam adicionar vão crashar. A solução é refatorar a hierarquia: ReadOnlyList e List são irmãs, não pai e filha, e podem compartilhar uma interface comum só para leitura.

ISP — Interface Segregation Principle

"Clientes não deveriam ser forçados a depender de métodos que não usam." Se você tem uma interface IRepository com vinte métodos e o seu serviço só usa FindById, está acoplado a dezenove métodos que não importam — e qualquer mudança neles pode afetar você. O remédio é interfaces pequenas, segregadas pelo papel do cliente: IFinder, IWriter, IDeleter.

Em linguagens estaticamente tipadas e nominais (C#, Java), isso é uma decisão explícita de design. Em Go, é praticamente automática: o idioma da comunidade é "interfaces pequenas, definidas no consumidor, não no produtor". A interface io.Reader da stdlib tem um método. io.Writer tem um método. Compor é trivial (io.ReadWriter). Esse é ISP em forma extrema, e funciona muito bem para serviços e adaptadores. Em Python, o equivalente é o structural typing via Protocols (PEP 544).

DIP — Dependency Inversion Principle

Provavelmente o mais transformador dos cinco e o mais mal-entendido. A formulação: módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações. Em vez de o seu serviço de domínio importar diretamente um cliente de Postgres, ele define uma interface UserRepository e a infraestrutura implementa essa interface. A direção da seta de dependência se inverte: a infraestrutura aponta para o domínio, não o contrário.

O ganho não é teórico. É concreto: você consegue testar o domínio sem subir banco, consegue trocar Postgres por DynamoDB sem reescrever regras, consegue rodar a aplicação em modo "in-memory" para testes de integração rápidos. Toda arquitetura hexagonal, todo Clean Architecture, todo DDD tático moderno é DIP aplicado em grande escala. É o princípio que mais paga ao longo do tempo.

A confusão habitual é tratar DIP como "use injeção de dependência sempre". DI é a técnica que viabiliza DIP, mas DI sem inversão de direção é só passagem de parâmetro. O teste é: o módulo de alto nível define a interface? Se a resposta é "não, ele importa a interface do módulo de baixo nível", então a dependência ainda aponta para baixo, e você não tem DIP — só tem código menos legível.

Comparando: o mesmo princípio em três linguagens

O DIP é particularmente interessante porque cada uma das três linguagens da formação expressa a mesma ideia de forma idiomática diferente. Veja como ficaria um caso simples — um UserService que precisa persistir, mas não depende de um banco específico:

C#
public interface IUserRepository
{
    Task<User?> FindById(Guid id);
    Task Save(User u);
}

public class UserService
{
    private readonly IUserRepository _repo;
    public UserService(IUserRepository repo) => _repo = repo;

    public async Task RegisterAsync(string email)
    {
        var u = new User(Guid.NewGuid(), email);
        await _repo.Save(u);
    }
}

Interface explícita, injeção via construtor. O Container DI (built-in do .NET 10) faz o resto.

Python
from typing import Protocol
from uuid import UUID, uuid4

class UserRepository(Protocol):
    def find_by_id(self, id: UUID) -> User | None: ...
    def save(self, u: User) -> None: ...

class UserService:
    def __init__(self, repo: UserRepository):
        self._repo = repo

    def register(self, email: str) -> None:
        u = User(uuid4(), email)
        self._repo.save(u)

Protocol é structural typing — qualquer classe com os métodos certos serve, sem herança explícita.

Go
type UserRepository interface {
    FindByID(id uuid.UUID) (*User, error)
    Save(u *User) error
}

type UserService struct {
    repo UserRepository
}

func (s *UserService) Register(email string) error {
    u := &User{ID: uuid.New(), Email: email}
    return s.repo.Save(u)
}

A interface vive no pacote do consumidor (UserService), não no produtor. Isso é ISP + DIP idiomático em Go.

Note que o conceito é exatamente o mesmo nas três — o serviço de alto nível define o contrato; a infraestrutura implementa. Mas as ferramentas são diferentes: interface nominal em C#, Protocol estrutural em Python, interface implícita e definida no consumidor em Go. Aprender a reconhecer o conceito por baixo do idioma é exatamente o ponto de fazer cada projeto nas três linguagens nesta formação.

Quando NÃO aplicar — o lado que poucos ensinam

Esta é a parte que distingue um sênior. Aplicar SOLID exige um custo: mais arquivos, mais indireção, mais cognitive load para quem lê. Esse custo é justificado quando a mudança é esperada. Quando não é, você está pagando overhead sem retorno.

Há três contextos onde aplicar SOLID religiosamente é provavelmente errado. Primeiro: scripts e ferramentas pontuais. Um script Python de cento e cinquenta linhas que processa um CSV uma vez por mês não precisa de CsvReaderInterface, RowProcessorFactory e injeção de dependência — precisa funcionar e estar legível. YAGNI ("You Aren't Gonna Need It") vence aqui.

Segundo: protótipos e MVPs com horizonte de poucos meses. Você não sabe ainda quais são os pontos de variação reais — abstrair preventivamente é adivinhação. É melhor escrever direto, descobrir as fronteiras na prática, e refatorar quando o pattern emerge. Esse é exatamente o sentido da regra dos três mencionada antes.

Terceiro: domínios genuinamente estáveis. Um parser de RFC2616 (HTTP/1.1) não muda há vinte anos. Você pode ter código bem direto, sem múltiplas camadas de abstração para "facilitar mudança", porque a mudança não vai vir. Quando a próxima especificação chegar (e levou décadas), reescrever é provavelmente mais barato do que pagar overhead arquitetural durante todo esse tempo.

heurística do sênior

Antes de aplicar um princípio de SOLID, pergunte: qual mudança específica esta estrutura me ajuda a fazer? Se você não consegue nomear uma mudança plausível em até seis meses, provavelmente está aplicando o princípio sem motivo. Apague a abstração e volte ao código direto.

Clean Code: o que sobreviveu, o que envelheceu

O livro Clean Code de Robert Martin (2008) virou referência canônica e, uma década e meia depois, é também alvo de críticas legítimas. Algumas recomendações envelheceram bem; outras nem tanto. Vale separar.

O que envelheceu bem: nomes intencionais (variáveis e funções que revelam propósito), funções pequenas com um nível de abstração consistente, evitar flags booleanas em parâmetros (que viram doStuff(true, false, true) em produção), tratar comentários como código falhado quando possível. Essas heurísticas são quase universalmente aceitas hoje, e custam pouco para aplicar.

O que envelheceu mal ou é polêmico: a recomendação de "funções devem ter no máximo 4 linhas" é hoje vista como exagero — ela leva a explosão de micro-funções que prejudicam legibilidade tanto quanto métodos de duzentas linhas. O exemplo da refatoração do Sparkle no livro é estudado hoje como o que não fazer. A obsessão por classes pequenas pode produzir o anti-pattern anemic domain model, com objetos que são meras bags de getters/setters.

A leitura honesta de Clean Code é: pegue as heurísticas baratas (nomes, funções, ausência de magic numbers, evitar duplicação grosseira) e aplique-as sistematicamente. Trate o resto como sugestão, não regra. Casey Muratori, Hillel Wayne e John Carmack publicaram críticas substantivas ao livro nos últimos anos que vale ler — não para abandonar Clean Code, mas para usá-lo com olho crítico.

O método: Boy Scout Rule e refatoração contínua

A regra do escoteiro — "deixe o acampamento mais limpo do que encontrou" — é talvez a contribuição mais útil de Clean Code para a prática diária. Quando você toca num arquivo para uma feature, deixe-o um pouco melhor: renomeie uma variável obscura, extraia uma sub-função óbvia, apague um comentário desatualizado. São cinco minutos por commit; ao longo de meses, transformam codebases.

Isso só funciona se você tem testes que protegem a refatoração. Sem testes, "limpar" vira gambiarra com risco. É por isso que o módulo seguinte (Testes & TDD) vem antes de Bancos e tudo o que segue: você não pode aplicar Clean Code seriamente sem uma rede de segurança. Esses dois conceitos — código limpo e testes — são interdependentes; tentar fazer um sem o outro é meio caminho.

A versão dura desse método é o ciclo de Kent Beck: Make the change easy, then make the easy change. Antes de adicionar uma feature, refatore o código existente até que a feature vire uma mudança trivial. Em vez de "implementar e depois refatorar", você refatora primeiro. É lento no início e rapidíssimo depois — e é como times maduros operam em codebases que duram décadas.

Como praticar este conceito

Teoria vira prática quando você consegue olhar para um pedaço de código e nomear o que está errado. Para internalizar SOLID, dois exercícios funcionam bem:

  1. Cace violações em código real. Pegue um projeto open-source que você não conhece (algum repo da Microsoft em C#, Django em Python, Kubernetes em Go). Abra um arquivo aleatório de mais de 200 linhas. Tente identificar: quantas razões para mudar essa classe tem? Há violação de OCP escondida em algum switch? Os subtipos respeitam LSP? Faça isso por uma hora; você vai começar a ver padrões.
  2. Refatore um código antigo seu. Pegue algum repo seu de um ano atrás. Aplique SRP onde claramente faltava, DIP onde a infraestrutura estava importada direto no domínio. Mas — e isso é parte do exercício — pare de propósito antes de ir longe demais. O ponto é descobrir onde o ganho real compensa o custo, não passar de SOLID a "sobre-engineering".

O projeto deste módulo (URL Shortener) é desenhado especificamente para você praticar SOLID em escala mínima viável: cinco classes, uma interface de repositório, três endpoints. Pequeno o bastante para você ver cada princípio agindo, grande o bastante para sentir o ganho de DIP quando trocar o storage de in-memory para Postgres.

Referências para aprofundar

  1. livro Clean Code: A Handbook of Agile Software Craftsmanship — Robert C. Martin (2008). Capítulos 1-4 e 10. Leia com olho crítico — veja a discussão sobre o que envelheceu.
  2. livro Agile Software Development: Principles, Patterns, and Practices — Robert C. Martin (2002). Os capítulos 7-12 são a fonte original e mais rigorosa dos 5 princípios SOLID.
  3. livro A Philosophy of Software Design — John Ousterhout (2018, 2nd ed. 2021). Contraponto fundamental ao Clean Code. Defende módulos profundos e questiona o "small functions". Leitura obrigatória.
  4. artigo The Single Responsibility Principle — Robert C. Martin. blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html — formulação original com o exemplo Employee.
  5. artigo Refactoring (catalog online) — Martin Fowler. refactoring.com — catálogo navegável dos refactorings nomeados. Companhia perfeita para Boy Scout Rule.
  6. artigo "Clean" Code, Horrible Performance — Casey Muratori (2023). Crítica importante ao Clean Code do ponto de vista de performance — mostra que abstração não é gratuita.
  7. paper Data Abstraction and Hierarchy — Barbara Liskov (1987). SIGPLAN Notices, 23(5). A formulação original do que viria a ser LSP. Curto e legível.
  8. docs Effective Go — Interfaces. go.dev/doc/effective_go#interfaces — define o estilo idiomático que naturaliza ISP+DIP em Go.
  9. docs PEP 544 — Protocols: Structural Subtyping. peps.python.org/pep-0544/ — equivalente Pythônico das interfaces estruturais de Go.
  10. docs Dependency Injection in .NET. learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection — DI container built-in com exemplos.
  11. vídeo SOLID is not solid — Examining the Single Responsibility Principle — Hillel Wayne. YouTube. Crítica rigorosa e bem-humorada à formulação clássica de SRP. Essencial.
  12. vídeo Why I Don't Teach SOLID — Dan North (2018, GOTO Copenhagen). YouTube. Provocação útil de um dos pais do BDD. Pelo menos pra você defender a sua posição.