Explore fundamental software design patterns that are widely used in modern application development. Understanding these patterns can lead to more robust, maintainable, and flexible code.
Detailed Explanations and Examples
Click on a pattern name above or browse through the sections below for in-depth explanations, problem statements, solutions, and code examples in various programming languages.
Singleton Pattern
Problem
Ensure that a class only has one instance and provide a global point of access to it. This is often useful for managing shared resources like database connections or configuration settings.
Solution
The Singleton pattern involves a private constructor, a static member to hold the single instance, and a public static method to access that instance. Lazy initialization or eager initialization can be used.
Example Snippet (Conceptual)
// Conceptual Singleton
class Singleton {
private static instance: Singleton | null = null;
private constructor() {
// Private constructor to prevent direct instantiation
}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
public someOperation(): void {
console.log("Executing some operation.");
}
}
// Usage
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true
s1.someOperation();
Factory Method Pattern
Problem
Define an interface for creating an object, but allow subclasses to decide which class to instantiate. This pattern decouples the client code from the concrete classes it needs to create.
Solution
A creator class declares the factory method, which is supposed to return an object of a Product type. The concrete creator classes implement the factory method to return an instance of a concrete product.
Example Snippet (Conceptual)
// Conceptual Factory Method
interface Product {
operation(): string;
}
class ConcreteProductA implements Product {
operation(): string {
return "Result of ConcreteProductA";
}
}
class ConcreteProductB implements Product {
operation(): string {
return "Result of ConcreteProductB";
}
}
abstract class Creator {
public abstract factoryMethod(): Product;
public someOperation(): string {
const product = this.factoryMethod();
return `Creator: The same creator's code has just worked with ${product.operation()}`;
}
}
class ConcreteCreatorA extends Creator {
public factoryMethod(): Product {
return new ConcreteProductA();
}
}
class ConcreteCreatorB extends Creator {
public factoryMethod(): Product {
return new ConcreteProductB();
}
}
// Usage
function clientCode(creator: Creator) {
console.log("Client: I'm not aware of the creator's class, but it still works.");
console.log(creator.someOperation());
}
clientCode(new ConcreteCreatorA());
clientCode(new ConcreteCreatorB());
Adapter Pattern
Problem
Allow objects with incompatible interfaces to collaborate. This is common when working with legacy code or third-party libraries.
Solution
The Adapter pattern acts as a bridge between two incompatible interfaces. It wraps one of the objects and provides an interface that the client expects.
Example Snippet (Conceptual)
// Conceptual Adapter
class Target {
request(): string {
return "Target: The default target's behavior.";
}
}
class Adaptee {
specificRequest(): string {
return "Adaptee: The adaptee's business logic.";
}
}
class Adapter extends Target {
private adaptee: Adaptee;
constructor(adaptee: Adaptee) {
super();
this.adaptee = adaptee;
}
public request(): string {
return `Adapter: (TRANSLATED) ${this.adaptee.specificRequest()}`;
}
}
// Usage
function clientCode(target: Target) {
console.log(target.request());
}
const adaptee = new Adaptee();
clientCode(new Adapter(adaptee));
Observer Pattern
Problem
Define a one-to-many dependency between objects. When one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.
Solution
The Subject maintains a list of Observers. When its state changes, it iterates over the list and calls an update method on each Observer.
Example Snippet (Conceptual)
// Conceptual Observer
interface Observer {
update(subject: Subject): void;
}
interface Subject {
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
}
class ConcreteSubject implements Subject {
public state: number = 0;
private observers: Observer[] = [];
public attach(observer: Observer): void {
const isObserverAttached = this.observers.includes(observer);
if (isObserverAttached) {
return console.log('Subject: Observer attached twice.');
}
this.observers.push(observer);
console.log('Subject: Attached an observer.');
}
public detach(observer: Observer): void {
const observerIndex = this.observers.indexOf(observer);
if (observerIndex === -1) {
return console.log('Subject: Nonexistent observer.');
}
this.observers.splice(observerIndex, 1);
console.log('Subject: Detached an observer.');
}
public notify(): void {
console.log('Subject: Notifying observers...');
for (const observer of this.observers) {
observer.update(this);
}
}
public businessLogic(): void {
console.log('Subject: Performing business logic...');
this.state = Math.floor(Math.random() * (10 + 1));
console.log(`Subject: My state has changed to: ${this.state}`);
this.notify();
}
}
class ConcreteObserverA implements Observer {
public update(subject: Subject): void {
if ((subject as ConcreteSubject).state % 2 === 0) {
console.log('ConcreteObserverA: Reacted to the event.');
}
}
}
class ConcreteObserverB implements Observer {
public update(subject: Subject): void {
console.log('ConcreteObserverB: Reacted to the event.');
}
}
// Usage
const subject = new ConcreteSubject();
const observerA = new ConcreteObserverA();
subject.attach(observerA);
const observerB = new ConcreteObserverB();
subject.attach(observerB);
subject.businessLogic();
subject.businessLogic();
subject.detach(observerB);
subject.businessLogic();
Strategy Pattern
Problem
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Solution
The Strategy pattern involves a context class that holds a reference to a Strategy object and delegates the execution of the algorithm to the Strategy object. Concrete strategies implement the algorithm in different ways.
Example Snippet (Conceptual)
// Conceptual Strategy
interface Strategy {
execute(data: string): string;
}
class ConcreteStrategyA implements Strategy {
execute(data: string): string {
return `ConcreteStrategyA: ${data.split('').reverse().join('')}`;
}
}
class ConcreteStrategyB implements Strategy {
execute(data: string): string {
return `ConcreteStrategyB: ${data.toUpperCase()}`;
}
}
class Context {
private strategy: Strategy;
constructor(strategy: Strategy) {
this.strategy = strategy;
}
public setStrategy(strategy: Strategy) {
this.strategy = strategy;
}
public doSomeBusinessLogic(): string {
const result = this.strategy.execute("some data");
return `Context: The result of the strategy. \n${result}`;
}
}
// Usage
const context = new Context(new ConcreteStrategyA());
console.log("Client: Strategy is set to normal sorting.");
console.log(context.doSomeBusinessLogic());
console.log("\nClient: Strategy is changed to upper-casing.");
context.setStrategy(new ConcreteStrategyB());
console.log(context.doSomeBusinessLogic());
Visitor Pattern
Problem
Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
Solution
The Visitor pattern consists of two main parts: the Visitor interface (which declares a visit method for each concrete element type) and the concrete Visitor classes (which implement the operations). The element classes have an accept method that takes a visitor and calls the appropriate visit method on it.
Example Snippet (Conceptual)
// Conceptual Visitor
interface Visitor {
visitConcreteElementA(concreteElementA: ConcreteElementA): void;
visitConcreteElementB(concreteElementB: ConcreteElementB): void;
}
interface Element {
accept(visitor: Visitor): void;
}
class ConcreteElementA implements Element {
accept(visitor: Visitor): void {
visitor.visitConcreteElementA(this);
}
public exclusiveA(): string {
return "This is exclusive method for ConcreteElementA.";
}
}
class ConcreteElementB implements Element {
accept(visitor: Visitor): void {
visitor.visitConcreteElementB(this);
}
public exclusiveB(): string {
return "This is exclusive method for ConcreteElementB.";
}
}
class ConcreteVisitor1 implements Visitor {
visitConcreteElementA(concreteElementA: ConcreteElementA): void {
console.log(`${concreteElementA.exclusiveA()} -- ConcreteVisitor1`);
}
visitConcreteElementB(concreteElementB: ConcreteElementB): void {
console.log(`${concreteElementB.exclusiveB()} -- ConcreteVisitor1`);
}
}
class ConcreteVisitor2 implements Visitor {
visitConcreteElementA(concreteElementA: ConcreteElementA): void {
console.log(`${concreteElementA.exclusiveA()} -- ConcreteVisitor2`);
}
visitConcreteElementB(concreteElementB: ConcreteElementB): void {
console.log(`${concreteElementB.exclusiveB()} -- ConcreteVisitor2`);
}
}
// Usage
const elements: Element[] = [
new ConcreteElementA(),
new ConcreteElementB(),
];
const visitor1 = new ConcreteVisitor1();
for (const element of elements) {
element.accept(visitor1);
}
console.log("\n");
const visitor2 = new ConcreteVisitor2();
for (const element of elements) {
element.accept(visitor2);
}