Padrões Estruturais
Baixar PDFAdapter, Decorator, Facade e Composite em TypeScript — como compor e organizar classes de forma limpa.
Padrões Estruturais em TypeScript
Padrões estruturais respondem à pergunta: "Como devo organizar e compor objetos?". Eles lidam com como classes e objetos são compostos para formar estruturas maiores.
Adapter Pattern
O que é
Converte a interface de um objeto em outra interface que o cliente espera. É como um adaptador de tomada: você conecta um plugue europeu em uma tomada brasileira.
Quando usar
- Integrar bibliotecas de terceiros com interface diferente da sua
- Adaptar APIs legadas para interfaces modernas
- Conectar sistemas que não foram feitos para conversar entre si
Frontend — Adapter para biblioteca de gráficos
// Sua interface interna — como seu app pensa em dados
interface ChartData {
labels: string[];
values: number[];
colors?: string[];
}
// Biblioteca de terceiros com interface diferente
interface ExternalChartLibrary {
setCategories(categories: string[]): void;
setSeries(data: { name: string; data: number[] }[]): void;
setTheme(theme: { palette: string[] }): void;
render(container: HTMLElement): void;
}
// Adapter — traduz sua interface para a da biblioteca
class ChartLibraryAdapter {
constructor(private lib: ExternalChartLibrary) {}
render(container: HTMLElement, data: ChartData): void {
this.lib.setCategories(data.labels);
this.lib.setSeries([{ name: 'Valores', data: data.values }]);
if (data.colors) {
this.lib.setTheme({ palette: data.colors });
}
this.lib.render(container);
}
}
// Seu componente só conhece ChartData — não a biblioteca externa
function SalesChart({ data }: { data: ChartData }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (containerRef.current) {
const adapter = new ChartLibraryAdapter(externalChartLib);
adapter.render(containerRef.current, data);
}
}, [data]);
return <div ref={containerRef} />;
}Backend — Adapter para APIs externas
// Sua interface — como seu app pensa em clima
interface WeatherService {
getTemperature(city: string): Promise<{ temp: number; unit: 'C' }>;
getForecast(city: string, days: number): Promise<DailyForecast[]>;
}
// API externa com interface completamente diferente
interface OpenWeatherAPI {
getWeather(params: { q: string; units: string; appid: string }): Promise<{
main: { temp: number; feels_like: number };
weather: { description: string }[];
}>;
}
// Adapter
class OpenWeatherAdapter implements WeatherService {
constructor(private api: OpenWeatherAPI, private apiKey: string) {}
async getTemperature(city: string) {
const response = await this.api.getWeather({
q: city,
units: 'metric',
appid: this.apiKey,
});
return { temp: response.main.temp, unit: 'C' as const };
}
async getForecast(city: string, days: number): Promise<DailyForecast[]> {
// Traduz a interface da API externa para a sua
const response = await this.api.getForecast({ q: city, cnt: days, appid: this.apiKey });
return response.list.map(day => ({
date: new Date(day.dt * 1000),
tempMax: day.temp.max,
tempMin: day.temp.min,
description: day.weather[0].description,
}));
}
}
// Seu service só conhece WeatherService — não OpenWeatherAPI
class WeatherController {
constructor(private weather: WeatherService) {} // ✅ abstração
async getCityWeather(req: Request, res: Response) {
const temp = await this.weather.getTemperature(req.params.city);
res.json(temp);
}
}Decorator Pattern
O que é
Adiciona responsabilidades a um objeto dinamicamente, sem alterar sua classe. É como colocar um capa em um celular: o celular funciona igual, mas agora tem proteção extra.
Analogia TypeScript
O Decorator do TypeScript (@decorator) é uma feature da linguagem inspirada neste pattern, mas o pattern clássico funciona via composição.
Backend — Decorator para repositório com cache
// Interface base
interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
create(data: CreateUserDto): Promise<User>;
}
// Implementação base
class MongoUserRepository implements UserRepository {
async findById(id: string) { /* ... */ }
async findByEmail(email: string) { /* ... */ }
async create(data: CreateUserDto) { /* ... */ }
}
// Decorator — adiciona cache sem modificar o repositório original
class CachedUserRepository implements UserRepository {
private cache = new Map<string, User>();
constructor(private inner: UserRepository) {} // ✅ composição
async findById(id: string): Promise<User | null> {
if (this.cache.has(id)) {
return this.cache.get(id)!;
}
const user = await this.inner.findById(id); // delega para o original
if (user) {
this.cache.set(id, user);
}
return user;
}
async findByEmail(email: string): Promise<User | null> {
return this.inner.findByEmail(email); // sem cache para este método
}
async create(data: CreateUserDto): Promise<User> {
const user = await this.inner.create(data);
this.cache.set(user.id, user);
return user;
}
}
// Decorator para logging
class LoggingUserRepository implements UserRepository {
constructor(private inner: UserRepository, private logger: Logger) {}
async findById(id: string): Promise<User | null> {
this.logger.info(`Buscando usuário: ${id}`);
const start = Date.now();
const user = await this.inner.findById(id);
this.logger.info(`Busca concluída em ${Date.now() - start}ms`);
return user;
}
// ... outros métodos delegam com logging
}
// Uso — empilha decorators como camadas
const repo = new LoggingUserRepository(
new CachedUserRepository(
new MongoUserRepository(), // repositório original
),
logger,
);Frontend — Decorator para componentes com loading
// HOC (Higher-Order Component) — o "decorator" do React
function withLoading<P extends object>(
Component: React.ComponentType<P>,
LoadingComponent: React.ComponentType = () => <div>Carregando...</div>,
) {
return function WithLoading(props: P & { isLoading: boolean }) {
const { isLoading, ...rest } = props;
if (isLoading) return <LoadingComponent />;
return <Component {...(rest as P)} />;
};
}
// Componente original
function UserProfile({ user }: { user: User }) {
return <div>{user.name} — {user.email}</div>;
}
// Componente "decorado" — ganha loading automaticamente
const UserProfileWithLoading = withLoading(UserProfile);
// Uso
<UserProfileWithLoading user={user} isLoading={isLoading} />Facade Pattern
O que é
Fornece uma interface simplificada para um subsistema complexo. É como o painel de um carro: você pressiona um botão e o carro liga — não precisa saber sobre injeção eletrônica, bomba de combustível e motor de partida.
Backend — Facade para checkout
// Subsistemas complexos
class InventoryService {
async checkStock(productId: string, quantity: number): Promise<boolean> { /* ... */ }
async reserve(productId: string, quantity: number): Promise<void> { /* ... */ }
}
class PaymentService {
async charge(method: string, amount: number): Promise<PaymentResult> { /* ... */ }
async refund(transactionId: string): Promise<void> { /* ... */ }
}
class ShippingService {
async calculateRate(address: Address, weight: number): Promise<number> { /* ... */ }
async createShipment(order: Order): Promise<TrackingCode> { /* ... */ }
}
class NotificationService {
async sendOrderConfirmation(user: User, order: Order): Promise<void> { /* ... */ }
}
// Facade — o cliente vê UMA interface simples
class CheckoutFacade {
constructor(
private inventory: InventoryService,
private payment: PaymentService,
private shipping: ShippingService,
private notifications: NotificationService,
) {}
async checkout(cart: Cart, user: User, paymentMethod: string): Promise<Order> {
// 1. Verifica estoque
for (const item of cart.items) {
const available = await this.inventory.checkStock(item.productId, item.quantity);
if (!available) throw new InsufficientStockError(item.productId);
}
// 2. Cria o pedido
const order = await this.orderService.create(cart, user);
// 3. Cobre o pagamento
const payment = await this.payment.charge(paymentMethod, order.total);
if (!payment.success) throw new PaymentDeclinedError(payment.reason);
// 4. Cria envio
const tracking = await this.shipping.createShipment(order);
// 5. Notifica o cliente
await this.notifications.sendOrderConfirmation(user, order);
return { ...order, trackingCode: tracking };
}
}
// Controller — vê UMA chamada, não 4 serviços
class CheckoutController {
constructor(private checkout: CheckoutFacade) {}
async handle(req: Request, res: Response) {
const order = await this.checkout.checkout(req.body.cart, req.user, req.body.paymentMethod);
res.json(order);
}
}Frontend — Facade para gerenciamento de estado
// Subsistemas complexos
class CartStorage {
get(): CartItem[] { /* localStorage */ }
set(items: CartItem[]): void { /* localStorage */ }
}
class CartValidator {
validate(items: CartItem[]): ValidationResult { /* ... */ }
}
class CartCalculator {
calculate(items: CartItem[]): CartTotals { /* ... */ }
}
// Facade
class CartFacade {
constructor(
private storage: CartStorage,
private validator: CartValidator,
private calculator: CartCalculator,
) {}
getCart(): Cart {
const items = this.storage.get();
const validation = this.validator.validate(items);
const totals = this.calculator.calculate(items);
return { items, ...totals, isValid: validation.isValid, errors: validation.errors };
}
addItem(product: Product, quantity: number): Cart {
const items = this.storage.get();
const existing = items.find(i => i.productId === product.id);
if (existing) {
existing.quantity += quantity;
} else {
items.push({ productId: product.id, name: product.name, price: product.price, quantity });
}
this.storage.set(items);
return this.getCart();
}
removeItem(productId: string): Cart {
const items = this.storage.get().filter(i => i.productId !== productId);
this.storage.set(items);
return this.getCart();
}
}
// Hook — componente vê interface simples
function useCart() {
const facade = useMemo(() => new CartFacade(new CartStorage(), new CartValidator(), new CartCalculator()), []);
const [cart, setCart] = useState(() => facade.getCart());
return {
cart,
addItem: (product: Product, qty: number) => setCart(facade.addItem(product, qty)),
removeItem: (id: string) => setCart(facade.removeItem(id)),
};
}Composite Pattern
O que é
Permite tratar objetos individuais e composições de objetos de forma uniforme. É como uma pasta de arquivos: uma pasta pode conter arquivos e outras pastas, mas você pode listar o conteúdo de ambos da mesma forma.
Frontend — Menu/nav tree
// Componente individual
interface MenuItem {
label: string;
href?: string;
children?: MenuItem[]; // composição — pode ter filhos
}
// Dados em árvore
const navigation: MenuItem[] = [
{ label: 'Início', href: '/' },
{
label: 'Mercado Financeiro',
children: [
{ label: 'FIIs', href: '/mercado-financeiro/fii' },
{ label: 'ETFs', href: '/mercado-financeiro/ativos/etf' },
{
label: 'Ativos',
children: [
{ label: 'Ações', href: '/mercado-financeiro/ativos/acoes' },
{ label: 'Forex', href: '/mercado-financeiro/ativos/forex' },
],
},
],
},
{ label: 'Sobre mim', href: '/sobre-mim' },
];
// Renderização recursiva — trata MenuItem e MenuItem[] igualmente
function NavMenu({ items, depth = 0 }: { items: MenuItem[]; depth?: number }) {
return (
<ul className={depth > 0 ? 'ml-4' : ''}>
{items.map(item => (
<li key={item.label}>
{item.href ? (
<Link href={item.href}>{item.label}</Link>
) : (
<span className="font-semibold">{item.label}</span>
)}
{item.children && <NavMenu items={item.children} depth={depth + 1} />}
</li>
))}
</ul>
);
}