Introduction
Design patterns are reusable solutions to common software design problems. They help developers write more maintainable, scalable, and efficient code. This guide covers the most important design patterns that every developer should know, with practical examples and use cases.
Creational Patterns
- Singleton Pattern Ensures a class has only one instance and provides a global point of access to it.
class Database {
private static instance: Database;
private constructor() {}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
public query(sql: string): void {
console.log(`Executing query: ${sql}`);
}
}
// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true
- Factory Method Pattern Defines an interface for creating objects but lets subclasses decide which class to instantiate.
interface Product {
operation(): string;
}
class ConcreteProductA implements Product {
operation(): string {
return 'Product A';
}
}
class ConcreteProductB implements Product {
operation(): string {
return 'Product B';
}
}
abstract class Creator {
abstract factoryMethod(): Product;
someOperation(): string {
const product = this.factoryMethod();
return `Creator: ${product.operation()}`;
}
}
class ConcreteCreatorA extends Creator {
factoryMethod(): Product {
return new ConcreteProductA();
}
}
class ConcreteCreatorB extends Creator {
factoryMethod(): Product {
return new ConcreteProductB();
}
}
- Builder Pattern Separates the construction of a complex object from its representation.
class Computer {
private parts: string[] = [];
add(part: string): void {
this.parts.push(part);
}
listParts(): string {
return `Computer parts: ${this.parts.join(', ')}`;
}
}
interface Builder {
reset(): void;
setCPU(): void;
setRAM(): void;
setStorage(): void;
}
class ComputerBuilder implements Builder {
private computer: Computer;
constructor() {
this.reset();
}
reset(): void {
this.computer = new Computer();
}
setCPU(): void {
this.computer.add('Intel i7');
}
setRAM(): void {
this.computer.add('16GB RAM');
}
setStorage(): void {
this.computer.add('1TB SSD');
}
getResult(): Computer {
const result = this.computer;
this.reset();
return result;
}
}
Structural Patterns
- Adapter Pattern Allows incompatible interfaces to work together.
interface OldSystem {
oldMethod(): string;
}
interface NewSystem {
newMethod(): string;
}
class OldImplementation implements OldSystem {
oldMethod(): string {
return 'Old system method';
}
}
class Adapter implements NewSystem {
private oldSystem: OldSystem;
constructor(oldSystem: OldSystem) {
this.oldSystem = oldSystem;
}
newMethod(): string {
return this.oldSystem.oldMethod();
}
}
- Decorator Pattern Attaches additional responsibilities to objects dynamically.
interface Component {
operation(): string;
}
class ConcreteComponent implements Component {
operation(): string {
return 'ConcreteComponent';
}
}
class Decorator implements Component {
protected component: Component;
constructor(component: Component) {
this.component = component;
}
operation(): string {
return this.component.operation();
}
}
class ConcreteDecoratorA extends Decorator {
operation(): string {
return `ConcreteDecoratorA(${super.operation()})`;
}
}
- Facade Pattern Provides a simplified interface to a complex subsystem.
class SubsystemA {
operationA(): string {
return 'SubsystemA: Ready!';
}
}
class SubsystemB {
operationB(): string {
return 'SubsystemB: Go!';
}
}
class Facade {
private subsystemA: SubsystemA;
private subsystemB: SubsystemB;
constructor() {
this.subsystemA = new SubsystemA();
this.subsystemB = new SubsystemB();
}
operation(): string {
return `${this.subsystemA.operationA()}\n${this.subsystemB.operationB()}`;
}
}
Behavioral Patterns
- Observer Pattern Defines a one-to-many dependency between objects.
interface Observer {
update(data: string): void;
}
class Subject {
private observers: Observer[] = [];
private state: string = '';
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(): void {
for (const observer of this.observers) {
observer.update(this.state);
}
}
setState(state: string): void {
this.state = state;
this.notify();
}
}
- Strategy Pattern Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
interface Strategy {
execute(a: number, b: number): number;
}
class AddStrategy implements Strategy {
execute(a: number, b: number): number {
return a + b;
}
}
class SubtractStrategy implements Strategy {
execute(a: number, b: number): number {
return a - b;
}
}
class Context {
private strategy: Strategy;
constructor(strategy: Strategy) {
this.strategy = strategy;
}
setStrategy(strategy: Strategy): void {
this.strategy = strategy;
}
executeStrategy(a: number, b: number): number {
return this.strategy.execute(a, b);
}
}
- Command Pattern Encapsulates a request as an object.
interface Command {
execute(): void;
}
class SimpleCommand implements Command {
private payload: string;
constructor(payload: string) {
this.payload = payload;
}
execute(): void {
console.log(`SimpleCommand: ${this.payload}`);
}
}
class ComplexCommand implements Command {
private receiver: Receiver;
private a: string;
private b: string;
constructor(receiver: Receiver, a: string, b: string) {
this.receiver = receiver;
this.a = a;
this.b = b;
}
execute(): void {
this.receiver.doSomething(this.a);
this.receiver.doSomethingElse(this.b);
}
}
- When to Use Design Patterns Use patterns to solve recurring problems Don't over-engineer simple solutions Consider the context and requirements Keep patterns simple and maintainable
- Common Pitfalls Overusing patterns Using wrong patterns Not understanding the problem Ignoring simpler solutions
- Implementation Tips Start with the simplest solution Refactor when patterns become necessary Document pattern usage Consider maintainability Real-World Examples
- Singleton in Database Connections
class DatabaseConnection {
private static instance: DatabaseConnection;
private connection: any;
private constructor() {
// Initialize connection
}
public static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
public query(sql: string): any {
// Execute query
}
}
- Observer in Event Systems
class EventEmitter {
private listeners: Map<string, Function[]> = new Map();
on(event: string, callback: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(callback);
}
emit(event: string, data: any): void {
if (this.listeners.has(event)) {
this.listeners.get(event)!.forEach(callback => callback(data));
}
}
}
Conclusion
Design patterns are powerful tools that can help you write better code, but they should be used judiciously. Remember:
Patterns are solutions to common problems
Not every problem needs a pattern
Keep your code simple and maintainable
Choose patterns based on your specific needs
Key Takeaways
Understand the different types of patterns
Know when to use each pattern
Implement patterns correctly
Consider maintainability and simplicity
Document pattern usage
Test pattern implementations
Refactor when necessary
Keep learning and practicing
๐ Ready to kickstart your tech career?
๐ [Apply to 10000Coders]
๐ [Learn Web Development for Free]
๐ [See how we helped 2500+ students get jobs]
Top comments (0)