Main Design Patterns

Design patterns are typical solutions to common problems in software design. They are like pre-made blueprints that one can customize to solve recurring design problems in code. There are 23 classic design patterns defined by the “Gang of Four”. But some are more frequently used in real-world applications than others.

Categories of Design Patterns

There are three main categories of design patterns:

1. Creational Patterns

Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of object creation could result in design problems or add complexity to the design. Creational design patterns solve this problem by somehow controlling this object creation.

Key characteristics:

  • They abstract the instantiation process.
  • They help make a system independent of how its objects are created, composed, and represented.

2. Structural Patterns

Structural patterns are concerned with how classes and objects are composed to form larger structures. They use inheritance to compose interfaces or implementations.

Key characteristics:

  • They focus on simplifying the structure by identifying the relationships between entities.
  • They help in building flexible and efficient software by forming large object structures between many disparate objects.

3. Behavioral Patterns

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. They characterize complex control flow that’s difficult to follow at run-time.

Key characteristics:

  • They focus on communication between objects.
  • They deal with how objects distribute work and describe how disparate parts of a system communicate.

Now, let’s focus on the most widely used design patterns across these categories, with multiple examples for each.

Widely Used Design Patterns With Examples

1. Singleton (Creational Pattern)

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

Real-World Usage: Database connections, logging, caching, thread pools.

Example 1: Classic Singleton

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# Usage
s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # Output: True

Example 2: Thread-Safe Singleton

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

# Usage
def worker():
    singleton = ThreadSafeSingleton()
    print(f"ID of singleton in {threading.current_thread().name}: {id(singleton)}")

threads = [threading.Thread(target=worker) for _ in range(5)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

2. Factory Method (Creational Pattern)

Purpose: Defines an interface for creating an object, but lets subclasses decide which class to instantiate.

Real-World Usage: GUI libraries, database access layers, file format parsers.

Example 1: Simple Factory Method

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

class AnimalFactory:
    def create_animal(self, animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        else:
            raise ValueError("Unknown animal type")

# Usage
factory = AnimalFactory()
dog = factory.create_animal("dog")
cat = factory.create_animal("cat")
print(dog.speak())  # Output: Woof!
print(cat.speak())  # Output: Meow!

Example 2: Factory Method with Subclasses

from abc import ABC, abstractmethod

class Creator(ABC):
    @abstractmethod
    def factory_method(self):
        pass

    def some_operation(self):
        product = self.factory_method()
        result = f"Creator: The same creator's code has just worked with {product.operation()}"
        return result

class ConcreteCreator1(Creator):
    def factory_method(self):
        return ConcreteProduct1()

class ConcreteCreator2(Creator):
    def factory_method(self):
        return ConcreteProduct2()

class Product(ABC):
    @abstractmethod
    def operation(self):
        pass

class ConcreteProduct1(Product):
    def operation(self):
        return "Result of ConcreteProduct1"

class ConcreteProduct2(Product):
    def operation(self):
        return "Result of ConcreteProduct2"

# Usage
creator1 = ConcreteCreator1()
print(creator1.some_operation())

creator2 = ConcreteCreator2()
print(creator2.some_operation())

3. Observer (Behavioral Pattern)

Purpose: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Real-World Usage: Event handling systems, Model-View-Controller (MVC) architectures, publish-subscribe systems.

Example 1: Weather Station

class WeatherStation:
    def __init__(self):
        self._observers = []
        self._temperature = 0

    def register_observer(self, observer):
        self._observers.append(observer)

    def remove_observer(self, observer):
        self._observers.remove(observer)

    def notify_observers(self):
        for observer in self._observers:
            observer.update(self._temperature)

    def set_temperature(self, temperature):
        self._temperature = temperature
        self.notify_observers()

class Display:
    def update(self, temperature):
        print(f"Current temperature: {temperature}°C")

# Usage
weather_station = WeatherStation()
display1 = Display()
display2 = Display()

weather_station.register_observer(display1)
weather_station.register_observer(display2)

weather_station.set_temperature(25)  # Both displays will show: Current temperature: 25°C
weather_station.set_temperature(30)  # Both displays will show: Current temperature: 30°C

Example 2: Stock Market

class Stock:
    def __init__(self, symbol, price):
        self.symbol = symbol
        self.price = price
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)

    def set_price(self, price):
        self.price = price
        self.notify()

class Investor:
    def __init__(self, name):
        self.name = name

    def update(self, stock):
        print(f"{self.name} notified. {stock.symbol} now at ${stock.price}")

# Usage
stock = Stock("AAPL", 150)
investor1 = Investor("John")
investor2 = Investor("Jane")

stock.attach(investor1)
stock.attach(investor2)

stock.set_price(155)
stock.set_price(160)

4. Strategy (Behavioral Pattern)

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

Real-World Usage: Sorting algorithms, payment processing systems, compression algorithms.

Example 1: Sorting Strategy

from abc import ABC, abstractmethod

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data):
        pass

class BubbleSort(SortStrategy):
    def sort(self, data):
        print("Sorting using bubble sort")
        return sorted(data)

class QuickSort(SortStrategy):
    def sort(self, data):
        print("Sorting using quick sort")
        return sorted(data)

class Sorter:
    def __init__(self, strategy):
        self._strategy = strategy

    def sort(self, data):
        return self._strategy.sort(data)

# Usage
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
sorter = Sorter(BubbleSort())
print(sorter.sort(data))

sorter = Sorter(QuickSort())
print(sorter.sort(data))

Example 2: Payment Strategy

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number, cvv):
        self.card_number = card_number
        self.cvv = cvv

    def pay(self, amount):
        print(f"Paid ${amount} using Credit Card {self.card_number}")

class PayPalPayment(PaymentStrategy):
    def __init__(self, email):
        self.email = email

    def pay(self, amount):
        print(f"Paid ${amount} using PayPal account {self.email}")

class ShoppingCart:
    def __init__(self):
        self.items = []

    def add_item(self, item, price):
        self.items.append((item, price))

    def calculate_total(self):
        return sum(price for _, price in self.items)

    def checkout(self, payment_strategy):
        total = self.calculate_total()
        payment_strategy.pay(total)

# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 1000)
cart.add_item("Mouse", 50)

credit_card = CreditCardPayment("1234-5678-9012-3456", "123")
paypal = PayPalPayment("user@example.com")

cart.checkout(credit_card)
cart.checkout(paypal)

5. Decorator (Structural Pattern)

Purpose: Attaches additional responsibilities to an object dynamically.

Real-World Usage: UI component enhancements, adding features to objects without affecting other objects of the same class, middleware in web frameworks.

Example 1: Coffee Shop

class Coffee:
    def cost(self):
        return 5

    def description(self):
        return "Plain coffee"

class MilkDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 2

    def description(self):
        return f"{self._coffee.description()}, milk"

class SugarDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 1

    def description(self):
        return f"{self._coffee.description()}, sugar"

# Usage
coffee = Coffee()
coffee_with_milk = MilkDecorator(coffee)
coffee_with_milk_and_sugar = SugarDecorator(coffee_with_milk)

print(f"{coffee_with_milk_and_sugar.description()}: ${coffee_with_milk_and_sugar.cost()}")
# Output: Plain coffee, milk, sugar: $8

Example 2: Text Formatting

class Text:
    def __init__(self, content):
        self._content = content

    def render(self):
        return self._content

class BoldDecorator:
    def __init__(self, wrapped):
        self._wrapped = wrapped

    def render(self):
        return f"<b>{self._wrapped.render()}</b>"

class ItalicDecorator:
    def __init__(self, wrapped):
        self._wrapped = wrapped

    def render(self):
        return f"<i>{self._wrapped.render()}</i>"

class UnderlineDecorator:
    def __init__(self, wrapped):
        self._wrapped = wrapped

    def render(self):
        return f"<u>{self._wrapped.render()}</u>"

# Usage
text = Text("Hello, World!")
bold_text = BoldDecorator(text)
italic_bold_text = ItalicDecorator(bold_text)
final_text = UnderlineDecorator(italic_bold_text)

print(final_text.render())
# Output: <u><i><b>Hello, World!</b></i></u>

6. Builder (Creational Pattern)

Purpose: Separates the construction of a complex object from its representation, allowing the same construction process to create various representations.

Real-World Usage: Creating complex objects step by step, constructing composite structures, implementing fluent interfaces.

Example 1: Computer Builder

class Computer:
    def __init__(self):
        self.cpu = None
        self.ram = None
        self.storage = None

    def __str__(self):
        return f"Computer [CPU: {self.cpu}, RAM: {self.ram}GB, Storage: {self.storage}GB]"

class ComputerBuilder:
    def __init__(self):
        self.computer = Computer()

    def configure_cpu(self, cpu):
        self.computer.cpu = cpu
        return self

    def configure_ram(self, ram):
        self.computer.ram = ram
        return self

    def configure_storage(self, storage):
        self.computer.storage = storage
        return self

    def build(self):
        return self.computer

# Usage
builder = ComputerBuilder()
computer = builder.configure_cpu("Intel i7").configure_ram(16).configure_storage(512).build()
print(computer)  # Output: Computer [CPU: Intel i7, RAM: 16GB, Storage: 512GB]

Example 2: Pizza Builder

class Pizza:
    def __init__(self):
        self.size = None
        self.cheese = False
        self.pepperoni = False
        self.mushrooms = False

    def __str__(self):
        toppings = []
        if self.cheese:
            toppings.append("cheese")
        if self.pepperoni:
            toppings.append("pepperoni")
        if self.mushrooms:
            toppings.append("mushrooms")
        return f"{self.size} inch Pizza with {', '.join(toppings)}"

class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()

    def size(self, size):
        self.pizza.size = size
        return self

    def add_cheese(self):
        self.pizza.cheese = True
        return self

    def add_pepperoni(self):
        self.pizza.pepperoni = True
        return self

    def add_mushrooms(self):
        self.pizza.mushrooms = True
        return self

    def build(self):
        return self.pizza

# Usage
builder = PizzaBuilder()
pizza = builder.size(12).add_cheese().add_pepperoni().build()
print(pizza)  # Output: 12 inch Pizza with cheese, pepperoni

7. Adapter (Structural Pattern)

Purpose: Allows objects with incompatible interfaces to collaborate.

Real-World Usage: Integrating new systems with legacy code, working with third-party libraries, creating wrappers for existing classes.

Example 1: Power Adapter

class EuropeanSocketInterface:
    def voltage(self):
        return 230

    def live(self):
        return 1

    def neutral(self):
        return -1

class USASocketInterface:
    def voltage(self):
        return 120

    def live(self):
        return 1

    def neutral(self):
        return -1

class Adapter:
    def __init__(self, socket):
        self.socket = socket

    def voltage(self):
        return 110

    def live(self):
        return self.socket.live()

    def neutral(self):
        return self.socket.neutral()

class ElectricKettle:
    def __init__(self, power):
        self.power = power

    def boil(self):
        if self.power.voltage() > 110:
            print("Kettle on fire!")
        else:
            print("Coffee time!")

# Usage
eu_socket = EuropeanSocketInterface()
adapter = Adapter(eu_socket)
kettle = ElectricKettle(adapter)
kettle.boil()  # Output: Coffee time!

Example 2: Media Player Adapter

from abc import ABC, abstractmethod

class MediaPlayer(ABC):
    @abstractmethod
    def play(self, audio_type, file_name):
        pass

class AdvancedMediaPlayer(ABC):
    @abstractmethod
    def play_vlc(self, file_name):
        pass

    @abstractmethod
    def play_mp4(self, file_name):
        pass

class VlcPlayer(AdvancedMediaPlayer):
    def play_vlc(self, file_name):
        print(f"Playing vlc file. Name: {file_name}")

    def play_mp4(self, file_name):
        pass

class Mp4Player(AdvancedMediaPlayer):
    def play_vlc(self, file_name):
        pass

    def play_mp4(self, file_name):
        print(f"Playing mp4 file. Name: {file_name}")

class MediaAdapter(MediaPlayer):
    def __init__(self, audio_type):
        if audio_type == "vlc":
            self.advanced_music_player = VlcPlayer()
        elif audio_type == "mp4":
            self.advanced_music_player = Mp4Player()

    def play(self, audio_type, file_name):
        if audio_type == "vlc":
            self.advanced_music_player.play_vlc(file_name)
        elif audio_type == "mp4":
            self.advanced_music_player.play_mp4(file_name)

class AudioPlayer(MediaPlayer):
    def play(self, audio_type, file_name):
        if audio_type == "mp3":
            print(f"Playing mp3 file. Name: {file_name}")
        elif audio_type == "vlc" or audio_type == "mp4":
            media_adapter = MediaAdapter(audio_type)
            media_adapter.play(audio_type, file_name)
        else:
            print("Invalid media. " + audio_type + " format not supported")

# Usage
audio_player = AudioPlayer()
audio_player.play("mp3", "beyond_the_horizon.mp3")
audio_player.play("mp4", "alone.mp4")
audio_player.play("vlc", "far_far_away.vlc")
audio_player.play("avi", "mind_me.avi")

8. Facade (Structural Pattern)

Purpose: Provides a simplified interface to a library, a framework, or any other complex set of classes.

Real-World Usage: Simplifying complex library interactions, providing a single entry point for a subsystem, wrapping poorly designed collections of APIs.

Example 1: Home Theater Facade

class Amplifier:
    def on(self):
        print("Amplifier on")

    def off(self):
        print("Amplifier off")

    def set_volume(self, level):
        print(f"Setting volume to {level}")

class DVDPlayer:
    def on(self):
        print("DVD Player on")

    def off(self):
        print("DVD Player off")

    def play(self, movie):
        print(f"Playing '{movie}'")

class Projector:
    def on(self):
        print("Projector on")

    def off(self):
        print("Projector off")

class HomeTheaterFacade:
    def __init__(self, amp, dvd, projector):
        self.amp = amp
        self.dvd = dvd
        self.projector = projector

    def watch_movie(self, movie):
        print("Get ready to watch a movie...")
        self.amp.on()
        self.amp.set_volume(5)
        self.dvd.on()
        self.projector.on()
        self.dvd.play(movie)

    def end_movie(self):
        print("Shutting movie theater down...")
        self.amp.off()
        self.dvd.off()
        self.projector.off()

# Usage
amp = Amplifier()
dvd = DVDPlayer()
projector = Projector()
home_theater = HomeTheaterFacade(amp, dvd, projector)

home_theater.watch_movie("Inception")
home_theater.end_movie()

Example 2: Online Shopping Facade

class Inventory:
    def check(self, item_name):
        print(f"Checking inventory for {item_name}")
        return True

class Payment:
    def process(self, amount):
        print(f"Processing payment of ${amount}")
        return True

class Shipping:
    def ship(self, address):
        print(f"Shipping to {address}")

class OnlineShoppingFacade:
    def __init__(self):
        self.inventory = Inventory()
        self.payment = Payment()
        self.shipping = Shipping()

    def place_order(self, item, amount, address):
        if self.inventory.check(item):
            if self.payment.process(amount):
                self.shipping.ship(address)
                print("Order placed successfully!")
            else:
                print("Payment failed.")
        else:
            print("Item out of stock.")

# Usage
shop = OnlineShoppingFacade()
shop.place_order("Laptop", 999, "123 Main St, Anytown, USA")

9. Command (Behavioral Pattern)

Purpose: Encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

Real-World Usage: GUI buttons and menu items, macro recording, multi-level undo/redo, job queues.

Example 1: Remote Control

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

class Light:
    def on(self):
        print("Light is on")

    def off(self):
        print("Light is off")

class LightOnCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        self.light.on()

class LightOffCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        self.light.off()

class RemoteControl:
    def __init__(self):
        self.command = None

    def set_command(self, command):
        self.command = command

    def press_button(self):
        self.command.execute()

# Usage
light = Light()
light_on = LightOnCommand(light)
light_off = LightOffCommand(light)

remote = RemoteControl()
remote.set_command(light_on)
remote.press_button()  # Output: Light is on

remote.set_command(light_off)
remote.press_button()  # Output: Light is off

Example 2: Stock Trading

from abc import ABC, abstractmethod

class Order(ABC):
    @abstractmethod
    def execute(self):
        pass

class Stock:
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    def buy(self):
        print(f"Stock [ Name: {self.name}, Quantity: {self.quantity} ] bought")

    def sell(self):
        print(f"Stock [ Name: {self.name}, Quantity: {self.quantity} ] sold")

class BuyStock(Order):
    def __init__(self, stock):
        self.stock = stock

    def execute(self):
        self.stock.buy()

class SellStock(Order):
    def __init__(self, stock):
        self.stock = stock

    def execute(self):
        self.stock.sell()

class Broker:
    def __init__(self):
        self.order_list = []

    def take_order(self, order):
        self.order_list.append(order)

    def place_orders(self):
        for order in self.order_list:
            order.execute()
        self.order_list = []

# Usage
stock = Stock("ABC", 100)
buy_stock_order = BuyStock(stock)
sell_stock_order = SellStock(stock)

broker = Broker()
broker.take_order(buy_stock_order)
broker.take_order(sell_stock_order)

broker.place_orders()

Comparison of all Design Patterns

🍨Creational Patterns

1. Singleton

  • Purpose: Ensure a class has only one instance and provide a global point of access to it.
  • Applicability:
    • When exactly one instance of a class is needed to coordinate actions across the system.
  • Pros:
    • Ensures single instance
    • Provides global access point
    • Allows lazy initialization
  • Cons:
    • Can make unit testing difficult
    • Violates Single Responsibility Principle
    • Can be overused

2. Factory Method

  • Purpose: Define an interface for creating an object, but let subclasses decide which class to instantiate.
  • Applicability:
    • When a class can’t anticipate the class of objects it must create
    • When a class wants its subclasses to specify the objects it creates
  • Pros:
    • Promotes loose coupling
    • Adheres to Single Responsibility Principle
    • Supports Open/Closed Principle
  • Cons:
    • Can lead to many subclasses
    • Can be overkill for simple cases

3. Abstract Factory

  • Purpose: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
  • Applicability:
    • When a system should be independent of how its products are created, composed, and represented
    • When a system should be configured with one of multiple families of products
  • Pros:
    • Promotes consistency among products
    • Isolates concrete classes
    • Makes exchanging product families easy
  • Cons:
    • Adding new products can be difficult
    • Can become too complex

4. Builder

  • Purpose: Separate the construction of a complex object from its representation.
  • Applicability:
    • When the algorithm for creating a complex object should be independent of the parts that make up the object and how they’re assembled
    • When the construction process must allow different representations for the object that’s constructed
  • Pros:
    • Allows finer control over construction process
    • Isolates code for construction and representation
    • Provides better control over final product
  • Cons:
    • Can lead to mutable objects
    • Requires creating separate ConcreteBuilder for each type of product

5. Prototype

  • Purpose: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
  • Applicability:
    • When a system should be independent of how its products are created, composed, and represented
    • When instances of a class can have one of only a few different combinations of state
  • Pros:
    • Adds/removes products at runtime
    • Specifies new objects by varying values
    • Reduces subclassing
    • Configures application dynamically
  • Cons:
    • Each subclass must implement the cloning method
    • Complicated to implement if objects have circular references

🏢Structural Patterns

6. Adapter

  • Purpose: Convert the interface of a class into another interface clients expect.
  • Applicability:
    • When you want to use an existing class, and its interface does not match the one you need
    • When you want to create a reusable class that cooperates with unrelated or unforeseen classes
  • Pros:
    • Allows two incompatible interfaces to work together
    • Improves reusability of existing code
  • Cons:
    • Can add complexity to the system
    • Sometimes many adaptations are required along an adapter chain

7. Bridge

  • Purpose: Decouple an abstraction from its implementation so that the two can vary independently.
  • Applicability:
    • When you want to avoid a permanent binding between an abstraction and its implementation
    • When both the abstractions and their implementations should be extensible by subclassing
  • Pros:
    • Decouples interface from implementation
    • Improves extensibility
    • Hides implementation details from clients
  • Cons:
    • Increases complexity
    • Can be overkill for simple scenarios

8. Composite

  • Purpose: Compose objects into tree structures to represent part-whole hierarchies.
  • Applicability:
    • When you want clients to be able to ignore the difference between compositions of objects and individual objects
  • Pros:
    • Defines class hierarchies consisting of primitive and complex objects
    • Makes client code simple
    • Makes it easy to add new kinds of components
  • Cons:
    • Can make design overly general
    • Can be difficult to restrict components of a composite

9. Decorator

  • Purpose: Attach additional responsibilities to an object dynamically.
  • Applicability:
    • When you want to add responsibilities to individual objects dynamically and transparently, without affecting other objects
    • When you want to add responsibilities that can be withdrawn
  • Pros:
    • More flexible than static inheritance
    • Avoids feature-laden classes high up in the hierarchy
    • Allows for a pay-as-you-go approach
  • Cons:
    • Can result in many small objects
    • Can be confusing to developers unfamiliar with the pattern

10. Facade

  • Purpose: Provide a unified interface to a set of interfaces in a subsystem.
  • Applicability:
    • When you want to provide a simple interface to a complex subsystem
    • When there are many dependencies between clients and the implementation classes of an abstraction
  • Pros:
    • Simplifies the interface of a set of classes
    • Promotes weak coupling between subsystems and clients
  • Cons:
    • Can become a “god object” coupled to all classes of an app
    • May not provide enough customization options

11. Flyweight

  • Purpose: Use sharing to support large numbers of fine-grained objects efficiently.
  • Applicability:
    • When an application uses a large number of objects
    • When storage costs are high because of the sheer quantity of objects
  • Pros:
    • Reduces memory usage
    • Improves performance in apps with many similar objects
  • Cons:
    • May introduce complexity
    • Requires careful synchronization in multithreaded environment

12. Proxy

  • Purpose: Provide a surrogate or placeholder for another object to control access to it.
  • Applicability:
    • When you need a more versatile or sophisticated reference to an object than a simple pointer
  • Pros:
    • Controls access to the original object
    • Allows operations before/after the request gets through
    • Can manage the lifecycle of the original object
  • Cons:
    • Introduces another layer of abstraction
    • Response might be delayed

🤗Behavioral Patterns

13. Chain of Responsibility

  • Purpose: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.
  • Applicability:
    • When more than one object may handle a request, and the handler isn’t known a priori
    • When you want to issue a request to one of several objects without specifying the receiver explicitly
  • Pros:
    • Reduces coupling
    • Increases flexibility in assigning responsibilities to objects
  • Cons:
    • Request can go unhandled
    • Can be hard to debug

14. Command

  • Purpose: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
  • Applicability:
    • When you want to parameterize objects by an action to perform
    • When you want to specify, queue, and execute requests at different times
  • Pros:
    • Decouples the object that invokes the operation from the object that performs it
    • Easy to add new commands
  • Cons:
    • Can lead to a large number of small classes

15. Interpreter

  • Purpose: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
  • Applicability:
    • When you need to interpret a simple language with simple grammar
  • Pros:
    • Easy to change and extend the grammar
    • Implementing the grammar is straightforward
  • Cons:
    • Complex grammars are hard to maintain
    • Can be inefficient for large grammars

16. Iterator

  • Purpose: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
  • Applicability:
    • When you want to access an aggregate object’s contents without exposing its internal representation
    • When you want to support multiple traversals of aggregate objects
  • Pros:
    • Simplifies the aggregate interface
    • Supports variations in the traversal of an aggregate
  • Cons:
    • Can be overkill for simple collections
    • Might not be as efficient as direct access

17. Mediator

  • Purpose: Define an object that encapsulates how a set of objects interact.
  • Applicability:
    • When a set of objects communicate in well-defined but complex ways
    • When you want to customize a behavior that’s distributed between several classes without creating subclasses
  • Pros:
    • Reduces coupling between Colleagues
    • Centralizes control
    • Simplifies object protocols
  • Cons:
    • Can become a monolith
    • Can be difficult to maintain

18. Memento

  • Purpose: Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.
  • Applicability:
    • When you need to provide undo functionality
    • When a snapshot of an object’s state must be saved so that it can be restored later
  • Pros:
    • Preserves encapsulation boundaries
    • Simplifies the originator
  • Cons:
    • Can be expensive if a lot of state needs to be copied
    • Caretakers might accumulate many mementos

19. Observer

  • Purpose: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
  • Applicability:
    • When a change to one object requires changing others, and you don’t know how many objects need to be changed
    • When an object should be able to notify other objects without making assumptions about who these objects are
  • Pros:
    • Supports loose coupling
    • Allows for broadcast communication
  • Cons:
    • Unexpected updates
    • Can be difficult to track the flow of callbacks

20. State

  • Purpose: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
  • Applicability:
    • When an object’s behavior depends on its state, and it must change its behavior at runtime depending on that state
    • When operations have large, multipart conditional statements that depend on the object’s state
  • Pros:
    • Localizes state-specific behavior
    • Makes state transitions explicit
  • Cons:
    • Can lead to a large number of classes
    • Can be overkill for simple state machines

21. Strategy

  • Purpose: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
  • Applicability:
    • When many related classes differ only in their behavior
    • When you need different variants of an algorithm
  • Pros:
    • Defines a family of algorithms
    • Eliminates conditional statements
    • Allows the choice of implementations at runtime
  • Cons:
    • Clients must be aware of different strategies
    • Increased number of objects

22. Template Method

  • Purpose: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
  • Applicability:
    • When you want to implement the invariant parts of an algorithm once and leave it up to subclasses to implement the behavior that can vary
  • Pros:
    • Reuses common code
    • Provides a framework for creating algorithms
  • Cons:
    • Difficult to maintain a large number of algorithms
    • Can lead to a rigid design

23. Visitor

  • Purpose: 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.
  • Applicability:
    • When an object structure contains many classes of objects with differing interfaces, and you want to perform operations on these objects that depend on their concrete classes
  • Pros:
    • Makes adding new operations easy
    • Gathers related operations and separates unrelated ones
  • Cons:
    • Adding new ConcreteElement classes is hard
    • Can result in a loss of encapsulation