RealWorld Design Patterns in TypeScript

repository·master·Indexed 43 days ago

https://github.com/vahidvdn/realworld-design-patterns

An educational repository demonstrating real-world software design patterns using TypeScript. It provides practical examples and comparisons between 'Bad Practice' and 'Good Practice' implementations for patterns including Abstract Factory, Adapter, Bridge, Builder, Chain of Responsibility, and Command.

Tokens
25.1K
Snippets
67
Records
85
Agent score
55%

What's inside RealWorld Design Patterns

  1. What is the Bridge Pattern and when to use it

    master

    The Bridge pattern is a structural design pattern used to decouple an abstraction from its implementation so that the two can vary independently.

    It is particularly useful when you face a 'class explosion' problem—where you find yourself creating a new class for every possible combination of two different dimensions (e.g., OnlineOrderWithPayPal, OnlineOrderWithStripe, InStoreOrderWithPayPal, etc.).

    Use the Bridge pattern when:

    • You want to avoid a permanent binding between abstractions and their implementations.
    • Both abstractions and implementations should be extensible independently.
    • Changes in the implementation should not impact the client code.
    • You have a proliferation of classes resulting from a coupled interface and numerous implementations.
    • You want to share an implementation among multiple objects.
  2. What is the Proxy Pattern and when to use it

    master

    The Proxy Pattern is used to limit or control access to an original object. While it is structurally similar to the Decorator Pattern (which adds functionality), the Proxy Pattern's primary purpose is to act as a surrogate or placeholder to manage access.

    Key Advantages

    • Decouple limitation logic: Keep access control logic separate from the original object's core business logic.
    • Control access: Manage how and when the original object is accessed.
    • Caching: Store results of expensive operations to avoid redundant calls to the original object.
    • 3rd Party Control: Apply control logic to classes you do not own (3rd party libraries).

    Common Use Case: Caching API Responses

    Instead of calling an expensive API (like OpenAI) for every request, a Proxy can intercept the request, check if a cached response exists for a specific prompt, and return it immediately if available.

  3. What is the Template Method Pattern and when to use it

    master

    The Template Method Pattern is used when you have a series of similar tasks that share identical steps, optional steps, and unique steps.

    Mental Model:

    1. Parent Class (Template): Defines the skeleton of an algorithm. It implements the identical steps and provides default implementations for optional steps. It also declares abstract methods for the steps that must be unique to each implementation.
    2. Template Method: A specific method in the parent class that orchestrates the execution of all steps in a fixed order. This method is typically the same for all child classes.
    3. Child Classes: Implement the specific logic for the abstract methods defined by the parent.

    Example Scenario: Fetching products from different providers (e.g., Amazon, Alibaba). The process of sending a request is identical, but the data parsing logic is unique to each provider.

    // The parent class defines the structure
    export abstract class BaseHandler<T, K> implements IBaseHandler<T, K> {
      // Identical step
      async fetchData(data: K): Promise<T> { ... }
    
      // Optional step with default implementation
      async paginate(data: T): Promise<T | Paginated<T>> {
        return data;
      }
    
      // Abstract step to be implemented by children
      abstract parseData(data: T | Paginated<T>): number[];
    
      // The Template Method: orchestrates the steps
      async handle(data: K) {
        const result = await this.fetchData(data);
        const paginated = await this.paginate(result);
        const parsed = this.parseData(paginated);
        return parsed;
      }
    }
  4. What is the Facade Pattern and when to use it

    master

    The Facade pattern provides a unified, simplified interface to a complex subsystem. It is used to:

    • Simplify a complex subsystem by providing a single entry point.
    • Decouple client code from the internal components of a subsystem.
    • Layer subsystems by providing controlled entry points to each layer.

    In the provided implementation, the pattern is used to wrap the sharp image processing library, hiding its complex configuration and method chaining behind a high-level ImageProcessingFacade class.

  5. Understand the Abstract Factory pattern

    master
    The Abstract Factory pattern is used to create families of related products rather than a single product. While the Factory Method pattern focuses on deciding which single product to instantiate, the Abstract Factory pattern ensures that a set of related products (e.g., all components for a specific UI theme or all strategies for a specific social network) are created consistently and work together.
  6. Apply the Single Responsibility Principle (SRP)

    master

    A class should have only one reason to change. Avoid creating 'God classes' that handle multiple unrelated responsibilities. Instead, split logic into specialized services.

    // ✅ Correct: Split responsibilities
    class OrderService {
      createOrder() {
        // logic to create order
      }
    }
    
    class EmailService {
      sendConfirmationEmail() {
        // logic to send email
      }
    }
  7. Apply the Interface Segregation Principle (ISP)

    master

    Clients should not be forced to depend on methods they do not use. Instead of one large, general-purpose interface, create multiple smaller, specific interfaces so that implementing classes only need to satisfy the requirements relevant to them.

    // ✅ Correct: Split interfaces
    interface Workable {
      work(): void;
    }
    
    interface Eatable {
      eat(): void;
    }
    
    class Human implements Workable, Eatable {
      work() {}
      eat() {}
    }
    
    class Robot implements Workable {
      work() {}
    }
  8. Apply the KISS (Keep It Simple, Stupid) principle

    master

    KISS states that systems work best when kept simple. Avoid unnecessary complexity by using the simplest solution that meets requirements and breaking complex problems into smaller, manageable parts.

    Instead of using deeply nested conditional logic to handle business rules, use data-driven approaches like configuration arrays or rule objects to simplify the execution flow.

    interface DiscountRule {
      userType: string;
      minPurchases: number;
      baseDiscount: number;
      seasonalBonus: number;
    }
    
    const discountRules: DiscountRule[] = [
      { userType: 'premium', minPurchases: 10, baseDiscount: 0.2, seasonalBonus: 0.05 },
      { userType: 'premium', minPurchases: 0, baseDiscount: 0.1, seasonalBonus: 0.02 },
      { userType: 'regular', minPurchases: 20, baseDiscount: 0.15, seasonalBonus: 0.03 },
      { userType: 'regular', minPurchases: 0, baseDiscount: 0.05, seasonalBonus: 0.01 },
    ];
    
    function calculateDiscount(price: number, userType: string, purchaseCount: number, seasonalPromotion: boolean): number {
      const rule = discountRules.find(r => 
        r.userType === userType && purchaseCount >= r.minPurchases
      );
      
      if (!rule) return 0;
      
      let discount = price * rule.baseDiscount;
      if (seasonalPromotion) {
        discount += price * rule.seasonalBonus;
      }
      
      return discount;
    }
  9. Apply the Liskov Substitution Principle (LSP)

    master

    Subtypes must be substitutable for their base types without breaking the application. If a subclass cannot fulfill the contract of the parent class (e.g., by throwing errors for expected behaviors), the hierarchy is likely flawed.

    // ✅ Correct: Use more specific interfaces to avoid breaking behavior
    interface Bird {
      layEggs(): void;
    }
    
    interface FlyingBird extends Bird {
      fly(): void;
    }
    
    class Sparrow implements FlyingBird {
      fly() { console.log("Flying"); }
      layEggs() {}
    }
    
    class Ostrich implements Bird {
      layEggs() {}
    }
  10. Understand Polymorphism in OOP

    master

    Polymorphism allows objects of different types to be treated through a common interface. This promotes flexibility and extensibility, allowing you to add new implementations without changing the code that consumes them.

    // Base interface
    interface Notifier {
      send(message: string): void;
    }
    
    class EmailNotifier implements Notifier {
      send(message: string): void {
        console.log(`Sending EMAIL: ${message}`);
      }
    }
    
    class SMSNotifier implements Notifier {
      send(message: string): void {
        console.log(`Sending SMS: ${message}`);
      }
    }
    
    // Polymorphic function
    function notifyUser(notifier: Notifier, message: string) {
      notifier.send(message);
    }
  11. Understand Abstraction in OOP

    master

    Abstraction involves hiding implementation details and exposing only the necessary interface to the outside world. In TypeScript, this is typically done using the private keyword to restrict access to internal helper methods.

    class MusicPlayer {
      play(fileName: string) {
        this.loadFile(fileName);
        this.decodeFile(fileName);
        this.playAudio(fileName);
        console.log(`Now playing: ${fileName}`);
      }
    
      private loadFile(fileName: string) {
        console.log(`Loading file: ${fileName}`);
      }
    
      private decodeFile(fileName: string) {
        console.log(`Decoding file: ${fileName}`);
      }
    
      private playAudio(fileName: string) {
        console.log(`Streaming audio to speakers: ${fileName}`);
      }
    }
    
    const player = new MusicPlayer();
    player.play("song.mp3");
  12. Handle Cross-Cutting Concerns using separation of concerns

    master

    Cross-cutting concerns are aspects of a program that affect multiple modules (e.g., logging, security, validation, transaction management, caching). If not handled correctly, they lead to 'tangled' code where business logic is mixed with infrastructure logic.

    To keep business logic clean, use one of the following approaches to separate these concerns:

    • Middleware: For request/response pipelines (e.g., Express.js).
    • Decorators: To wrap methods with specific behaviors (e.g., @transactional, @logMethod).
    • Aspect-Oriented Programming (AOP).
    • Higher-Order Functions.
    // Example of using Decorators to separate concerns
    
    function transactional(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const originalMethod = descriptor.value;
      descriptor.value = async function(...args: any[]) {
        const transaction = await db.beginTransaction();
        try {
          const result = await originalMethod.apply(this, args);
          await transaction.commit();
          return result;
        } catch (error) {
          await transaction.rollback();
          throw error;
        }
      };
    }
    
    class OrderService {
      @transactional
      async createOrder(orderData: OrderData): Promise<Order> {
        // Core business logic only
        return await db.orders.create(orderData);
      }
    }