Logo

dev-resources.site

for different kinds of informations.

Harnessing Functional Programming with JavaScript

Published at
1/11/2025
Categories
javascript
typescript
webdev
programming
Author
Shafayet Hossain
Harnessing Functional Programming with JavaScript

Design patterns are established solutions to frequent challenges in software design. By utilizing them, developers can enhance code readability, scalability, and maintainability. This article explores how TypeScript, an increasingly popular superset of JavaScript improves these patterns with its type safety and modern features. Whether you’re developing a large-scale application or a side project, grasping design patterns in TypeScript will boost your development skills.

What Are Design Patterns?

Design patterns are reusable, generic solutions to common challenges in software design. They aren't actual code but rather templates for addressing these issues. Originating from the "Gang of Four" (GoF) book, these patterns fall into three main categories:

  1. Creational Patterns: Concerned with the mechanisms of object creation.
  2. Structural Patterns: Emphasize the composition and organization of objects.
  3. Behavioral Patterns: Focus on the interactions and communication between objects.

Why Use Design Patterns in TypeScript?

TypeScript’s features make implementing design patterns more robust:

1. Static Typing: Errors are caught at compile time, reducing runtime bugs.
2. Interfaces and Generics: Allow more precise and flexible implementations.
3. Enum and Union Types: Simplify certain patterns, such as state management.
4. Enhanced Tooling: With IDE support, TypeScript boosts productivity.

Some Key Design Patterns in TypeScript

1. Singleton Pattern

Ensures that a class has only one instance and provides a global point of access to it.

Implementation in TypeScript:

class Singleton {
  private static instance: Singleton;

  private constructor() {} // Prevent instantiation

  public static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true

Use Case: Managing configuration settings or database connections.

2. Factory Pattern

Provides an interface for creating objects without specifying their exact classes.

Implementation:

interface Button {
  render(): void;
}

class WindowsButton implements Button {
  render() {
    console.log("Rendering Windows button");
  }
}

class MacButton implements Button {
  render() {
    console.log("Rendering Mac button");
  }
}

class ButtonFactory {
  static createButton(os: string): Button {
    if (os === "Windows") return new WindowsButton();
    if (os === "Mac") return new MacButton();
    throw new Error("Unknown OS");
  }
}

const button = ButtonFactory.createButton("Mac");
button.render(); // Output: Rendering Mac button

Use Case: UI frameworks for cross-platform applications.

3. Observer Pattern

Defines a one-to-many relationship, where changes in one object are notified to all its dependents.

Implementation:

class Subject {
  private observers: Array<() => void> = [];

  addObserver(observer: () => void) {
    this.observers.push(observer);
  }

  notifyObservers() {
    this.observers.forEach(observer => observer());
  }
}

const subject = new Subject();
subject.addObserver(() => console.log("Observer 1 notified!"));
subject.addObserver(() => console.log("Observer 2 notified!"));
subject.notifyObservers();

Use Case: Reactivity in front-end frameworks like Angular or React.

4. Strategy Pattern

Defines a family of algorithms, encapsulates each, and makes them interchangeable.

Implementation:

interface PaymentStrategy {
  pay(amount: number): void;
}

class CreditCardPayment implements PaymentStrategy {
  pay(amount: number) {
    console.log(`Paid ${amount} using Credit Card`);
  }
}

class PayPalPayment implements PaymentStrategy {
  pay(amount: number) {
    console.log(`Paid ${amount} using PayPal`);
  }
}

class PaymentContext {
  constructor(private strategy: PaymentStrategy) {}

  executePayment(amount: number) {
    this.strategy.pay(amount);
  }
}

const payment = new PaymentContext(new PayPalPayment());
payment.executePayment(100); // Paid 100 using PayPal

Use Case: Payment systems in e-commerce platforms.

5. Decorator Pattern

Adds new functionality to an object dynamically.

Implementation:

class SimpleCoffee {
  cost(): number {
    return 5;
  }
}

class MilkDecorator {
  constructor(private coffee: SimpleCoffee) {}

  cost(): number {
    return this.coffee.cost() + 2;
  }
}

const coffee = new SimpleCoffee();
const milkCoffee = new MilkDecorator(coffee);
console.log(milkCoffee.cost()); // Output: 7

Use Case: Adding features to a product in a shopping cart.

Design Patterns Table

Pattern Category Use Case Benefit
Singleton Creational Managing global state like configurations Guarantees single instance
Factory Creational UI components or APIs Decouples creation logic
Observer Behavioral Event systems in frameworks Simplifies communication
Strategy Behavioral Algorithm selection in runtime Enhances flexibility
Decorator Structural Extending class functionality Adds dynamic capabilities

Best Practices for Implementing Design Patterns

1. Understand the Problem: Don't complicate things with unnecessary patterns.
2. Combine Patterns: Consider using combinations such as Singleton with Factory.
3. Leverage TypeScript Features: Utilize interfaces, generics, and enums to make implementation easier.
4. Write Tests: Ensure that the patterns function as intended.

Additional Resources

  • হাতে কলমে জাভাস্ক্রিপ্ট by Junayed Ahmed
  • Refactoring to Patterns by Joshua Kerievsky

See you in the next article, lad! 😊

My personal website: https://shafayet.zya.me

Bit windy, innit bruv?😇😇

Image description

Featured ones: