Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptDesign Patterns

Padrões Estruturais

Adapter, 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>
  );
}

Referências

On this page