VapVup👤
← Voltar
🏗️

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.

🧰Analogia

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.

💡Conceito

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).

🔧Exemplo

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);
    }
}
🏨Analogia

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.

💡Conceito

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.

🔧Exemplo

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);
    }
}
👨‍🍳Analogia

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.

💡Conceito

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).

🔧Exemplo

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);
    }
}