dev-resources.site
for different kinds of informations.
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:
- Creational Patterns: Concerned with the mechanisms of object creation.
- Structural Patterns: Emphasize the composition and organization of objects.
- 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?😇😇
Featured ones: