Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptDesign Patterns

Padrões Criacionais

Factory, Singleton, Builder e Abstract Factory em TypeScript — como criar objetos de forma controlada e flexível.

Padrões Criacionais em TypeScript

Padrões criacionais respondem à pergunta: "Como devo criar este objeto?". Eles encapsulam a lógica de instanciação para que o código cliente não precise saber os detalhes.


Factory Pattern

O que é

Em vez de usar new diretamente, você usa uma função/método que decide qual implementação criar baseado em algum critério.

Analogia

Um restaurante com cardápio: você pede "prato vegetariano", e a cozinha decide se prepara risoto, salada ou lasanha. Você não precisa saber qual ingrediente vai em cada um.

Frontend — Component factory

// ❌ Sem factory — if/else em todo lugar
function NotificationDisplay({ type, message }: { type: string; message: string }) {
  if (type === 'success') {
    return <div className="bg-green-100 border-green-500 text-green-800 p-4 rounded">{message}</div>;
  } else if (type === 'error') {
    return <div className="bg-red-100 border-red-500 text-red-800 p-4 rounded">{message}</div>;
  } else if (type === 'warning') {
    return <div className="bg-yellow-100 border-yellow-500 text-yellow-800 p-4 rounded">{message}</div>;
  }
  return <div className="bg-gray-100 border-gray-500 text-gray-800 p-4 rounded">{message}</div>;
}

// ✅ Factory — cada variante isolada
const notificationStyles: Record<string, string> = {
  success: 'bg-green-100 border-green-500 text-green-800',
  error: 'bg-red-100 border-red-500 text-red-800',
  warning: 'bg-yellow-100 border-yellow-500 text-yellow-800',
  info: 'bg-gray-100 border-gray-500 text-gray-800',
};

function createNotificationStyle(type: string): string {
  return notificationStyles[type] ?? notificationStyles.info;
}

function NotificationDisplay({ type, message }: { type: string; message: string }) {
  return <div className={`${createNotificationStyle(type)} p-4 rounded`}>{message}</div>;
}

Backend — Factory para pagamentos

// contracts/payment-processor.ts
interface PaymentProcessor {
  readonly method: string;
  process(amount: number, details: PaymentDetails): Promise<PaymentResult>;
  refund(transactionId: string): Promise<RefundResult>;
}

// Implementações
class CreditCardProcessor implements PaymentProcessor {
  readonly method = 'credit_card';
  async process(amount: number, details: CreditCardDetails) { /* ... */ }
  async refund(transactionId: string) { /* ... */ }
}

class PixProcessor implements PaymentProcessor {
  readonly method = 'pix';
  async process(amount: number, details: PixDetails) { /* ... */ }
  async refund(transactionId: string) { /* ... */ }
}

class BoletoProcessor implements PaymentProcessor {
  readonly method = 'boleto';
  async process(amount: number, details: BoletoDetails) { /* ... */ }
  async refund(transactionId: string) { /* ... */ }
}

// Factory — quem chama não precisa saber qual classe usar
class PaymentProcessorFactory {
  private static processors = new Map<string, () => PaymentProcessor>([
    ['credit_card', () => new CreditCardProcessor()],
    ['pix', () => new PixProcessor()],
    ['boleto', () => new BoletoProcessor()],
  ]);

  static create(method: string): PaymentProcessor {
    const factory = this.processors.get(method);
    if (!factory) {
      throw new Error(`Método de pagamento não suportado: ${method}`);
    }
    return factory();
  }

  static register(method: string, factory: () => PaymentProcessor): void {
    this.processors.set(method, factory);
  }
}

// Uso no service
class CheckoutService {
  async processPayment(method: string, amount: number, details: PaymentDetails) {
    const processor = PaymentProcessorFactory.create(method); // ✅ não usa new diretamente
    return processor.process(amount, details);
  }
}

Singleton Pattern

O que é

Garante que uma classe tenha apenas uma instância e forneça um ponto de acesso global a ela.

Analogia

O presidente de um país: existe apenas um por vez. Todo mundo acessa o mesmo presidente, não cria um novo.

Quando usar

  • Conexões de banco de dados
  • Cache em memória
  • Logger
  • Configurações globais

Quando NÃO usar

  • Quase sempre. Singleton é considerado um anti-pattern por muitos porque cria acoplamento global e dificulta testes. Prefira injeção de dependência.

Backend — Logger singleton (com ressalvas)

// ❌ Singleton clássico — difícil de testar, acoplamento global
class Logger {
  private static instance: Logger;

  private constructor() {} // impede new Logger()

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  info(message: string) { console.log(`[INFO] ${message}`); }
  error(message: string) { console.error(`[ERROR] ${message}`); }
}

// Qualquer lugar do código:
Logger.getInstance().info('Usuário logado'); // ❌ acoplamento global

// ✅ Melhor: singleton via módulo ES (instância única por natureza)
// logger.ts
export const logger = {
  info: (message: string) => console.log(`[INFO] ${message}`),
  error: (message: string, error?: Error) => console.error(`[ERROR] ${message}`, error),
  warn: (message: string) => console.warn(`[WARN] ${message}`),
};

// Qualquer lugar:
import { logger } from './logger'; // ✅ import simples, fácil de mockar em testes
logger.info('Usuário logado');

Frontend — Config singleton

// ❌ Singleton clássico
class AppConfig {
  private static instance: AppConfig;
  private constructor(public apiUrl: string, public version: string) {}

  static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig(
        process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000',
        process.env.NEXT_PUBLIC_VERSION ?? '1.0.0',
      );
    }
    return AppConfig.instance;
  }
}

// ✅ Melhor: módulo ES com congelamento
export const appConfig = Object.freeze({
  apiUrl: process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000',
  version: process.env.NEXT_PUBLIC_VERSION ?? '1.0.0',
});

Builder Pattern

O que é

Separa a construção de um objeto complexo de sua representação, permitindo criar diferentes tipos e representações com o mesmo código de construção.

Analogia

Um restaurante fast-food com combos: você monta o pedido passo a passo — "hambúrguer + batata + refrigerante" ou "hambúrguer + salva + água". O builder garante que a montagem é válida.

Frontend — Builder para formulários dinâmicos

// Builder para configurar formulários complexos
class FormBuilder {
  private fields: FormField[] = [];
  private validation: ValidationRule[] = [];
  private layout: LayoutConfig = { columns: 1, gap: 16 };

  addTextField(name: string, label: string): this {
    this.fields.push({ type: 'text', name, label, required: false });
    return this; // permite chaining
  }

  addSelectField(name: string, label: string, options: string[]): this {
    this.fields.push({ type: 'select', name, label, options });
    return this;
  }

  require(name: string, message = 'Campo obrigatório'): this {
    this.validation.push({ field: name, rule: 'required', message });
    return this;
  }

  withLayout(columns: number, gap = 16): this {
    this.layout = { columns, gap };
    return this;
  }

  build(): FormConfig {
    return { fields: this.fields, validation: this.validation, layout: this.layout };
  }
}

// Uso — legível e flexível
const checkoutForm = new FormBuilder()
  .addTextField('name', 'Nome completo')
  .require('name')
  .addTextField('email', 'Email')
  .require('name', 'Email é obrigatório')
  .addSelectField('paymentMethod', 'Pagamento', ['pix', 'credit_card', 'boleto'])
  .withLayout(2, 20)
  .build();

Backend — Builder para queries

// Builder para construir queries de forma segura
class QueryBuilder {
  private filters: Filter[] = [];
  private sortField: string | null = null;
  private sortDirection: 'asc' | 'desc' = 'asc';
  private limitValue = 20;
  private offsetValue = 0;

  where(field: string, operator: string, value: unknown): this {
    this.filters.push({ field, operator, value });
    return this;
  }

  sortBy(field: string, direction: 'asc' | 'desc' = 'asc'): this {
    this.sortField = field;
    this.sortDirection = direction;
    return this;
  }

  limit(value: number): this {
    this.limitValue = value;
    return this;
  }

  offset(value: number): this {
    this.offsetValue = value;
    return this;
  }

  build(): Query {
    return {
      filters: this.filters,
      sort: this.sortField ? { field: this.sortField, direction: this.sortDirection } : null,
      limit: this.limitValue,
      offset: this.offsetValue,
    };
  }
}

// Uso — legível
const query = new QueryBuilder()
  .where('status', '=', 'active')
  .where('age', '>=', 18)
  .sortBy('createdAt', 'desc')
  .limit(10)
  .offset(0)
  .build();

const users = await userRepository.find(query);

Abstract Factory

O que é

Cria famílias de objetos relacionados sem especificar as classes concretas. É uma "factory de factories".

Backend — Factory para repositórios por banco de dados

// Família de repositórios por banco de dados
interface RepositoryFactory {
  createUserRepository(): UserRepository;
  createOrderRepository(): OrderRepository;
  createProductRepository(): ProductRepository;
}

// Implementação MongoDB
class MongoRepositoryFactory implements RepositoryFactory {
  constructor(private db: MongoDatabase) {}

  createUserRepository() { return new MongoUserRepository(this.db); }
  createOrderRepository() { return new MongoOrderRepository(this.db); }
  createProductRepository() { return new MongoProductRepository(this.db); }
}

// Implementação PostgreSQL
class PostgresRepositoryFactory implements RepositoryFactory {
  constructor(private prisma: PrismaClient) {}

  createUserRepository() { return new PostgresUserRepository(this.prisma); }
  createOrderRepository() { return new PostgresOrderRepository(this.prisma); }
  createProductRepository() { return new PostgresProductRepository(this.prisma); }
}

// Bootstrap — quem decide qual família usar
function createRepositoryFactory(): RepositoryFactory {
  switch (process.env.DB_DRIVER) {
    case 'postgres':
      return new PostgresRepositoryFactory(new PrismaClient());
    case 'mongo':
    default:
      return new MongoRepositoryFactory(connectMongo());
  }
}

Referências

On this page