Padrões Criacionais
Baixar PDFFactory, 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());
}
}