Design Patterns
What are Design Patterns?
Design Patterns are solutions to commonly-occurring problems in software design.
Design patterns help developers by providing a common language and set of best practices for solving common design problems.
They were originally introduced by the "Gang of Four" (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their book, "Design Patterns: Elements of Reusable Object-Oriented Software."
Disadvantages of Design Patterns
- Overengineering: Design patterns can sometimes lead to overengineering, where developers use complex patterns to solve simple problems.
- Complexity: Design patterns can bring unnecessary complexity, if applied at wrong places.
- Learning Curve: Developers who are not familiar with them might end up getting confused and spend more time on code changes.
Types of Design Patterns
There are three main types of design patterns: Creational, Structural and Behavioral.
Creational Patterns deal with object creation mechanisms for specific situation without revealing the creation method. Creational design patterns are:
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
Structural Patterns deal with object and classes composition and relationships between different objects. Structural design patterns are:
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
Behavioral Patterns deal with communication between different objects. Behavioral design patterns are:
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
Examples of Design Patterns
Creational Patterns
- Singleton: Ensures that a class has only one instance and provides a global point of access to it.
class Singleton {
private static instance: Singleton;
private constructor() {}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // true
- Factory Method: Defines a method for creating objects without specifying their concrete classes. This method is used
instead of using a direct constructor call (
new
operator).
interface Animal {
speak(): void;
}
class Dog implements Animal {
public speak(): void {
console.log("The dog barks.");
}
}
class Cat implements Animal {
public speak(): void {
console.log("The cat meows.");
}
}
class AnimalFactory {
public getAnimal(type: string): Animal {
switch (type) {
case "dog":
return new Dog();
case "cat":
return new Cat();
default:
throw new Error("Invalid animal type.");
}
}
}
const animalFactory = new AnimalFactory();
const dog = animalFactory.getAnimal("dog");
const cat = animalFactory.getAnimal("cat");
dog.speak(); // Outputs: The dog barks.
cat.speak(); // Outputs: The cat meows.
- Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
interface Button {
paint(): void;
}
interface Checkbox {
paint(): void;
}
class WinButton implements Button {
public paint(): void {
console.log('Windows button is painted.');
}
}
class WinCheckbox implements Checkbox {
public paint(): void {
console.log('Windows checkbox is painted.');
}
}
class MacButton implements Button {
public paint(): void {
console.log('Mac button is painted.');
}
}
class MacCheckbox implements Checkbox {
public paint(): void {
console.log('Mac checkbox is painted.');
}
}
interface GUIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
class WinFactory implements GUIFactory {
public createButton(): Button {
return new WinButton();
}
public createCheckbox(): Checkbox {
return new WinCheckbox();
}
}
class MacFactory implements GUIFactory {
public createButton(): Button {
return new MacButton();
}
public createCheckbox(): Checkbox {
return new MacCheckbox();
}
}
const config = 'WIN'; // or "MAC"
let factory: GUIFactory;
if (config === 'WIN') {
factory = new WinFactory();
} else if (config === 'MAC') {
factory = new MacFactory();
} else {
throw new Error('Unknown OS.');
}
const button = factory.createButton();
const checkbox = factory.createCheckbox();
button.paint(); // Outputs: Windows button is painted.
checkbox.paint(); // Outputs: Windows checkbox is painted.
- Builder: Constructs complex objects step by step. It allows constructing different representations of an object using the same construction functions.
class Task {
name: string;
description: string;
priority: 'low' | 'medium' | 'high';
dueDate: Date;
}
interface ITaskBuilder {
setName(name: string): void;
setDescription(text: string): void;
setPriority(priority: 'low' | 'medium' | 'high'): void;
setDueDate(date: Date): void;
}
class TaskBuilder implements ITaskBuilder {
private task: Task;
constructor() {
this.task = new Task();
}
public reset(): void {
this.task = new Task();
}
public setDescription(text: string): TaskBuilder {
this.task.description = text;
return this;
}
public setPriority(priority: 'low' | 'medium' | 'high'): TaskBuilder {
this.task.priority = priority;
return this;
}
public setDueDate(date: Date): TaskBuilder {
this.task.dueDate = date;
return this;
}
public setName(name: string): TaskBuilder {
this.task.name = name;
return this;
}
public build(): Task {
const result = this.task;
this.reset();
return result;
}
}
function buildSimpleTask(): Task {
const builder = new TaskBuilder();
builder
.setName('Finish book')
.setPriority('low');
return builder.build();
}
function buildUrgentMeetingTask(): Task {
const builder = new TaskBuilder();
builder
.setName('Meet friend')
.setDescription('Meet friend at the train station')
.setPriority('high')
.setDueDate(new Date('2024-12-31'));
return builder.build();
}
- Prototype: Creates new objects by copying an existing object, known as the prototype. Usually involves creating a
clone
method, which returns a copy of object.
class Sheep {
name: string;
weight: number;
constructor(name: string, weight: number) {
this.name = name;
this.weight = weight;
}
clone(): Sheep {
return new Sheep(this.name, this.weight);
}
}
const originalSheep = new Sheep('Jolly', 20);
const clonedSheep = originalSheep.clone();
console.log(clonedSheep.name); // Output: Jolly
console.log(clonedSheep.weight); // Output: 20
Structural Patterns
- Adapter: Allows objects with incompatible interfaces to collaborate.
interface MicroUsbPort { // Target interface
connect(): string;
}
class UsbCable { // Adaptee
public specificConnect(): string {
return 'UsbCable';
}
}
class MicroUsbAdapter implements MicroUsbPort { // Adapter
private adaptee: UsbCable;
constructor(adaptee: UsbCable) {
this.adaptee = adaptee;
}
public connect(): string {
return `MicroUsbAdapter connects usb port with ${this.adaptee.specificConnect()}`;
}
}
const adaptee = new UsbCable();
const adapter = new MicroUsbAdapter(adaptee);
adapter.connect(); // Output: MicroUsbAdapter connects usb port with UsbCable
- Bridge: Decouples an abstraction from its implementation so that the two can vary independently.
interface Device { // Implementation
isEnabled(): boolean;
enable(): void;
disable(): void;
}
class TV implements Device { // Concrete implementation
private enabled = false;
public isEnabled(): boolean {
return this.enabled;
}
public enable(): void {
this.enabled = true;
}
public disable(): void {
this.enabled = false;
}
}
class Remote { // Abstraction
protected device: Device;
constructor(device: Device) {
this.device = device;
}
public togglePower(): void {
if (this.device.isEnabled()) {
this.device.disable();
} else {
this.device.enable();
}
}
}
const tv = new TV();
const remote = new Remote(tv);
remote.togglePower();
- Composite: Composes objects into tree structures to represent hierarchies where larger object is composed of smaller objects (parts). Composite lets clients work with a single object or a group of objects in the same way, interchangeably.
interface Component {
getSize(): number;
getName(): string;
}
class File implements Component { // Leaf
private name: string;
private size: number;
constructor(name: string, size: number) {
this.name = name;
this.size = size;
}
public getSize(): number {
return this.size;
}
public getName(): string {
return this.name;
}
}
class Folder implements Component { // Composite
private files: Component[] = [];
private name: string;
constructor(name: string) {
this.name = name;
}
public getSize(): number {
return this.files.reduce((acc, child) => acc + child.getSize(), 0);
}
public getName(): string {
return this.name;
}
public add(component: Component): void {
this.files.push(component);
}
}
const file1 = new File('file1', 100);
const file2 = new File('file2', 200);
const folder1 = new Folder('folder1');
folder1.add(file1);
folder1.add(file2);
file1.getSize(); // 100
folder1.getSize(); // 300
- Decorator: Dynamically adds new behavior to objects without altering their structure.
interface Coffee {
getCost(): number;
getDescription(): string;
}
class SimpleCoffee implements Coffee {
public getCost(): number {
return 10;
}
public getDescription(): string {
return 'Simple coffee';
}
}
class MilkDecorator implements Coffee {
private coffee: Coffee;
constructor(coffee: Coffee) {
this.coffee = coffee;
}
public getCost(): number {
return this.coffee.getCost() + 2;
}
public getDescription(): string {
return `${this.coffee.getDescription()}, milk`;
}
}
const coffee: Coffee = new SimpleCoffee();
const milkCoffee: Coffee = new MilkDecorator(coffee);
console.log(coffee.getCost()); // Output: 12
console.log(coffee.getDescription()); // Output: Simple coffee, milk
- Facade: Provides a simplified interface to a complex system.
class Subsystem1 {
public operation1(): string {
return 'Subsystem1: Ready!';
}
}
class Subsystem2 {
public operation2(): string {
return 'Subsystem2: Get ready!';
}
}
class Facade {
private subsystem1: Subsystem1;
private subsystem2: Subsystem2;
constructor(subsystem1: Subsystem1, subsystem2: Subsystem2) {
this.subsystem1 = subsystem1;
this.subsystem2 = subsystem2;
}
public operation(): string {
let result = 'Facade initializes subsystems:\n';
result += this.subsystem1.operation1();
result += this.subsystem2.operation2();
return result;
}
}
const subsystem1 = new Subsystem1();
const subsystem2 = new Subsystem2();
const facade = new Facade(subsystem1, subsystem2);
facade.operation();
- Flyweight: Minimizes memory usage or computational expenses by sharing as much as possible with related objects. Provides a caching mechanism to reuse objects.
class TreeType {
private name: string;
private color: string;
private texture: string;
constructor(name: string, color: string, texture: string) {
this.name = name;
this.color = color;
this.texture = texture;
}
public draw(x: number, y: number): void {
console.log(`Drawing tree ${this.name} at (${x}, ${y}) with color ${this.color} and texture ${this.texture}`);
}
}
class TreeFactory {
private treeTypes: { [key: string]: TreeType } = {};
public getTreeType(name: string, color: string, texture: string): TreeType {
if (!this.treeTypes[name]) {
this.treeTypes[name] = new TreeType(name, color, texture);
}
return this.treeTypes[name];
}
}
class Tree {
private x: number;
private y: number;
private type: TreeType;
constructor(x: number, y: number, type: TreeType) {
this.x = x;
this.y = y;
this.type = type;
}
public draw(): void {
this.type.draw(this.x, this.y);
}
}
const treeFactory = new TreeFactory();
const treeType1 = treeFactory.getTreeType('pine', 'green', 'smooth');
const treeType2 = treeFactory.getTreeType('oak', 'brown', 'rough');
const tree1 = new Tree(1, 2, treeType1);
const tree2 = new Tree(3, 4, treeType2);
tree1.draw(); // Output: Drawing tree pine at (1, 2) with color green and texture smooth
tree2.draw(); // Output: Drawing tree oak at (3, 4) with color brown and texture rough
- Proxy: Provides a placeholder for another object to control access to it.
interface Image {
display(): void;
}
class RealImage implements Image {
private filename: string;
constructor(filename: string) {
this.filename = filename;
}
public display(): void {
console.log(`Displaying image ${this.filename}`);
}
}
class ProxyImage implements Image {
private realImage: RealImage;
constructor(realImage: RealImage) {
this.realImage = realImage;
}
public display(): void {
if(this.canAccess()) {
this.realImage.display();
}
}
private canAccess(): boolean {
return true; // Add logic to check if image can be displayed
}
}
const image: Image = new Image('picture.jpg');
const proxy: Image = new ProxyImage(image);
proxy.display(); // Output: Displaying image picture.jpg
Behavioral Patterns
- Chain of Responsibility: Passes a request along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler.
abstract class Handler {
protected nextHandler: Handler;
public setNextHandler(nextHandler: Handler) {
this.nextHandler = nextHandler;
}
public handleRequest(request: string) {
if (this.nextHandler) {
this.nextHandler.handleRequest(request);
}
}
}
class TechnicalSupportHandler extends Handler {
public handleRequest(request: string): void {
if (request === 'technical') {
console.log('Technical support is handling request');
} else {
super.handleRequest(request);
}
}
}
class GeneralSupportHandler extends Handler {
public handleRequest(request: string): void {
if (request === 'general') {
console.log('General support is handling request');
} else {
super.handleRequest(request);
}
}
}
const handler1 = new TechnicalSupportHandler();
const handler2 = new GeneralSupportHandler();
handler1.setNextHandler(handler2);
handler1.handleRequest('technical'); // Output: Technical support is handling request
handler1.handleRequest('general'); // Output: General support is handling request
- Command: Converts requests or simple operations into objects. It decouples the objects that send requests from the objects responsible for executing them.
interface Command {
execute(): void;
}
class BankAccount {
private balance: number;
constructor(balance: number) {
this.balance = balance;
}
public deposit(amount: number) {
this.balance += amount;
}
public withdraw(amount: number) {
this.balance -= amount;
}
public getBalance(): number {
return this.balance;
}
}
class WithdrawCommand implements Command {
private account: BankAccount;
private amount: number;
constructor(account: BankAccount, amount: number) {
this.account = account;
this.amount = amount;
}
public execute() {
this.account.withdraw(this.amount);
}
}
class DepositCommand implements Command {
private account: BankAccount;
private amount: number;
constructor(account: BankAccount, amount: number) {
this.account = account;
this.amount = amount;
}
public execute(): void {
this.account.deposit(this.amount);
}
}
const account = new BankAccount(100);
const deposit = new DepositCommand(account, 50);
const withdraw = new WithdrawCommand(account, 25);
const commands: Command[] = [deposit, withdraw];
commands.forEach(command => command.execute());
- Iterator: Provides a way to traverse the elements of a collection sequentially without exposing its underlying representation.
interface Iterator<T> {
next(): T | null;
hasNext(): boolean;
}
class ListIterator<T> implements Iterator<T> {
private collection: T[];
private position: number = 0;
constructor(collection: T[]) {
this.collection = collection;
}
public next(): T | null {
if (this.hasNext()) {
return this.collection[this.position++];
}
return null;
}
public hasNext(): boolean {
return this.position < this.collection.length;
}
}
const collection = [1, 2, 3, 4, 5];
const iterator = new ListIterator(collection);
while (iterator.hasNext()) {
console.log(iterator.next()); // Output: 1, 2, 3, 4, 5
}
- Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
interface Strategy {
execute(a: number, b: number): number;
}
class AddStrategy implements Strategy {
public execute(a: number, b: number): number {
return a + b;
}
}
class SubtractStrategy implements Strategy {
public execute(a: number, b: number): number {
return a - b;
}
}
class Calculator {
private strategy: Strategy;
constructor(strategy: Strategy) {
this.strategy = strategy;
}
public setStrategy(strategy: Strategy): void {
this.strategy = strategy;
}
public executeStrategy(a: number, b: number): number {
return this.strategy.execute(a, b);
}
}
const addStrategy = new AddStrategy();
const subtractStrategy = new SubtractStrategy();
const calculator = new Calculator(addStrategy);
calculator.executeStrategy(5, 3); // Output: 8
calculator.setStrategy(subtractStrategy);
calculator.executeStrategy(5, 3); // Output: 2