Princípios e Padrões de Engenharia de Software
Guia técnico único e direto: dos princípios fundamentais (KISS, DRY, YAGNI, SOLID) aos design patterns e padrões de arquitetura (MVC, MVVM, Clean Architecture), com métricas de qualidade, checklist de revisão, exemplos de código e armadilhas comuns — tudo para construir sistemas manuteníveis, confiáveis, seguros e eficientes.
1) O que são princípios e padrões?
São soluções consagradas para problemas que se repetem ao escrever software. Não são bibliotecas que você instala, e sim receitas de organização — formas testadas de estruturar código para que ele seja fácil de ler, testar, mudar e reaproveitar. Existem três grandes níveis, do mais geral ao mais concreto:
- Princípios (KISS, DRY, YAGNI, SOLID): regras de bom senso que guiam decisões do dia a dia.
- Design Patterns (Factory, Strategy, Observer): soluções para problemas recorrentes entre classes e objetos.
- Padrões de arquitetura (MVC, MVVM, Clean Architecture): como organizar o sistema inteiro em camadas.
2) Fundamentos: KISS, DRY e YAGNI
Antes de qualquer padrão mais sofisticado, três princípios simples evitam boa parte dos problemas de código:
- KISS (Keep It Simple, Stupid): prefira soluções simples e legíveis; complexidade excessiva aumenta bugs e custo de manutenção.
- DRY (Don't Repeat Yourself): elimine duplicações para reduzir inconsistência e retrabalho — cada regra de negócio deve existir em um único lugar.
- YAGNI (You Aren't Gonna Need It): implemente apenas o que gera valor agora; evite construir hoje funcionalidades hipotéticas que talvez nunca sejam usadas.
3) SOLID — os 5 princípios da orientação a objetos
SOLID é um acrônimo de cinco princípios (popularizados por Robert C. Martin) que tornam o código flexível, desacoplado e testável. A ideia central: mudanças futuras devem ser baratas.
| Letra | Princípio | Em uma frase |
|---|---|---|
| S | Single Responsibility (Responsabilidade Única) | Uma classe deve ter apenas um motivo para mudar. |
| O | Open/Closed (Aberto/Fechado) | Aberto para extensão, fechado para modificação. |
| L | Liskov Substitution (Substituição de Liskov) | Subclasses devem poder substituir a classe-mãe sem quebrar nada. |
| I | Interface Segregation (Segregação de Interface) | Muitas interfaces específicas são melhores que uma genérica. |
| D | Dependency Inversion (Inversão de Dependência) | Dependa de abstrações, não de implementações concretas. |
Exemplo: Responsabilidade Única (SRP)
Antes — a classe faz coisas demais (calcula, salva e envia e-mail):
class Pedido:
def calcular_total(self): ...
def salvar_no_banco(self): ... # responsabilidade de persistência
def enviar_email(self): ... # responsabilidade de notificação
Depois — cada classe tem um único motivo para mudar:
class Pedido:
def calcular_total(self): ...
class RepositorioPedido:
def salvar(self, pedido): ...
class Notificador:
def enviar_email(self, pedido): ...
Exemplo: Inversão de Dependência (DIP)
O código de alto nível depende de uma abstração, então trocar o serviço de envio não exige mexer na lógica de negócio:
class CanalDeEnvio: # abstração
def enviar(self, msg): raise NotImplementedError
class Email(CanalDeEnvio):
def enviar(self, msg): print("e-mail:", msg)
class SMS(CanalDeEnvio):
def enviar(self, msg): print("sms:", msg)
class Notificador:
def __init__(self, canal: CanalDeEnvio): # recebe a abstração
self.canal = canal
def avisar(self, msg):
self.canal.enviar(msg)
Notificador(Email()).avisar("Pedido confirmado")
Notificador(SMS()).avisar("Pedido confirmado")
4) Design Patterns (Gang of Four)
Os 23 padrões clássicos do livro "Design Patterns" (1994) se dividem em três famílias. Você não precisa decorar todos — conheça os mais usados e aplique quando o problema aparecer, nunca de forma forçada.
Criacionais — como objetos são criados
Factory Method Abstract Factory Builder Singleton Prototype
Factory: centraliza a criação de objetos para que o resto do código não precise saber qual classe concreta está sendo instanciada.
def criar_transporte(tipo):
fabrica = {"carro": Carro, "navio": Navio, "aviao": Aviao}
if tipo not in fabrica:
raise ValueError("Transporte inválido")
return fabrica[tipo]() # quem chama não conhece as classes concretas
Estruturais — como objetos se compõem
Adapter Decorator Facade Proxy Composite
Adapter: faz duas interfaces incompatíveis trabalharem juntas — como um adaptador de tomada. Decorator: adiciona comportamento a um objeto sem alterar sua classe.
Comportamentais — como objetos colaboram
Strategy Observer Command State Iterator
Strategy: permite trocar um algoritmo em tempo de execução (ótimo aliado do princípio Open/Closed):
class Frete:
def __init__(self, estrategia):
self.estrategia = estrategia
def calcular(self, peso):
return self.estrategia(peso)
normal = Frete(lambda p: p * 2)
expresso = Frete(lambda p: p * 5)
print(normal.calcular(10)) # 20
print(expresso.calcular(10)) # 50
Observer: quando um objeto muda de estado, todos os "inscritos" são avisados automaticamente — é a base de sistemas de eventos e da reatividade (ex.: React, Vue).
Padrões de camada: Repository e Service
Muito usados em aplicações com banco de dados, esses dois padrões aplicam SRP separando responsabilidades em camadas e abrem caminho para os padrões de arquitetura da próxima seção:
- Repository: isola o acesso a dados (SQL, ORM, API externa) atrás de uma interface simples, para que o resto do código não saiba como os dados são armazenados.
- Service: concentra as regras de negócio, orquestrando repositories e outros serviços.
5) Padrões de arquitetura: MVC, MVVM e Clean Architecture
Enquanto os design patterns organizam classes, os padrões de arquitetura organizam o sistema inteiro, normalmente separando responsabilidades em camadas.
MVC — Model, View, Controller
O padrão mais conhecido para aplicações web e desktop. Divide a aplicação em três papéis:
- Model: os dados e as regras de negócio (o "cérebro").
- View: a interface que o usuário vê (HTML, telas).
- Controller: recebe as ações do usuário, conversa com o Model e escolhe a View.
Usuário → Controller → Model (busca/atualiza dados)
↓
View (renderiza o resultado)
Usado por frameworks como Spring (Java), Laravel (PHP), Ruby on Rails e ASP.NET.
MVVM — Model, View, ViewModel
Variação do MVC pensada para interfaces reativas. O ViewModel expõe os dados já prontos para a tela e mantém a View sincronizada com o Model por meio de data binding. Comum em apps com Angular, Vue, WPF e SwiftUI.
Clean Architecture / Hexagonal
Organiza o código em círculos concêntricos: a regra de negócio fica no centro e não conhece banco de dados, framework nem interface. As dependências sempre apontam de fora para dentro, o que mantém o núcleo testável e independente de tecnologia.
[ Frameworks / UI / Banco ] → [ Adaptadores ] → [ Casos de uso ] → [ Entidades ]
(externo, troca fácil) (núcleo estável)
Layered Architecture (Arquitetura em Camadas)
Organiza o código em camadas horizontais — geralmente apresentação, negócio/domínio e dados/persistência — em que cada camada só conhece a camada imediatamente abaixo. É a base de muitos sistemas corporativos e combina bem com Repository e Service.
Microsserviços
Divide o sistema em serviços independentes, cada um com seu próprio banco e ciclo de deploy. Ganha-se escalabilidade e times autônomos, mas paga-se com complexidade operacional — é essencial controlar acoplamento e contratos (APIs) entre serviços, sob pena de trocar "código espaguete" por "arquitetura espaguete distribuída".
6) Quadro comparativo: quando usar cada um
| Padrão | Problema que resolve | Use quando… |
|---|---|---|
| SOLID | Código rígido e difícil de mudar | Sempre, como base de qualquer projeto OO. |
| Factory | Criação de objetos espalhada e acoplada | Há várias classes concretas para um mesmo tipo. |
| Strategy | Muitos if/else escolhendo algoritmos |
O comportamento precisa variar em tempo de execução. |
| Observer | Objetos que precisam reagir a mudanças | Eventos, notificações, UI reativa. |
| Repository | Lógica de acesso a dados misturada ao negócio | Quer isolar o banco do restante do código. |
| MVC / MVVM | Interface e regra de negócio embaralhadas | Aplicações com tela (web, mobile, desktop). |
| Clean Architecture | Dependência forte de framework/banco | Sistemas grandes e de vida longa. |
| Layered / Microsserviços | Sistema monolítico difícil de escalar ou times pisando uns nos outros | Organização em camadas (layered) ou necessidade de deploy independente (microsserviços). |
7) Impacto prático dos princípios
- Manutenção: código modular e coeso reduz esforço de evolução.
- Confiabilidade: menos regressões com design claro e testes automatizados.
- Segurança: validação de entrada, tratamento de erro e menor superfície de ataque.
- Performance: simplicidade facilita encontrar gargalos reais sem overengineering.
- Custo: menos retrabalho e menor débito técnico ao longo do projeto.
8) Métricas para acompanhar qualidade
| Princípio | Benefício | Métricas/Sinais | Ferramentas |
|---|---|---|---|
| KISS | Legibilidade e menor risco de bugs | Complexidade ciclomática baixa (ideal < 10) | ESLint, Pylint, SonarQube |
| DRY | Menos retrabalho e inconsistência | % de duplicação e code smells | SonarQube, PMD |
| YAGNI | Entrega rápida de valor | Código morto, funcionalidades não usadas | Revisão de backlog, métricas de uso |
| SOLID | Arquitetura extensível e testável | Coesão/acoplamento, classes grandes, mudanças em cascata | SonarQube, ArchUnit, revisão arquitetural |
| TDD e testes | Confiabilidade e prevenção de regressões | Cobertura, taxa de falha, tempo da suíte | pytest, JUnit, Jest, CI/CD |
| Secure by Design | Redução de vulnerabilidades | Achados SAST/DAST, falhas OWASP Top 10 | OWASP ZAP, CodeQL, Dependency Check |
9) Checklist rápido de revisão
- Código está simples, sem abstrações desnecessárias?
- Há duplicação que pode virar função/módulo reutilizável?
- Cada classe/módulo tem responsabilidade única?
- Testes cobrem fluxos críticos e cenários de erro?
- Entradas são validadas e exceções tratadas corretamente?
- Há proteção contra injeção, XSS e vazamento de segredos?
- CI/CD bloqueia merge com falha de testes/qualidade?
10) Exemplos de código: modularidade e segurança
Python (modularidade + tratamento de erro)
def calcular_produto(valor):
return valor * 0.9
def processar_pedido(tipo, valor):
calculadoras = {"produto": calcular_produto, "servico": lambda v: v}
if tipo not in calculadoras:
raise ValueError("Tipo inválido")
return calculadoras[tipo](valor)
Java (segurança contra SQL Injection)
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE login = ?"
);
stmt.setString(1, user);
ResultSet rs = stmt.executeQuery();
11) Armadilhas comuns
- Overengineering: aplicar 5 padrões num CRUD simples. Padrão demais é tão ruim quanto padrão de menos — respeite o KISS.
- Padrão pelo padrão: use um padrão porque o problema pede, não para parecer sofisticado.
- Abstração prematura: criar interfaces para algo que tem uma única implementação (viola YAGNI).
- Singleton como variável global: fácil de abusar, dificulta testes e esconde dependências.
- Métrica como meta, não como guia: perseguir 100% de cobertura ou zero de complexidade sem julgamento gera testes triviais e código fragmentado só para "passar no número".
12) Recursos para se aprofundar
- Refactoring Guru — Design Patterns (em português)
- Catálogo completo dos 23 padrões GoF
- Martin Fowler — Arquitetura de Software
- The Clean Architecture (Robert C. Martin)
- OWASP Top 10
- SonarQube
- GitHub Actions (CI/CD)
- pytest
- JUnit 5
Resumo: domine primeiro os princípios (KISS, DRY, YAGNI, SOLID), meça a qualidade com métricas objetivas, reconheça os design patterns quando o problema realmente aparecer e escolha a arquitetura adequada ao tamanho do sistema. Padrões são ferramentas, não objetivos — o resultado final deve ser sempre código simples, modular, testável, seguro e fácil de mudar.