• Clean code
  • SOLID
  • Programação
  • TypeScript

Um checklist prática para desenvolver códigos melhores

O que é um bom código? Descubra por que o código limpo vai muito além de apenas funcionar. Entenda a filosofia, a empatia no desenvolvimento e o impacto do design de software na manutenibilidade a longo prazo.

O que é um bom código? Essa definição varia de acordo com a perspectiva técnica e a vivência de cada engenheiro. Porém, apesar da esfera subjetiva, boa parte das opiniões convergem para um ponto central: legibilidade, facilidade de alteração e empatia. Com quem? Oras, com quem lerá o software no futuro. Acredito que essa construção conceitual seja suficiente para introduzirmos o principal ponto desse texto: um guia didático, que busca nortear questionamentos que podem impulsionar a qualidade do seu código.

Gosto de pensar o desenvolvimento de códigos como um exercício de metalinguagem. Por definição: metalinguagem é o uso da linguagem para falar sobre a própria linguagem ou sobre o próprio código de comunicação. Assim, ao dedicar um olhar reflexivo diante de nossas features, estamos verdadeiramente projetando a linguagem na qual a história do sistema é contada.

Nós não estamos apenas instruindo a máquina; estamos estruturando uma Linguagem Específica de Domínio (DSL). Ao refinar nossas funções e classes de forma reflexiva, fazemos com que o código atue como sua própria metalinguagem. Nós descobrimos e refinamos uma Linguagem Ubíqua que traduz o modelo mental do domínio diretamente para as linhas de código, eliminando o abismo de comunicação entre o negócio e a implementação técnica.

Diante desse mergulho filosófico, apresento a vocês 3 sessões que englobam algumas perguntas norteadoras. Cada tópico acompanha um exemplo prático que permite analisar como essas indagações podem ser utilizadas para encontrar problemas no seu código.

  • Nomenclatura e Semântica
    1. Existe apenas uma palavra sendo usada para este conceito em todo o módulo?
    2. O nome revela a intenção de forma que o uso de um comentário seria redundante?
    3. Evitamos trocadilhos (puns), como usar o mesmo verbo para ações semanticamente diferentes?

No código abaixo, o desenvolvedor usa nomes genéricos que exigem comentários, além de usar o verbo add com dois sentidos completamente diferentes: adicionar um elemento a uma lista interna e registrar uma entidade no banco de dados.

Trecho de códigotypescript
class Group {
  private items: string[] = [];

  // d: número de dias desde a criação
  public d: number = 0;

  // Trocadilho: "add" aqui insere na lista em memória
  public add(item: string): void {
    this.items.push(item);
  }

  // Trocadilho: "add" aqui realiza uma operação de banco de dados (I/O) complexa
  public addInDatabase(user: any): void {
    db.insert(user);
  }
}

Após aplicar os questionamentos a e b e refatorar o código, os nomes expressam claramente suas intenções sem a necessidade de comentários. O verbo add é reservado estritamente para a inserção em coleções na memória (comportamento de lista), enquanto register é usado para persistência no banco de dados.

Trecho de códigotypescript
class UserGroup {
  private userIds: string[] = [];
  public daysSinceCreation: number = 0;

  // Semântica correta para coleções em memória
  public add(userId: string): void {
    this.userIds.push(userId);
  }

  // Semântica de persistência clara, sem trocadilho com o método 'add'
  public async register(user: User): Promise<void> {
    await this.database.save(user);
  }
}
  • Estrutura e Consistência
    1. Esta implementação segue o padrão exato de funções ou módulos similares no sistema?
    2. O código segue estritamente as regras de formatação visual soberanas do time?
    3. O código parece ter sido escrito pelo mesmo autor que o restante do projeto?

Em um mesmo projeto, dois controladores lidam com requisições HTTP e erros de maneiras totalmente diferentes. O primeiro usa um bloco try/catch manual e retorna respostas de erro ad-hoc; o segundo usa um tratamento implícito esperando que um middleware capture o erro, gerando inconsistência de comportamento.

Trecho de códigotypescript
// Controlador A (Tratamento manual de erro)
class UserController {
  async handleRequest(req: Request, res: Response) {
    try {
      const users = await this.service.getAll();
      return res.status(200).json(users);
    } catch (error) {
      return res.status(500).json({ error: 'Erro interno', message: error.message });
    }
  }
}

// Controlador B (Tratamento inconsistente, assume que outro componente trata o erro)
class ProductController {
  async handleRequest(req: Request, res: Response) {
    const products = await this.service.getAll(); // Se falhar, quebra a aplicação ou depende de middleware oculto
    return res.status(200).json({ data: products }); // Estrutura de retorno JSON diferente de UserController!
  }
}

Após aplicar o questionamento a, Ambos os controladores seguem exatamente a mesma convenção de retorno, tratamento de erros e encapsulamento de resposta HTTP. Parece ter sido escrito pela mesma pessoa (ou pelo mesmo time).

Trecho de códigotypescript
// Base Controller abstrato para forçar a consistência estrutural
abstract class BaseController {
  protected sendSuccess<T>(res: Response, data: T, statusCode = 200): Response {
    return res.status(statusCode).json({ success: true, data });
  }

  protected sendError(res: Response, message: string, statusCode = 500): Response {
    return res.status(statusCode).json({ success: false, error: message });
  }
}

class UserController extends BaseController {
  async handleRequest(req: Request, res: Response) {
    try {
      const users = await this.service.getAll();
      return this.sendSuccess(res, users);
    } catch (error) {
      return this.sendError(res, 'Falha ao buscar usuários');
    }
  }
}

class ProductController extends BaseController {
  async handleRequest(req: Request, res: Response) {
    try {
      const products = await this.service.getAll();
      return this.sendSuccess(res, products);
    } catch (error) {
      return this.sendError(res, 'Falha ao buscar produtos');
    }
  }
}
  • Arquitetura e SOLID
    1. SRP: O módulo é responsável por apenas um único Ator e possui apenas uma razão para mudar?
    2. OCP: Esta nova funcionalidade foi adicionada criando novos arquivos em vez de modificar lógicas complexas existentes?
    3. DIP: As dependências do código-fonte apontam para abstrações e políticas de alto nível, longe dos detalhes de IO?
    4. Testabilidade: O código é refutável? Podemos provar sua inexatidão através de testes automatizados?

A classe Employee é responsável por três atores distintos: o departamento de finanças (cálculo de salário), o departamento de RH (controle de horas/relatório) e a administração de banco de dados (salvar informações). Ela possui múltiplos motivos para mudar.

Trecho de códigotypescript
class Employee {
  constructor(public id: string, public name: string, public hourlyRate: number) {}

  // Ator: Finanças (Razão de mudança 1)
  public calculatePay(hoursWorked: number): number {
    return hoursWorked * this.hourlyRate;
  }

  // Ator: Recursos Humanos (Razão de mudança 2)
  public generateHoursReport(hoursWorked: number): string {
    return `Relatório de Horas do Funcionário ${this.name}: ${hoursWorked} horas trabalhadas.`;
  }

  // Ator: DBA / Infraestrutura (Razão de mudança 3)
  public async saveToDatabase(): Promise<void> {
    await db.query(`INSERT INTO employees VALUES (${this.id}, ${this.name})`);
  }
}

Seguindo a partir do questionamento a, cada classe agora tem apenas um Ator e uma única razão para mudar. A entidade de domínio apenas guarda os dados e as regras puras de negócio, enquanto as operações de cálculo de folha, geração de relatórios e persistência são delegadas a componentes específicos.

Trecho de códigotypescript
// Entidade de domínio simples (Representação de dados e regras de domínio puras)
class Employee {
  constructor(
    public readonly id: string,
    public readonly name: string,
    public readonly hourlyRate: number
  ) {}
}

// Classe focada no Ator: Finanças
class PayrollCalculator {
  public calculatePay(employee: Employee, hoursWorked: number): number {
    return hoursWorked * employee.hourlyRate;
  }
}

// Classe focada no Ator: Recursos Humanos
class EmployeeReporter {
  public generateHoursReport(employee: Employee, hoursWorked: number): string {
    return `Relatório de Horas do Funcionário ${employee.name}: ${hoursWorked} horas trabalhadas.`;
  }
}

// Classe focada no Ator: Infraestrutura/DBA
class EmployeeRepository {
  public async save(employee: Employee): Promise<void> {
    await db.query(`INSERT INTO employees VALUES (${employee.id}, ${employee.name})`);
  }
}

A classe PaymentProcessor precisa ser modificada toda vez que um novo método de pagamento é adicionado ao sistema. Adicionar 'Pix' exigiria mexer diretamente no método processPayment existente, testá-lo novamente por completo e correr o risco de quebrar os fluxos estáveis de Boleto ou Cartão de Crédito.

Trecho de códigotypescript
class PaymentProcessor {
  public processPayment(method: string, amount: number): void {
    switch (method) {
      case 'CREDIT_CARD':
        console.log(`Processando R$ ${amount} no Cartão de Crédito...`);
        // Lógica de integração específica do gateway de cartão
        break;
      case 'BOLETO':
        console.log(`Gerando boleto no valor de R$ ${amount}...`);
        // Lógica específica de emissão de boleto
        break;
      // Se quisermos adicionar PIX, teremos que modificar esta classe!
      default:
        throw new Error('Método de pagamento não suportado.');
    }
  }
}

Após o questionamento b, o sistema é fechado para modificação de código existente, mas aberto para expansão. Criamos uma abstração IPaymentStrategy. Para adicionar novos meios de pagamento (como o Pix), basta criar um arquivo com uma nova classe que implementa essa interface — sem alterar as classes existentes que já funcionam de forma estável.

Trecho de códigotypescript
// Abstração que dita o contrato para qualquer forma de pagamento
interface IPaymentStrategy {
  process(amount: number): void;
}

// Implementações específicas e isoladas em arquivos próprios
class CreditCardPayment implements IPaymentStrategy {
  public process(amount: number): void {
    console.log(`Processando R$ ${amount} no Cartão de Crédito.`);
    // Lógica isolada do gateway de cartão
  }
}

class BoletoPayment implements IPaymentStrategy {
  public process(amount: number): void {
    console.log(`Gerando boleto no valor de R$ ${amount}.`);
    // Lógica isolada de emissão de boleto
  }
}

// NOVA FUNCIONALIDADE: Adicionada em um arquivo totalmente novo, sem mexer nas lógicas antigas!
class PixPayment implements IPaymentStrategy {
  public process(amount: number): void {
    console.log(`Gerando QR Code Pix de R$ ${amount}.`);
    // Lógica isolada de geração de Pix
  }
}

// Classe processadora estável que não precisa mudar quando novos meios de pagamento surgem
class PaymentService {
  public executePayment(paymentMethod: IPaymentStrategy, amount: number): void {
    paymentMethod.process(amount);
  }
}

A classe de alto nível OrderService depende diretamente de uma implementação concreta de banco de dados (MySqlConnection) e de um serviço de envio de e-mails (SendGridService). Não é possível testar esta classe de forma isolada (sem conectar ao DB/servidor de e-mail real) e qualquer mudança na tecnologia de persistência exigirá alterações no fluxo de negócio.

Trecho de códigotypescript
import { MySqlConnection } from './infra/MySqlConnection';
import { SendGridService } from './infra/SendGridService';

class OrderService {
  private dbConnection: MySqlConnection;
  private emailService: SendGridService;

  constructor() {
    this.dbConnection = new MySqlConnection(); // Dependência acoplada diretamente
    this.emailService = new SendGridService(); // Dependência acoplada diretamente
  }

  async checkout(orderId: string, userEmail: string): Promise<void> {
    const order = await this.dbConnection.findOrder(orderId);
    order.markAsPaid();
    await this.dbConnection.saveOrder(order);
    await this.emailService.send(userEmail, 'Seu pedido foi confirmado!');
  }
}

Com o questionamento c, a classe de negócio OrderService agora depende exclusivamente de interfaces (abstrações de alto nível). As dependências reais (detalhes de implementação) são injetadas no construtor. Isso permite substituir facilmente o banco de dados, o serviço de envio de e-mail ou simular mock para testes unitários em milissegundos.

Trecho de códigotypescript
// Abstrações de alto nível (definidas próximas às regras de negócio)
interface IOrderRepository {
  findOrder(id: string): Promise<Order>;
  saveOrder(order: Order): Promise<void>;
}

interface IEmailService {
  send(to: string, message: string): Promise<void>;
}

// Regra de negócio isolada dos detalhes de infraestrutura
class OrderService {
  constructor(
    private readonly orderRepository: IOrderRepository, // Depende de abstração
    private readonly emailService: IEmailService       // Depende de abstração
  ) {}

  async checkout(orderId: string, userEmail: string): Promise<void> {
    const order = await this.orderRepository.findOrder(orderId);
    order.markAsPaid();
    await this.orderRepository.saveOrder(order);
    await this.emailService.send(userEmail, 'Seu pedido foi confirmado!');
  }
}

Como você pode perceber, para todas as primeiras situações dos códigos, a resposta para as perguntas foi não. Apesar de nem sempre termos de forma assertiva os principais pontos a serem consertados, questionar-se permite ter um norte. É nesse momento que acredito ser válido utilizar um agente de IA para debater soluções para cada contexto. Siglas como SRP, OCP, DIP parecem coisas de outro mundo, entretanto, não são nada além de princípios basilares que muitas das vezes (a depender do seu nível) já são aplicados inconscientemente durante a programação. Se você tiver interesse nesses tópicos, basta pesquisar sobre o Clean Code e SOLID.

O último questionamento, que se refere à testabilidade, será debatido em um próximo encontro. Por quê? Testes são um assunto gigantesco, e quero trabalhar esse conceito e suas estratégias com bastante calma. Se você leu e entendeu o que debatemos aqui, tente aplicar na prática. Com certeza você perceberá que escrever um código de qualidade não é tão complicado quanto parece. Até a próxima