DEV Community

Cover image for Design patterns na era da IA: Ainda vale estudar?

Design patterns na era da IA: Ainda vale estudar?

Escrever código está mudando, mas a qualidade deveria cair?

Entenda, não estou aqui para criticar o código gerado pelas LLMs, afinal, ela provavelmente escreve muito melhor que eu, desde que receba as instruções corretas.

E é justamente aí que mora o problema. Nessa nova era do desenvolvimento, onde todo mundo está fascinado com o que a IA consegue fazer, pouca gente está prestando atenção no que está pedindo pra ela. Quando você pede algo genérico, a IA entrega algo genérico. Funciona? Funciona. Mas sem querer, você pode estar gerando um backlog de débito técnico silencioso na sua base de código.

Design patterns

Lembram quando os maias precisavam estudar design patterns para resolver problemas de arquitetura e falar a mesma língua que o resto do time? Pois é, estudar patterns continua importante, mas o motivo principal mudou. Não é mais só sobre comunicação entre devs, embora isso ainda importe. Agora, patterns funcionam como instruções de alta precisão para a IA. São a diferença entre receber código que "funciona" e código que é extensível, testável e que não vai te assombrar em seis meses.

Exemplo

Imagine que você vai criar um sistema de processamento de pagamentos. Você tem cartão, boleto e PIX. Cada tipo de pagamento tem regras diferentes, embora no final todos tenham a mesma finalidade, cobrar o cliente. Para isso, a gente poderia optar pelo padrão Strategy.

Agora vamos pensar em como um dev sem esse conhecimento pediria isso pra IA.
Ele descreve o que precisa, e só:

"Crie um serviço em NestJS que processa pagamentos via cartão de crédito,
boleto e PIX."
Enter fullscreen mode Exit fullscreen mode

A IA vai entregar algo funcional, provavelmente um PaymentService com um método cheio de condicionais. Vai funcionar? Vai. Mas pensando em manutenção futura é horrível, difícil de testar.

Código da service gerado pela IA (Sonnet 4.6 no Claude Code):

import { BadRequestException, Injectable } from "@nestjs/common";
import { ProcessPaymentDto } from "./dto/process-payment.dto";

@Injectable()
export class PaymentsService {
  process(dto: ProcessPaymentDto) {
    switch (dto.method) {
      case "credit_card":
        return this.processCreditCard(dto.amount, dto.description);
      case "boleto":
        return this.processBoleto(dto.amount, dto.description);
      case "pix":
        return this.processPix(dto.amount, dto.description);
      default:
        throw new BadRequestException(
          `Método de pagamento inválido: ${(dto as any).method}`,
        );
    }
  }

  private processCreditCard(amount: number, description?: string) {
    return {
      method: "credit_card",
      status: "approved",
      amount,
      description,
      authorizationCode: this.generateCode(),
      processedAt: new Date().toISOString(),
    };
  }

  private processBoleto(amount: number, description?: string) {
    const dueDate = new Date();
    dueDate.setDate(dueDate.getDate() + 3);

    return {
      method: "boleto",
      status: "pending",
      amount,
      description,
      barcode: this.generateBarcode(),
      dueDate: dueDate.toISOString(),
      processedAt: new Date().toISOString(),
    };
  }

  private processPix(amount: number, description?: string) {
    return {
      method: "pix",
      status: "approved",
      amount,
      description,
      txid: this.generateCode(),
      pixKey: "pagamentos@exemplo.com",
      processedAt: new Date().toISOString(),
    };
  }

  private generateCode(): string {
    return Math.random().toString(36).substring(2, 14).toUpperCase();
  }

  private generateBarcode(): string {
    return Array.from({ length: 44 }, () =>
      Math.floor(Math.random() * 10),
    ).join("");
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora pensa comigo num prompt feito por alguém que sabe o que quer, e sabe dizer como quer:

"Crie um serviço de processamento de pagamento em NestJS, usando o pattern Strategy.
Defina uma interface PaymentStrategy com um método process(). Implemente uma classe
concreta para cada meio de pagamento (cartão, boleto, PIX).
Use o módulo de injeção de dependência do NestJS para resolver a estratégia correta
em tempo de execução."
Enter fullscreen mode Exit fullscreen mode

A diferença aqui fica clara. Não estamos falando apenas de ter citado o nome do padrão. Estamos falando de passar pra IA uma planta arquitetural. Ela sabe exatamente o que você espera.

O resultado? O output gerado vai ser extensível: precisa adicionar outra forma de pagamento como Apple Pay ou Stripe? É só criar uma nova classe. Nenhum outro método é afetado, o que reduz drasticamente a chance de efeito colateral. E cada meio de pagamento pode ser testado de forma isolada, com seu próprio teste unitário.

import { BadRequestException, Inject, Injectable } from "@nestjs/common";
import { PaymentStrategy } from "./interfaces/payment-strategy.interface";
import { CreatePaymentDto } from "./dto/create-payment.dto";

export const PAYMENT_STRATEGIES = "PAYMENT_STRATEGIES";

@Injectable()
export class PaymentsService {
  constructor(
    @Inject(PAYMENT_STRATEGIES)
    private readonly strategies: PaymentStrategy[],
  ) {}

  process(dto: CreatePaymentDto) {
    const strategy = this.strategies.find((s) => s.method === dto.method);

    if (!strategy) {
      throw new BadRequestException(
        `Método de pagamento inválido: ${dto.method}`,
      );
    }

    return strategy.process(dto.amount);
  }
}
Enter fullscreen mode Exit fullscreen mode

Interface da nossa Strategy:

export interface PaymentResult {
  transactionId: string;
  method: string;
  amount: number;
  status: string;
}

export interface PaymentStrategy {
  readonly method: string;
  process(amount: number): PaymentResult;
}
Enter fullscreen mode Exit fullscreen mode

Estratégias concretas:

BoletoStrategy:

import { Injectable } from "@nestjs/common";
import {
  PaymentResult,
  PaymentStrategy,
} from "../interfaces/payment-strategy.interface";

@Injectable()
export class BoletoStrategy implements PaymentStrategy {
  readonly method = "boleto";

  process(amount: number): PaymentResult {
    return {
      transactionId: `BOL-${Math.floor(Math.random() * 10000)}`,
      method: this.method,
      amount,
      status: "pending",
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

CreditCardStrategy:

import { Injectable } from "@nestjs/common";
import {
  PaymentResult,
  PaymentStrategy,
} from "../interfaces/payment-strategy.interface";

@Injectable()
export class CreditCardStrategy implements PaymentStrategy {
  readonly method = "credit_card";

  process(amount: number): PaymentResult {
    return {
      transactionId: `CC-${Math.floor(Math.random() * 10000)}`,
      method: this.method,
      amount,
      status: "approved",
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

PixStrategy:

import { Injectable } from "@nestjs/common";
import {
  PaymentResult,
  PaymentStrategy,
} from "../interfaces/payment-strategy.interface";

@Injectable()
export class PixStrategy implements PaymentStrategy {
  readonly method = "pix";

  process(amount: number): PaymentResult {
    return {
      transactionId: `PIX-${Math.floor(Math.random() * 10000)}`,
      method: this.method,
      amount,
      status: "approved",
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Lembrando que isso aqui é um código de exemplo, não vai gerar pix, fiquem tranquilos.

Cenas dos próximos capítulos

Esse é apenas o começo. Nos próximos posts dessa série, vou explorar outros design patterns e repetir o mesmo exercício: prompt genérico vs. prompt com pattern, lado a lado. O objetivo é sempre o mesmo, te provocar a escrever prompts melhores, que geram código que não vai virar uma bola de neve nos seus projetos.

A ideia é simples: mostrar que na era do desenvolvimento assistido por IA, saber o que pedir é tão importante quanto saber codar.

Mais em: https://ojeremias.dev/

Top comments (0)