Escrever código é fácil
Escrever código sustentável é difícil
Escrever código em que uma equipe pode confiar é engenharia de software
Não é apenas sintaxe
Não é apenas “código limpo”
Não é apenas mecânica do GitHub
Esta aula é sobre disciplina de engenharia
Um projeto raramente falha porque as pessoas não conseguem escrever nenhum código.
Um projeto frequentemente falha porque o código se torna:
Incompreensível
Intestável
Irrevisável
Difícil de alterar
“Funciona, então está bom o suficiente.”
“Podemos limpar depois.”
“Testes levam muito tempo.”
“Revisão não é necessária para pequenas mudanças.”
“Meus colegas vão entender.”
Depois geralmente nunca chega
Pequenos maus hábitos se tornam cultura da equipe
Cultura da equipe se torna qualidade do produto
Um engenheiro profissional pergunta:
Outra pessoa consegue entender isso rapidamente?
Isso pode ser alterado com segurança no futuro?
Isso pode ser testado?
Isso pode ser bem revisado?
Isso vai tornar a equipe mais rápida ou mais lenta?
Código ruim causa:
Mais bugs
Integração mais lenta de novos membros
Depuração dolorosa
Trabalho duplicado
Funcionalidades quebradas após mudanças
Medo de mexer no código
Código bom proporciona:
Desenvolvimento mais rápido
Colaboração mais fácil
Refatoração mais segura
Melhores revisões
Mais confiança
Produtos mais consistentes
“Seu código não é apenas para o computador. Seu código é para seus colegas de equipe, para o seu eu futuro e para o produto.”
Fazer o código funcionar
Tornar o código confiável, sustentável, colaborativo e pronto para produção
Lógica correta
Estrutura clara
Testes
Revisões
Controle de versão
Deploy previsível
Compreensão da equipe
Você escreve uma função uma vez
Seus colegas a leem dezenas de vezes
O seu eu futuro a lê meses depois sem nenhuma lembrança
Legibilidade não é opcional
Legibilidade faz parte da correção
def f(x, y, z): if x: if y > 0: return z * 0.2 return 0
O que é f? O que ela calcula?
O que são x, y, z?
O que 0.2 representa?
Ninguém consegue revisar, testar ou manter isso com confiança
def calculate_discount(price, has_membership, loyalty_points): if not has_membership: return 0 if loyalty_points <= 0: return 0 return price * 0.2
O nome da função explica o propósito
Os parâmetros são significativos
A lógica se lê como uma frase
Um revisor pode verificar a correção imediatamente
Não pergunte apenas: “Funciona?”
Pergunte também:
É legível?
É testável?
É seguro de alterar?
Eu aprovaria isso em um PR?
Nomenclatura ruim cria confusão. Boa nomenclatura cria clareza.
Nomes devem revelar: o que algo é, o que faz, por que existe.
def calc(a, b): return a * b
O que calc calcula?
O que são a e b?
Pode ser área, preço, qualquer coisa - impossível saber
Um revisor não consegue verificar a correção
def calculate_total_price(unit_price, quantity): return unit_price * quantity
O propósito é imediatamente claro
Os parâmetros se explicam sozinhos
O revisor pode verificar: “Sim, preço total = preço unitário × quantidade”
Desenvolvedores futuros não usarão esta função incorretamente
Reduz confusão ao ler código desconhecido
Previne suposições erradas sobre o comportamento da função
Previne uso incorreto de parâmetros
Previne bugs futuros por mal-entendidos
Quando uma função faz muitas coisas, testar se torna difícil, reutilização se torna impossível e depurar se torna doloroso.
def register_user(user): validate_user(user) save_user_to_database(user) send_welcome_email(user) log_activity(user) notify_admin(user)
Cinco responsabilidades em uma única função
Não é possível testar o registro sem enviar e-mail
Se o e-mail falhar, o usuário ainda é salvo?
Impossível reutilizar qualquer etapa individualmente
def register_user(user): validate_user(user) save_user_to_database(user) def onboard_user(user): send_welcome_email(user) log_activity(user) def handle_registration(user): register_user(user) onboard_user(user) notify_admin(user)
Cada função tem um propósito claro e único
Cada uma pode ser testada independentemente
O tratamento de falhas se torna gerenciável
Lógica duplicada significa correções perdidas, inconsistência no código e manutenção cara.
# In checkout.py final_price = price - price * 0.1 # In invoice.py final_price = price - price * 0.1 # In report.py final_price = price - price * 0.1
def apply_discount(price, discount_rate): return price * (1 - discount_rate) # Em todos os lugares: final_price = apply_discount(price, 0.1)
Fonte única da verdade
Altere uma vez, corrija em todos os lugares
Fácil de testar
Fácil de encontrar e auditar
Se seu código é difícil de testar, provavelmente está mal projetado.
Código testável é modular, explícito e tem entradas e saídas claras.
def process_order(order_id): order = db.get_order(order_id) # database call user = db.get_user(order.user_id) # database call send_email(user.email, order.summary) # side effect db.mark_processed(order_id) # database call
Não é possível testar sem um banco de dados real
Não é possível testar sem enviar e-mails reais
Todos os efeitos colaterais estão fortemente acoplados
def process_order(order, user, email_sender, db): email_sender.send(user.email, order.summary) db.mark_processed(order.id)
Dependências são injetadas - fácil de simular
Cada dependência pode ser testada independentemente
A lógica é separada da infraestrutura
A assinatura da função diz exatamente o que ela precisa
Falhas silenciosas são o tipo mais perigoso de bug.
Quando algo dá errado, faça com que seja alto e óbvio.
def get_user_age(user): if user is None: return None if user.birthday is None: return None return calculate_age(user.birthday)
O chamador recebe None e não sabe por quê
O bug está oculto e se propaga silenciosamente
Depurar se torna um jogo de adivinhação
def get_user_age(user): if user is None: raise ValueError("User must not be None") if user.birthday is None: raise ValueError("User birthday is missing") return calculate_age(user.birthday)
O erro é imediatamente visível
A mensagem diz exatamente o que está errado
O bug é capturado cedo, não mais adiante
Código complicado não é inteligente. É caro de manter, revisar e depurar.
def get_price(user, product): if user.is_member: if product.on_sale: if user.loyalty_points > 100: return product.price * 0.7 else: return product.price * 0.8 else: return product.price * 0.9 else: if product.on_sale: return product.price * 0.95 else: return product.price
Profundamente aninhado, difícil de seguir, fácil de introduzir bugs
def get_discount_rate(user, product): if not user.is_member and not product.on_sale: return 0 if not user.is_member and product.on_sale: return 0.05 if user.is_member and not product.on_sale: return 0.1 if user.loyalty_points > 100: return 0.3 return 0.2 def get_price(user, product): return product.price * (1 - get_discount_rate(user, product))
Plano, legível, cada caso é explícito e testável
Difíceis de revisar
Alto risco de bugs
Dolorosas para fazer merge
Histórico git confuso
Fáceis de revisar
Baixo risco
Rápidas para fazer merge
Histórico claro
Prefira:
Funções menores
Commits menores
PRs menores
Unidades de revisão menores
Sem um fluxo de trabalho, equipes criam:
código sobrescrito, mudanças não revisadas, branches main quebradas, confusão.
Main deve sempre estar pronta para deploy
Commits diretos pulam a revisão
Conflitos de merge se tornam destrutivos
Nenhuma forma de reverter de forma limpa
A equipe perde confiança no código
fix my-branch test123 update
feature/user-registration fix/login-redirect-bug chore/update-dependencies refactor/split-order-service
O nome de uma branch deve descrever a intenção do trabalho
fix update wip asdf changed stuff
fix: redirect to login after session timeout feat: add email validation to registration refactor: extract discount logic into helper test: add edge cases for price calculator docs: update API authentication section
Pequeno e focado em uma coisa
Tem um título e descrição claros
Explica por que a mudança foi feita
Inclui testes quando aplicável
Não mistura mudanças não relacionadas
Título: “atualizações”
Sem descrição
47 arquivos alterados
Mistura feature + refatoração + correção de estilo
Sem testes
Título: “feat: add email validation to registration”
Descrição explica a abordagem
4 arquivos alterados
Focado em uma feature
Inclui testes unitários
Um repositório sério inclui estrutura, documentação, configuração e padrões - não apenas arquivos fonte.
project/ ├── src/ │ ├── models/ │ ├── services/ │ ├── routes/ │ └── utils/ ├── tests/ │ ├── unit/ │ └── integration/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── README.md ├── requirements.txt ├── Makefile └── .env.example
Um README deve dizer a um novo membro da equipe:
O que o projeto faz
Como configurá-lo
Como executá-lo
Como rodar os testes
Como contribuir
Nome do projeto e descrição em uma linha
Pré-requisitos (versão do Python, dependências)
Passos de instalação
Como executar localmente
Como rodar os testes
Variáveis de ambiente necessárias
Visão geral da estrutura do projeto
Diretrizes de contribuição
“Não temos tempo para testes”
“Funciona na minha máquina”
“Testes nos atrasam”
Você não tem tempo para não testar
Precisa funcionar em todas as máquinas
Testes te salvam de desacelerar depois
Confiança para alterar código
Depuração mais rápida - testes mostram onde as coisas quebraram
Documentação do comportamento esperado
Rede de segurança para refatoração
Prova de que seu código funciona
def add(a, b): return a + b
Função simples. Como sabemos que funciona corretamente?
Escrevemos um teste.
def test_add_returns_sum(): assert add(2, 3) == 5
Claro, legível, rápido de executar
Se add algum dia quebrar, este teste captura imediatamente
Qualquer pessoa pode entender o que este teste verifica
O que mais devemos testar?
Números negativos
Valores zero
Números muito grandes
Incompatibilidades de tipo (se aplicável)
Condições de contorno
Casos de erro
def test_add_positive_numbers(): assert add(2, 3) == 5 def test_add_negative_numbers(): assert add(-1, -2) == -3 def test_add_with_zero(): assert add(0, 5) == 5 assert add(5, 0) == 5 def test_add_mixed_signs(): assert add(-3, 5) == 2
Cada teste tem um nome claro descrevendo o que verifica
Bugs chegam à produção
Refatoração se torna aterrorizante
Ninguém sabe se uma mudança quebrou algo
Testes manuais são lentos e não confiáveis
Regressões continuam voltando
A equipe perde confiança no código
Bugs são capturados antes do merge
Refatoração é segura e rápida
CI captura regressões automaticamente
Novos membros da equipe entendem o comportamento esperado
Deploys são feitos com confiança
A velocidade da equipe aumenta com o tempo
CI = Integração Contínua
Verifique automaticamente cada mudança antes que ela chegue à branch main.
Código quebrado chegando à main
Código não testado sendo mergeado
Inconsistências de estilo
Problemas de “funciona na minha máquina”
Verificações de qualidade automatizadas
Feedback rápido em cada PR
Padrões consistentes
Confiança da equipe
Executar todos os testes (unitários + integração)
Verificar formatação de código (black, prettier, etc.)
Executar linter (pylint, eslint, flake8)
Verificar anotações de tipo (mypy)
Medir cobertura de testes
Varredura de segurança
Verificação de build (o projeto compila/builda?)
name: Python Checks on: pull_request: push: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' - run: pip install -r requirements.txt - run: pytest
Cada PR executa testes automaticamente
PRs quebrados não podem ser mergeados (com proteção de branch)
A equipe não precisa lembrar de rodar os testes
O feedback é rápido e consistente
Cobertura de testes mede quanto do seu código é exercitado pelos testes.
Alta cobertura não significa ausência de bugs
Baixa cobertura significa grandes áreas não testadas
Cobertura é uma diretriz, não uma garantia
Boas metas para a maioria dos projetos:
70-85% de cobertura de testes como linha de base
Toda lógica de negócio crítica testada
Todos os casos extremos das funções centrais testados
CI deve passar antes do merge (sem exceções)
Verificações de linter e formatador em cada PR
Uma formalidade
Um carimbo de aprovação
Uma demonstração de poder
Uma conversa de qualidade
Uma oportunidade de aprendizado
Um mecanismo de defesa da equipe
Capturar bugs antes que cheguem à produção
Compartilhar conhecimento na equipe
Manter qualidade consistente do código
Distribuir a responsabilidade pelo código
Ensinar e aprender uns com os outros
Construir confiança na equipe
Olhar o diff por 30 segundos
Clicar em “Aprovar”
Não deixar comentários
Não executar o código
Ler cada linha alterada
Verificar lógica e casos extremos
Fazer perguntas de esclarecimento
Sugerir melhorias respeitosamente
Verificar se existem testes
Verifique:
Correção - a lógica faz o que deveria?
Legibilidade - você consegue entender sem perguntar?
Nomenclatura - os nomes são claros e precisos?
Duplicação - algo está repetido desnecessariamente?
Tratamento de erros - os casos extremos estão cobertos?
Testes - existem testes para a nova lógica?
Escopo - o PR se mantém focado?
Durante uma revisão, responda estas perguntas:
Eu entendo o que este código faz?
Eu me sentiria confortável mantendo isso?
Existe algo que possa quebrar silenciosamente?
Existem testes faltando?
A descrição do PR é precisa?
Um novo membro da equipe entenderia isso?
Não explica o que está errado
Não explica por que está errado
Não sugere uma melhoria
Parece um ataque, não um feedback
Desperdiça o tempo do autor
None quando o usuário não é encontrado, mas o chamador na linha 42 não verifica None. Isso pode causar um AttributeError em tempo de execução. Poderíamos lançar uma exceção aqui ou adicionar uma verificação de None no chamador?”
Explica o problema
Mostra o impacto
Sugere uma correção concreta
Trata o autor como um colaborador
Em vez de comandos, use perguntas curiosas:
“O que acontece se esta entrada for negativa?”
“Poderíamos simplificar isso com um retorno antecipado?”
“Existe alguma razão para não usarmos o helper existente para isso?”
“Seria mais claro extrair isso em uma função separada?”
Um bom revisor percebe:
Números mágicos sem explicação
Tratamento de erro ausente para casos extremos
Lógica duplicada que deveria ser extraída
Funções fazendo mais de uma coisa
Convenções de nomenclatura inconsistentes
Testes ausentes ou insuficientes
Preocupações de segurança (segredos hardcoded, SQL injection, etc.)
def calc(items): t = 0 for i in items: if i["type"] == "book": t += i["price"] * 0.9 else: t += i["price"] return t
Como revisor, pergunte:
O que calc calcula?
O que 0.9 significa? Por que livros especificamente?
O que acontece se um item não tiver a chave “type” ou “price”?
Onde estão os testes?
BOOK_DISCOUNT_RATE = 0.1 def get_item_price(item): price = item["price"] if item["type"] == "book": return price * (1 - BOOK_DISCOUNT_RATE) return price def calculate_cart_total(items): return sum(get_item_price(item) for item in items)
Número mágico substituído por constante nomeada
Lógica de precificação extraída em sua própria função
Cada função é testável independentemente
Pegar no pé de estilo quando existe um linter para isso
Reescrever o código do autor no seu próprio estilo
Aprovar sem ler
Bloquear um PR por preferência pessoal (não correção)
Ser condescendente ou desdenhoso
Atrasar revisões por dias sem comunicação
“Se você não entende o código, não aprove.”
Levar feedback para o lado pessoal
Discutir sem ouvir
Descartar sugestões imediatamente
Se sentir atacado
Ler cada comentário com cuidado
Pedir esclarecimentos se necessário
Agradecer aos revisores pelo tempo
Explicar seu raciocínio respeitosamente
Fazer mudanças prontamente
Essas respostas encerram a colaboração e corroem a confiança
Essas respostas constroem confiança e fortalecem a equipe
Provar que você está certo
Defender seu ego
Ser aprovado rapidamente
Entregar código melhor
Aprender uns com os outros
Construir um produto confiável
Crescer como engenheiros
IA pode tornar engenheiros mais rápidos, mas também mais desleixados.
Gerar código boilerplate para economizar tempo
Elaborar casos de teste para funções existentes
Explicar código ou bibliotecas desconhecidas
Sugerir abordagens alternativas
Ajudar a escrever documentação
Auxiliar na depuração analisando mensagens de erro
Pré-revisar código antes de submeter um PR
Usar IA sem entender:
Se você não consegue explicar o código, você não é dono dele
Código gerado por IA com bugs que você não entende é pior do que nenhum código
Melhores prompts, melhores resultados:
Prompts específicos e focados produzem saídas úteis e revisáveis
# Prompt to AI: "Write pytest tests for this function. Include: happy path, negative input, zero, empty list, and type error. Use descriptive test names." # Function: def calculate_average(numbers): if not numbers: raise ValueError("Cannot average empty list") return sum(numbers) / len(numbers)
O prompt é específico sobre o que testar e como nomear os testes
Você ainda revisa e ajusta os testes gerados
# Prompt to AI: "Review this function as a senior engineer. Check for: - edge cases - naming clarity - error handling - testability - any potential bugs Be specific and suggest improvements."
Use IA como um “primeiro revisor” antes que seu colega revise
Isso captura problemas óbvios e economiza tempo do revisor
Ferramentas de IA úteis para desenvolvimento:
GitHub Copilot - sugestões de código inline
Claude Code (CLI) - assistente de IA no terminal
Cursor - editor de código nativo com IA
Codeium - autocompletar gratuito com IA
Bons casos de uso:
Autocompletar padrões repetitivos
Gerar docstrings
Explicar seções complexas de código
Sugerir estrutura de testes
Antes de mergear qualquer código gerado por IA, você deve:
Entendê-lo - ler cada linha
Explicá-lo - ser capaz de descrever o que faz e por quê
Testá-lo - verificar com seus próprios testes
Aprová-lo - tratá-lo como se você o tivesse escrito no seu PR
Vamos inspecionar um PR real juntos e identificar:
• Nomenclatura ruim
• Tratamento de erro ausente
• Lógica duplicada
• Testes ausentes
• Mensagens de commit pouco claras
• Escopo descontrolado
Após esta demonstração, você deve ser capaz de:
Ler um diff de PR criticamente
Identificar pelo menos 5 tipos de problemas
Escrever comentários de revisão construtivos
Sugerir melhorias específicas
Distinguir entre problemas bloqueantes e sugestões
Atividade:
Encontre um PR de código aberto no GitHub (ou use um fornecido pelo instrutor)
Escreva uma revisão com pelo menos 5 comentários substantivos
Cada comentário deve identificar um problema específico e sugerir uma melhoria
Requisitos de entrega:
Link para o PR que você revisou
Seus comentários de revisão (captura de tela ou markdown)
Uma breve reflexão: o que você aprendeu ao revisar o código de outra pessoa?
“Grandes engenheiros não apenas fazem o código funcionar. Eles tornam o código compreensível, testável, revisável e confiável.”
“O código que você está ocupado demais para limpar hoje se torna o problema que domina sua equipe amanhã.”
Emre Varol · A2SV · University of Rwanda