Skip to main content

03 - Adapter, Decorator and Facade Pattern: Flexible Structures

Essential structural patterns: Adapter for compatibility, Decorator for dynamic extensibility and Facade for simplifying complex systems.

What do you think about this article?

At a Glance

Essential structural patterns: Adapter for compatibility, Decorator for dynamic extensibility and Facade for simplifying complex systems.

Adapter, Decorator and Facade Pattern

Structural patterns deal with the composition of classes and objects. Adapter makes incompatible interfaces compatible, Decorator adds responsibilities dynamically, and Facade provides a simplified interface to a complex system.

🎯 What You Will Learn

  • Adapter pattern: compatibility between different interfaces
  • Decorator pattern: extending functionality dynamically
  • Facade pattern: simplifying complex systems
  • When to use each structural pattern

Adapter Pattern

The Adapter pattern (or Wrapper) converts the interface of a class into another interface that clients expect. It allows classes with incompatible interfaces to collaborate.

TypeScript - Basic Adapter
// Target: interface that the client expects
interface MediaPlayer {{ '{' }}
  play(fileName: string): void;
{{ '}' }}

// Adaptee: existing class with incompatible interface
class Mp3Player {{ '{' }}
  playMp3(fileName: string): void {{ '{' }}
    console.log(`🎵 Playing MP3: ${{ '{' }}fileName{{ '}' }}`);
  {{ '}' }}
{{ '}' }}

class Mp4Player {{ '{' }}
  playMp4(fileName: string): void {{ '{' }}
    console.log(`🎬 Playing MP4: ${{ '{' }}fileName{{ '}' }}`);
  {{ '}' }}
{{ '}' }}

class VlcPlayer {{ '{' }}
  playVlc(fileName: string): void {{ '{' }}
    console.log(`📺 Playing VLC: ${{ '{' }}fileName{{ '}' }}`);
  {{ '}' }}
{{ '}' }}

// Adapter: adapts the interface
class Mp4Adapter implements MediaPlayer {{ '{' }}
  private mp4Player: Mp4Player;

  constructor() {{ '{' }}
    this.mp4Player = new Mp4Player();
  {{ '}' }}

  play(fileName: string): void {{ '{' }}
    // Adapts the call
    this.mp4Player.playMp4(fileName);
  {{ '}' }}
{{ '}' }}

class VlcAdapter implements MediaPlayer {{ '{' }}
  private vlcPlayer: VlcPlayer;

  constructor() {{ '{' }}
    this.vlcPlayer = new VlcPlayer();
  {{ '}' }}

  play(fileName: string): void {{ '{' }}
    this.vlcPlayer.playVlc(fileName);
  {{ '}' }}
{{ '}' }}

// Client: uses the target interface
class AudioPlayer implements MediaPlayer {{ '{' }}
  private mp3Player: Mp3Player = new Mp3Player();

  play(fileName: string): void {{ '{' }}
    const extension = fileName.split('.').pop()?.toLowerCase();

    if (extension === 'mp3') {{ '{' }}
      this.mp3Player.playMp3(fileName);
    {{ '}' }} else if (extension === 'mp4') {{ '{' }}
      const adapter = new Mp4Adapter();
      adapter.play(fileName);
    {{ '}' }} else if (extension === 'vlc') {{ '{' }}
      const adapter = new VlcAdapter();
      adapter.play(fileName);
    {{ '}' }} else {{ '{' }}
      console.log(`❌ Format not supported: ${{ '{' }}extension{{ '}' }}`);
    {{ '}' }}
  {{ '}' }}
{{ '}' }}

// Usage
const player = new AudioPlayer();
player.play("song.mp3");    // 🎵 Playing MP3: song.mp3
player.play("video.mp4");   // 🎬 Playing MP4: video.mp4
player.play("movie.vlc");   // 📺 Playing VLC: movie.vlc
player.play("file.avi");    // ❌ Format not supported: avi

Adapter for External APIs

Practical use: adapting third-party APIs to your own interface:

Payment API Adapter
// Internal interface of your system
interface PaymentProcessor {{ '{' }}
  processPayment(amount: number, currency: string): Promise<{{ '{' }} success: boolean; transactionId: string {{ '}' }}>;
{{ '}' }}

// External Stripe API (simulated)
class StripeAPI {{ '{' }}
  async charge(cents: number, currencyCode: string): Promise<{{ '{' }} id: string; status: string {{ '}' }}> {{ '{' }}
    console.log(`💳 Stripe: Charging ${{ '{' }}cents / 100{{ '}' }} ${{ '{' }}currencyCode{{ '}' }}`);
    return {{ '{' }} id: 'stripe_' + Math.random(), status: 'succeeded' {{ '}' }};
  {{ '}' }}
{{ '}' }}

// External PayPal API (simulated)
class PayPalAPI {{ '{' }}
  async createPayment(amount: number, currency: string): Promise<{{ '{' }} paymentId: string; state: string {{ '}' }}> {{ '{' }}
    console.log(`🅿️ PayPal: Creating payment ${{ '{' }}amount{{ '}' }} ${{ '{' }}currency{{ '}' }}`);
    return {{ '{' }} paymentId: 'paypal_' + Math.random(), state: 'approved' {{ '}' }};
  {{ '}' }}
{{ '}' }}

// Stripe Adapter
class StripeAdapter implements PaymentProcessor {{ '{' }}
  private stripe: StripeAPI;

  constructor() {{ '{' }}
    this.stripe = new StripeAPI();
  {{ '}' }}

  async processPayment(amount: number, currency: string): Promise<{{ '{' }} success: boolean; transactionId: string {{ '}' }}> {{ '{' }}
    // Stripe uses cents instead of dollars
    const cents = Math.round(amount * 100);
    const result = await this.stripe.charge(cents, currency.toUpperCase());

    return {{ '{' }}
      success: result.status === 'succeeded',
      transactionId: result.id
    {{ '}' }};
  {{ '}' }}
{{ '}' }}

// PayPal Adapter
class PayPalAdapter implements PaymentProcessor {{ '{' }}
  private paypal: PayPalAPI;

  constructor() {{ '{' }}
    this.paypal = new PayPalAPI();
  {{ '}' }}

  async processPayment(amount: number, currency: string): Promise<{{ '{' }} success: boolean; transactionId: string {{ '}' }}> {{ '{' }}
    const result = await this.paypal.createPayment(amount, currency.toLowerCase());

    return {{ '{' }}
      success: result.state === 'approved',
      transactionId: result.paymentId
    {{ '}' }};
  {{ '}' }}
{{ '}' }}

// Client: uses the unified interface
class CheckoutService {{ '{' }}
  constructor(private processor: PaymentProcessor) {{ '{' }}{{ '}' }}

  async checkout(amount: number): Promise<void> {{ '{' }}
    console.log(`🛒 Processing checkout: $${{ '{' }}amount{{ '}' }}`);
    const result = await this.processor.processPayment(amount, 'USD');

    if (result.success) {{ '{' }}
      console.log(`✅ Payment successful! Transaction: ${{ '{' }}result.transactionId{{ '}' }}`);
    {{ '}' }} else {{ '{' }}
      console.log(`❌ Payment failed`);
    {{ '}' }}
  {{ '}' }}
{{ '}' }}

// Usage: switch between providers without modifying CheckoutService
const stripeCheckout = new CheckoutService(new StripeAdapter());
await stripeCheckout.checkout(99.99);
// 💳 Stripe: Charging 9999 USD
// ✅ Payment successful! Transaction: stripe_0.123...

const paypalCheckout = new CheckoutService(new PayPalAdapter());
await paypalCheckout.checkout(49.99);
// 🅿️ PayPal: Creating payment 49.99 usd
// ✅ Payment successful! Transaction: paypal_0.456...

Decorator Pattern

The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to inheritance for extending functionality.

TypeScript - Basic Decorator
// Component: base interface
interface Coffee {{ '{' }}
  cost(): number;
  description(): string;
{{ '}' }}

// Concrete Component
class SimpleCoffee implements Coffee {{ '{' }}
  cost(): number {{ '{' }}
    return 2;
  {{ '}' }}

  description(): string {{ '{' }}
    return "Simple coffee";
  {{ '}' }}
{{ '}' }}

// Abstract Decorator
abstract class CoffeeDecorator implements Coffee {{ '{' }}
  constructor(protected coffee: Coffee) {{ '{' }}{{ '}' }}

  abstract cost(): number;
  abstract description(): string;
{{ '}' }}

// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {{ '{' }}
  cost(): number {{ '{' }}
    return this.coffee.cost() + 0.5;
  {{ '}' }}

  description(): string {{ '{' }}
    return this.coffee.description() + ", milk";
  {{ '}' }}
{{ '}' }}

class SugarDecorator extends CoffeeDecorator {{ '{' }}
  cost(): number {{ '{' }}
    return this.coffee.cost() + 0.2;
  {{ '}' }}

  description(): string {{ '{' }}
    return this.coffee.description() + ", sugar";
  {{ '}' }}
{{ '}' }}

class WhipDecorator extends CoffeeDecorator {{ '{' }}
  cost(): number {{ '{' }}
    return this.coffee.cost() + 0.7;
  {{ '}' }}

  description(): string {{ '{' }}
    return this.coffee.description() + ", whipped cream";
  {{ '}' }}
{{ '}' }}

// Usage: dynamic composition
let coffee: Coffee = new SimpleCoffee();
console.log(`${{ '{' }}coffee.description(){{ '}' }} - $${{ '{' }}coffee.cost(){{ '}' }}`);
// "Simple coffee - $2"

// Add milk
coffee = new MilkDecorator(coffee);
console.log(`${{ '{' }}coffee.description(){{ '}' }} - $${{ '{' }}coffee.cost(){{ '}' }}`);
// "Simple coffee, milk - $2.5"

// Add sugar
coffee = new SugarDecorator(coffee);
console.log(`${{ '{' }}coffee.description(){{ '}' }} - $${{ '{' }}coffee.cost(){{ '}' }}`);
// "Simple coffee, milk, sugar - $2.7"

// Add whipped cream
coffee = new WhipDecorator(coffee);
console.log(`${{ '{' }}coffee.description(){{ '}' }} - $${{ '{' }}coffee.cost(){{ '}' }}`);
// "Simple coffee, milk, sugar, whipped cream - $3.4"

Decorator for Logging and Caching

Practical example: adding logging and caching to a service:

Service Decorators
// Component interface
interface DataService {{ '{' }}
  fetchData(id: string): Promise<any>;
{{ '}' }}

// Concrete component
class APIDataService implements DataService {{ '{' }}
  async fetchData(id: string): Promise<any> {{ '{' }}
    console.log(`🌐 Fetching data from API for ID: ${{ '{' }}id{{ '}' }}`);
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000));
    return {{ '{' }} id, data: `Data for ${{ '{' }}id{{ '}' }}` {{ '}' }};
  {{ '}' }}
{{ '}' }}

// Logging Decorator
class LoggingDecorator implements DataService {{ '{' }}
  constructor(private service: DataService) {{ '{' }}{{ '}' }}

  async fetchData(id: string): Promise<any> {{ '{' }}
    console.log(`📝 [LOG] Fetching data for: ${{ '{' }}id{{ '}' }}`);
    const startTime = Date.now();

    try {{ '{' }}
      const result = await this.service.fetchData(id);
      const duration = Date.now() - startTime;
      console.log(`📝 [LOG] Success in ${{ '{' }}duration{{ '}' }}ms`);
      return result;
    {{ '}' }} catch (error) {{ '{' }}
      console.log(`📝 [LOG] Error: ${{ '{' }}error{{ '}' }}`);
      throw error;
    {{ '}' }}
  {{ '}' }}
{{ '}' }}

// Caching Decorator
class CachingDecorator implements DataService {{ '{' }}
  private cache: Map<string, any> = new Map();

  constructor(private service: DataService) {{ '{' }}{{ '}' }}

  async fetchData(id: string): Promise<any> {{ '{' }}
    if (this.cache.has(id)) {{ '{' }}
      console.log(`💾 [CACHE] Cache hit for: ${{ '{' }}id{{ '}' }}`);
      return this.cache.get(id);
    {{ '}' }}

    console.log(`💾 [CACHE] Cache miss for: ${{ '{' }}id{{ '}' }}`);
    const result = await this.service.fetchData(id);
    this.cache.set(id, result);
    return result;
  {{ '}' }}
{{ '}' }}

// Retry Decorator
class RetryDecorator implements DataService {{ '{' }}
  constructor(
    private service: DataService,
    private maxRetries: number = 3
  ) {{ '{' }}{{ '}' }}

  async fetchData(id: string): Promise<any> {{ '{' }}
    let lastError;

    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {{ '{' }}
      try {{ '{' }}
        console.log(`🔄 [RETRY] Attempt ${{ '{' }}attempt{{ '}' }}/${{ '{' }}this.maxRetries{{ '}' }}`);
        return await this.service.fetchData(id);
      {{ '}' }} catch (error) {{ '{' }}
        lastError = error;
        if (attempt < this.maxRetries) {{ '{' }}
          console.log(`🔄 [RETRY] Failed, retrying...`);
          await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
        {{ '}' }}
      {{ '}' }}
    {{ '}' }}

    throw lastError;
  {{ '}' }}
{{ '}' }}

// Usage: compose decorators
let service: DataService = new APIDataService();

// Add logging
service = new LoggingDecorator(service);

// Add caching
service = new CachingDecorator(service);

// Add retry
service = new RetryDecorator(service, 3);

// First call: cache miss, API call
await service.fetchData("123");
// 📝 [LOG] Fetching data for: 123
// 💾 [CACHE] Cache miss for: 123
// 🔄 [RETRY] Attempt 1/3
// 🌐 Fetching data from API for ID: 123
// 📝 [LOG] Success in 1002ms

// Second call: cache hit, no API call
await service.fetchData("123");
// 📝 [LOG] Fetching data for: 123
// 💾 [CACHE] Cache hit for: 123
// 📝 [LOG] Success in 2ms

Facade Pattern

The Facade pattern provides a unified, simplified interface to a set of interfaces in a subsystem. It makes the subsystem easier to use.

TypeScript - Basic Facade
// Complex subsystem: Home Theater
class Amplifier {{ '{' }}
  on(): void {{ '{' }} console.log("🔊 Amplifier ON"); {{ '}' }}
  setVolume(level: number): void {{ '{' }} console.log(`🔊 Volume set to ${{ '{' }}level{{ '}' }}`); {{ '}' }}
  off(): void {{ '{' }} console.log("🔊 Amplifier OFF"); {{ '}' }}
{{ '}' }}

class DvdPlayer {{ '{' }}
  on(): void {{ '{' }} console.log("📀 DVD Player ON"); {{ '}' }}
  play(movie: string): void {{ '{' }} console.log(`📀 Playing: ${{ '{' }}movie{{ '}' }}`); {{ '}' }}
  stop(): void {{ '{' }} console.log("📀 Stopped"); {{ '}' }}
  off(): void {{ '{' }} console.log("📀 DVD Player OFF"); {{ '}' }}
{{ '}' }}

class Projector {{ '{' }}
  on(): void {{ '{' }} console.log("📽️ Projector ON"); {{ '}' }}
  setInput(source: string): void {{ '{' }} console.log(`📽️ Input: ${{ '{' }}source{{ '}' }}`); {{ '}' }}
  off(): void {{ '{' }} console.log("📽️ Projector OFF"); {{ '}' }}
{{ '}' }}

class Lights {{ '{' }}
  dim(level: number): void {{ '{' }} console.log(`💡 Lights dimmed to ${{ '{' }}level{{ '}' }}%`); {{ '}' }}
  on(): void {{ '{' }} console.log("💡 Lights ON"); {{ '}' }}
{{ '}' }}

// ❌ Without Facade: client must manage everything
function watchMovieWithoutFacade(movie: string): void {{ '{' }}
  const amp = new Amplifier();
  const dvd = new DvdPlayer();
  const projector = new Projector();
  const lights = new Lights();

  lights.dim(10);
  projector.on();
  projector.setInput("DVD");
  amp.on();
  amp.setVolume(5);
  dvd.on();
  dvd.play(movie);
  // ... high complexity!
{{ '}' }}

// ✅ With Facade: simplified interface
class HomeTheaterFacade {{ '{' }}
  constructor(
    private amp: Amplifier,
    private dvd: DvdPlayer,
    private projector: Projector,
    private lights: Lights
  ) {{ '{' }}{{ '}' }}

  watchMovie(movie: string): void {{ '{' }}
    console.log("🎬 Getting ready to watch a movie...\n");
    this.lights.dim(10);
    this.projector.on();
    this.projector.setInput("DVD");
    this.amp.on();
    this.amp.setVolume(5);
    this.dvd.on();
    this.dvd.play(movie);
    console.log("\n🍿 Enjoy the movie!");
  {{ '}' }}

  endMovie(): void {{ '{' }}
    console.log("\n👋 Shutting down movie theater...\n");
    this.dvd.stop();
    this.dvd.off();
    this.amp.off();
    this.projector.off();
    this.lights.on();
    console.log("\n✅ Movie theater shut down");
  {{ '}' }}
{{ '}' }}

// Usage: simple and clear
const homeTheater = new HomeTheaterFacade(
  new Amplifier(),
  new DvdPlayer(),
  new Projector(),
  new Lights()
);

homeTheater.watchMovie("Inception");
// 🎬 Getting ready to watch a movie...
// 💡 Lights dimmed to 10%
// 📽️ Projector ON
// 📽️ Input: DVD
// 🔊 Amplifier ON
// 🔊 Volume set to 5
// 📀 DVD Player ON
// 📀 Playing: Inception
// 🍿 Enjoy the movie!

homeTheater.endMovie();
// 👋 Shutting down movie theater...
// 📀 Stopped
// 📀 DVD Player OFF
// 🔊 Amplifier OFF
// 📽️ Projector OFF
// 💡 Lights ON
// ✅ Movie theater shut down

Facade for Database Operations

Practical example: simplifying complex database operations:

Database Facade
// Complex subsystem
class ConnectionPool {{ '{' }}
  getConnection(): void {{ '{' }} console.log("🔌 Getting connection from pool"); {{ '}' }}
  releaseConnection(): void {{ '{' }} console.log("🔌 Releasing connection"); {{ '}' }}
{{ '}' }}

class QueryBuilder {{ '{' }}
  buildSelectQuery(table: string, conditions: any): string {{ '{' }}
    console.log(`🔨 Building SELECT query for ${{ '{' }}table{{ '}' }}`);
    return `SELECT * FROM ${{ '{' }}table{{ '}' }} WHERE ...`;
  {{ '}' }}

  buildInsertQuery(table: string, data: any): string {{ '{' }}
    console.log(`🔨 Building INSERT query for ${{ '{' }}table{{ '}' }}`);
    return `INSERT INTO ${{ '{' }}table{{ '}' }} ...`;
  {{ '}' }}
{{ '}' }}

class TransactionManager {{ '{' }}
  begin(): void {{ '{' }} console.log("⚙️ Transaction started"); {{ '}' }}
  commit(): void {{ '{' }} console.log("✅ Transaction committed"); {{ '}' }}
  rollback(): void {{ '{' }} console.log("↩️ Transaction rolled back"); {{ '}' }}
{{ '}' }}

class CacheManager {{ '{' }}
  get(key: string): any {{ '{' }}
    console.log(`💾 Cache lookup: ${{ '{' }}key{{ '}' }}`);
    return null;
  {{ '}' }}

  set(key: string, value: any): void {{ '{' }}
    console.log(`💾 Cache set: ${{ '{' }}key{{ '}' }}`);
  {{ '}' }}
{{ '}' }}

// Facade: simplifies everything
class DatabaseFacade {{ '{' }}
  private pool = new ConnectionPool();
  private queryBuilder = new QueryBuilder();
  private txManager = new TransactionManager();
  private cache = new CacheManager();

  async findUser(id: string): Promise<any> {{ '{' }}
    // Check cache
    const cached = this.cache.get(`user:${{ '{' }}id{{ '}' }}`);
    if (cached) return cached;

    // Execute query
    this.pool.getConnection();
    const query = this.queryBuilder.buildSelectQuery('users', {{ '{' }} id {{ '}' }});
    console.log(`🔍 Executing: ${{ '{' }}query{{ '}' }}`);

    const user = {{ '{' }} id, name: 'Alice' {{ '}' }}; // Simulate result

    this.cache.set(`user:${{ '{' }}id{{ '}' }}`, user);
    this.pool.releaseConnection();

    return user;
  {{ '}' }}

  async createUser(userData: any): Promise<void> {{ '{' }}
    this.pool.getConnection();
    this.txManager.begin();

    try {{ '{' }}
      const query = this.queryBuilder.buildInsertQuery('users', userData);
      console.log(`➕ Executing: ${{ '{' }}query{{ '}' }}`);

      this.txManager.commit();
      this.cache.set(`user:${{ '{' }}userData.id{{ '}' }}`, userData);
    {{ '}' }} catch (error) {{ '{' }}
      this.txManager.rollback();
      throw error;
    {{ '}' }} finally {{ '{' }}
      this.pool.releaseConnection();
    {{ '}' }}
  {{ '}' }}
{{ '}' }}

// Usage: simple and clean
const db = new DatabaseFacade();

await db.findUser("123");
// 💾 Cache lookup: user:123
// 🔌 Getting connection from pool
// 🔨 Building SELECT query for users
// 🔍 Executing: SELECT * FROM users WHERE ...
// 💾 Cache set: user:123
// 🔌 Releasing connection

await db.createUser({{ '{' }} id: "456", name: "Bob" {{ '}' }});
// 🔌 Getting connection from pool
// ⚙️ Transaction started
// 🔨 Building INSERT query for users
// ➕ Executing: INSERT INTO users ...
// ✅ Transaction committed
// 💾 Cache set: user:456
// 🔌 Releasing connection

When to Use Each Pattern

✅ Use Adapter when:

  • You want to use existing classes but their interfaces are incompatible
  • Integrating third-party libraries (payment APIs, logging, etc.)
  • Gradually migrating from an old system to a new one
  • Uniform interface for heterogeneous components

✅ Use Decorator when:

  • You want to add responsibilities dynamically without inheritance
  • Inheritance is impractical (too many combinations)
  • Cross-cutting concerns: logging, caching, validation
  • Flexible composition of functionality

✅ Use Facade when:

  • You want to simplify a complex system
  • Reduce dependencies between client and subsystem
  • Define entry points for layers (database, API)
  • Hide internal complexity

All Three Patterns Combined

Complete E-Commerce System
// ADAPTER: Adapts external APIs
interface ShippingService {{ '{' }}
  calculateShipping(weight: number): number;
{{ '}' }}

class FedExAPI {{ '{' }}
  computeCost(kg: number): number {{ '{' }} return kg * 5; {{ '}' }}
{{ '}' }}

class FedExAdapter implements ShippingService {{ '{' }}
  private fedex = new FedExAPI();

  calculateShipping(weight: number): number {{ '{' }}
    return this.fedex.computeCost(weight);
  {{ '}' }}
{{ '}' }}

// DECORATOR: Adds functionality
class DiscountDecorator implements ShippingService {{ '{' }}
  constructor(
    private service: ShippingService,
    private discount: number
  ) {{ '{' }}{{ '}' }}

  calculateShipping(weight: number): number {{ '{' }}
    const cost = this.service.calculateShipping(weight);
    return cost * (1 - this.discount);
  {{ '}' }}
{{ '}' }}

class InsuranceDecorator implements ShippingService {{ '{' }}
  constructor(private service: ShippingService) {{ '{' }}{{ '}' }}

  calculateShipping(weight: number): number {{ '{' }}
    const cost = this.service.calculateShipping(weight);
    return cost + 2.99; // Fixed insurance
  {{ '}' }}
{{ '}' }}

// FACADE: Simplifies checkout
class PaymentGateway {{ '{' }}
  charge(amount: number): void {{ '{' }} console.log(`💳 Charged $${{ '{' }}amount{{ '}' }}`); {{ '}' }}
{{ '}' }}

class InventorySystem {{ '{' }}
  reserve(productId: string): void {{ '{' }} console.log(`📦 Reserved product ${{ '{' }}productId{{ '}' }}`); {{ '}' }}
{{ '}' }}

class EmailService {{ '{' }}
  sendConfirmation(email: string): void {{ '{' }} console.log(`📧 Confirmation sent to ${{ '{' }}email{{ '}' }}`); {{ '}' }}
{{ '}' }}

class CheckoutFacade {{ '{' }}
  constructor(
    private shippingService: ShippingService,
    private payment: PaymentGateway,
    private inventory: InventorySystem,
    private email: EmailService
  ) {{ '{' }}{{ '}' }}

  processOrder(productId: string, weight: number, userEmail: string): void {{ '{' }}
    console.log("🛒 Processing order...\n");

    // Calculate shipping (with adapter + decorators)
    const shippingCost = this.shippingService.calculateShipping(weight);
    console.log(`📮 Shipping: $${{ '{' }}shippingCost.toFixed(2){{ '}' }}`);

    // Process payment
    const total = 50 + shippingCost; // 50 = product price
    this.payment.charge(total);

    // Reserve inventory
    this.inventory.reserve(productId);

    // Send confirmation
    this.email.sendConfirmation(userEmail);

    console.log("\n✅ Order completed!");
  {{ '}' }}
{{ '}' }}

// Combined usage
let shipping: ShippingService = new FedExAdapter();
shipping = new DiscountDecorator(shipping, 0.1); // 10% discount
shipping = new InsuranceDecorator(shipping); // Insurance

const checkout = new CheckoutFacade(
  shipping,
  new PaymentGateway(),
  new InventorySystem(),
  new EmailService()
);

checkout.processOrder("PROD123", 2.5, "user@example.com");
// 🛒 Processing order...
// 📮 Shipping: $14.24  (2.5kg * 5 * 0.9 + 2.99)
// 💳 Charged $64.24
// 📦 Reserved product PROD123
// 📧 Confirmation sent to user@example.com
// ✅ Order completed!

Conclusion

These three structural patterns solve complementary problems. Adapter integrates incompatible systems, Decorator extends functionality dynamically without inheritance, and Facade hides complexity by providing simple interfaces. Use them together for flexible and maintainable architectures.

🎯 Key Takeaways

  • Adapter = compatibility between incompatible interfaces
  • Decorator = add functionality dynamically (composition > inheritance)
  • Facade = simplify complex systems with a unified interface
  • Adapter for integrating external APIs
  • Decorator for cross-cutting concerns (logging, caching)
  • Facade for hiding complexity and reducing coupling

Frequently asked questions

What is the main purpose of the Adapter pattern?

The Adapter pattern converts the interface of a class into another interface that clients expect, allowing classes with incompatible interfaces to collaborate.

How does the Decorator pattern differ from inheritance in extending functionality?

The Decorator pattern attaches additional responsibilities to an object dynamically, providing a flexible alternative to inheritance for extending functionality.

What is the primary function of the Facade pattern?

The Facade pattern provides a unified, simplified interface to a set of interfaces in a subsystem, making it easier to use.

Why are the Adapter, Decorator, and Facade patterns useful when used together?

They solve complementary problems, allowing for flexible and maintainable architectures by integrating incompatible systems, extending functionality dynamically, and hiding complexity with simple interfaces.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects