Engenharia de Software
Desenvolver software sustentável vai muito além de codificar recursos. Para desenvolvedores experientes, a armadilha comum é a dependência cega de frameworks (como injeções mágicas do Spring) ou de sugestões de IA, o que enfraquece o entendimento dos fundamentos de arquitetura. Este módulo revisita os princípios de SOLID, Clean Code, Design Patterns, e a anatomia de testes unitários e TDD com profundidade técnica e sem superficialidades.
SOLID — A Maleta de Ferramentas Modulares
Imagine uma maleta de ferramentas mecânicas onde cada chave tem um encaixe perfeito e cumpre apenas um papel. Se a chave de fenda estragar, você substitui apenas ela, e não a maleta inteira. Isso é um design modular (SOLID). Se o seu código se assemelha a um canivete suíço gigante fundido em uma única peça de metal disforme (onde tentar usar o saca-rolhas entorta a lâmina principal), você tem alta fragilidade e acoplamento. SOLID é sobre criar peças independentes e fáceis de substituir.
SOLID — Além dos Acrônimos
Os cinco princípios clássicos gerenciam a facilidade de alteração de código (maintainability): - SRP (Single Responsibility): Uma classe deve ter uma, e apenas uma, razão para mudar. Se ela gerencia persistência e gera PDFs, ela tem duas razões para ser alterada. - OCP (Open-Closed): O comportamento deve ser extensível sem modificar o código original, usando polimorfismo. - LSP (Liskov Substitution): Subclasses devem poder substituir superclasses sem quebrar o comportamento esperado. Não lance UnsupportedOperationException para anular métodos da classe pai. - ISP (Interface Segregation): Interfaces focadas. É melhor ter três interfaces pequenas do que uma gigante contendo métodos inúteis para certas classes. - DIP (Dependency Inversion): Classes de alto nível não devem depender de classes de baixo nível. Ambas devem depender de abstrações (interfaces/classes abstratas).
DIP e OCP em Ação
interface PaymentMethod {
void process(double amount);
}
class PixPayment implements PaymentMethod {
public void process(double amount) {
System.out.println("Processing Pix: " + amount);
}
}
class PaymentProcessor {
private final PaymentMethod method;
// DIP: Depende de abstração
public PaymentProcessor(PaymentMethod method) {
this.method = method;
}
public void checkout(double amount) {
method.process(amount);
}
}Clean Code — O Quarto do Hotel
Código limpo é como um quarto de hotel de alto padrão. Quando você entra, tudo está em seu devido lugar: a cama está feita, as toalhas estão no banheiro e o interruptor de luz liga exatamente o lustre do teto, sem mistérios ou fiação exposta. No desenvolvimento, adote a 'Regra do Escoteiro': deixe a área que você alterou um pouco mais limpa do que encontrou. Um pequeno rename, a extração de um método confuso ou a remoção de um comentário obsoleto antes do commit economizam horas de manutenção futura da equipe.
Nomenclatura, Métodos Pequenos e Exceções
Funções limpas devem realizar apenas uma tarefa (Single Responsibility), ter um nível de abstração consistente e ser pequenas (raramente ultrapassando 20 linhas). Nomes de variáveis devem expressar intenção clara (use 'isUserSuspended' e evite flags genéricas como 'status'). Comentários devem explicar decisões de design excepcionais (o 'porquê') e não tentar justificar código mal escrito (o 'o que'). No tratamento de erros, encapsule exceções de infraestrutura (como SQL) em exceções de negócio limpas e nunca silencie exceções com blocos catch vazios.
Refatoração de Código para Clean Code
// ANTES: Acoplamento, mistura de abstração e engole exceções
public void handle(User u) {
if (u != null) {
try {
String sql = "INSERT INTO users VALUES (" + u.id + ", '" + u.name + "')";
db.execute(sql);
String json = "{\"id\":" + u.id + "}";
sendNotification(json);
} catch (Exception e) {}
}
}
// DEPOIS: Coeso, legível, trata exceções e tem injeção
public void registerUser(User user) {
Objects.requireNonNull(user, "User cannot be null");
try {
userRepository.save(user);
notificationService.sendRegistrationAlert(user);
} catch (DataAccessException e) {
log.error("Failed to register user", e);
throw new UserServiceException("Registration failed", e);
}
}Design Patterns — Receitas de Chef vs Overengineering
Design Patterns são receitas de chefs renomados para problemas de design recorrentes. Você não precisa redescobrir como bater claras em neve para fazer um suflê; você usa a técnica clássica. No entanto, o pior erro de engenharia é a 'Superengenharia' (Overengineering): aplicar padrões onde uma solução simples resolveria perfeitamente. Padrões de projeto introduzem complexidade acidental (mais classes, delegações e indireções). Só os utilize quando as vantagens de variabilidade ou desacoplamento superarem esse custo.
Design Patterns Comuns e Testes Automatizados
Dentre os padrões clássicos, conheça o Builder (criação legível de objetos imutáveis complexos), o Decorator (extensão dinâmica sem herança) e o Strategy (algoritmos intercambiáveis em tempo de execução). Na camada de testes, siga a Pirâmide de Testes: crie uma base sólida de testes de unidade extremamente rápidos. Testes de integração validam contratos externos (como persistência usando Testcontainers), e testes E2E validam a jornada inteira. Com TDD, siga rigorosamente o ciclo Red (escrever teste que falha), Green (fazer passar rápido) e Refactor (limpar sem quebrar os testes).
Estratégia + Factory e Testes de Unidade
// Strategy & Unit Test com JUnit 5 + Mockito
interface DiscountStrategy {
double apply(double total);
}
class VIPDiscount implements DiscountStrategy {
public double apply(double total) { return total * 0.90; }
}
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private DiscountStrategy strategy;
@InjectMocks
private OrderService orderService; // SUT
@Test
void shouldApplyDiscountCorrectly() {
// Arrange
when(strategy.apply(100.0)).thenReturn(90.0);
// Act
double result = orderService.calculateTotal(100.0, strategy);
// Assert
assertThat(result).isEqualTo(90.0);
verify(strategy).apply(100.0);
}
}