Software Design Patterns — An Introduction

Tobias Lang
12 min readApr 22, 2023

Design patterns are reusable solutions to common problems that arise during software development. They are not ready-made code snippets, but rather general templates that can be adapted to specific situations. Thus for the initiated, they not only provide ready-to-use solutions to common problems but also help recognize them in other people's code, making it easier to read and understand.

As mentioned in GPT-4 vs. GitHub Copilot: A Comparison understanding the core principles of software development is becoming increasingly important. Therefore, I will explore the 23 most widely-used design patterns, categorized into Creational, Structural, and Behavioral groups¹.

To keep the introduction simple, no code will be provided here. However, there will be some more articles dealing with each of these patterns including code. The blog posts will be linked here after they have been published, so stay tuned.

A computer screen magnified through classes.
Photo by Kevin Ku: https://www.pexels.com/de-de/foto/schwarze-bewirtschaftete-brille-vor-laptop-computer-577585/

Creational Patterns

Creational patterns are used to create objects. They decouple the construction of an object from its representation. Object creation is encapsulated and outsourced (e.g., to a factory) to keep the context of object creation independent of the concrete implementation, following the rule: “Program against the interface, not the implementation!”

Often and depending on the situation, generation patterns can be combined with each other. For example, the Prototype pattern can be realized with the help of a Singleton.

Singleton

The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. It is useful when you need to coordinate actions across the system or when creating multiple instances is resource-intensive.

Example usage: A logging system ensures that only one instance of a logger is created, providing a consistent way to access and manage logs throughout the application.

Have a look here, for a deep dive: https://tobiaslang.medium.com/the-singleton-design-pattern-a-double-edged-sword-27cec4fbc0c7

Builder

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. This pattern is useful when creating objects with many optional or variable components.

Example usage: A meal ordering system at a fast-food restaurant allows customers to build custom meals by choosing a main dish, side dish, drink, and dessert. The Builder pattern simplifies the meal creation process by providing a step-by-step approach to building different meal combinations without the need for numerous constructors or complex logic.

Take a gander at this link for a thorough exploration: https://medium.com/@tobiaslang/the-builder-design-pattern-simplifying-object-construction-8102ea344df8.

Prototype

The Prototype pattern specifies the kind of objects to create using a prototypical instance and creates new objects by cloning this prototype. This pattern is useful when object creation is expensive or when you want to create objects with a shared state.

Example usage: In a video game, multiple instances of a terrain object (grass, water, hills) are used to compose the game world. The Prototype pattern allows the game engine to clone terrain objects efficiently, reducing the cost of creating new instances and ensuring that all objects share the same underlying state.

Factory Method

The Factory Method pattern provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It allows the client code to create objects without specifying the exact class of object that will be created. This pattern encapsulates the object creation logic and allows for flexibility in the type of objects that can be created.

The Factory Method Pattern often comes in a flavor, called a Simple Factory. In a Simple Factory, the object creation logic is placed in a single class, as shown in the previous example. While it is not technically considered one of the classic design patterns, it is often a useful stepping stone to understanding the Factory Method.

Example usage: A restaurant ordering system offering different types of pizzas such as Margherita, Pepperoni, and Hawaiian. The Factory method pattern would create an interface or superclass for creating pizzas, and each type of pizza (Margherita, Pepperoni, Hawaiian) would be a subclass that implements the pizza creation method. The customer would simply request a pizza without specifying the exact class, and the system would use the appropriate subclass to create the requested pizza. This allows for flexibility in the types of pizzas offered and reduces the coupling between the client code and the pizza creation logic.

Abstract Factory

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It allows the client code to create objects without specifying their concrete types, and it ensures that the objects created by a factory are compatible with each other. The Abstract Factory pattern is useful when a system should be independent of how its objects are created, composed, and represented, and it can help achieve high cohesion, low coupling, and separation of concerns.

The main difference between the Factory Method and Abstract Factory patterns is that the Factory Method pattern is used to create a single product or a family of related products, while the Abstract Factory pattern is used to create a family of related or dependent products. In other words, the Factory Method pattern focuses on creating objects of a single type, while the Abstract Factory pattern focuses on creating objects of multiple types that work together. Additionally, the Factory Method pattern uses inheritance to defer the instantiation of a class to its subclasses, while the Abstract Factory pattern uses composition to encapsulate the creation of objects.

Example usage: Consider an online shopping application that sells different types of products, such as clothes, electronics, and books. The application needs to display a product catalog that includes images, descriptions, and prices of the products, and it needs to process orders and payments for the selected products. The Abstract Factory pattern can be used to create an abstract product factory that defines methods for creating product objects and related objects, such as images and descriptions. Each type of product, such as clothes, electronics, and books, would be a concrete product factory that implements the product creation methods. The client code can then create a product catalog without knowing the concrete classes of the products or their related objects, and the abstract product factory will ensure that the objects created are compatible with each other.

Structural Patterns

Structural design patterns facilitate software design by providing ready-made templates for relationships between classes. They define how different parts of an application work together to provide specific functionality without compromising the flexibility, maintainability, and extensibility of the software.

Decorator

The Decorator pattern allows for adding new functionality to an existing object without altering its structure. This is achieved by wrapping the original object with a decorator class that implements the same interface and adds or overrides behavior.

Example usage: The decorator pattern is widely used in multiple languages. You most likely have e.g. an @classmethod in Python or an @Override in Java. Congratulations, you used a Decorator.

Take a closer look at this link for an in-depth exploration: https://tobiaslang.medium.com/the-decorator-design-pattern-a-gateway-to-flexible-code-customization-f94cad1fbe1

Adapter and Facade

The Adapter pattern helps make incompatible interfaces compatible by converting one interface into another. It allows objects with different interfaces to work together, promoting reusability and flexibility.

Example usage: An image processing library works with different image file formats (JPEG, PNG, GIF) by providing adapters that convert each format into a common interface, allowing the library to process images regardless of their format.

You can find a more detailed explanation on the Adapter pattern here: https://tobiaslang.medium.com/the-adapter-design-pattern-bridging-incompatible-interfaces-efficiently-2221ba8ed7dc

The Facade pattern provides a simplified interface to a more complex system, hiding its complexities and making it easier to use. Facades are commonly used to wrap multiple subsystems, offering a single entry point for client code.

Example usage: A home automation system provides a simplified interface to control complex subsystems like lighting, heating, and security, making it easier for users to interact with the system.

Proxy

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. This is useful when you want to add a level of security, caching, or remote access functionality to the underlying object.

Example usage: A video streaming service uses a proxy to control access to high-quality video content, ensuring that only authorized users can view the content. The proxy can also provide additional functionality like caching or load balancing.

Bridge

The Bridge pattern decouples an abstraction from its implementation, allowing both to vary independently. This pattern is useful when you want to separate the interface of a class from its implementation, making it easier to extend or swap implementations without affecting clients.

Example usage: A drawing application supports multiple rendering engines (OpenGL, DirectX, Vulkan) and different shape objects (circle, rectangle, triangle). The Bridge pattern separates the shape abstractions from their rendering implementations. By doing so, the application can easily add new shapes or rendering engines without modifying the existing code or creating a combinatorial explosion of classes.

Flyweight

The Flyweight pattern reduces the memory footprint of a large number of similar objects by sharing common parts, typically by using a shared pool of objects. This pattern is useful when you need to create many instances of a class, but most of their state can be shared to save memory.

Example usage: In a word processing application, each character in a document has a font, size, color, and style. Instead of storing these attributes for every single character, the Flyweight pattern can be used to create a shared pool of character attributes. Each character in the document then refers to the shared attribute objects, reducing memory usage and improving performance.

Behavioral Patterns

Behavioral design patterns focus on solving common communication and behavior-related problems that occur in software development. These patterns focus on the interactions and responsibilities between objects and classes rather than their internal structures.

Observer

The Observer pattern defines a one-to-many dependency between objects so that when one object (the subject) changes its state, all its dependents (observers) are notified and updated automatically. This pattern promotes loose coupling and is widely used in event-driven systems.

Example usage: In a weather monitoring system, different displays (current conditions, forecasts, and statistics) update automatically when the weather data changes. The weather data is the subject, and the displays are observers.

For a more comprehensive understanding, examine this link: https://tobiaslang.medium.com/the-observer-design-pattern-unlocking-efficient-notification-systems-77dbbf6f2604

Command

The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. It separates the object that invokes the operation from the object that implements it.

Example usage: A text editor provides undo and redo functionality by encapsulating text editing operations (insert, delete, replace) as command objects, which can be executed, stored, and reversed.

Template Method

The Template Method pattern defines the skeleton of an algorithm in a superclass, allowing subclasses to override certain steps without changing the algorithm’s overall structure. This promotes code reusability and adherence to the “Don’t Repeat Yourself” (DRY) principle.

Example usage: A data processing pipeline consists of multiple stages (data loading, preprocessing, analysis, and reporting), each stage having different implementations for various data types. The template method pattern defines the pipeline structure and allows customization of individual stages.

Iterator and Composite

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. It promotes a uniform way of traversing different data structures.

Example usage: A social media platform allows users to traverse their friends’ posts, regardless of the underlying data structure (arrays, lists, trees), using a unified iterator interface.

The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It enables clients to treat individual objects and compositions uniformly.

Example usage: A file system represents files and directories as a tree structure, allowing users to interact with individual files and directories or their compositions in a uniform manner.

State

The State pattern allows an object to change its behavior when its internal state changes. It encapsulates state-dependent behavior into separate classes and delegates the behavior to the current state object, promoting the Single Responsibility Principle.

Example usage: A vending machine changes its behavior based on its current state (idle, has_selection, has_payment, out_of_stock). The state pattern encapsulates state-dependent behavior and allows the machine to transition between states seamlessly.

Chain of Responsibility

The Chain of Responsibility pattern creates a chain of processing objects, where each object handles a request or passes it to the next object in the chain. This pattern is useful when you want to decouple the sender of a request from its receiver, giving multiple objects a chance to handle the request.

Example usage: In an email filtering system, different filters (spam, phishing, attachment) process incoming emails. Using the Chain of Responsibility pattern, the filters are linked in a chain, allowing each filter to either process the email or pass it to the next filter in the chain. This makes it easy to add or remove filters without modifying the existing filtering logic.

Interpreter

The Interpreter pattern defines a representation of a language’s grammar and provides an interpreter to process expressions in that language. This pattern is useful when you need to build a custom language, parser, or expression evaluator for a specific domain.

Example usage: A calculator application allows users to input mathematical expressions and evaluates them to produce a result. The Interpreter pattern can be used to define the grammar for the expressions, parse the input, and evaluate the expressions to produce the final result.

Mediator

The Mediator pattern defines an object that encapsulates how a set of objects interact, promoting loose coupling by preventing objects from referring to each other explicitly. This pattern is useful for managing complex interactions between multiple objects or subsystems.

Example usage: In a chat application, users send messages to each other through a central server. The Mediator pattern is used to create a server object that manages message routing between users, ensuring that users do not need to communicate directly with each other, which simplifies the overall architecture.

Memento

The Memento pattern captures and externalizes an object’s internal state so that it can be restored later, without violating encapsulation. This pattern is useful for implementing undo/redo functionality or taking snapshots of an object’s state.

Example usage: In a photo editing application, users can apply various transformations to an image, such as resizing, cropping, or applying filters. The Memento pattern can be used to save the state of the image before each transformation, allowing users to revert the image to a previous state without undoing all the changes.

Strategy

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently from the clients that use it, promoting modularity and flexibility.

Example usage: A navigation application calculates routes between locations using various algorithms, such as shortest distance, fastest time, or least fuel consumption. The Strategy pattern encapsulates each algorithm as a separate object, allowing the application to easily switch between them or add new routing algorithms without changing the existing code.

Visitor

The Visitor pattern represents an operation to be performed on the elements of an object structure without changing the classes on which it operates. This pattern is useful when you need to perform multiple, unrelated operations on an object structure, without modifying the structure itself.

Example usage: In a 3D modeling application, users create complex scenes composed of various objects (spheres, cubes, cylinders). The Visitor pattern can be used to implement operations like rendering, exporting, or calculating bounding boxes for the objects, without having to add these operations to each object class. This makes it easier to add new operations or extend existing ones without changing the object structure.

Conclusion

Understanding and applying these design patterns can significantly improve your software development skills and help you create more robust, maintainable, and scalable software. By leveraging these patterns, you can tackle complex problems with well-tested and proven solutions. However, it’s essential to remember that design patterns should not be applied blindly. Always consider the specific problem at hand, and use a design pattern only when it genuinely simplifies the design and enhances code maintainability.

As you gain more experience with these patterns, you’ll develop a deeper understanding of when and how to use them effectively. Continuously learning about new and emerging patterns will also enable you to stay up-to-date with the ever-evolving landscape of software development. Happy coding!

Recommended Further Reading

Besides this series, where I explore each pattern more thoroughly, there are lots of books and online resources out there. Here are two books I can recommend. One for helping my brain to work out the sometimes rather complex intricate details of design patterns, the other for being so influential to software engineering and design patterns.

¹ Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, also known as the “Gang of Four” book, is a seminal work in the field of software design patterns. Due to its influence, bits and pieces have been used in the creation of this blog article. The book used C++ and Smalltalk to provide actual code examples.

Head First Design Patterns by Eric Freeman, Elisabeth Robson, Bert Bates, and Kathy Sierra. This book is an excellent resource for beginners and experienced developers alike, offering an engaging and interactive approach to learning design patterns. It uses a variety of visual aids, quizzes, and real-world examples to make complex concepts more digestible. The book uses Java as the language of choice.

--

--