Power of Eloquence

Demystifying Common Design Patterns in Modern Software Development

| Comments

Generated AI image by Microsoft Bing Image Creator

Introduction

In my previous post, I mentioned the usefulness of apply software patterns for scalable Javascript applications so far. I thought of revisiting this again because after working as a software engineer/developer for a while, I always found myself that design patterns are heavily applied across all projects (outside of Javascript domain) I have seen. Those projects were anything around Java, PHP, Python, C++, etc, thus I found there’s a common notion that design patterns are ubiquitous when applying to solve interesting and challenging software problems in our modern times.

To recap, design patterns are essential tools in a software developer’s toolkit, offering proven solutions to recurring problems in software design. In this blog post, we’ll explore some of more common design patterns, their use cases, and how they enhance the structure and flexibility of modern software applications.

Table of Contents

  1. What are Design Patterns?
  2. Creational Patterns
  3. Structural Patterns
  4. Behavioral Patterns
  5. Use Cases and Examples
  6. What does this all mean?
  7. Conclusion

What are Design Patterns?

Design patterns are fundamentally general reusable solutions to common problems encountered in software design. They represent best practices and provide a clear, effective way to solve design issues that aim to accomplish 3 main objectives. They are to:

  • Promote code reusability
  • Promote ease of maintainability
  • Promote flexibility for future changes or enhancements

Creational Patterns

Creational patterns are concerned with the creation of objects. They provide various mechanisms for object creation, allowing developers to decouple the instantiation process from the actual implementation of the objects.

Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. This pattern is useful when you want to restrict the instantiation of a class to a single object, such as managing a shared resource or controlling access to a global configuration.

Factory Method Pattern

The Factory Method pattern defines an interface for creating objects but allows subclasses to alter the type of objects that will be created. This pattern is useful when you want to delegate the creation of objects to subclasses, providing a way to create objects without specifying their exact class.

Abstract Factory Pattern

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. This pattern is useful when you want to ensure that the created objects are compatible with each other and belong to the same family.

Structural Patterns

Structural patterns are concerned with the composition of classes or objects to form larger structures. They focus on simplifying the structure of a system and promoting flexibility and reusability.

Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together by wrapping an existing class with a new interface. This pattern is useful when integrating new features or components into an existing system without modifying its existing codebase.

Decorator Pattern

The Decorator pattern attaches additional responsibilities to an object dynamically. This pattern is useful for extending the functionalities of classes in a flexible and reusable way without modifying their code.

Facade Pattern

The Facade pattern provides a unified interface to a set of interfaces in a subsystem. This pattern is useful when simplifying complex systems and presenting a simplified interface to the client, hiding the complexities of the subsystem.

Behavioral Patterns

Behavioral patterns are concerned with the interaction and communication between objects. They focus on defining how objects collaborate to achieve common goals and behaviors.

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This pattern is useful when you want to establish a loosely coupled communication between objects, allowing them to communicate and synchronize changes efficiently.

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern is useful when you want to allow clients to choose algorithms from a family dynamically, providing a way to switch between different algorithms at runtime.

Command Pattern

The Command pattern encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of the parameters. This pattern is useful for decoupling the sender and receiver of a request, providing a way to decouple command execution from command invocation.

Use Cases and Examples

In this section, we’ll explore real-world use cases for each design pattern, providing examples and insights into how they can be applied effectively in modern software development. We can achieve these using in any programming languages we like. For the intents and purposes of this blog, I’m going to pick Python as a language of choice because it’s a beginner friendly language to understand.

Creational Patterns

Singleton Pattern

Use Case: Logging Systems
In a logging system, you may want to ensure that there’s only one instance of the logger throughout your application to maintain a single log file and prevent multiple instances from causing conflicts.

Example:

class Logger:
    _instance = None

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

# Usage
logger1 = Logger()
logger2 = Logger()

print(logger1 is logger2)  # Output: True

Factory Method Pattern

Use Case: Document Creation
In a document editor application, different types of documents (e.g., text documents, spreadsheets) may need to be created. Using a factory method pattern, you can define an interface for creating documents and let subclasses decide which type of document to instantiate.

Example:

from abc import ABC, abstractmethod

class Document(ABC):
    @abstractmethod
    def create(self):
        pass

class TextDocument(Document):
    def create(self):
        return "Text Document Created"

class SpreadsheetDocument(Document):
    def create(self):
        return "Spreadsheet Document Created"

class DocumentFactory(ABC):
    @abstractmethod
    def create_document(self):
        pass

class TextDocumentFactory(DocumentFactory):
    def create_document(self):
        return TextDocument()

class SpreadsheetDocumentFactory(DocumentFactory):
    def create_document(self):
        return SpreadsheetDocument()

# Usage
text_document_factory = TextDocumentFactory()
text_document = text_document_factory.create_document()
print(text_document.create())  # Output: Text Document Created

spreadsheet_document_factory = SpreadsheetDocumentFactory()
spreadsheet_document = spreadsheet_document_factory.create_document()
print(spreadsheet_document.create())  # Output: Spreadsheet Document Created

Abstract Factory Method Pattern

Use Case: GUI Toolkit
In a GUI toolkit, you may need to create different types of UI elements (e.g., buttons, text fields) for different operating systems. Using an abstract factory pattern, you can create families of related UI elements without specifying their concrete classes.

Example:

from abc import ABC, abstractmethod

class Button(ABC):
    @abstractmethod
    def paint(self):
        pass

class LinuxButton(Button):
    def paint(self):
        return "Rendering a Linux Button"

class WindowsButton(Button):
    def paint(self):
        return "Rendering a Windows Button"

class GUIFactory(ABC):
    @abstractmethod
    def create_button(self):
        pass

class LinuxGUIFactory(GUIFactory):
    def create_button(self):
        return LinuxButton()

class WindowsGUIFactory(GUIFactory):
    def create_button(self):
        return WindowsButton()

# Usage
linux_factory = LinuxGUIFactory()
linux_button = linux_factory.create_button()
print(linux_button.paint())  # Output: Rendering a Linux Button

windows_factory = WindowsGUIFactory()
windows_button = windows_factory.create_button()
print(windows_button.paint())  # Output: Rendering a Windows Button

Structural Patterns

Adapter Pattern

Use Case: Legacy System Integration
When integrating a new system with existing legacy systems, the adapter pattern can be used to adapt the interface of the new system to match the interface expected by the legacy system.

Example:

class Target:
    def request(self) -> str:
        return "Target: The default target's behavior."

class Adaptee:
    def specific_request(self) -> str:
        return ".eetpadA eht fo roivaheb laicepS"

class Adapter(Target):
    def __init__(self, adaptee: Adaptee):
        self.adaptee = adaptee

    def request(self) -> str:
        return f"Adapter: (TRANSLATED) {self.adaptee.specific_request()[::-1]}"

# Usage
adaptee = Adaptee()
adapter = Adapter(adaptee)
print(adapter.request())  # Output: Adapter: (TRANSLATED) Special behavior of the Adaptee.

Decorator Pattern

Use Case: Text Formatting
In a text editor application, the decorator pattern can be used to add additional formatting options to text, such as bold, italic, or underline, without modifying the original text class.

Example:

from abc import ABC, abstractmethod

class TextComponent(ABC):
    @abstractmethod
    def render(self):
        pass

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

    def render(self):
        return f"Content: {self._content}"

class TextDecorator(TextComponent):
    def __init__(self, text_component: TextComponent):
        self._text_component = text_component

    def render(self):
        return self._text_component.render()

class BoldDecorator(TextDecorator):
    def render(self):
        return f"<b>{self._text_component.render()}</b>"

class ItalicDecorator(TextDecorator):
    def render(self):
        return f"<i>{self._text_component.render()}</i>"

# Usage
text = Text("Hello, world!")
bold_text = BoldDecorator(text)
italic_text = ItalicDecorator(text)

print(text.render())  # Output: Content: Hello, world!
print(bold_text.render())  # Output: <b>Content: Hello, world!</b>
print(italic_text.render())  # Output: <i>Content: Hello, world!</i>

Facade Pattern

Use Case: Complex Subsystem Simplification
In a computer system, the facade pattern can be used to provide a simplified interface to a complex subsystem, hiding its internal details and providing a single entry point for client interaction.

Example:

class SubsystemA:
    def operation_a1(self) -> str:
        return "SubsystemA: Ready!"

    def operation_a2(self) -> str:
        return "SubsystemA: Go!"

class SubsystemB:
    def operation_b1(self) -> str:
        return "SubsystemB: Fire!"

class Facade:
    def __init__(self, subsystem_a: SubsystemA, subsystem_b: SubsystemB):
        self._subsystem_a = subsystem_a
        self._subsystem_b = subsystem_b

    def operation(self) -> str:
        results = []
        results.append("Facade initializes subsystems:")
        results.append(self._subsystem_a.operation_a1())
        results.append(self._subsystem_b.operation_b1())
        results.append("Facade orders subsystems to perform the action:")
        results.append(self._subsystem_a.operation_a2())
        return "\n".join(results)

# Usage
subsystem_a = SubsystemA()
subsystem_b = SubsystemB()
facade = Facade(subsystem_a, subsystem_b)

print(facade.operation())

Behavioral Patterns

Observer Pattern

Use Case: Event Handling
In a graphical user interface (GUI) framework, the observer pattern can be used to implement event handling mechanisms, where multiple objects (observers) are notified when a particular event occurs.

Example:

from abc import ABC, abstractmethod

class Subject(ABC):
    @abstractmethod
    def attach(self, observer: 'Observer') -> None:
        pass

    @abstractmethod
    def detach(self, observer: 'Observer') -> None:
        pass

    @abstractmethod
    def notify(self) -> None:
        pass

class ConcreteSubject(Subject):
    _state: int = None
    _observers: List['Observer'] = []

    def attach(self, observer: 'Observer') -> None:
        print("Subject: Attached an observer.")
        self._observers.append(observer)

    def detach(self, observer: 'Observer') -> None:
        self._observers.remove(observer)

    def notify(self) -> None:
        print("Subject: Notifying observers...")
        for observer in self._observers:
            observer.update(self)

    def some_business_logic(self) -> None:
        print("\nSubject: I'm doing something important.")
        self._state = 5
        print(f"Subject: My state has just changed to: {self._state}")
        self.notify()

class Observer(ABC):
    @abstractmethod
    def update(self, subject: Subject) -> None:
        pass

class ConcreteObserverA(Observer):
    def update(self, subject: Subject) -> None:
        if subject._state < 3:
            print("ConcreteObserverA: Reacted to the event")

class ConcreteObserverB(Observer):
    def update(self, subject: Subject) -> None:
        if subject._state == 0 or subject._state >= 2:
            print("ConcreteObserverB: Reacted to the event")

# Usage
subject = ConcreteSubject()

observer_a = ConcreteObserverA()
subject.attach(observer_a)

observer_b = ConcreteObserverB()
subject.attach(observer_b)

subject.some_business_logic()
subject.some_business_logic()

Strategy Pattern

Use Case: Sorting Algorithms
In a sorting algorithm library, the strategy pattern can be used to encapsulate various sorting algorithms (e.g., bubble sort, quicksort) into separate strategy classes, allowing clients to choose the desired sorting strategy dynamically.

Example:

from abc import ABC, abstractmethod
from typing import List

class Strategy(ABC):
    @abstractmethod
    def execute(self, data: List[int]) -> None:
        pass

class BubbleSortStrategy(Strategy):
    def execute(self, data: List[int]) -> None:
        print(f"Bubble Sort: Sorting {data}")
        # Implement bubble sort algorithm

class QuickSortStrategy(Strategy):
    def execute(self, data: List[int]) -> None:
        print(f"Quick Sort: Sorting {data}")
        # Implement quicksort algorithm

class Context:
    def __init__(self, strategy: Strategy) -> None:
        self._strategy = strategy

    def execute_strategy(self, data: List[int]) -> None:
        self._strategy.execute(data)

# Usage
bubble_sort_strategy = BubbleSortStrategy()
context = Context(bubble_sort_strategy)
context.execute_strategy([3, 1, 2, 5, 4])  # Output: Bubble Sort: Sorting [3, 1, 2, 5, 4]

quick_sort_strategy = QuickSortStrategy()
context = Context(quick_sort_strategy)
context.execute_strategy([3, 1, 2, 5, 4])  # Output: Quick Sort: Sorting [3, 1, 2, 5, 4]

Command Pattern

Use Case: Remote Control
In a remote control application, the command pattern can be used to encapsulate various actions (e.g., turning on/off a TV, adjusting volume) into separate command objects, allowing clients to execute these actions dynamically.

Example:

from abc import ABC, abstractmethod

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

class TurnOnCommand(Command):
    def __init__(self, device: 'Device') -> None:
        self._device = device

    def execute(self) -> None:
        self._device.turn_on()

class TurnOffCommand(Command):
    def __init__(self, device: 'Device') -> None:
        self._device = device

    def execute(self) -> None:
        self._device.turn_off()

class Device(ABC):
    @abstractmethod
    def turn_on(self) -> None:
        pass

    @abstractmethod
    def turn_off(self) -> None:
        pass

class TV(Device):
    def turn_on(self) -> None:
        print("Turning on the TV...")

    def turn_off(self) -> None:
        print("Turning off the TV...")

class RemoteControl:
    def __init__(self) -> None:
        self._commands = {}

    def register_command(self, command_name: str, command: Command) -> None:
        self._commands[command_name] = command

    def press_button(self, command_name: str) -> None:
        if command_name in self._commands:
            self._commands[command_name].execute()
        else:
            print("Unknown command.")

# Usage
remote_control = RemoteControl()

tv = TV()
turn_on_command = TurnOnCommand(tv)
turn_off_command = TurnOffCommand(tv)

remote_control.register_command("turn_on_tv", turn_on_command)
remote_control.register_command("turn_off_tv", turn_off_command)

remote_control.press_button("turn_on_tv")  # Output: Turning on the TV...
remote_control.press_button("turn_off_tv")  # Output: Turning off the TV...

What does this all mean?

If you notice in the above patterns and use case examples so far, they are geared towards traditional object-oriented languages like Python, Java or similar. This is because design patterns were invented to solve object-oriented software application many many years ago circa in the mid 1990s, which was popularised by the software engineering book: Design Patterns: Elements of Reusable Object-Oriented Software. You can find out more by either googling or ask ChatGPT or similar to recite history behind the inception of such a book. I won’t bore you the details of their motivation in this post, other than offering you the refresher you are going to face a lot of these concept usages over the course of your programming career many times no matter how new or seasoned you are in mastering these.

What all this means to you, if you’re coming from web or front-end development background like ReactJS/Javascript. You can expect to apply the patterns in the following ways (which may have been covered in my previous post already.)

Singleton Pattern

  • Use Case: Managing Global State
    • In ReactJS applications, you might need to manage global state, such as user authentication status or theme settings. You can use the Singleton pattern to ensure there’s only one instance of your state management object (e.g., Redux store) throughout your application.

Factory Method Pattern

  • Use Case: Component Creation
    • In ReactJS, you often create components dynamically based on certain conditions or data. You can use the Factory Method pattern to define an interface for creating React components and allow subclasses or factory functions to decide which type of component to create based on input parameters.

Abstract Factory Pattern

  • Use Case: UI Component Libraries
    • When building complex user interfaces in ReactJS, you might use UI component libraries like Material-UI or Ant Design. These libraries often provide abstract factory mechanisms for creating different types of UI components (e.g., buttons, inputs) with consistent styles and behaviors across the application.

Adapter Pattern

  • Use Case: Data Integration
    • When fetching data from APIs in a ReactJS application, you may need to adapt the format of the API response to match the expected format of your components. You can use the Adapter pattern to create adapter functions or components that transform API data into a format that’s compatible with your components.

Decorator Pattern

  • Use Case: Higher-Order Components (HOCs)
    • In ReactJS, Higher-Order Components (HOCs) are functions that take a component and return a new component with additional functionality. This is akin to the Decorator pattern, where you enhance the behavior of a component without modifying its underlying implementation.

Facade Pattern

  • Use Case: Complex State Management
    • React applications often involve complex state management logic, especially in large-scale applications. You can use the Facade pattern to create a facade or wrapper around your state management solution (e.g., Redux, Context API) to provide a simpler interface for interacting with the application state.

Observer Pattern

  • Use Case: Event Handling
    • ReactJS applications often handle user interactions and events (e.g., button clicks, form submissions). You can use the Observer pattern to implement event handling mechanisms where components subscribe to specific events and are notified when those events occur.

Strategy Pattern

  • Use Case: Conditional Rendering
    • React components often need to render different content or UI elements based on certain conditions or user inputs. You can use the Strategy pattern to encapsulate different rendering strategies (e.g., conditional rendering logic) and dynamically choose the appropriate strategy based on runtime conditions.

Command Pattern

  • Use Case: User Actions and Undo/Redo
    • In interactive applications built with ReactJS, users perform various actions (e.g., editing text, moving elements). You can use the Command pattern to encapsulate user actions as command objects, allowing you to implement features like undo/redo functionality by maintaining a history of executed commands.

Then what about people coming from non-object-oriented programming backgrounds? They are still applicable but if not for all.

Functional Programming Languages:

  • Functional programming languages like Haskell, Lisp, and Scala rely heavily on functions as first-class citizens. In these languages, design patterns may be expressed differently but still serve the same purpose of solving common software design problems.
  • For example, the Strategy pattern can be implemented using higher-order functions or function composition. Instead of defining classes, you can define functions that encapsulate different algorithms and pass them as arguments to other functions.

Procedural Programming Languages:

  • In procedural programming languages like C, design patterns can be implemented using procedural techniques such as function pointers, structs, and modules.
  • For example, the Singleton pattern can be implemented using static variables within functions to ensure that only one instance of a resource is created.

Declarative Programming Languages:

  • Declarative programming languages like SQL and Prolog focus on expressing what should be done rather than how to do it. Design patterns in these languages may involve expressing common query patterns or rule-based systems.
  • For example, in SQL, common design patterns include the use of JOINs, subqueries, and views to structure and manipulate data efficiently.

Scripting Languages:

  • Scripting languages like JavaScript and Ruby support both object-oriented and functional programming paradigms. Design patterns in these languages may involve a combination of object-oriented techniques, higher-order functions, and closures.
  • For example, the Observer pattern can be implemented using event listeners in JavaScript to respond to changes in state or user interactions.

Aspect-Oriented Programming Languages:

  • Aspect-oriented programming languages like AspectJ allow developers to modularize cross-cutting concerns such as logging, security, and transaction management. Design patterns in these languages may involve defining aspects and weaving them into the codebase.
  • For example, the Decorator pattern can be implemented using aspect-oriented techniques to add logging or caching behavior to methods without modifying their code.

Concurrency-Oriented Programming Languages:

  • Concurrency-oriented programming languages like Erlang and Go focus on building scalable and fault-tolerant concurrent systems. Design patterns in these languages may involve patterns for message passing, supervision, and error handling.
  • For example, the Actor model in Erlang provides a pattern for building concurrent and distributed systems using lightweight processes that communicate via message passing.

Conclusion

As you can see, having the core understanding of design patterns and knowing when to apply in your projects can significantly improve the quality and maintainability of your software. By incorporating these proven solutions into your development practices, you’ll be better equipped to tackle complex design challenges and build robust, scalable applications.

Depending on where you are in your career journey, some of these patterns may not look very obvious to you yet. Maybe you’re a graduate or junior developer or mid developer striving to becoming a senior developer and beyond, all this will only make sense to you over time as you build up your programming craft and expertise in your projects. Or perhaps you’re a seasoned developer or senior developer who may have accomplished some if not all of these in your career, it’s a good refresher to come here and revisit them over and over again. Quite often once you’ve developed your domain-knowledge expertise at this level, you’ve have grown wiser and knowledgable when to apply these patterns in your next projects (or not) with great confidence. The key here is about work programming experience as well as having a life-long learning mindset.

Before I wrap this up, as a disclaimer note, all the Python sample codes I’ve provided here so far are by no means prescriptive and that you should not use them across in production environments - at all! The idea behind in illustrating these fundamentals so that you can grasp the mental model behind them and look to apply these patterns in your future projects when you see them fit. Understand and analyse the problems first; determine design patterns fit for them; apply them when necessarily.

Here’s to your happy learning and future software project success!

Till then, Happy coding!

PS: For more software design patterns to learn, you can check this link out - Refactoring Guru. It’s got great learning material (and content is always fresh) on software design patterns to solidify my understanding of best software engineering practices.

Comments