Boas Práticas¶
Clean Code¶
Definição: Clean Code
Conjunto de boas práticas definido por Robert C. Martin (também conhecido como Uncle Bob) para escrever código legível e de boa qualidade — o objetivo é que qualquer desenvolvedor, mesmo alguém que não participou da criação daquele código, consiga entendê-lo com facilidade. Reduz a chance de bugs introduzidos por incompreensão do código existente, e o tempo de correção quando eles aparecem. O SOLID (ver a seguir) é um dos conjuntos de princípios mais importantes dentro do Clean Code, especificamente voltado a design orientado a objetos.
DRY e KISS¶
Definição: DRY — Don't Repeat Yourself
A mesma regra de negócio (ou o mesmo trecho de lógica) não deveria existir duplicada em vários pontos do código — quando ela precisa mudar, é fácil esquecer de atualizar uma das cópias, criando inconsistência. A recomendação é extrair a lógica repetida para um método ou classe reutilizável, chamado de todo lugar que precisar dela.
Definição: KISS — Keep It Simple, Stupid
Mantenha o código o mais simples possível: evite funções longas, condicionais encadeados demais, e nomes que não deixam clara a utilidade de uma variável ou método. Complexidade desnecessária (a que não vem do problema em si, mas de como ele foi resolvido) é sempre um custo, nunca um benefício.
Nomenclatura de variáveis e métodos¶
Definição: Nomes devem ser autoexplicativos, não apoiados em comentários
O nome de uma variável ou método deve deixar claro, por si só, o que ele representa ou faz — sem exigir que quem lê analise a lógica ao redor (ou um comentário) para entender.
// versão sem o uso do Clean Code
public int soma(int n1, int n2) {
int n = n1 + n2; // resultado da soma
return n;
}
// versão aplicando o Clean Code
public int soma(int n1, int n2) {
int sumResult = n1 + n2;
return sumResult;
}
Na segunda versão, sumResult já comunica seu propósito sem depender do comentário nem
da lógica de atribuição para ser entendido.
Definição: Nomes devem ser fáceis de localizar numa pesquisa no código
Nomes curtos e genéricos demais (um limite numérico solto como 100, por exemplo)
são difíceis de encontrar ou de saber o significado ao reencontrá-los meses depois.
Duas melhorias comuns resolvem isso ao mesmo tempo: externalizar valores mágicos
como constantes nomeadas, e renomear métodos genéricos para que o próprio nome já
expresse o que fazem.
// antes: "100" é um número mágico, e o nome do método não diz o intervalo
public void printEvenNumbers() {
List<Integer> l = new ArrayList<>();
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) l.add(i);
}
System.out.println(l);
}
// depois: constante nomeada + nome de método que expressa o intervalo
private static final Integer MAX_NUMBERS_TO_VERIFY_EVEN_VALUE = 100;
public void printEvenNumbersFromZeroToOneHundred() {
List<Integer> evenNumbers = new ArrayList<>();
for (int i = 0; i <= MAX_NUMBERS_TO_VERIFY_EVEN_VALUE; i++) {
if (i % 2 == 0) evenNumbers.add(i);
}
System.out.println(evenNumbers);
}
Mais regras de bons nomes¶
Complementando o que está acima, as orientações de código limpo para nomes:
| Regra | Em resumo |
|---|---|
| Revele a intenção | diasDesdeUltimoPagamento, não d ou dias |
| Não cause confusão | Um nome não deve sugerir algo que não é (uma variável listaDeContas que não é uma lista) |
| Cuidado com caracteres parecidos | 0/O, 1/l/I |
| Evite nomes quase iguais | contaCliente, contaDoCliente e contasClientes no mesmo escopo |
| Ações semelhantes, nomes semelhantes | buscarPorId, buscarPorEmail (e não buscarPorId e obterPorEmail) |
| Mesma palavra, mesmo propósito | Escolha obter ou buscar ou recuperar e use sempre o mesmo |
| Pronunciável | Se travou a língua ou causou constrangimento, troque |
| Sem caracteres especiais ou prefixos de tipo | Não precisa dizer o que a entidade é no nome (strNome, IContaService, ContaClass) |
| Sem mapeamentos mentais | Evite i, j, x fora de laços curtíssimos; ninguém deveria precisar "traduzir" |
| Classes são substantivos | Pedido, CalculadoraDeFrete; evite Manager, Processor, Data genéricos |
| Métodos são verbos | calcularFrete, estaAtivo, salvar |
| Sem humor nem gírias | Quem lê daqui a anos não vai entender a piada |
| Use termos técnicos a seu favor | Nomes de padrões e de domínio (Factory, Repository, Strategy) comunicam muito em pouco espaço |
| Sem repetição desnecessária | Cliente.nomeDoCliente → Cliente.nome |
Funções¶
- Faça uma coisa só, e bem (alta coesão). Muitas funções pequenas e específicas são melhores que uma gigante: cada uma tem um nome que documenta o passo, e pode ser reaproveitada e testada.
- Poucos parâmetros: o ideal é de zero a três. Mais que isso, agrupe em um objeto de argumento (Argument Object, como
ParametrosAuxilio) — e esse objeto pode concentrar a validação. - Sem parâmetros flag: um
booleanque faz a função ter dois comportamentos revela duas funções disfarçadas. Separe em duas (calcularFeriasComAbono,calcularFeriasSemAbono). - Listas como argumento (varargs) são aceitáveis quando os valores são do mesmo tipo e a ordem e a quantidade não mudam o significado (somar descontos).
- Sem surpresas: a função deve fazer o que o nome diz e nada além — nada de
validarSalario()que, escondido, demite alguém (efeito colateral oculto). - Faça algo OU conte algo (separação comando-consulta): uma função que realiza uma ação e devolve um booleano de sucesso força
if (transferir(...))e esconde o motivo da falha. Prefira lançar uma exceção na falha. - DRY: extraia a regra repetida para uma função e a chame de todo lugar; mudou a regra, muda em um só ponto.
# antes: flag e função que faz duas coisas
def calcular_ferias(salario, com_abono):
...
# depois: duas funções coesas
def calcular_ferias_com_abono(salario): ...
def calcular_ferias_sem_abono(salario): ...
Comentários¶
Comentários não "limpam" código ruim: o código muda e o comentário fica desatualizado (e passa a mentir). Antes de comentar, melhore os nomes e reduza o tamanho das funções. Um bom código pode ter comentários somente quando o código não consegue dizer o porquê:
- explicar a intenção ou uma decisão não óbvia (por que foi preciso um atalho, uma regra de negócio, uma limitação externa);
- alertar sobre consequências (um teste lento, uma chamada cara);
- TODO / FIXME / HACK (marcadores convencionados): com moderação e rastreáveis, nunca como lixeira;
- documentação de API pública com as ferramentas da linguagem (Javadoc em Java, docstrings/PEP 257 em Python).
Evite: comentários que repetem o código, código comentado (o controle de versão guarda o histórico), comentários de autoria/data (o Git sabe) e blocos de "diário de mudanças".
Formatação¶
Formatação é comunicação: o código bem organizado é lido mais rápido. As IDEs indentam, mas não substituem o critério:
- Indentação consistente; quebrar cadeias longas (
builder().a().b()...) uma chamada por linha, para a estrutura aparecer. - Agrupar por proximidade: linhas relacionadas juntas, linhas em branco separando blocos de ideias distintas (como parágrafos).
- Linhas curtas (uma convenção comum é de 80 a 120 caracteres): evite rolagem horizontal e expressões gigantes.
- Siga o padrão da equipe e use um formatador automático (Spotless/google-java-format, Black, Prettier) no pipeline.
Objetos, abstração e Lei de Demeter¶
- Abstraia os dados: o importante não é "atributo privado ou público", e sim esconder como o dado é guardado por trás de métodos com significado (
depositar(valor),sacar(valor)), concentrando as regras em um ponto — veja Encapsulamento. Em Python não existeprivatede fato: a convenção é_atributo(uso interno) e__atributo(name mangling). - Lei de Demeter (princípio do menor conhecimento): um objeto deve conversar só com seus amigos imediatos.
conta.getTitular().getEnderecos().get(0)expõe a estrutura interna; crieconta.enderecoPrincipalDoTitular().
| Conceito | O que é |
|---|---|
| DTO (Data Transfer Object) | Objeto que só carrega dados entre camadas (ex.: do controller para o serviço, ou entre serviços), sem regras de negócio |
| VO (Value Object) | Objeto imutável definido por seus valores: duas instâncias com os mesmos valores são iguais (equals/hashCode) — dinheiro, CPF, endereço |
| POJO / POPO | Objeto simples, sem herdar de/ depender de um framework específico |
| JavaBean | Classe com construtor sem argumentos, atributos privados e getters/setters, serializável (convenção antiga de ferramentas) |
| Entidade | Objeto com identidade (id) que persiste e muda ao longo do tempo |
Em Java moderno, os records e o Lombok reduzem o código de DTOs e VOs.
Tratamento de erros¶
- Use exceções, não códigos de retorno (
-1,false), para sinalizar falhas. - Não retorne
null: obriga todo chamador a se defender deNullPointerException. Lance uma exceção, devolva uma coleção vazia ou umOptional/objeto nulo (Null Object). - Crie suas próprias exceções com nomes do domínio (
SaldoInsuficienteException) e mensagens úteis. - Em Java, prefira exceções não checadas (
RuntimeException) na maior parte do código: as checadas acoplam as assinaturas e propagam detalhes pelas camadas. Veja Tratamento de exceções.
Testes de unidade e o código limpo¶
Código limpo e testes se reforçam: sem testes, ninguém refatora com segurança. Boas práticas (detalhes):
- Nomes de teste que dizem o cenário e o resultado esperado.
- Um conceito por teste (alguns defendem uma asserção por teste, aplicando o "S" do SOLID aos testes; são propostas de agrupamento diferentes, e o essencial é cada teste falhar por uma razão).
- Princípios F.I.R.S.T.: Fast (rápidos), Independent (independentes entre si e da ordem), Repeatable (mesmo resultado em qualquer ambiente), Self-validating (passam ou falham sozinhos, sem inspeção manual), Timely (escritos junto do código, de preferência antes).
- Cobertura (percentual do código exercitado) mede o que foi executado, não se está bem testado; a de linhas (e de ramos) é mais informativa que a de classes/métodos. Defina um mínimo no pipeline sem transformá-lo em fim em si.
Classes, regra do escoteiro e convenções¶
- Organização da classe: constantes, atributos, construtores, métodos públicos e, por fim, os privados (do mais geral ao mais específico), numa ordem previsível.
- Baixa coesão é cheiro de classe grande demais: se ela faz muitas coisas ou tem muitos atributos usados por poucos métodos, divida (SRP, veja SOLID).
- Regra do escoteiro: deixe o código mais limpo do que o encontrou. Ao mexer num trecho, faça pequenas melhorias (renomear, extrair função); ignorar um código ruim por "não ser meu" só aumenta o custo coletivo.
- Convenções não são regras, mas poupam atenção: em Java (e Kotlin, Groovy, JavaScript, C#): classes e interfaces em
PascalCase; variáveis, atributos e métodos emcamelCase; constantes emMAIUSCULAS_COM_UNDERSCORE. Em Python (PEP 8): módulos e pacotes emsnake_case, funções e variáveis emsnake_case, classes emPascalCase,self/clscomo primeiros parâmetros. - Ferramentas: linters e análise estática (Checkstyle, SonarLint/SonarQube, SpotBugs, Pylint, Flake8, Ruff) e formatadores (Black, Prettier) mantêm o padrão automaticamente — veja Qualidade.
Código limpo x arquitetura limpa¶
São coisas distintas: Código Limpo (Robert Martin, 2008) trata da qualidade do código-fonte (nomes, funções, comentários, formatação, exceções, testes). Arquitetura Limpa (2012) trata da organização do projeto: separação de responsabilidades, camadas e inversão de dependências, para o sistema evoluir (Arquiteturas de código). Código limpo não são normas: são orientações, como práticas de uma arte marcial; iniciantes devem segui-las até ter maturidade para saber quando adaptar — sempre em função do objetivo (código fácil de ler, entender e alterar).
SOLID: cinco princípios de design orientado a objetos¶
SOLID é um acrônimo (cunhado por Robert C. Martin) para cinco princípios que, juntos,
guiam o design de classes para serem mais fáceis de manter, estender e reutilizar — o
oposto de "código procedural disfarçado de orientado a objetos" (classes que só têm
if/else e getters/setters, sem nenhuma decisão de design real).
Definição: SOLID
- S — Single Responsibility Principle (Responsabilidade Única)
- O — Open-Closed Principle (Aberto-Fechado)
- L — Liskov Substitution Principle (Substituição de Liskov)
- I — Interface Segregation Principle (Segregação de Interface)
- D — Dependency Inversion Principle (Inversão de Dependência)
Os cinco não são regras isoladas — todos giram em torno da mesma tensão central entre coesão (uma classe fazer só uma coisa) e acoplamento (o quanto uma classe depende de outras). Coesão alta e acoplamento baixo é o alvo; cada princípio ataca esse alvo de um ângulo diferente.
SRP — Single Responsibility Principle¶
Definição: Coesão e o SRP
Uma classe coesa tem uma única responsabilidade — representa um conceito do sistema, nada mais. O SRP formaliza isso: a classe deve ter uma, e apenas uma, razão para mudar. Dois comportamentos "pertencem" ao mesmo conceito/responsabilidade se ambos mudam juntos — se um requisito novo for alterar só um dos dois, sem tocar no outro, são responsabilidades diferentes e deveriam estar em classes diferentes.
Classes coesas são mais simples de manter (menos código, então menos chance de esconder bug), mais fáceis de reutilizar (dependem de menos coisa) e reduzem o "efeito dominó" de uma mudança se propagar para pontos inesperados do sistema.
Exemplo clássico de falta de coesão: uma classe que cresce indefinidamente por causa de
if/else encadeados, um por cada variação de uma mesma regra:
class CalculadoraDeSalario {
public double calcula(Funcionario funcionario) {
if (Cargo.DESENVOLVEDOR.equals(funcionario.getCargo())) {
return dezOuVintePorcento(funcionario);
}
if (Cargo.DBA.equals(funcionario.getCargo()) || Cargo.TESTER.equals(funcionario.getCargo())) {
return quinzeOuVinteCincoPorcento(funcionario);
}
throw new RuntimeException("funcionario invalido");
}
// um método privado por regra de cálculo — cresce a cada cargo novo
}
O problema não é só estético: essa classe nunca para de crescer (todo cargo novo é
mais um if), e reaproveitar a regra de um cargo específico em outro lugar do sistema
obriga a depender da classe inteira. A saída é isolar cada regra atrás de uma interface
comum, uma implementação por regra:
public interface RegraDeCalculo {
double calcula(Funcionario f);
}
public class DezOuVintePorcento implements RegraDeCalculo { /* ... */ }
public class QuinzeOuVinteCincoPorcento implements RegraDeCalculo { /* ... */ }
E a associação cargo → regra vira dado, não if, guardada no próprio enum:
public enum Cargo {
DESENVOLVEDOR(new DezOuVintePorcento()),
DBA(new QuinzeOuVinteCincoPorcento()),
TESTER(new QuinzeOuVinteCincoPorcento());
private RegraDeCalculo regra;
Cargo(RegraDeCalculo regra) { this.regra = regra; }
public RegraDeCalculo getRegra() { return regra; }
}
Um cargo novo passa a exigir só uma nova regra e uma linha no enum — nunca mais tocar
na classe de cálculo em si. Isso é o Open-Closed Principle (ver abaixo) surgindo como
consequência natural de perseguir o SRP.
Definição: Quando extrair um método privado
Extrair um trecho de código para um método privado melhora a legibilidade do método público que o chama (uma linha com nome descritivo, em vez de várias linhas de detalhe) — mas não resolve reuso (métodos privados só a própria classe enxerga) nem, sozinho, resolve falta de coesão. Extraia um método privado quando o objetivo é só legibilidade; quando duas responsabilidades diferentes estão misturadas na mesma classe, a solução é outra classe, não outro método privado dela.
Falta de coesão em controllers, e "inveja da outra classe"¶
Um exemplo muito comum de baixa coesão: um método de controller (na arquitetura MVC — Model-View-Controller) que mistura, no mesmo lugar, regra de negócio e detalhe de infraestrutura (acesso a banco, envio de e-mail, chamada a webservice):
@Path("/notaFiscal/nova")
public void cadastraNotaFiscal(NotaFiscal nf) {
if (nf.ehValida()) {
if (nf.ehDeSaoPaulo()) { nf.duplicaImpostos(); }
if (nf.ultrapassaValorLimite()) {
SMTP smtp = new SMTP();
smtp.enviaEmail(nf.getUsuario(), template);
}
// + SQL direto, + chamada SOAP a um webservice...
}
}
A solução não é mágica: quebrar esse método em pedaços coesos — as regras de negócio isoladas em classes próprias, o acesso a banco isolado num DAO, o envio de e-mail isolado numa classe de infraestrutura — deixando o controller fazer só uma coisa: coordenar o processo, chamando cada peça na ordem certa.
Definição: Feature envy (\"inveja de funcionalidade\")
Um code smell (cheiro de código problemático) em que um método de uma classe usa quase só dados/comportamento de outra classe, em vez dos seus próprios — sinal de que aquele comportamento provavelmente deveria morar na outra classe:
@Post("/contrato/fecha")
public void fecha(Contrato contrato) {
contrato.setData("23/01/2015");
contrato.fecha();
List<Pagamento> pagamentos = contrato.geraPagamentos();
if (contrato.isPessoaJuridica()) {
contrato.marcaEmissaoDeNF();
} else {
contrato.marcaImpostoPF();
}
}
contrato — é ele quem deveria orquestrar essa
sequência sozinho, não um método externo fazendo isso por ele.
Definição: Arquitetura hexagonal (ports and adapters)
Padrão arquitetural que formaliza a separação entre infraestrutura (frameworks, banco de dados, webservices — tudo que pode ser trocado) e modelo/domínio (as regras de negócio da aplicação, que devem ser as mesmas independente de qual infraestrutura estiver por trás), aplicando o DIP na escala de todo o projeto. Ver a definição completa, com diagrama e exemplo, em Padrões Arquiteturais.
DIP — Dependency Inversion Principle¶
Depois de separar responsabilidades (SRP), o problema que sobra é o acoplamento: o quanto uma classe depende de outras. Eliminar acoplamento por completo é impossível (todo sistema de porte médio/grande tem dependências entre classes) — a pergunta certa não é "como acabar com o acoplamento", mas "como diferenciar acoplamento bom de acoplamento ruim".
public class GeradorDeNotaFiscal {
private final EnviadorDeEmail email;
private final NotaFiscalDao dao;
// ...
}
Uma classe com muitas dependências fica frágil: uma mudança em qualquer uma delas (um método renomeado, uma assinatura alterada) se propaga para a classe principal. Quanto mais dependências, maior a área exposta a problemas alheios — e mais difícil reutilizar essa classe em outro lugar sem levar junto toda a árvore de dependências dela.
Definição: Estabilidade de uma classe/interface
Uma classe (ou interface) é estável quando muda com pouca frequência — como
List do Java: sua interface é usada por praticamente todo sistema Java, então
qualquer mudança nela teria um impacto gigantesco, o que faz a equipe responsável
evitar mexer nela ao máximo. Acoplar-se com algo estável é um acoplamento bom: a
chance de aquela mudança se propagar para a sua classe é baixa. O problema não é o
acoplamento em si — é acoplar-se com algo instável.
Interfaces tendem a ser mais estáveis que implementações concretas, porque só definem um contrato (não têm código próprio que possa quebrar) e, para não forçar todas as suas implementações a mudar junto, tendem a ser pensadas com cuidado antes de alterar. Isso leva ao princípio:
Definição: DIP — Dependency Inversion Principle
Módulos de alto nível não devem depender de módulos de baixo nível — ambos devem depender de abstrações. E abstrações não devem depender de detalhes — detalhes devem depender de abstrações. Na prática: quando uma classe precisa depender de outra, prefira depender de uma interface estável a depender diretamente de uma implementação instável. "Inversão" porque, tradicionalmente, o código de alto nível chamaria direto a implementação de baixo nível — aqui, os dois passam a depender de uma abstração no meio, e é essa abstração que "aponta" para qual implementação será usada (frequentemente via injeção de dependência, que é a técnica mais comum de aplicar o DIP, não o princípio em si).
Definição: Nem toda classe pode/deve ser estável
Não é possível (nem desejável) que todas as classes do sistema sejam estáveis — se fossem, o sistema não poderia evoluir. O equilíbrio é: módulos estáveis (poucas dependências alheias, mudam raramente — tipicamente abstrações/interfaces) convivem com módulos instáveis (implementações concretas, que absorvem a maior parte das mudanças). O objetivo do DIP é garantir que a direção da dependência sempre vá do instável para o estável, nunca o contrário.
Um exemplo prático ajuda a fixar as duas premissas do DIP. Considere um serviço que consulta eventos através de um repositório JPA, ambos no mesmo pacote:
public class EventsService {
private final EventsJPARepository repository;
public EventsService() {
this.repository = new EventsJPARepository();
}
public List getEvents() {
return repository.getEvents();
}
}
public class EventsJPARepository {
public List getEvents() {
/* obtém os eventos no banco */
}
}
O princípio é ferido nas duas premissas: EventsService (que deveria ser um módulo de
alto nível — regra de negócio) depende diretamente de EventsJPARepository, um
detalhe de baixo nível (acesso a banco, framework). Uma versão que respeita o DIP
separa as duas coisas em pacotes diferentes, ligados por uma interface:
// pacote de alto nível (service) — regras de negócio da aplicação
public interface EventsRepository {
List getEvents();
}
public class EventsService {
private final EventsRepository repository;
// a implementação concreta é injetada em tempo de execução
public EventsService(EventsRepository repository) {
this.repository = repository;
}
public List getEvents() {
return repository.getEvents();
}
}
// pacote de baixo nível (dao) — detalhe de implementação
public class EventsJPARepository implements EventsRepository {
public List getEvents() {
/* obtém os eventos no banco */
}
}
classDiagram
class EventsService {
%% pacote de alto nível (service)
+getEvents()
}
class EventsRepository {
<<interface>>
%% pacote de alto nível (service)
+getEvents()
}
class EventsJPARepository {
%% pacote de baixo nível (dao)
+getEvents()
}
EventsService ..> EventsRepository : usa
EventsRepository <|.. EventsJPARepository : implementa
Tanto EventsService quanto EventsJPARepository passam a depender da mesma abstração
(EventsRepository) — nenhum dos dois depende diretamente do outro. Trocar o
repositório JPA por uma API externa ou por um banco NoSQL não exige modificar
EventsService: basta uma implementação nova de EventsRepository, injetada no lugar
da antiga. Essa separação entre pacote de "alto nível" (regras de negócio) e "baixo
nível" (frameworks, banco, detalhes técnicos) é a mesma ideia por trás dos padrões
arquiteturais de camadas, vistos em
Padrões Arquiteturais.
Nem todo acoplamento evitável precisa de uma interface nova — às vezes o problema real é responsabilidade demais numa única classe (de volta ao SRP), não falta de abstração. Uma classe que coordena 4 dependências distintas fazendo 4 coisas diferentes pode, em vez de ganhar uma interface para cada uma, ser quebrada extraindo um pedaço da lógica (e algumas das dependências) para uma classe nova — reduzindo tanto o acoplamento quanto aumentando a coesão de ambas ao mesmo tempo.
Definição: Acoplamento lógico
Acoplamento que não aparece no código-fonte (não é um import, nem um atributo
de outro tipo) — duas partes do sistema que precisam mudar juntas por uma
convenção externa, não por uma referência direta. Exemplo clássico: um método
lista() de um controller que precisa bater exatamente com o nome de um arquivo de
view (lista.html) — mudar o nome de um exige mudar o do outro, mesmo sem nenhuma
linha de código conectando os dois diretamente. É mais perigoso que o acoplamento
estrutural (visível num import) justamente por ser invisível a uma leitura rápida
do código.
OCP — Open-Closed Principle¶
Um sinal clássico de classe que precisa evoluir mal: uma regra de negócio com várias
variações, todas resolvidas com if/else encadeados sobre um código de regra:
public class CalculadoraDePrecos {
public double calcula(Compra produto) {
Frete correios = new Frete();
double desconto;
if (REGRA_1) {
desconto = new TabelaDePrecoPadrao().descontoPara(produto.getValor());
}
if (REGRA_2) {
desconto = new TabelaDePrecoDiferenciada().descontoPara(produto.getValor());
}
double frete = correios.para(produto.getCidade());
return produto.getValor() * (1 - desconto) + frete;
}
}
Toda regra nova é mais um if — a classe nunca para de crescer, e cada mudança exige
reabrir e editar um código que já funcionava (com todo o risco de quebrar algo que
já estava certo).
Definição: OCP — Open-Closed Principle
Uma classe deve ser aberta para extensão, mas fechada para modificação. "Aberta para extensão" significa que é fácil fazer a classe se comportar de um jeito novo; "fechada para modificação" significa que, para isso, não é preciso alterar o código já existente dela. Uma regra nova deve ser resolvida criando código (uma implementação nova de uma abstração já existente), nunca editando o código de quem já funcionava.
A técnica para conseguir isso é a mesma do DIP: extrair cada variação para trás de uma interface, e receber a implementação concreta pelo construtor em vez de instanciá-la dentro do método:
public interface TabelaDePreco {
double descontoPara(double valor);
}
public interface ServicoDeEntrega {
double para(String cidade);
}
public class CalculadoraDePrecos {
private TabelaDePreco tabela;
private ServicoDeEntrega entrega;
public CalculadoraDePrecos(TabelaDePreco tabela, ServicoDeEntrega entrega) {
this.tabela = tabela;
this.entrega = entrega;
}
// nenhum "new" aqui dentro — só uso das dependências recebidas
public double calcula(Compra produto) {
double desconto = tabela.descontoPara(produto.getValor());
double frete = entrega.para(produto.getCidade());
return produto.getValor() * (1 - desconto) + frete;
}
}
Uma regra de desconto nova vira só uma nova classe implementando TabelaDePreco — a
classe CalculadoraDePrecos nunca mais precisa ser reaberta.
Definição: Receber dependências pelo construtor, não instanciá-las
Sempre que uma classe instancia (new) diretamente outra classe concreta dentro
dela, perde-se a oportunidade de trocar essa implementação em tempo de execução — a
classe fica "fechada" para a variação que ela mesma precisaria permitir. Passar a
implementação como parâmetro do construtor é o jeito mais simples de deixar essa
decisão para quem usa a classe, em vez de travá-la no código-fonte.
Definição: A testabilidade é consequência do OCP/DIP, não coincidência
Uma classe que recebe dependências pelo construtor pode ser testada substituindo essas dependências por mocks (objetos falsos que simulam o comportamento real, sem executar a implementação de verdade) — o teste então isola o comportamento da classe sob teste, sem depender de banco de dados, rede ou qualquer infraestrutura real. Uma classe difícil de testar é, geralmente, sintoma de uma classe mal projetada (acoplada demais, ou instanciando concretamente o que deveria receber por fora) — não um problema separado de design.
Nem todo if é um problema de OCP: flexibilizar demais tem custo (mais abstrações, mais
indireção, mais complexidade para quem lê o código depois). Vale a pena criar uma
abstração quando a regra realmente varia com frequência — um if simples, isolado,
resolvendo algo que raramente muda, pode ser a solução mais simples e correta.
Definição: Pense em abstrações antes de implementação
Ao projetar uma classe nova, a pergunta "qual é a abstração certa aqui?" deve vir
antes da implementação — o oposto do hábito de programação procedural, onde a
preocupação central é só "como fazer funcionar". Um exercício mental simples: ao
modelar uma entidade nova, pergunte que tipo mais geral ela representa (uma
MultipleChoiceExercise é, antes de tudo, um Exercise; um Cachorro é, antes de
tudo, um Animal) — pensar nessa hierarquia antes de escrever código ajuda a decidir
onde a extensão via polimorfismo deve entrar no lugar de um if que testa o tipo
concreto.
SOLID: resumo rápido¶
| Princípio | O que diz | Benefício prático |
|---|---|---|
| SRP | Uma classe deve ter uma única responsabilidade | Facilita manutenção e testes |
| OCP | Aberto para extensão, fechado para modificação | Permite adicionar funcionalidades sem mexer no que já funciona |
| LSP | Subtipos devem ser substituíveis por seus tipos base | Evita erros inesperados com herança |
| ISP | Interfaces específicas ao cliente | Reduz implementação desnecessária |
| DIP | Dependa de abstrações, não de implementações | Favorece flexibilidade e testes |
Críticas e limites do SOLID¶
Os princípios são orientações, não leis, e têm críticos sérios (David Copeland, Dan North, Kevlin Henney, Ted Kaminski). Conhecer os contrapontos é o que diferencia quem aplica de quem decora:
| Princípio | Crítica comum | Equilíbrio |
|---|---|---|
| SRP | "Uma responsabilidade" é vago e subjetivo; levado ao extremo gera dezenas de classes minúsculas e difíceis de acompanhar (Dan North o chamou de "princípio inutilmente vago") | Use "uma razão para mudar" e a coesão como guia, não a contagem de métodos |
| DIP | Interfaces com uma única implementação aumentam a indireção sem benefício, e abstrações criadas "por via das dúvidas" mais atrapalham que ajudam | Abstraia as fronteiras (infraestrutura, bibliotecas voláteis, regras de negócio x detalhes) e não tudo |
| OCP | Prever os pontos de extensão é impossível; flexibilidade demais complica; extensibilidade importa nas bordas (APIs públicas, plugins), não em cada classe | Refatore para abrir o ponto quando a segunda variação aparecer (regra dos três) |
| LSP | O artigo original é confuso (exemplo do quadrado/retângulo); o essencial é o comportamento do subtipo respeitar o contrato do supertipo (Liskov, 1988) | Prefira composição a herança quando o contrato não se mantém |
| ISP | Interfaces pequenas demais aumentam o número de tipos; o ganho é separar clientes com necessidades diferentes | Segregue por cliente, não por método |
Dois eixos de OO
Há duas leituras complementares de "para que serve Orientação a Objetos": alinhar o código à linguagem do negócio (modelos de domínio e contextos delimitados — DDD) e gerenciar dependências (SOLID e princípios de módulos, abaixo). Use as ideias do DDD para o núcleo e os princípios de dependência para isolar esse núcleo. Priorizar flexibilidade além do necessário também é um custo.
OCP extremo: plugins e a Service Loader API¶
Quando o código que vai estender o sistema ainda não existe (outras equipes, outras empresas), o OCP vira arquitetura de plugins: o sistema define uma interface de extensão (SPI, Service Provider Interface, como AoRenderizarHTML
ou GeradorDeTema) e descobre implementações em tempo de execução, sem recompilar. Em Java, isso é a ServiceLoader (Java 6+):
- O núcleo declara a interface do plugin.
- Cada plugin (um JAR separado) a implementa e se registra em
META-INF/services/<nome da interface>(ou, com módulos, emprovides ... with ...). - O núcleo faz
ServiceLoader.load(Interface.class)e itera pelos providers encontrados.
É o mecanismo por trás dos drivers JDBC (que antes exigiam Class.forName), de provedores de logging e de partes do próprio JDK. Cuidados: o plugin deve enxergar só a interface de leitura
do domínio (ISP: EbookSoParaLeitura, sem setters), a SPI vira contrato público (mudanças quebram plugins) e as dependências do plugin precisam estar no classpath (ou use fat JAR).
Princípios de módulos (coesão e acoplamento)¶
Robert Martin estendeu o SOLID para a organização de módulos (aqui, entregáveis como JARs/DLLs; nos textos antigos, "pacotes"):
| Princípio | Pergunta que responde | Tendência |
|---|---|---|
| REP (Release Reuse Equivalence) | A granularidade de reúso é a de entrega: o que se reutiliza junto deve ser versionado e publicado junto | Módulos maiores |
| CCP (Common Closure) | Reúna as classes que mudam pelo mesmo motivo (SRP para módulos) | Módulos maiores |
| CRP (Common Reuse) | Quem usa um módulo deve usar todas as suas classes; não force dependência do que não usa (ISP para módulos) | Módulos menores |
| ADP (Acyclic Dependencies) | O grafo de dependências não pode ter ciclos | |
| SDP (Stable Dependencies) | Dependa na direção da estabilidade: módulos voláteis dependem de estáveis | |
| SAP (Stable Abstractions) | Um módulo estável deve ser abstrato (para poder ser estendido) |
REP e CCP puxam para módulos grandes; o CRP, para pequenos. Não há resposta única: depende do contexto, e a decisão deve ser reavaliada com o projeto. Um módulo por pacote atende ao CCP, mas gera muitos artefatos para versionar (REP) e arrasta dependências transitivas desnecessárias (CRP); apenas dois módulos (interface e núcleo) simplificam a entrega mas aumentam os motivos de mudança do núcleo. Quebrar ciclos (ADP) costuma exigir inverter a dependência (uma interface no módulo que usa, implementada pelo outro).
Métricas de estabilidade: acoplamento aferente (Ca, quem depende de mim) e eferente (Ce, de quem dependo); instabilidade \(I = \dfrac{C_e}{C_a + C_e}\) (0 = estável, 1 = instável) — veja métricas.
Módulos em Java: Maven e JPMS¶
- Módulos Maven (e Gradle): cada módulo é um artefato com suas dependências declaradas; a granularidade e a direção das dependências são a materialização dos princípios acima.
- JPMS (Java Platform Module System, Java 9+): um
module-info.javadeclara o que o módulorequires(dependências), o queexports(pacotes públicos),opens(para reflexão), e os serviços queuses/provides. Ganho: encapsulamento forte (um pacote não exportado é inacessível mesmo sendopublic; o classpath "plano" não tinha isso), verificação de dependências na compilação e na inicialização, e imagens de runtime menores (jlink). Jars semmodule-infoviram módulos automáticos no modulepath; oServiceLoaderfunciona comprovides ... with.
Imutabilidade, Builder e Iterator no domínio¶
Para um modelo de domínio previsível: prefira objetos imutáveis (sem setters, campos final) — record do Java 16+ elimina o código repetitivo; crie-os com Builder quando há muitos campos opcionais (Builder);
exponha coleções por Iterator/visões somente leitura em vez de listas mutáveis; e mantenha o encapsulamento (a seção abaixo).
Hexágonos, plugins e barreiras arquiteturais¶
A Arquitetura Hexagonal (conectores e adaptadores) e a Clean Architecture são o OCP/DIP em escala: o núcleo (regras de negócio) não depende de detalhes (UI, banco, formatos); adaptadores os conectam por portas (interfaces), e plugins estendem por SPIs. Para impedir que a arquitetura erode, use barreiras arquiteturais automáticas: módulos separados (um módulo que não declara a dependência não pode importá-la) e testes de arquitetura (ArchUnit), veja Arquiteturas de código.
Encapsulamento e a propagação de mudanças¶
O conceito de encapsulamento já foi apresentado — esconder como uma classe faz seu trabalho, expondo só o quê ela faz. Esta seção aprofunda por que isso importa tanto: encapsulamento malfeito é a razão pela qual uma mesma regra de negócio acaba espalhada por vários lugares diferentes do sistema, obrigando a repetir a mesma mudança em vários pontos toda vez que ela muda.
public class ProcessadorDeBoletos {
public void processa(List<Boleto> boletos, Fatura fatura) {
double total = 0;
for (Boleto boleto : boletos) {
fatura.getPagamentos().add(new Pagamento(boleto.getValor(), MeioDePagamento.BOLETO));
total += boleto.getValor();
}
if (total >= fatura.getValor()) {
fatura.setPago(true); // regra de "quando a fatura está paga" vazando pra cá
}
}
}
A regra "a fatura está paga quando a soma dos pagamentos cobre o valor dela" é uma regra
sobre Fatura — mas está implementada fora dela, em ProcessadorDeBoletos. No dia
em que surgir um ProcessadorDeCartaoDeCredito, essa mesma regra terá que ser copiada lá
também; e se ela mudar, será preciso lembrar de todos os lugares que a duplicam.
Definição: Intimidade inapropriada
Quando uma classe entende demais sobre o funcionamento interno de outra — lendo
seus dados e decidindo, por fora, algo que deveria ser decisão da própria classe
dona do dado. Ex.: calcular o imposto de uma NotaFiscal fora dela, olhando seus
atributos, em vez de perguntar à própria NotaFiscal (nf.calculaValorImposto()).
A solução é sempre mover o comportamento para dentro da classe que tem o dado.
Definição: Um sistema OO é um quebra-cabeça
Cada classe é uma peça; a interface dela (o conjunto de métodos públicos) é o formato do encaixe, e a implementação por trás é o desenho interno da peça — invisível para quem encaixa as peças ao redor. Trocar a implementação de uma peça, mantendo o mesmo encaixe, não afeta o resto do quebra-cabeça. É por isso que uma interface pública clara e estável importa mais, no longo prazo, do que qualquer detalhe de implementação por trás dela.
Tell, don't ask¶
Definição: Tell, don't ask (\"diga, não pergunte\")
Princípio que orienta a ordem de um código orientado a objetos: em vez de
perguntar o estado de um objeto para então decidir algo por fora dele (if
(nf.getValorSemImposto() > 10000) { ... }), diga ao objeto para fazer a decisão
e a ação sozinho (nf.calculaValorImposto()). Código que primeiro pergunta o estado
de um objeto para depois decidir tende a ser procedural — é o objeto quem deveria
tomar essa decisão, escondendo (encapsulando) o if dentro de si.
A Lei de Demeter¶
Encadear chamadas através de vários objetos (fatura.getCliente().marcaComoInadimplente())
parece inofensivo, mas cria um tipo sutil de acoplamento:
public void algumMetodo() {
Fatura fatura = pegaFaturaDeAlgumLugar();
fatura.getCliente().marcaComoInadimplente();
}
Definição: Lei de Demeter
Regra que recomenda evitar cadeias de chamadas (a.getB().getC().metodo()) —
"fale só com seus amigos diretos, não com os amigos dos seus amigos". Se Cliente
mudar sua interface pública, todo código que faz getCliente().algumMetodo() quebra
— o código depende de Fatura diretamente, e de Cliente indiretamente, um
acoplamento difícil de enxergar de relance. A solução é a própria classe do meio
(Fatura) esconder esse repasse: um método fatura.marcaClienteComoInadimplente()
que, por dentro, delega para cliente.marcaComoInadimplente() — encapsulando o
caminho até Cliente, e reduzindo a mudança a um único lugar caso Cliente mude.
Definição: A Lei de Demeter não é absoluta
Seguir a Lei de Demeter à risca em toda cadeia (inclusive getters simples, como
exibir um dado numa tela: fatura.getCliente().getEndereco().getRua()) é exagero —
o problema real é encadear chamadas que fazem alguma coisa (mudam estado), não
ler um dado para exibição. Tenha a regra na cabeça e use bom senso para decidir
quando ela realmente evita um acoplamento problemático.
O risco de getters e setters genéricos¶
Criar um getX()/setX() para todo atributo, por hábito, sem que exista uma
necessidade real, tende a furar o encapsulamento aos poucos — um setSaldo(double)
"genérico" numa conta permite que qualquer classe cliente atribua qualquer valor ao
saldo, ignorando qualquer regra de negócio que deveria valer para essa mudança.
Definição: Prefira comportamentos a setters genéricos
Em vez de um setSaldo(valor) que aceita qualquer coisa, prefira métodos que
representem uma ação de negócio específica — saca(valor), deposita(valor) —
cada um aplicando as regras que fazem sentido para aquela ação (ex.: não permitir
saldo negativo). Um getter é geralmente menos arriscado que um setter (só devolve
informação), mas também merece cuidado quando devolve uma referência mutável
internamente — getPagamentos() devolvendo a List interna da classe permite que
quem chamou insira itens diretamente nela, por fora de qualquer regra da classe dona.
Devolver uma cópia, ou uma view somente-leitura
(Collections.unmodifiableList(pagamentos) em Java), evita esse vazamento.
Modelos anêmicos¶
Definição: Modelo anêmico
Uma classe que só tem atributos e getters/setters, sem nenhum método de
comportamento/regra de negócio — todas as regras que deveriam estar nela vivem em
outra classe (frequentemente sufixada BLL, Service, Delegate), que lê e escreve
os atributos por fora. É código procedural disfarçado de orientado a objetos: a
classe "modelo" é só uma estrutura de dados; quem tem o comportamento é outra coisa
inteiramente.
Modelo anêmico não é, por si só, sempre um erro — para uma aplicação realmente simples (um cadastro fino, sem regra de negócio nenhuma), pode ser a solução mais direta. O problema é quando ele acontece o tempo todo, em todo o sistema, por hábito — nesse caso, é sinal de que o time está programando de forma procedural dentro de uma linguagem OO, perdendo a chance de encapsular regras onde elas pertencem.
Herança x composição¶
A herança já foi apresentada no lado da sintaxe (override, retorno covariante, tipo da referência). Falta o lado do design: herança é a ferramenta mais fácil de usar mal em OO, porque parece resolver reúso de código, mas na prática cria um dos acoplamentos mais fortes que existem entre duas classes.
public class ContaComum {
protected double saldo;
public void deposita(double valor) { this.saldo += valor; }
public void rende() { this.saldo *= 1.1; }
}
public class ContaDeEstudante extends ContaComum {
@Override
public void rende() {
throw new ContaNaoRendeException(); // conta de estudante não rende
}
}
Essa sobrescrita parece inofensiva isoladamente, mas quebra qualquer código que trata
contas polimorficamente esperando que rende() sempre funcione sem lançar exceção — o
contrato implícito definido pela classe pai deixou de valer para a classe filha.
LSP — Liskov Substitution Principle¶
Definição: Pré-condição e pós-condição
Pré-condição é o que precisa ser verdade antes de um método rodar
corretamente (ex.: deposita(valor) exige valor > 0). Pós-condição é o que o
método garante depois de rodar (ex.: rende() sempre atualiza o saldo, nunca
lança exceção). Toda classe/método tem pré e pós-condições, explícitas ou não.
Definição: LSP — Liskov Substitution Principle
Uma subclasse deve poder substituir sua superclasse em qualquer lugar do
código, sem quebrar o comportamento esperado por quem usa a referência do tipo pai.
Formalizado em duas regras sobre o contrato herdado: a subclasse pode afrouxar
uma pré-condição (aceitar mais do que o pai exigia), mas nunca torná-la mais
restritiva; e pode apertar uma pós-condição (garantir mais do que o pai
garantia), mas nunca entregar menos do que o pai prometia. ContaDeEstudante viola
o LSP porque sua pós-condição (pode lançar exceção) é mais fraca que a da classe
pai (nunca lança).
O exemplo mais clássico do LSP é a dupla Quadrado/Retângulo: um quadrado é,
matematicamente, um caso particular de retângulo (lados iguais), o que tenta várias
vezes justificar Quadrado extends Retangulo. Mas a pré-condição de Quadrado (os dois
lados sempre iguais) é mais forte que a de Retangulo (lados independentes) —
qualquer código cliente que dependa de alterar só um lado de um Retangulo quebra ao
receber, por polimorfismo, um Quadrado.
Outro exemplo igualmente comum, desta vez visível em tempo de execução (não só em teoria): uma hierarquia de animais em que nem todo animal sabe voar.
abstract class Animal {
abstract void walk();
abstract void run();
abstract void eat();
abstract void fly();
}
class Bird extends Animal {
void walk() { /* ... */ }
void run() { /* ... */ }
void eat() { /* ... */ }
void fly() { /* ... */ }
}
class Dog extends Animal {
void walk() { /* ... */ }
void run() { /* ... */ }
void eat() { /* ... */ }
void fly() {
throw new RuntimeException("Cães não podem voar");
}
}
Animal a = new Bird();
a.fly(); // funciona
a = new Dog();
a.fly(); // RuntimeException: Cães não podem voar
O erro só aparece em tempo de execução, dependendo de qual subclasse foi atribuída à
referência Animal — exatamente o tipo de surpresa que o LSP existe para prevenir. A
correção não é tratar a exceção com try/catch: é reconhecer que fly() nunca deveria
estar na superclasse Animal, já que nem todo animal cumpre esse comportamento.
abstract class Animal {
abstract void walk();
abstract void run();
abstract void eat();
}
interface FlyableAnimal {
void fly();
}
class Bird extends Animal implements FlyableAnimal {
// implementação de walk, run, eat e fly
}
class Dog extends Animal {
// implementação de walk, run e eat — sem fly()
}
Com fly() extraído para uma interface separada, Dog simplesmente não a implementa —
não sobra nenhum método órfão lançando exceção, e o compilador (não mais uma exceção em
produção) impede tentar chamar fly() numa referência do tipo Animal ou Dog.
Definição: LSP em interfaces de acesso a dados
O mesmo problema aparece com frequência fora de hierarquias de animais — por
exemplo, uma interface Sensor com readValue(), da qual deriva um
OfflineSensor que lança exceção ao tentar ler (porque, por definição, um sensor
offline não tem valor para entregar). A correção segue a mesma lógica: extrair
readValue() para uma interface própria (ReadableSensor), implementada só pelos
sensores que realmente sabem responder a ela.
Acoplamento entre classe pai e classe filha¶
Herança acopla a classe filha aos detalhes de implementação da classe pai, não só à
sua interface pública — um problema visível, por exemplo, ao sobrescrever um método de
uma HttpServlet (API do Java para aplicações web):
public class MinhaServlet extends HttpServlet {
@Override
public void service(HttpServletRequest req, HttpServletResponse res) {
// se esquecer de chamar o pai, a servlet não funcionará
super.service(req, res);
}
}
Sem olhar o código-fonte (ou a documentação) da classe pai, não há como o desenvolvedor
saber que esquecer de chamar super.service() (ou super.init(), noutro método)
quebra silenciosamente o comportamento esperado. Modelar hierarquias em que a classe
filha precisa conhecer pouco (ou nada) dos detalhes internos do pai reduz esse risco —
entre outras táticas, evitar protected (que expõe atributos internos à classe filha) e
preferir que a filha só dependa da parte pública e documentada do contrato do pai.
Favoreça a composição¶
Definição: Composição
Montar uma classe usando outra classe como atributo, em vez de herdar dela —
"X tem um Y" (Carro tem um Motor), em oposição a "X é um Y" (Gerente é um
Funcionário), que é a relação que justifica herança.
class ManipuladorDeSaldo {
private double saldo;
public void adiciona(double valor) { /* ... */ }
public void retira(double valor) { /* ... */ }
public void juros(double taxa) { /* ... */ }
}
class ContaComum {
private ManipuladorDeSaldo manipulador = new ManipuladorDeSaldo();
public void saca(double valor) { manipulador.retira(valor); }
public void rende() { manipulador.juros(0.1); }
}
class ContaDeEstudante {
private ManipuladorDeSaldo manipulador = new ManipuladorDeSaldo();
public void saca(double valor) { manipulador.retira(valor); }
// sem "rende" — nem precisa fingir que tem, nem lançar exceção pra dizer que não tem
}
A composição resolve o problema do exemplo de ContaDeEstudante de outra forma: em vez
de herdar um comportamento que não se aplica e sobrescrevê-lo para lançar exceção, a
classe simplesmente não tem aquele método. A relação entre a classe principal e a
dependida também é mais fraca que a relação pai-filho — quebrar o encapsulamento da
classe usada fica mais difícil, e trocar sua implementação (por outra que cumpra o mesmo
papel) é mais simples. É por isso que a maioria dos padrões de projeto do GoF usa
composição, não herança, para ganhar flexibilidade — inclusive Comparator (múltiplas
implementações plugáveis) é um exemplo do mesmo princípio já visto no Collections
Framework
(Comparable/compareTo).
Definição: Quando usar herança, então?
Herança faz sentido quando a relação é genuinamente "X é um Y", não "X tem um
Y" ou "X faz uso de Y" — Gerente é um Funcionário, mas uma calculadora de
imposto não "é" Matemática, só usa funções dela (nesse caso, composição). Um
bom conjunto de classes usando herança corretamente evita que a classe filha conheça
detalhes de implementação do pai, e respeita as restrições de pré/pós-condição
(LSP) ao sobrescrever um método.
Uma exceção prática vale registrar: herança também é aceitável quando usada para dar
legibilidade/fluência a código de teste ou DSLs internas (ex.: uma classe de teste
que herda de uma classe base só para reaproveitar métodos auxiliares como
preenche(campo, valor)), mesmo sem uma relação "é um" tão clara — nesse caso, o ganho
de legibilidade compensa o acoplamento, desde que consciente.
Pacotes: como usá-los?¶
Classes num mesmo pacote devem ser relacionadas e reutilizadas juntas — pacotes
existem para agrupar por proximidade de responsabilidade, não por conveniência. Duas
regras práticas: evite ciclos entre pacotes (se o pacote a depende de b, b não
deve depender de a); e use subpacotes para separar partes de um mesmo módulo maior
sem perder a possibilidade de tratar o pacote pai como um todo único de fora.
Interfaces magras¶
Coesão não vale só para classes — uma interface também pode acumular responsabilidades demais, virando difícil de implementar bem:
interface Imposto {
NotaFiscal geraNota();
double imposto(double valorCheio);
}
class IXMX implements Imposto {
public double imposto(double valorCheio) { return 0.2 * valorCheio; }
public NotaFiscal geraNota() {
// esse imposto não emite nota fiscal — o que fazer aqui?
throw new NaoGeraNotaException(); // ou: return null;
}
}
Definição: Interface \"gorda\" (fat interface)
Uma interface com mais de uma responsabilidade — mistura, no mesmo contrato,
coisas que nem toda implementação precisa cumprir de verdade. Uma classe forçada a
implementar um método que não faz sentido para ela (lançando exceção, ou devolvendo
null) é sintoma direto de uma interface gorda — o mesmo problema de falta de
coesão do SRP, só que no nível da interface em vez da classe.
ISP — Interface Segregation Principle¶
Definição: ISP — Interface Segregation Principle
Nenhum cliente deveria ser forçado a depender de métodos que não usa. Na prática: prefira várias interfaces pequenas e coesas (cada uma com uma única responsabilidade) a uma única interface grande cobrindo várias responsabilidades diferentes — quebrando a interface gorda acima em duas:
Cada classe implementa só as interfaces que fazem sentido para ela — sem gambiarras para "cumprir" um método que não se aplica.Interfaces coesas trazem o mesmo ganho já visto no DIP: sendo mais simples, elas tendem a ser mais estáveis — poucas razões para mudar significa pouca chance de propagar mudança para quem depende delas.
Pensando na interface mais magra possível¶
O ISP também se aplica a parâmetros de método: em vez de receber um objeto inteiro (com muitos atributos e métodos, a maioria irrelevantes para aquele método específico), receba só a abstração mínima de que o método realmente precisa.
// Antes: acopla ao objeto NotaFiscal inteiro (cliente, itens, descontos, endereço, ...)
class CalculadorDeImposto {
public double calcula(NotaFiscal nf) { /* usa só nf.getItens() */ }
}
// Depois: acopla só ao que é usado de fato
interface Tributavel {
List<Item> itensASeremTributados();
}
class NotaFiscal implements Tributavel { /* ... */ }
class CalculadorDeImposto {
public double calcula(Tributavel t) { /* ... */ }
}
A interface Tributavel é muito mais estável que a classe NotaFiscal inteira
(objeto grande, com muitos atributos e métodos que podem mudar) — e dá semântica ao
parâmetro (o método precisa de algo "tributável", não de qualquer List ou double
soltos). Isso reduz o acoplamento do cliente e reforça a ideia central do ISP: depender
só do que realmente se usa.
Definição: Repositório (DDD)
Termo do Domain-Driven Design (Eric Evans) para uma abstração de acesso a dados
mais próxima da linguagem do domínio (RepositorioDeFaturas, com todas() e
salva(f)) do que um DAO tradicional. Assim como qualquer interface, só vale a pena
criá-la quando existe uma necessidade real de trocar a forma de acesso aos dados —
DAOs concretos já tendem a ser estáveis por natureza (raramente se troca de
framework de persistência), então a interface pode ser dispensável se essa
flexibilidade nunca for exercida na prática.
Fábricas ou injeção de dependência?¶
Se as classes devem receber dependências pelo construtor (ver OCP/DIP), alguém, em algum
lugar, ainda precisa instanciá-las com new. Duas soluções comuns:
Definição: Fábrica (Factory, padrão GoF)
Uma classe cuja única responsabilidade é criar outra classe, escondendo os
detalhes de construção (new, e quais dependências passar) de quem só precisa do
objeto pronto. Diferente de um framework de injeção de dependência, uma fábrica é
só código Java comum — solução simples, sem dependência externa nenhuma, mas escrita
e mantida manualmente.
Definição: Injeção de dependência (framework) x Fábrica
Um framework de injeção de dependência (Spring, Guice, CDI, ...) resolve o mesmo
problema de forma automática: a árvore inteira de dependências (A depende de B,
que depende de C) é resolvida pelo framework, sem o desenvolvedor escrever o código
de construção manualmente. O ganho é menos código repetitivo; o custo é uma peça de
infraestrutura a mais no projeto. Nenhuma das duas soluções é universalmente
"melhor" — a fábrica é suficiente para sistemas simples ou sem outros frameworks já
presentes; DI compensa em sistemas maiores, que já usam outras bibliotecas com
suporte nativo a ela.
Definição: Uma fábrica pode (e deve) ser acoplada
Uma fábrica naturalmente conhece — e depende de — todas as classes concretas que ela sabe construir. Isso não é um problema de design: fábricas tendem a ser classes estáveis (só quebram se a forma de construir a classe principal mudar), não contêm regra de negócio, e sua responsabilidade (montar objetos) é clara para qualquer um que a leia. Acoplamento alto é um problema quando é acidental; numa fábrica, ele é a razão dela existir.
Consistência de objetos¶
Um conjunto de boas práticas mais contextuais (não amarradas a um princípio SOLID específico), sobre garantir que um objeto nunca exista num estado inválido.
Definição: Objeto em estado inválido
Um objeto cujos atributos têm valores que não deveriam ser aceitáveis juntos — um
Pedido sem cliente, um imposto de valor zero quando isso nunca deveria acontecer
no domínio. O problema prático de um objeto inválido é que ele quebra a confiança de
quem o usa: não dá para saber, olhando o tipo, se aquele objeto está "completo" ou
"pela metade".
Construtores ricos¶
A responsabilidade de garantir a integridade do próprio estado é do objeto, não de quem
o usa — e a ferramenta certa para isso é o construtor: se a classe tem atributos sem
os quais ela não pode existir de forma válida, eles devem ser exigidos já no construtor,
nunca deixados para um setter opcional depois:
class Pedido {
private Cliente cliente;
private double valorTotal;
private List<Item> itens;
public Pedido(Cliente cliente) {
this.cliente = cliente; // sem cliente, não existe Pedido
this.valorTotal = 0;
this.itens = new ArrayList<>();
}
}
Depois dessa mudança, é impossível criar um Pedido sem cliente — o próprio
compilador barra a tentativa. Quando um atributo tem um valor padrão razoável (ex.:
Carro sempre tem Pneu e Motor, mas aceita valores-padrão se não especificados),
uma sobrecarga de construtor resolve sem abrir mão da consistência:
class Carro {
private Pneu pneu;
private Motor motor;
public Carro(Pneu pneu, Motor motor) {
this.pneu = pneu;
this.motor = motor;
}
public Carro() {
this(new PneuPadrao(), new MotorPadrao());
}
}
Para objetos complexos demais para um construtor simples, os padrões de projeto Builder e Factory cumprem o mesmo papel (garantir que o objeto nasça em estado consistente), só que em etapas.
Definição: Frameworks que exigem construtor padrão
Alguns frameworks (o Hibernate, ORM bastante popular, é um exemplo) não lidam bem
com uma classe que só tem construtores ricos — exigem um construtor sem argumentos
por razões técnicas próprias (ex.: instanciar o objeto antes de popular os campos via
reflection). A solução prática é manter os construtores ricos como interface
principal, e acrescentar um construtor padrão com visibilidade reduzida, marcado
@Deprecated — sinalizando a quem lê o código que aquele constructor específico não
deveria ser usado diretamente.
Validando dados¶
Vale separar dois tipos de validação bem diferentes: validação de formato (o dado recebido é do tipo esperado — um "e-mail" parece um e-mail, uma "idade" é um número) e validação de negócio (regras específicas do domínio — um imposto precisa ser maior que 1%, um CPF precisa ser matematicamente válido).
Definição: Onde validar formato de dado
Validação de formato deve acontecer o quanto antes, na camada que recebe o dado externo (tipicamente o controller, numa aplicação web) — antes que um dado sujo (nulo, vazio, do tipo errado) chegue ao domínio. Esse tipo de código costuma ser verboso e repetitivo por natureza — não é sinal de má arquitetura, é o papel de uma camada adaptadora (arquitetura hexagonal): filtrar o que entra, deixando o domínio livre para assumir que só chegam dados já válidos em formato.
Validação de negócio é mais difícil de generalizar — depende de quão complexa é a regra. Uma abordagem comum é a própria entidade se responsabilizar pela validação no construtor, lançando exceção se os dados não passarem:
class CPF {
private String cpf;
public CPF(String possivelCpf) {
if (regrasOk(possivelCpf)) {
this.cpf = possivelCpf;
} else {
throw new IllegalArgumentException("CPF inválido");
}
}
}
Uma alternativa que evita forçar quem chama a tratar uma exceção é expor um método
valida() que só informa se o dado é válido, deixando a decisão de o que fazer com essa
informação para quem chama — ou, para validações mais complexas com múltiplos erros
possíveis, um builder dedicado (CPFBuilder) que devolve o objeto ou a lista de
erros encontrados, sem nunca deixar o CPF nascer inválido.
Teorema do bom vizinho¶
Definição: Teorema do bom vizinho
A ideia de que uma classe é "um bom vizinho" quando ela nunca passa dados inválidos
(em especial null) para outra classe — cada classe é responsável por tratar/validar
o dado antes de repassá-lo adiante, para que ninguém no sistema precise se
defender de null o tempo todo. Isso não vale para bibliotecas/frameworks de uso
genérico (onde não há controle sobre quem chama), mas é uma boa meta para código de
aplicação. Reduzir a necessidade de nulos, quando a linguagem oferece alternativa
(Optional, em Java, ou o uso de sobrecargas de método que não exigem todos os
parâmetros), evita boa parte dessa categoria inteira de bug.
Tiny Types¶
Definição: Tiny Type
Uma classe pequena e dedicada para representar um conceito que, de outra forma,
seria só uma String ou outro tipo primitivo "genérico demais" — um CPF, um
Email, um Telefone, em vez de todos representados por String. O ganho é a
própria assinatura de um método documentar o que ela espera (Aluno(Nome nome, Email
email) é mais claro que Aluno(String, String), e evita passar os parâmetros
trocados por engano) e cada tipo poder validar a si mesmo na criação. O custo é mais
classes no sistema — vale a pena quando o conceito é usado em muitos lugares
diferentes, não para um valor usado uma única vez.
DTOs do bem¶
Definição: DTO (Data Transfer Object)
Objeto usado só para transmitir dados entre camadas ou sistemas — geralmente sem comportamento, só atributos e getters/setters. DTOs ganharam má fama no passado por serem usados como substituto de classes de domínio inteiras (achatando todo o sistema em estruturas de dados sem comportamento) — mas isso é um problema de uso, não do padrão em si. Usados no lugar certo (representar exatamente o formato que uma tela precisa exibir, ou os dados que um serviço externo espera receber), DTOs aumentam a semântica do código (em vez de passar vários parâmetros soltos de tipos primitivos) sem contaminar as classes de domínio com preocupações de apresentação.
Imutabilidade x mutabilidade¶
Definição: Classe imutável
Uma classe cujo estado interno nunca muda depois de criada — não existem
setters; qualquer "alteração" devolve uma nova instância, com o novo valor,
deixando o objeto original intocado:
class Endereco {
private final String rua;
private final int numero;
public Endereco(String rua, int numero) {
this.rua = rua;
this.numero = numero;
}
public Endereco setRua(String novaRua) {
return new Endereco(novaRua, numero); // devolve objeto NOVO
}
}
LocalDate,
LocalDateTime, desde o Java 8) seguem esse padrão — diferente da antiga Calendar,
mutável, cujo add() altera a própria instância.
Imutabilidade não é bala de prata: nem todo conceito do mundo real é imutável (um saldo
de conta muda o tempo todo), e forçar imutabilidade onde ela não se aplica naturalmente
só adiciona código sem benefício. É uma boa candidata para valores que, no domínio, não
mudam de identidade quando um atributo muda — um endereço (a rua Rua Vergueiro sempre
foi e sempre será aquela rua) é um bom exemplo; um pedido ou um cliente, que representam
uma entidade cujo estado evolui ao longo do tempo, geralmente não são.
Classes que são feias por natureza, e nomenclatura¶
Nem todo código precisa (ou consegue) ser bonito. Controllers, adaptadores de
infraestrutura, e fábricas tendem a concentrar ifs de validação, verificações de
nulo e código repetitivo por natureza — são a "ponte" entre dois mundos diferentes
(web ↔ domínio, sistema ↔ infraestrutura externa), e essa ponte tende a ser feia mesmo
quando bem escrita. Isso não é um problema a resolver a qualquer custo: código feio
controlado e isolado numa camada adaptadora é aceitável, desde que o restante do
sistema (as classes de domínio, que mudam e evoluem com frequência) permaneça limpo.
Nomenclatura de métodos/variáveis não tem regra fixa (qtdItens() ou
quantidadeDeItens()? — depende do time), mas duas diretrizes práticas: siga uma
convenção consistente dentro da equipe (nomes de interface com I prefixado, ou
não — o que importa é escolher uma e manter), e evite os dois extremos de tamanho —
nomes tão curtos que não dizem nada (x), e tão longos que atrapalham a leitura.
Programar para produzir: pureza, complexidade e falhas¶
Código de produção é aquele que precisa funcionar sempre, mesmo com entradas estranhas, redes lentas e discos com defeito. Esta seção reúne os hábitos que separam um código "que roda na minha máquina" de um código confiável. A teoria de testes está em Qualidade; aqui o foco é como escrever o código para ele ser fácil de verificar e de manter.
Funções puras e impuras: separe para poder testar¶
Definição: efeito colateral e função pura
Efeito colateral é qualquer coisa que uma função altera ou lê fora das suas variáveis locais:
gravar em arquivo, enviar pela rede, mudar uma variável global, consultar o relógio ou um banco de dados.
Função pura é a que não tem efeito colateral e sempre devolve o mesmo resultado para os mesmos
argumentos (como fatorial(5)). Funções puras são as mais fáceis de testar.
A maioria dos programas mistura os dois tipos, e o problema é não perceber qual parte é qual. Esta função faz três coisas ao mesmo tempo — lê arquivo (impura), converte nota em conceito (pura) e grava em estrutura global (impura) — e, escrita assim, nenhuma das partes pode ser testada separadamente:
alunos = []
def importar_notas(caminho):
with open(caminho) as arquivo: # impura: I/O
for linha in arquivo:
nome, nota = linha.strip().split(",")
nota = int(nota)
if nota >= 90: conceito = "A" # pura: regra de negócio
elif nota >= 80: conceito = "B"
else: conceito = "C"
alunos.append((nome, conceito)) # impura: estado global
A solução é isolar a regra pura numa função própria e deixar a parte impura só com a "cola":
def nota_para_conceito(nota: int) -> str:
if not 0 <= nota <= 100:
raise ValueError(f"nota fora do intervalo: {nota}")
if nota >= 90: return "A"
if nota >= 80: return "B"
return "C"
# teste simples, sem arquivo, sem banco, sem mock
def test_conceito():
assert nota_para_conceito(100) == "A"
assert nota_para_conceito(85) == "B"
with pytest.raises(ValueError):
nota_para_conceito(-1)
Para a parte impura, use dublês de teste (mocks, veja Qualidade): o teste substitui o arquivo e a coleção por falsos e só verifica que a função certa foi chamada com os argumentos certos. Dois cuidados: o teste de unidade não pode sujar o estado do sistema (arquivos soltos, linhas no banco) e o dublê precisa ser desfeito ao final. Em linguagens sem mocks nativos, o mesmo efeito vem de injetar a dependência (passar o "abridor de arquivo" como parâmetro) — é a ideia de Injeção de Dependência e do D de SOLID.
O sistema de tipos e a documentação das expectativas¶
- Tipagem estática (Java, C#, Go) comunica e verifica a forma dos dados em tempo de compilação: se a
função recebe
longe devolvelong, o compilador recusa umaString. Tipagem dinâmica (Python, Ruby, JavaScript) só descobre em tempo de execução — por isso exige mais testes e anotações de tipo (type hints em Python, TypeScript em JavaScript). - Qualquer que seja a linguagem, documente as expectativas de cada parâmetro: faixa de valores, se aceita
null, unidade (segundos ou milissegundos?). O nome do parâmetro nem sempre diz isso. - Cobertura de código não é prova de qualidade: 100% de cobertura significa só que todas as linhas executaram, não que o resultado foi verificado. Um teste pode cobrir toda uma função e ainda deixar passar um vazamento de memória ou um erro de "um a menos" no tamanho de um buffer. Teste o que é razoável, entenda que casos como drivers e hardware são difíceis de simular e submeta o restante à revisão por pares.
Complexidade necessária x acidental¶
Definição: complexidade necessária e acidental
Complexidade necessária (ou essencial) é inerente ao problema: calcular imposto, tratar fusos horários e roteamento de pedidos é difícil por natureza. Complexidade acidental é a que criamos sem precisar: nomes confusos, abstrações demais, duplicação, soluções improvisadas. O trabalho do programador é reduzir a acidental e organizar a necessária.
Em sistemas que envolvem muita gente por muito tempo, a acidental tende a crescer mais rápido que a necessária, num ciclo que se retroalimenta (a "espiral da complexidade"):
flowchart LR
A[Código cresce] --> B[Ninguém entende tudo]
B --> C[Mais bugs]
C --> D[Pressão por correção rápida]
D --> E[Remendos que não tocam a causa-raiz]
E --> A
Três pontos para romper o ciclo:
- Tamanho de código é peso, não progresso. Linhas de código medem quanto o produto pesa, não quanto avançou (por analogia, medir o progresso de um avião por seu peso). Um produto rico em recursos terá muito código, mas deve ser o mais enxuto possível, porque cada linha extra precisa ser lida, testada e mantida.
- Corrija a causa-raiz, não o sintoma. Aumentar o timeout de uma fila "porque travou" quase sempre só desloca o problema para outro lugar. Use a técnica dos cinco porquês: pergunte "por quê?" até chegar a algo que pode ser eliminado de fato; depois escreva um teste que reproduza o defeito.
- Clareza de pensamento e de expressão. Isole cada assunto complexo (medição de tempo, formatos de data, protocolos) em um módulo próprio com uma interface pequena e testável. O código restante passa a ler como uma especificação do domínio, e não como uma mistura de regra de negócio com chamadas de baixo nível:
# antes: 20 linhas de manipulação de tempo misturadas à lógica
inicio = time.monotonic()
while True:
... # trabalho
if time.monotonic() - inicio > 0.5:
print("ainda trabalhando...")
inicio = time.monotonic()
# depois: o conceito "temporizador" vive numa classe testada separadamente
progresso = Temporizador(intervalo=0.5)
while True:
... # trabalho
if progresso.disparou():
print("ainda trabalhando...")
progresso.reiniciar()
Falhar graciosamente¶
Escrever código que falha bem é tão importante quanto escrever código que funciona. Disco cheio, rede fora, banco indisponível, memória esgotada: vão acontecer. Quando não der para se recuperar, o código deve ao menos não causar danos colaterais.
1. Ordem das operações. Monte o objeto novo por completo antes de mexer em qualquer estrutura compartilhada. Se uma consulta falhar no meio, o estado antigo continua íntegro:
def adicionar(self, cliente_id):
# ruim: se a 2ª consulta falhar, self._cabeca já aponta para um objeto pela metade
# bom: construir primeiro, publicar depois
novo = Cliente(
nome=self._banco.consultar(cliente_id, "nome"),
endereco=self._banco.consultar(cliente_id, "endereco"),
)
with self._trava:
novo.proximo = self._cabeca
self._cabeca = novo
2. Transações quando há mais de um objeto envolvido. Num débito seguido de crédito, se o crédito falhar não adianta "tentar devolver": a devolução também pode falhar. A ferramenta certa é a transação (tudo ou nada), como as de bancos de dados (SQL). O mesmo vale para alterações em múltiplos arquivos ou serviços (veja Saga em Microsserviços).
3. Liberar recursos sempre. Arquivos, conexões e travas precisam ser fechados em qualquer caminho de
saída: try/finally, with (Python), try-with-resources (Java), defer (Go), destrutores/RAII (C++).
Em C, o idioma goto para um rótulo de limpeza no final da função é uma convenção aceita (regra "um ponto
de saída" só vale quando serve a isso).
4. Injeção de falhas. Teste o caminho ruim de propósito: faça um dublê levantar exceção e verifique que o estado continuou consistente.
def test_falha_no_banco_nao_corrompe_a_lista(mocker):
banco = mocker.Mock()
banco.consultar.side_effect = ["Ana", RuntimeError("banco fora")] # 2ª chamada falha
lista = ListaClientes(banco)
with pytest.raises(RuntimeError):
lista.adicionar(1)
assert lista.tamanho() == 0 # nada foi publicado pela metade
5. Testes aleatórios (fuzzing). Para pegar o que ninguém imaginou, jogue entradas válidas aleatórias (ou eventos de tela, no caso de apps) contra o programa por horas. Ferramentas: Hypothesis (Python), jqwik (Java), fuzzers de cobertura (AFL, libFuzzer), Monkey (Android). Complementa — não substitui — os testes exemplares. A versão em produção disso é a engenharia do caos.
Estilo, nomes e comentários¶
O compilador ignora estilo, mas pessoas não. Siga o guia de estilo da equipe (ou o da comunidade da
linguagem: PEP 8, Google Java Style, gofmt), combine com o código ao redor — um arquivo com três estilos
é pior do que um com estilo ruim — e automatize com linters e formatadores
(Clean Code). Dois sinais úteis:
- Nome difícil de dar costuma indicar finalidade confusa: uma classe
GerenteInfonão diz o que faz. Prefira nomes de domínio (Cliente,Endereco) e métodos que soem naturais (cliente.nome). - Comentário que repete o código é ruído. O comentário bom explica o porquê (regra de negócio, decisão,
limitação externa), documenta APIs públicas (Javadoc, docstrings), usa marcadores
TODO/FIXMEcomo lembrete temporário e traz o cabeçalho de direitos autorais e licença quando a política da empresa exige.
Trabalhando com código legado¶
Definição: código legado
Código legado é o que já existe, está em produção e dá lucro, mas ninguém tem coragem de mexer — funções de milhares de linhas, classes que dependem de vinte outras, sem testes. Michael Feathers o define de forma curta: legado é código sem testes.
A tentação é jogar tudo fora e reescrever. Resista: a "grande reescrita" costuma levar o dobro do tempo, porque o código feio guarda conhecimento de negócio que ninguém lembrava (antipadrões). Em vez disso:
- Comece pequeno: escolha uma mudança minúscula, faça-a e observe o impacto.
- Cerque de testes antes de mexer, mesmo que sejam testes de caracterização (registram o comportamento atual, certo ou errado). É a parte mais difícil; sem ela, qualquer refatoração é um tiro no escuro (Refatoração).
- Encontre as costuras (seams): pontos naturais onde dá para separar o código sem editá-lo por dentro. Exemplo: em vez de trocar cem chamadas à API do sistema operacional, extraia o acesso a arquivos para um módulo próprio; isso isola a dependência e facilita uma futura migração.
- Migrações de plataforma: reaproveite o que funciona (chamar código C de outra linguagem via interface nativa, rodar o legado em contêiner) e troque por partes (padrão Strangler Fig), em vez de uma virada única.
- Cuidado ao "consertar bugs": nem todo comportamento estranho é um defeito. Em HTTP, o cabeçalho
Referercarrega um erro de grafia que existe desde a RFC 1945 e muitos servidores dependem dele; "corrigir" quebraria a internet. Antes de mudar, pergunte a quem conhece o histórico (veja mentoria).
Livros-referência: Working Effectively with Legacy Code (Feathers) e Refactoring (Fowler). Estudar projetos de código aberto antigos e bem cuidados (servidores web e bancos de dados tradicionais) mostra como o código se mantém limpo por décadas: estilo único, funções curtas, testes e revisão rigorosa.
Revisão com um "camarada" antes do commit¶
Uma revisão informal e rápida antes de integrar a mudança evita revisões longas e penosas depois:
- Liste os arquivos alterados (
git status). - Abra o diff de cada um numa ferramenta gráfica.
- Explique para si mesmo cada alteração. Se não consegue justificar uma linha, reverta-a (muitas vezes é sobra de depuração ou mudança acidental).
- Chame uma pessoa — de preferência mais experiente — e explique o objetivo da mudança, o que testou e como; ela aponta o que ficou de fora.
Quem pede revisão cedo e com frequência aprende mais rápido; o código não é o seu valor pessoal, e encontrar falhas nele é o trabalho normal da revisão. Para o processo completo, veja Code review.
Maus cheiros de design (code smells)¶
Definição: Code smell (mau cheiro de código)
Um padrão recorrente no código que não é, por si só, um bug — o programa funciona —, mas é sintoma de um problema maior de design (baixa coesão, acoplamento alto, abstração faltando), que tende a causar bugs e dificultar manutenção mais adiante. Conhecer os nomes desses padrões ajuda tanto a identificá-los mais rápido quanto a se comunicar sobre eles com outros desenvolvedores.
Alguns já foram vistos ao longo deste capítulo, sob o nome de princípios/soluções específicas — vale reconhecê-los também pelo nome de smell: Feature envy (um método mais interessado em outro objeto do que no seu próprio — ver SRP, acima) e Intimidade inapropriada (uma classe conhecendo/alterando demais os detalhes internos de outra — ver Encapsulamento, acima) são dois dos mais comuns.
Definição: Refused bequest (\"herança recusada\")
Quando uma classe filha herda de uma classe pai, mas não usa (ou não quer) parte dos métodos herdados — sinal de que a relação de herança não é uma verdadeira "X é um Y", e a classe filha deveria estar recebendo só o que realmente usa (composição, ou uma hierarquia diferente), não herdando uma interface inteira por conveniência.
Definição: God class (\"classe deus\")
Uma classe que controla/conhece muitos outros objetos do sistema, tentando "fazer tudo" — o oposto de uma classe coesa. É particularmente perigosa porque qualquer uma das dezenas de classes das quais ela depende pode forçar uma mudança nela, tornando-a extremamente frágil (muitas razões diferentes para quebrar). É o resultado natural de nunca aplicar SRP/coesão ao longo do tempo.
Definição: Divergent changes (\"mudanças divergentes\")
Quando uma classe não coesa precisa ser alterada com frequência, por motivos diferentes a cada vez (uma vez porque mudou a regra de cálculo, outra porque mudou o formato de e-mail, outra porque mudou o acesso a dados) — o oposto do SRP: a classe deveria ter uma única razão para mudar, não várias não relacionadas.
Definição: Shotgun surgery (\"cirurgia de espingarda\")
O oposto do smell anterior: uma mudança de negócio que parece simples ("uma coisa só mudou") obriga a alterar muitos arquivos diferentes de uma vez, porque a lógica daquela mudança está espalhada (não encapsulada num único lugar). É consequência direta de falta de encapsulamento — a mesma regra de negócio deveria viver num único lugar, não replicada em vários pontos do sistema.
Refatoração¶
Definição: Refatoração
Segundo Martin Fowler (Refactoring: Improving the Design of Existing Code, 1999), refatorar é melhorar o design de um código já existente, sem alterar seu comportamento externo. A definição tem três partes que valem a pena isolar: (1) melhorar o design existente — uma alteração que adiciona funcionalidade nova não é refatoração; (2) fazer isso em pequenos passos — quanto menor cada mudança, menor a chance de algo dar errado; (3) nunca deixar o sistema quebrado entre um passo e outro — o que exige uma boa suíte de testes cobrindo o comportamento que não deve mudar.
Um exemplo simples ilustra a mecânica: renomear um método sem quebrar quem já o chama. Em vez de simplesmente trocar o nome (o que quebraria todo mundo que usa o método antigo), o caminho seguro é criar o método novo delegando para o antigo, migrar as chamadas (e os testes) uma a uma para o novo nome, e só então apagar o método antigo — o sistema nunca fica, em nenhum momento intermediário, com testes quebrados.
Extrair Método¶
Definição: Extrair Método
Técnica usada quando um método acumula mais de uma responsabilidade — parte da lógica é isolada num método novo, com um nome que já comunica o que aquele trecho faz, e o método original passa a chamá-lo.
public class DesativarUsuariosWorker {
public void desativarUsuarios() {
RepositorioUsuarios repositorio = new RepositorioUsuarios();
List<Usuarios> usuarios = repositorio.all().stream()
.filter(usuario -> usuario.semLoginRecente() && usuario.estaAtivo())
.collect(Collectors.toList());
usuarios.forEach(usuario -> repositorio.desativar(usuario));
NotificadorViaEmail.usuariosDesativados(usuarios);
}
}
public class DesativarUsuariosWorker {
public void desativarUsuarios() {
List<Usuarios> usuarios = usuariosParaDesativar();
usuarios.forEach(usuario -> repositorio.desativar(usuario));
NotificadorViaEmail.usuariosDesativados(usuarios);
}
private List<Usuarios> usuariosParaDesativar() {
RepositorioUsuarios repositorio = new RepositorioUsuarios();
return repositorio.all().stream()
.filter(usuario -> usuario.semLoginRecente() && usuario.estaAtivo())
.collect(Collectors.toList());
}
}
Definição: Cuidado ao extrair — variáveis locais e testes
Ao extrair um trecho para um método novo, variáveis locais usadas por ele precisam continuar existindo dentro dele — copiadas para lá, ou recebidas por parâmetro. Se o método original já tinha mais de uma responsabilidade, é comum que ele também tivesse mais de um teste unitário — vale a pena atualizar os testes junto, para garantir que o método novo se comporte exatamente como o trecho que ele substituiu.
Mover Método¶
Definição: Mover Método
Técnica usada quando um método utiliza mais informações de outra classe do que da própria — um sinal (já visto como Feature Envy, acima) de que aquele comportamento está morando na classe errada. Mover o método reduz a complexidade do código de origem, já que ele passa a ter acesso direto às informações que precisa, em vez de pedir emprestado a outro objeto o tempo todo.
O processo é sempre o mesmo dos dois exemplos acima: duplicar o método na classe de destino (copiando a lógica sem alterar comportamento), atualizar seus testes lá, trocar as chamadas na classe de origem para delegar à classe nova, e só então remover a versão antiga.
Mover Campo¶
Definição: Mover Campo
Semelhante ao Mover Método, mas para um atributo que é mais usado por outra classe do que pela sua própria. Move-lo garante que o dado fique protegido de modificações externas desnecessárias, junto da lógica que realmente o utiliza.
// antes: BANDEIRA_UM/BANDEIRA_DOIS vivem em Taxi, mas só são usadas em CalculadorDePreco
public class Taxi {
private static final float BANDEIRA_UM = 1.2f;
private static final float BANDEIRA_DOIS = 1.8f;
public float calcularCorrida(float kmRodados) {
CalculadorDePreco calculador = new CalculadorDePreco();
if (ehFinalDeSemana()) {
return calculador.calcularCorrida(kmRodados, BANDEIRA_DOIS);
} else {
return calculador.calcularCorrida(kmRodados, BANDEIRA_UM);
}
}
}
// depois: as constantes migraram para CalculadorDePreco, e a lógica de qual bandeira
// usar também — Taxi só informa se é fim de semana ou não
public class CalculadorDePreco {
private static final float VALOR_POR_KM = 0.48f;
private static final float BANDEIRA_UM = 1.2f;
private static final float BANDEIRA_DOIS = 1.8f;
public float calcularCorrida(float kmRodados, boolean bandeiraDois) {
if (bandeiraDois) {
return BANDEIRA_DOIS * (kmRodados * VALOR_POR_KM);
} else {
return BANDEIRA_UM * (kmRodados * VALOR_POR_KM);
}
}
}
public class Taxi {
public float calcularCorrida(float kmRodados) {
CalculadorDePreco calculador = new CalculadorDePreco();
return calculador.calcularCorrida(kmRodados, ehFinalDeSemana());
}
}
Extrair Classe¶
Definição: Extrair Classe
Pode ser vista como uma evolução do Extrair Método, aplicada quando uma classe acumula mais de uma responsabilidade — em vez de só isolar um trecho de código num método próprio, cria-se uma classe nova para hospedar aquela responsabilidade, usando Mover Método e Mover Campo para migrar até ela o que faz sentido.
// antes: uma classe só mistura acesso FTP com persistência no banco
public class BaixarRegistrosDeVendaFtpNoBancoWorker {
private String host, porta, usuario, senha;
private RepositorioDeVendas repositorioDeVendas;
public void requisitarFtp(String caminhoArquivo) {
ClienteFtp cliente = new ClienteFtp(host, porta);
cliente.login(usuario, senha);
ArquivoFtp arquivo = cliente.buscarArquivo(caminhoArquivo);
repositorioDeVendas.salvarDeArquivo(arquivo);
}
}
// depois: GerenciadorFtp assume só o acesso FTP; a classe original delega para ele
public class GerenciadorFtp {
private String host, porta, usuario, senha;
public ArquivoFtp requisitarFtp(String caminhoArquivo) {
ClienteFtp cliente = new ClienteFtp(host, porta);
cliente.login(usuario, senha);
return cliente.buscarArquivo(caminhoArquivo);
}
}
public class SalvarRegistroDeVendasWorker {
private RepositorioDeVendas repositorioDeVendas;
public void requisitarFtp(String caminhoArquivo) {
GerenciadorFtp gerenciador = new GerenciadorFtp();
ArquivoFtp arquivo = gerenciador.requisitarFtp(caminhoArquivo);
repositorioDeVendas.salvarDeArquivo(arquivo);
}
}
Definição: Nomes melhores aparecem depois da extração
Com as responsabilidades separadas, fica mais fácil enxergar um nome que descreva
exatamente o que cada classe faz — GerenciadorFtp é genérica o bastante para ser
reaproveitada por qualquer outro worker que precise de FTP; a classe original, antes
chamada de forma confusa (misturando "FTP" e "banco" no nome), pode virar algo tão
direto quanto SalvarRegistroDeVendasWorker.
Refatorando até um padrão de projeto¶
Refatoração e padrões de projeto resolvem problemas complementares: um code smell aponta que algo está errado; um padrão é, frequentemente, o formato final para onde a refatoração converge.
Definição: Passos práticos para refatorar rumo a um padrão
- Identificar a oportunidade — reconhecer o code smell que está incomodando (ver a seção anterior).
- Entender o contexto — nem todo problema justifica a estrutura extra de um padrão; adicionar abstração tem custo (mais classes, indireção), e imaginar o design "ideal" antes de simplificar de mais é o caminho para violar o YAGNI (You Aren't Gonna Need It — "você não vai precisar disso": não generalizar código além do que o problema atual realmente exige).
- Aplicar mudanças pequenas, com o TDD como guia — como refatorar não deveria mudar comportamento, alterar primeiro os testes (se a interface do componente precisar mudar) e só então o código, mantendo tudo passando a cada passo.
Definição: Por que vale a pena aprender o catálogo de padrões
Padrões de projeto são soluções já testadas e reconhecidas por qualquer desenvolvedor que os conheça — citar "isso é um Strategy" comunica, de uma vez, toda uma estrutura e suas consequências, sem precisar reexplicar o design do zero. Por já terem sido validados em vários contextos diferentes, tendem a ser genéricos o suficiente para acomodar mudanças futuras — mas, como toda solução genérica, só valem a pena quando o contexto realmente pede aquela flexibilidade (ver YAGNI, acima).
Métricas de código¶
Tudo discutido até aqui (coesão, acoplamento, princípios SOLID, code smells) é qualitativo — depende de julgamento e experiência para reconhecer. Métricas de código tentam colocar números nesses conceitos, para servir de filtro rápido: não dizem com certeza se uma classe tem problema, mas ajudam a decidir onde vale a pena olhar primeiro num sistema grande demais para revisar por completo.
Complexidade ciclomática¶
Definição: Complexidade ciclomática (Número de McCabe)
Mede quantos caminhos diferentes de execução um método pode ter, contando a
quantidade de instruções de desvio (if, for, while, case, ...) e somando 1
ao final (\(V(G) = \text{desvios} + 1\)). Um método com 2 ifs tem complexidade 3 (2 desvios + 1). Quanto maior o
número, mais difícil o método é de entender e de testar (mais combinações possíveis
de caminho) — a métrica pode ser generalizada para o nível de classe, somando a
complexidade de todos os seus métodos.
Tamanho de métodos e classes¶
Tamanho é o feedback mais simples de todos: métodos muito longos (muitas linhas), classes com muitos atributos, ou muitos métodos, tendem a ser mais difíceis de manter — uma classe com 80 métodos provavelmente tem 80 comportamentos diferentes, o que é um indício (não uma prova) de baixa coesão. A quantidade de parâmetros que um método recebe, e o tamanho da árvore de herança de uma classe, seguem a mesma lógica: números maiores tendem, na média, a indicar mais complexidade.
Coesão: LCOM¶
Definição: LCOM (Lack of Cohesion of Methods)
Mede a falta de coesão de uma classe: agrupa os métodos pelos atributos que cada
um manipula — se o método A() só mexe nos atributos a/b, e o método B() só
mexe em c/d, a classe parece estar dividida em dois grupos independentes de
responsabilidade (quanto maior o número, menos coesa a classe é considerada). A
versão mais aceita atualmente é a LCOM HS (Handerson-Sellers).
Definição: Limitação prática do LCOM
Uma classe cheia de getters/setters simples (cada um manipulando um único
atributo) tende a inflar o número do LCOM artificialmente, mesmo quando a classe é
genuinamente coesa — é uma heurística, não uma prova matemática. Use métricas como
filtro (para decidir quais classes olhar primeiro, entre milhares), não como
veredito automático.
Acoplamento aferente e eferente¶
Definição: Acoplamento eferente
Quantas outras classes uma classe depende (chamadas "para fora"). Quanto maior, mais frágil a classe é — mais coisas externas podem forçá-la a mudar (é o mesmo conceito de "quantas dependências uma classe tem", já discutido no DIP).
Definição: Acoplamento aferente
Quantas outras classes dependem da classe em questão (chamadas "de fora" para
ela). É o lado que importa para saber se uma classe é estável: List do Java
tem acoplamento aferente altíssimo (praticamente todo sistema Java depende dela) e
eferente zero (ela não depende de mais nada) — por isso é tão estável, e por que
quem a mantém evita ao máximo alterá-la.
Acoplamento aferente alto e complexidade ciclomática alta ao mesmo tempo é um sinal de atenção redobrada: uma classe muito reutilizada, mas também muito complicada — provavelmente merece ser revisada com prioridade.
Como avaliar os números encontrados¶
Não existe um "número mágico" universal válido para todo projeto — a literatura sugere valores, mas cada equipe pode (e talvez devesse) calibrar seu próprio limite: calcular as métricas do próprio sistema, olhar a distribuição real dos números, e definir como aceitável o que já é o padrão predominante naquele código (não um ideal abstrato e descontextualizado). Uma abordagem prática comum: se 80–90% das classes do sistema estão dentro de um determinado limite, esse limite vira a referência de "aceitável" para aquele sistema. Métricas nunca provam, com 100% de certeza, que uma classe tem problema — servem como filtro para não precisar revisar manualmente milhares de classes, focando a atenção onde a métrica sinaliza risco maior.
Tratamento de exceções¶
Uma exception (exceção) representa uma situação de risco ou erro que acontece durante a execução — desde acessar uma posição inválida de um array até tentar ler um arquivo que não existe. Os exemplos abaixo usam sintaxe Java, mas o conceito de "sinalizar e tratar um erro de forma estruturada, separado do fluxo normal" existe na maioria das linguagens.
try {
Produto produto = produtos[i];
System.out.println(produto.getValor());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("deu exception no índice: " + i);
}
O bloco try isola o código de risco; o catch só executa se aquele tipo específico
de exceção acontecer — o resto do programa continua normalmente depois disso, em vez de
travar.
Definição: Stacktrace
O rastro impresso quando uma exceção não é tratada: qual exceção ocorreu, sua mensagem, e a sequência de chamadas de método (do mais recente ao mais antigo) até o ponto exato do problema (arquivo e linha). É a primeira coisa a olhar ao investigar um erro em produção ou em um fórum de dúvidas.
Hierarquia: Throwable, Exception, Error¶
Toda exceção em Java é um objeto, e — como qualquer objeto — pertence a uma hierarquia de
herança. catch funciona por polimorfismo: capturar um tipo mais genérico (uma
superclasse) também captura suas subclasses.
graph TD
T[Throwable] --> E[Exception]
T --> Er[Error]
E --> RE[RuntimeException]
E --> IO[IOException]
RE --> NPE[NullPointerException]
RE --> CCE[ClassCastException]
RE --> AIOOBE[ArrayIndexOutOfBoundsException]
IO --> FNF[FileNotFoundException]
Error— problemas sérios, geralmente da própria JVM (ex.:OutOfMemoryError). Na prática, não são tratados nem lançados pelo código da aplicação.- Unchecked exceptions — filhas de
RuntimeException. O compilador não obriga a tratar (try/catch) nem declarar (throws) — o código compila com ou sem tratativa. Representam, em geral, erros de programação que poderiam ter sido evitados (NullPointerException,ArrayIndexOutOfBoundsException, ...). - Checked exceptions — as demais filhas diretas de
Exception(ex.:IOException/FileNotFoundException). O compilador obriga a tratar comtry/catchou declarar comthrows— geralmente representam condições externas que não dá para prevenir só com código correto (o arquivo pode simplesmente não existir).
Definição: catch de uma checked exception que nunca poderia acontecer
Para uma checked exception, o compilador verifica não só que ela foi tratada,
mas também que ela realmente pode ser lançada por algo dentro daquele try —
um catch para uma checked exception que nenhuma instrução do try declara/lança é
erro de compilação (exception X is never thrown in body of corresponding try
statement):
try {
System.out.println("nada de arriscado aqui");
} catch (java.sql.SQLException e) { // erro de compilação — nada no try lança SQLException
}
catch
(RuntimeException e) sempre compila, mesmo que nada ali "pareça" poder lançá-la,
porque o compilador não consegue (e não tenta) provar que uma unchecked exception é
impossível.
throws: delegando a tratativa¶
Em vez de tratar uma exceção ali mesmo, um método pode delegar a responsabilidade para
quem o chamar, com throws na assinatura:
public void abreArquivo() throws FileNotFoundException {
new java.io.FileInputStream("arquivo.txt");
}
Isso empurra a obrigação (tratar ou declarar de novo) para o método que chama
abreArquivo. Se ninguém tratar em nenhum nível, a exceção chega até a JVM, que imprime o
stacktrace e encerra o programa.
Definição: throw x throws
throw (sem "s") é o comando que efetivamente lança uma exceção, no imperativo:
throw new RuntimeException("mensagem"). throws (com "s") só aparece na assinatura
de um método, avisando ao compilador (e a quem for chamá-lo) que aquele método pode
lançar aquele tipo de exceção.
finally: executa sempre¶
Um terceiro bloco, opcional, roda independente de ter havido exceção ou não — comum para fechar conexões (banco de dados, arquivos) que precisam ser liberadas de qualquer jeito:
try {
// código de risco
} catch (Exception e) {
// tratando o problema
} finally {
// sempre executado — ex.: fechar uma conexão
}
Também é válido um try/finally sem nenhum catch — útil quando você só precisa
garantir a limpeza (liberar um recurso), sem querer tratar a exceção ali mesmo (ela
continua se propagando normalmente para quem chamou):
Desde o Java 7, também é possível capturar mais de um tipo de exceção no mesmo catch
(multicatch), quando a reação a ambas é igual:
} catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
System.out.println("foi uma das duas");
}
Definição: Ordem dos catch importa
Quando existe mais de um bloco catch num mesmo try, o Java testa na ordem em
que estão escritos e usa o primeiro que combina com o tipo lançado — os
demais nem são avaliados. Por isso, um catch de uma exceção mais genérica (uma
superclasse) colocado antes de um catch mais específico (uma subclasse) torna
o segundo inalcançável — e isso é erro de compilação, não só um bug silencioso:
Lançando e criando suas próprias exceções¶
Além de tratar, o próprio código pode lançar uma exceção quando detecta uma situação inválida — útil para validar uma regra de negócio logo na origem, em vez de deixar o problema se propagar silenciosamente:
public Livro(Autor autor) {
if (autor == null) {
throw new RuntimeException("O Autor do Livro não pode ser nulo");
}
this.autor = autor;
}
Quando o cenário é específico do seu domínio, vale criar uma exceção própria — herdando
de RuntimeException (mantendo-a unchecked, a menos que exista uma razão real para
forçar o tratamento) e delegando a mensagem para o construtor da superclasse:
public class AutorNuloException extends RuntimeException {
public AutorNuloException(String mensagem) {
super(mensagem);
}
}
Antes de criar uma exceção nova, vale conferir se a API do Java já não tem uma que represente bem a mesma situação — nem sempre vale a pena reinventar.
Definição: Checked exception em inicializador de atributo
Se o inicializador de um atributo de instância (o valor atribuído já na
declaração do campo, ex.: private InputStream is = new FileInputStream(...);)
pode lançar uma checked exception, essa exception precisa ser declarada com
throws em todos os construtores da classe — porque esses inicializadores
rodam como parte da construção do objeto, antes do corpo do construtor (ver
Orientação a Objetos, ordem de
inicialização de uma instância):
Catálogo de exceções e erros comuns¶
Reconhecer rapidamente qual exceção/erro combina com qual situação é útil tanto para depurar quanto para entrevista:
| Classe | Quando acontece |
|---|---|
ArrayIndexOutOfBoundsException |
índice inválido num array |
IndexOutOfBoundsException |
índice inválido numa List (nome diferente do array, mesma ideia) |
NullPointerException |
uso do operador . sobre uma referência null |
ClassCastException |
casting para um tipo incompatível com o objeto real, em tempo de execução |
NumberFormatException |
converter um texto inválido para número (ex.: Integer.parseInt("abc")) |
IllegalArgumentException |
um método recebeu um argumento que não faz sentido para ele (validação explícita, lançada pelo próprio código) |
IllegalStateException |
uma operação foi chamada num momento em que o estado atual do objeto não permite (ex.: andar estando dormindo) |
Além das exceptions "normais" do dia a dia, alguns erros (Error, não
Exception) específicos valem a pena reconhecer:
StackOverflowError— a pilha de execução estourou, tipicamente por uma recursão sem condição de parada (cada chamada empilha um novo quadro, sem nunca desempilhar).OutOfMemoryError— o heap acabou, geralmente por criar objetos demais sem nunca deixá-los elegíveis para o garbage collector (ver Java).NoClassDefFoundError— uma classe que existia (e compilou) em tempo de compilação não é encontrada no classpath em tempo de execução — diferente de um erro de compilação, esse só aparece rodando o programa.ExceptionInInitializerError— quando um bloco estático (static { ... }) ou a inicialização de uma variávelstaticlança uma exceção durante o carregamento da classe pela JVM, ela é embrulhada nesse erro.
Dívida técnica¶
Definição: Dívida técnica
Metáfora para o custo futuro de escolhas rápidas ou ruins no código/arquitetura: como uma dívida financeira, gera "juros" (cada mudança fica mais lenta e arriscada) até ser paga com refatoração. Pode ser deliberada (atalho consciente para cumprir um prazo) ou acidental (falta de conhecimento, código que envelheceu).
- Sinais: bugs recorrentes na mesma área, medo de mexer em certos módulos, testes ausentes ou frágeis, duplicação, builds lentos, dependências defasadas.
- Como gerir: registrar a dívida (no backlog, com impacto estimado), medir (complexidade, cobertura, SonarQube), reservar uma fatia fixa de capacidade de cada ciclo para pagá-la e seguir a regra do escoteiro (deixe o código um pouco melhor do que encontrou).
- Priorize pelos "juros": pague primeiro a dívida em áreas que mudam com frequência. Técnicas em Refatoração e Maus cheiros de design.
Código aberto: licenças e contribuição¶
Quase todo produto usa componentes de código aberto, e cada um vem com uma licença que diz o que a empresa pode e não pode fazer. Isso é tema recorrente em revisão de dependências e em conversas com o jurídico.
Definição: licença de software, copyleft
Licença é o contrato que autoriza o uso, a modificação e a redistribuição de um código. Copyleft é a filosofia (não uma forma de direito autoral) de licenças que exigem que os trabalhos derivados sejam distribuídos sob a mesma licença, preservando a liberdade do código.
Quem é o dono do código¶
- Num contrato de trabalho tradicional, o código que você escreve para a empresa pertence a ela; alguns contratos cobrem também projetos pessoais feitos durante o vínculo. Confirme antes de publicar algo seu.
- Código aberto tem dono: o detentor dos direitos autorais (pessoa ou empresa) aparece num bloco de comentário no topo de cada arquivo. Só o domínio público não tem dono.
Famílias de licenças¶
| Família | Exemplos | O que exige |
|---|---|---|
| Permissivas | MIT, BSD, Apache 2.0 | Manter o aviso de direitos autorais; pode ser usada em produto fechado (Apache também dá licença de patentes) |
| Copyleft fraco | LGPL, MPL | Modificações no próprio componente voltam à comunidade; ligar seu código a ele (como biblioteca) não obriga a abrir o seu |
| Copyleft forte | GPL, AGPL | Todo código ligado a ele também deve ser GPL; AGPL estende isso a software oferecido como serviço pela rede |
Consequência prática: GPL é problemática em produto comercial fechado, porque obriga a abrir o código próprio. Nunca copie trechos GPL para dentro de código proprietário "só dessa vez": uma auditoria que compare as bases de código revela a cópia. As licenças são interpretadas pelo jurídico e mudam com o tempo; a equipe técnica identifica a licença de cada dependência e leva a dúvida aos advogados. Ferramentas de análise de composição (SCA — Software Composition Analysis, veja Governança de dependências) automatizam o inventário.
Mantendo uma cópia modificada: o ramo de fornecedor¶
Se você altera uma biblioteca externa (versão 1.0) e meses depois sai a 1.2, uma mesclagem de duas vias (sua versão x a nova) não distingue o que você mudou do que a comunidade mudou. A solução é a ramificação de fornecedor (vendor branch):
- Mantenha um ramo que contém somente o código original de cada versão (1.0, depois 1.2).
- Mescle esse ramo no ramo principal, onde ficam as suas alterações: a mesclagem agora é de três vias (ancestral comum + sua versão + a nova) e resolve conflitos corretamente.
Com repositórios hospedados (GitHub, GitLab), o mesmo se faz com um fork que acompanha o repositório original (upstream).
Contribuindo de volta¶
Contribuir (correção, documentação, recurso) tem benefício prático: você deixa de manter sozinho uma modificação que precisaria ser reaplicada a cada versão nova. O caminho típico:
- Obtenha autorização da empresa (o trabalho é proprietário por padrão) e verifique a licença.
- Prepare a mudança: descrição clara, teste, estilo do projeto.
- Envie por pull request (ou patch para a lista de e-mails). Mantenedores podem pedir ajustes ou recusar; trate como uma revisão de código.
- Com histórico consistente, o projeto pode conceder permissão de commit.
Para convencer a gestão, o melhor argumento não é filosófico e sim econômico: contribuir sai mais barato do que manter um fork privado para sempre. Contribuições em projetos conhecidos também entram no portfólio.
Governança de dependências¶
- Gerencie versões em um só lugar: BOM (Bill of Materials) /
dependencyManagementno Maven, ou catálogo de versões no Gradle — evita versões conflitantes das mesmas bibliotecas. - Reprodutibilidade: fixe versões (sem
latestou faixas abertas) e use lockfile quando a ferramenta oferecer. - Vulnerabilidades e licenças: varredura contínua (OWASP Dependency-Check, Dependabot, Renovate) e revisão de licenças (GPL x MIT/Apache) antes de adotar uma biblioteca.
- Critérios para adotar uma dependência: manutenção ativa, comunidade, tamanho, necessidade real (não adicione uma biblioteca para uma função de 5 linhas) e plano de saída.
- Atualize em passos pequenos e frequentes — saltos grandes de versão são caros.
Controle de versão: conceitos essenciais¶
Definição: sistema de controle de versão (VCS)
Sistema de controle de versão acompanha o conteúdo (geralmente arquivos de código) ao longo do tempo: guarda cada versão, permite voltar a qualquer ponto e coordena o trabalho de várias pessoas sobre o mesmo código. Exemplos: Git, Mercurial, Subversion.
Por que usar: (1) desfazer — voltar ao último ponto bom quando algo dá errado; (2) reproduzir o que foi entregue — para corrigir um problema na versão que o cliente tem hoje; (3) colaborar sem sobrescrever o trabalho dos outros; (4) saber quem mudou o quê e por quê (histórico e blame).
| Conceito | O que é |
|---|---|
| Commit | "Fotografia" do estado do trabalho, com mensagem explicando a mudança; agrupa alterações relacionadas |
| Tag / rótulo | Nome fixo para uma versão importante (um release, v1.0) |
| Merge (mesclagem) | Combina duas variantes de um arquivo; mudanças em partes diferentes se juntam sozinhas; mudanças sobrepostas geram conflito que precisa ser resolvido manualmente |
| Branch (ramificação) | Linha de tempo paralela: o tronco (trunk/main) segue com o desenvolvimento, e o ramo de lançamento recebe só correções da versão em produção |
| Branch de funcionalidade | Isola uma mudança longa ou arriscada até ela estar pronta; quanto mais curta a vida do ramo, menos conflitos |
| Centralizado x distribuído | No centralizado (Subversion) há um servidor "dono" do histórico; no distribuído (Git, Mercurial) cada cópia tem o histórico completo, e ramificar/mesclar é barato |
Para treinar: crie um repositório, faça commits, peça a um colega (ou use duas cópias de trabalho) para editar os mesmos arquivos, provoque um conflito e resolva-o; crie um ramo de lançamento a partir de uma tag e leve uma correção até ele. Convenções de ramos, mensagens e pull requests estão em Fluxo de trabalho em equipe; a automação em cima disso (ganchos, pipelines) está em CI/CD.
Fluxo de trabalho em equipe¶
| Prática | Resumo |
|---|---|
| Estratégia de branches | Git Flow (main, develop, feature/*, release/*, hotfix/*) para ciclos de release longos; trunk-based development (ramos curtos integrados diariamente, com feature flags) para entrega contínua |
| Commits | Pequenos e atômicos, com mensagem clara; padrão Conventional Commits (feat:, fix:, docs:...) permite gerar changelog e versão automaticamente |
| Pull request | Mudança pequena, descrição do porquê, testes e checklist; CI verde antes do merge |
| Code review | Foco em correção, legibilidade, testes, segurança e design; comentar o código, não a pessoa; automatizar o que for estilo (linter, formatação) |
| Definition of Done (DoD) | Acordo da equipe sobre "pronto": testes passando, revisado, documentado, sem vulnerabilidades críticas, implantável |
| Pair/mob programming | Duas ou mais pessoas no mesmo problema: difunde conhecimento e reduz defeitos |
Code review (revisão de código)¶
Definição: Code review e pull request
Code review é o processo em que outra pessoa do time avalia o código antes de ele entrar na base principal: segue padrões técnicos, boas práticas e as regras de negócio? O objetivo não é só achar falhas, mas elevar a qualidade geral e espalhar conhecimento. Um pull request (PR) é a proposta de mesclar um branch em outro, com as diferenças visíveis para revisão e discussão.
Fluxo: o autor abre o PR → um ou mais revisores (alguns projetos exigem duas aprovações) revisam → ajustes → aprovação e merge. O tempo de revisão deve entrar na estimativa da tarefa no refinamento técnico.
O que procurar¶
| Foco | Pergunta |
|---|---|
| Design | Integra-se bem com o resto do sistema e com as interações entre componentes? |
| Funcionalidade | Faz o que a tarefa pede? (teste a funcionalidade e valide as regras de negócio) |
| Complexidade | Está mais complexo do que precisava? |
| Nomenclatura | Nomes de variáveis, métodos e classes estão claros? |
| Testes, segurança, desempenho | Há testes? Há riscos? |
Boas práticas¶
- Autor: releia o próprio código antes de enviar; escreva uma descrição do que mudou e por quê; mantenha o PR pequeno e no escopo.
- Revisor: entenda a mudança (leia classe por classe, linha por linha); comente com gentileza (perguntas e sugestões, ao menos um comentário positivo: "você não acha que fica melhor assim?"); aprove quando estiver bom o suficiente — não busque perfeição, mantenha um padrão alto sem ser excessivamente exigente.
- Nem sempre é erro, às vezes é estilo: abordagens diferentes não estão necessariamente erradas; pergunte-se se é um problema técnico ou só preferência (e automatize estilo com linters e formatadores).
- Escopo: a revisão deve focar no que foi proposto, não em reescrever partes alheias à tarefa (salvo justificativa técnica clara). Em impasses de dias, chame uma terceira pessoa (liderança técnica) para decidir.
- Velocidade x profundidade: nem tão rápida que perca a eficácia (aprovação automática), nem tão lenta que vire gargalo. Guias de grandes empresas recomendam revisar rápido para não bloquear o fluxo do time.
- Aprender e ensinar: se não conhecia um recurso da linguagem, pesquise; use a revisão para trocar boas práticas (nomenclatura, legibilidade, duplicação, refatoração).
- Receber críticas: não é pessoal, mantenha a mente aberta e agradeça o que melhorou o código (Soft Skills).
Gestão de releases e versionamento¶
Definição: Versionamento semântico (SemVer)
Formato MAJOR.MINOR.PATCH: MAJOR muda quando há quebra de compatibilidade, MINOR
quando se adiciona funcionalidade compatível, PATCH para correções compatíveis.
Ex.: 2.4.1 → 2.5.0 (nova feature) → 3.0.0 (quebra de contrato).
- Changelog e tags no Git marcam cada versão publicada; gere-os automaticamente a partir dos commits.
- Estratégias de implantação: blue-green (dois ambientes, troca instantânea), canary (libera para uma pequena fração dos usuários e amplia aos poucos), rolling (atualiza instância por instância) e feature flags (separam deploy de release). Veja Feature flags no Spring.
- Plano de rollback sempre pronto e testado; mudanças de banco devem ser compatíveis com a versão anterior do código (ver abaixo).
Compatibilidade e evolução de contratos¶
- Contratos de API: não quebre quem consome. Adicionar campos opcionais é compatível;
remover/renomear campos ou mudar tipos não é. Versione a API (
/v1,/v2ou cabeçalho), comunique a depreciação com prazo e mantenha a versão antiga até os clientes migrarem. - Contratos de eventos: o mesmo vale para mensagens — use Schema Registry, regras de compatibilidade (backward/forward) e versão no evento (Kafka avançado).
- Estratégias de versionamento de API:
| Estratégia | Exemplo | Observação |
|---|---|---|
| Na URI | /api/v1/clientes/123 |
Simples, visível e compatível com cache; a mais comum |
| Por cabeçalho | API-Version: 2 |
URLs limpas; exige cuidado com cache e testes |
| Por media type (content negotiation) | Accept: application/vnd.exemplo.clientes.v2+json |
Mais "REST puro"; mais complexo para clientes |
Ao descontinuar uma versão, avise com o cabeçalho Deprecation/Sunset, publique um
guia de migração e registre tudo no changelog. Em contratos de evento, adicionar campo
opcional é compatível (backward); remover ou renomear exige nova versão.
- Princípio de Postel: seja conservador no que envia e tolerante no que recebe
(tolerant reader: ignore campos desconhecidos).
- Leitor tolerante (Must Ignore): o consumidor valida só os campos de que precisa e ignora o resto, em vez de
rejeitar o documento por causa de um campo novo; o produtor segue o esquema à risca, usa valores padrão para campos
ausentes em versões antigas e mantém a versão antiga por uma janela de compatibilidade (ex.: dois anos). Veja
SOAP x REST e contratos rígidos.
- Valide com testes de contrato.
Migrações de dados sem parar o sistema¶
Ferramentas: Flyway e Liquibase versionam o esquema como código e o aplicam automaticamente no deploy (Flyway no Spring).
Padrão expand and contract (expandir e contrair) para mudanças que não podem quebrar a versão anterior do código — por exemplo, renomear uma coluna:
- Expandir: adicionar a nova coluna (nula/com padrão); a aplicação passa a gravar nas duas e a ler da nova com fallback.
- Migrar: copiar os dados antigos em lotes (sem bloquear a tabela).
- Contrair: depois que nenhuma versão usa a coluna antiga, removê-la em uma release posterior.
Boas práticas de scripts: nomes versionados (V1__criar_tabela_clientes.sql), scripts
pequenos e idempotentes quando possível (IF NOT EXISTS), um script de reversão
(rollback) para mudanças arriscadas, índices criados sem travar a tabela
(CREATE INDEX CONCURRENTLY no PostgreSQL), teste da migração em um ambiente parecido com a
produção e execução na pipeline de CI/CD. O Flyway usa SQL puro e versões numeradas; o
Liquibase descreve as mudanças em XML/YAML/JSON/SQL (changesets), com condições e
geração de rollback.
Regras: nunca alterar uma migração já aplicada (crie outra), testar a migração em cópia dos dados reais, ter backup e preferir mudanças aditivas e reversíveis.
Documentação do código e da arquitetura¶
- Código: nomes expressivos primeiro; Javadoc para APIs públicas (o porquê e o contrato, não o óbvio); comentários explicam decisões, não repetem o código.
- README: o que é, como rodar, testar e implantar, variáveis de ambiente e links úteis.
- API: OpenAPI/Swagger gerada a partir do código (Documentação e contratos de API).
- ADR (Architecture Decision Record): documento curto e versionado que registra uma decisão de arquitetura — contexto, alternativas consideradas, decisão e consequências. Ajuda a entender, meses depois, por que o sistema é como é.
- Diagramas como código (Mermaid, PlantUML) ficam no repositório e evoluem com o código.
Privacidade e LGPD no código¶
Princípios: minimização (colete só o necessário), finalidade e consentimento, segurança dos dados pessoais (criptografia, controle de acesso), direitos do titular (acesso, correção, exclusão), retenção limitada e registro/auditoria de acessos. Evite dados pessoais em logs, mascare-os em ambientes de teste e prefira anonimização/pseudonimização. Detalhes técnicos em Segurança e Segurança em produção no Spring.