E aí, galera.
Hoje vamos bater um papo sobre um assunto que todo dev acha que domina mas, na real, a maioria só decorou a sigla: SOLID, não estou dizendo que SOLID é inútil,estou dizendo que a forma como a maioria usa é inútil. A galera decora as cinco letras, cola num checklist de code review e acha que está fazendo arquitetura. Não está, está seguindo receita de bolo sem entender por que a farinha vai antes do ovo. A tese aqui é provocação deliberada: quem pensa em arquitetura já aplica SOLID sem saber.
E quem só sabe SOLID não sabe arquitetura, e você discorda, ótimo - vou tentar te convencer ao longo do texto.
O que a galera chama de SOLID
Pergunta pra qualquer dev intermediário o que é SOLID.
Você vai ouvir:
- S - uma classe faz só uma coisa
- O - aberto pra extensão, fechado pra modificação
- L - subclasse não pode quebrar o comportamento da classe pai
- I - interface pequena é melhor que interface grande
- D - dependa de abstração, não de implementação concreta
Guarda essa lista, ela vai ser útil daqui a pouco - mas não pelo motivo que você imagina.
Agora pergunta como ele aplica no dia a dia.
Aí começa o problema.
Você vai ouvir coisas como "esse método tem 15 linhas, viola o SRP" ou "não posso mudar essa classe porque viola o OCP".
A sigla virou dogma. O princípio virou regra de lint mental.
Isso não é SOLID, isso é superstição de dev.
O SOLID que você aprendeu não é o SOLID
Volta na lista de cinco itens que você acabou de ler e olha o S.
"Uma classe faz só uma coisa."
Isso não é o Single Responsibility Principle, é a versão folclórica dele, que se espalhou porque cabe em um tweet.
O que Robert Martin escreveu é diferente: responsabilidade é uma razão para mudar, e uma classe deve ter uma, e apenas uma, razão para ser mudada. Anos depois ele refinou ainda mais, para deixar explícito o que já estava implícito: um módulo deve ser responsável perante um, e apenas um, ator.
Percebe o que acontece quando você troca a definição?
"Faz uma coisa" é uma pergunta sobre o código, é subjetiva, não tem unidade de medida, e leva direto ao absurdo de fragmentar tudo até cada classe ter um método.
"Tem uma única razão para mudar" é uma pergunta sobre quem pede a mudança, e aí não tem como responder olhando só o arquivo aberto no editor. Você precisa saber quem são os atores do seu domínio, o que cada um quer, e com que frequência cada um muda de ideia.
Ou seja: o S de verdade já é uma pergunta de arquitetura, ele nunca foi inimigo do raciocínio de domínio - ele é esse raciocínio, mal citado por vinte anos.
E isso vale pra sigla inteira, a maioria dos devs discute uma versão simplificada dos cinco princípios, aprendida de terceira mão, e depois usa essa versão simplificada como régua em code review. É como corrigir prova com gabarito errado.
Já que é pra ser preciso: os princípios foram introduzidos por Martin no paper de 2000 chamado "Design Principles and Design Patterns", que é um texto sobre apodrecimento de software. O acrônimo SOLID veio só depois, cunhado por volta de 2004 por Michael Feathers, que rearranjou as letras numa ordem memorizável.
E nem todas as letras são do Martin. O O vem do Bertrand Meyer, do "Object-Oriented Software Construction", de 1988. O L vem da Barbara Liskov, de uma palestra de 1987, formalizado depois com a Jeannette Wing.
Repara no que isso significa, SOLID não é uma teoria unificada que alguém deduziu de primeiros princípios. É uma coletânea, montada a partir de fontes diferentes, com um acrônimo colado por cima anos depois porque acrônimo é fácil de ensinar.
Não tem nada de errado com isso. Só não trate uma embalagem didática como se fosse a estrutura da coisa.
SOLID é um guia, não uma lei
Os princípios existem pra ensinar desenvolvedores a escrever código que não vire uma bola de lama com o tempo. É um conjunto de heurísticas - não de leis imutáveis.
O problema é que heurística virou mandamento, e mandamento sem contexto é burrice organizada.
Tamanho de método não está em nenhuma das cinco letras, cyclomatic complexity também não. "Essa classe faz duas coisas" é subjetivo - o que é uma coisa pra você pode ser meia coisa pra mim.
SOLID diz o que evitar.
Não diz como construir.
Quem pensa em arquitetura já aplica
Vamos pegar um exemplo real em Rails.
Você tem um sistema multi-tenant de gestão de times de esports.
Cada módulo - players, analytics, scouting, matches - tem seus próprios controllers, services, models, serializers e policies.
Você não pensou "vou aplicar o S do SOLID aqui". Você pensou "analytics não tem nada a ver com players, então ficam separados". O resultado é o mesmo - responsabilidade única - mas o raciocínio é arquitetural, não de checklist.
app/modules/
├── players/
│ ├── controllers/
│ ├── services/
│ ├── models/
│ └── policies/
├── analytics/
│ ├── controllers/
│ └── services/
└── scouting/
├── controllers/
└── services/
S apareceu porque você pensou em domínio. I apareceu porque cada módulo expõe só o que precisa. D apareceu porque o controller não instancia o service diretamente - ele consome uma abstração.
Você aplicou quatro letras do SOLID sem abrir um artigo sobre SOLID.
E tem uma coisa melhor: pelo critério do ator, essa separação se justifica sozinha. Analytics muda quando o time de dados muda de ideia. Players muda quando o produto muda de ideia. São atores diferentes, com calendários diferentes e prioridades diferentes. Por isso ficam separados - não porque "fazem coisas diferentes", mas porque mudam por motivos diferentes.
O exemplo que expõe tudo
Sabe o que acontece quando alguém aplica SOLID sem pensar em arquitetura? Não é o horror óbvio que aparece nos artigos. É isso aqui, que passa em code review todo dia:
# app/modules/players/services/create_player_service.rb
class CreatePlayerService
def initialize(repository: PlayerRepository.new,
validator: PlayerValidator.new,
notifier: PlayerNotifier.new)
@repository = repository
@validator = validator
@notifier = notifier
end
def call(params)
result = @validator.validate(params)
return Result.failure(result.errors) unless result.valid?
player = @repository.create(result.attributes)
@notifier.notify_created(player)
Result.success(player)
end
end
# app/modules/players/repositories/player_repository.rb
class PlayerRepository
def create(attributes)
Player.create!(attributes)
end
def find(id)
Player.find(id)
end
end
Olha o que tem aí.
Um repository por cima do ActiveRecord, que já é um repository. Você embrulhou um padrão dentro dele mesmo e chamou de desacoplamento.
Uma injeção de dependência em três colaboradores que têm exatamente uma implementação cada e nunca vão ter outra. O construtor virou documentação de um ponto de extensão que ninguém vai usar.
Um Result artesanal, porque tratar exceção parecia impuro.
Um validator separado do model, duplicando validação que o ActiveRecord já faz - e agora existem dois lugares onde a regra pode estar, e eles vão divergir.
Cada peça dessas tem uma justificativa que cita uma letra. O conjunto é indefensável.
E olha a parte cruel: pelo critério correto do SRP, isso viola o princípio em vez de cumprir. CreatePlayerService, PlayerValidator e PlayerRepository mudam pelo mesmo motivo, a pedido da mesma pessoa - o PO que decidiu que jogador agora precisa de nickname único. Uma mudança, três arquivos, zero benefício. Isso é uma responsabilidade espalhada por três classes, que é exatamente o que o SRP existe pra evitar.
Fragmentar não é aplicar SRP. Às vezes é violar SRP com a estética de quem o aplicou.
As letras que ninguém lembra - e o que isso revela
Se você for honesto, sabe S e I, talvez D. O e L você lembra quando alguém pergunta, esquece na semana seguinte.
Não é coincidência. O e L são as letras que menos fazem sentido fora de um contexto de domínio bem modelado.
O OCP - aberto pra extensão, fechado pra modificação - só aparece naturalmente quando você já desenhou uma hierarquia de comportamento. No sistema de esports: você não modifica a lógica de cálculo de ranking toda vez que uma nova modalidade entra. Você estende. Mas pra chegar nessa decisão, você precisou primeiro entender que "modalidade" é um conceito do seu domínio que vai variar, crescer e se ramificar. Quem não pensou em domínio antes não tem onde encaixar o OCP - a letra flutua sem âncora.
O LSP - subclasse não pode quebrar o comportamento da classe pai - é ainda mais dependente de hierarquia bem pensada. Se você modelou herança por conveniência de reutilização de código em vez de por relação real entre conceitos do domínio, LSP vai ser violado quase por definição. A letra não conserta uma herança mal concebida. Ela só nomeia o sintoma.
É por isso que O e L somem da memória: sem domínio, elas não têm onde se apoiar.
Ruby quebra metade das letras
Aqui tem uma coisa que quase ninguém fala, e que é meio constrangedora pra quem recita SOLID em projeto Rails.
Os princípios nasceram num mundo de tipagem estática e herança pesada. C++, Java, interfaces declaradas explicitamente. Ruby não é esse mundo.
O I fica quase vazio. Interface Segregation diz pra não forçar um cliente a depender de métodos que não usa. Mas em Ruby não existe interface. Não existe declaração formal do contrato que o cliente é obrigado a implementar. Se um objeto responde a #call, ele serve. A "interface gorda" que o ISP combate é um problema de linguagem que Ruby simplesmente não tem no mesmo formato. Sobra uma versão diluída do princípio: não faça módulos gigantes. O que é bom conselho e não precisava de uma letra.
O D vira outra coisa. Dependency Inversion diz pra depender de abstração, não de implementação. Só que em Ruby, com duck typing, você já depende de abstração - não existe declaração de tipo pra violar. Qualquer objeto que responda às mensagens certas entra no lugar do outro. A inversão que o DIP prega em Java, com interface e container de injeção, em Ruby é o comportamento padrão da linguagem.
Então por que todo mundo injeta dependência em Rails?
Pra testar. É isso. Você recebe o colaborador no initialize porque quer passar um dublê no teste, não porque previu uma segunda implementação. É uma motivação legítima - mas é uma decisão de testabilidade, não de arquitetura, e chamar isso de DIP confunde as duas.
A prova é o exemplo da seção anterior: três dependências injetadas, uma implementação cada, nenhuma delas jamais substituída em produção. Se a razão fosse arquitetural, existiria uma segunda implementação. Não existe. A razão era o teste, e o teste podia ter sido resolvido de outro jeito.
Vale a pena ver a Sandi Metz falando sobre isso - ela deu no GoRuCo de 2009 uma palestra justamente sobre SOLID lido através de Ruby, e é a referência canônica de quem levou esses princípios a sério dentro da comunidade.
O ponto não é "SOLID não vale em Ruby". É que metade das letras responde a perguntas que a sua linguagem já respondeu, e recitar as cinco sem perceber isso é o sintoma mais claro de checklist sem pensamento.
O vocabulário que eu uso no lugar
Criticar é fácil. Se eu vou dizer que o checklist não serve, preciso dizer o que colocar no lugar em uma revisão de código real.
O que eu uso é conascência, um conceito do Meilir Page-Jones que é anterior à sigla SOLID, muito mais preciso, e que quase ninguém conhece porque não virou acrônimo bonitinho.
A ideia: dois trechos de código são conascentes quando mudar um obriga a mudar o outro pra manter o sistema correto. E aí você classifica o tipo:
- Conascência de nome - as duas partes concordam sobre um nome. É a mais fraca, e a mais fácil de mudar (renomear é refactor de IDE).
- Conascência de tipo - concordam sobre o tipo de algo.
- Conascência de significado - concordam que
0quer dizer pendente e1quer dizer aprovado. Alguém precisa saber disso nos dois lados. - Conascência de posição - concordam sobre a ordem. Trocar dois argumentos posicionais de mesmo tipo quebra tudo silenciosamente.
- Conascência de algoritmo - os dois lados implementam o mesmo cálculo e precisam continuar batendo.
- Conascência de execução e de timing - a ordem em que as coisas rodam, ou o momento, importa.
E dois eixos que transformam isso em ferramenta de decisão: força (quão difícil é descobrir e mudar aquele acoplamento) e localidade (quão perto as duas partes estão uma da outra).
A regra prática que sai daí é bonita de tão simples: acoplamento forte é aceitável quando é local, e inaceitável quando atravessa fronteiras. Conascência de posição dentro de um método privado de dez linhas: tudo bem. A mesma conascência de posição entre dois módulos diferentes: dívida.
Compara com o code review do mundo real.
Com SOLID: "isso aqui viola o SRP." A pessoa se defende, você repete, ninguém tem régua, a discussão termina em senioridade ou em quem fala mais alto.
Com conascência: "isso é conascência de significado entre o módulo de matches e o de analytics - o 3 quer dizer 'partida anulada' nos dois lugares e ninguém documentou. Sobe pra conascência de nome com um enum, e o acoplamento passa a ser detectável pelo compilador... perdão, pelo teste."
O segundo é específico, gradual e acionável e repara que ele não te obriga a eliminar o acoplamento - te obriga a saber qual você tem e a decidir se ele está no lugar certo. É essa a conversa que SOLID como checklist nunca produz.
Isso não é novidade nenhuma - e é esse o ponto
Em 1972, o David Parnas publicou um artigo chamado "On the Criteria To Be Used in Decomposing Systems into Modules".
Vinte e oito anos antes do paper do Martin, trinta e dois antes do acrônimo.
A tese dele: você não modulariza um sistema seguindo o fluxograma de execução, você modulariza escondendo, dentro de cada módulo, as decisões de design que têm chance de mudar. Cada módulo esconde uma decisão dos outros. Isso ficou conhecido como information hiding.
Lê de novo com os olhos de hoje: "decisões que têm chance de mudar" é a mesma coisa que "razões para mudar", que é a definição correta do SRP.
O S do SOLID é uma reformulação de uma ideia de 1972, uma boa reformulação, com um nome melhor. Mas a ideia original é mais profunda, porque ela nunca esteve amarrada a classe, objeto ou herança - vale pra módulo, pra serviço, pra pasta, pra repositório, pra time.
Por que isso importa pro artigo? Porque mostra que "pense em domínio antes de pensar na sigla" não é uma opinião minha contrária ao consenso. É o consenso original, que a sigla ofuscou justamente por ser mais fácil de memorizar do que de entender.
Quando o checklist está certo
Agora a parte honesta, porque senão eu estou construindo espantalho.
Tem três situações em que usar SOLID como checklist explícito é a coisa certa:
Onboarding e formação. Dev júnior não tem repertório pra "pensar em domínio". Pedir isso é pedir para desenhar sem saber segurar o lápis. As cinco letras são um andaime legítimo - e como todo andaime, o objetivo é sair depois.
Vocabulário compartilhado. Um time que fala "isso está violando o D" resolve em quinze segundos uma conversa que sem vocabulário levaria vinte minutos. Vocabulário impreciso continua sendo melhor que nenhum vocabulário. A minha proposta de trocar por conascência é justamente uma proposta de vocabulário melhor, não de abolir vocabulário.
Legado que ninguém conhece. Quando você cai num sistema de dez anos sem documentação e sem ninguém pra perguntar, você não tem o modelo de domínio na cabeça pra raciocinar arquiteturalmente. Aí um checklist funciona como diagnóstico inicial: onde estão as classes que mudam por dez motivos diferentes? É um começo, e é melhor que encarar a tela.
O problema nunca foi a sigla existir. Foi ela ser tratada como teto, quando é piso.
Então pra que serve SOLID?
Serve pra quem está aprendendo a organizar código e precisa de referência. É um mapa pra iniciante - útil pra não se perder, mas não é o território.
Quando você já pensa em termos de domínio, bounded contexts, separação de responsabilidades por módulo, SOLID é consequência - não ponto de partida.
E é por isso que a pergunta "esse código segue SOLID?" quase nunca é a pergunta certa. As perguntas certas são outras: quem pede mudança nesse código? com que frequência? quando essa pessoa pedir, quantos arquivos eu abro? o que é que essa fronteira aqui está escondendo do resto do sistema?
Responde essas e as letras aparecem sozinhas. Responde só as letras e você não fez nenhuma dessas perguntas.
Conclusão
O risco real do SOLID como checklist não é que ele produza código ruim. É que ele dá a sensação de que código bom foi produzido.
Você fragmenta classes, satisfaz as cinco letras, passa no code review - e entrega um sistema que ninguém consegue mudar sem medo, porque as peças foram cortadas no tamanho certo mas coladas no lugar errado.
Arquitetura é sobre onde as responsabilidades vivem em relação ao domínio. SOLID é sobre como essas responsabilidades se relacionam internamente. Um sem o outro é decoração.
Na próxima vez que alguém falar que seu método viola o SRP porque tem 15 linhas, pergunta de volta: "viola como? em relação a qual responsabilidade de domínio? quem é o ator que pede essa mudança?"
Se a pessoa travar, você sabe quem realmente entende o princípio.
Você já parou de pensar em SOLID e começou a pensar em domínio? Como foi essa virada?
E se você usa conascência no code review, me conta - eu quero saber quantos somos.
Até a próxima.
Referências
- Design Principles and Design Patterns - Robert C. Martin (2000)
- The Single Responsibility Principle - Robert C. Martin
- SOLID - Wikipedia
- SOLID Object-Oriented Design - Sandi Metz, GoRuCo 2009
- On the Criteria To Be Used in Decomposing Systems into Modules - David Parnas (1972)
- Connascence - Meilir Page-Jones
- Domain-Driven Design - Eric Evans
- Modular Monolith - Sam Newman
- Growing Object-Oriented Software, Guided by Tests - Freeman & Pryce