Are Design Patterns still relevant?

Over the course of my career in software development, I have come across various methodologies, techniques, and practices. Among these, one concept that continually proves its worth to me is design patterns. In today's article, I'll be discussing the evergreen question: are design patterns still relevant today?

What are these "Design Patterns"?

Design patterns, for those new to the term, are proven solutions to common problems in software design. They provide a shared language for developers, allowing us to convey complex ideas succinctly. They encapsulate solutions discovered by seasoned developers, often saving us from reinventing the wheel. Moreover, design patterns are typically designed to be adaptable, flexible, and reusable—qualities that define high-grade software.

However, it's important to note that while design patterns can be powerful tools, they are not a one-size-fits-all solution. There are situations where the use of design patterns might not be the best approach, such as when dealing with simple, straightforward problems where a design pattern could add unnecessary complexity.

That being said, design patterns indeed are as essential today as they were decades ago, and their relevance is only likely to grow as software development evolves.

Let's take a look at some design patterns that we encounter daily, many times not even knowing it, and discuss their advantages and potential pitfalls.

Lambdas and arrow functions unleash the power of the Strategy pattern

A common pattern we encounter is the Strategy pattern. It involves encapsulating behavior that varies independently from the client that uses it.

Lambdas in C# or arrow functions in JavaScript passed into methods are essentially defining a strategy for the method to use. These methods could be LINQ extension methods or JavaScript array methods like map, sort, or reduce.

var numbers = new List<int> { 5, 7, 2, 4, 8, 6 };
var sortedNumbers = numbers.OrderBy(n => n); // "n => n" is the strategy.
const numbers = [5, 7, 2, 4, 8, 6];
const sortedNumbers = numbers.sort((a, b) => a - b); // "(a, b) => a - b" is the strategy.

In the C# example, n => n is a lambda function that defines a strategy for ordering the numbers. In the JavaScript example, (a, b) => a - b is an arrow function that does the same thing. You could easily replace them with different strategies to change the order, like n => -n to sort in descending order in C#.

So, these lambda and arrow functions can be viewed as a form of the Strategy pattern. This is to show how fundamental design patterns can be, and how they can emerge naturally in different forms across various languages and libraries.

Dependency Injection containers leverage some creational patterns

Dependency Injection (DI), which is a crucial aspect of modern programming, can be viewed as an implementation of the Factory and Singleton patterns.

DI containers essentially act as complex factories. When you ask them to provide an instance of a certain type, they figure out how to create that object, which can involve creating other objects to satisfy a graph of dependencies, selecting specific implementations based on configuration, and so forth.

These containers often manage the lifetimes of the objects they create. This includes the ability to manage Singleton instances, which are objects that should only be instantiated once in a given context (e.g., per application, per request, per thread, etc.).

So, even though the concepts of DI and these design patterns are distinct in nature, there's definitely overlap in the problems they solve. Again, this highlights the power of these patterns and how they've been incorporated into modern frameworks and libraries we use every day.

React.js reacts with the Observer pattern

The way React updates the DOM in response to state changes is an illustration of the Observer pattern. With React components acting as "subjects", React's rendering mechanism as an "observer", and features like the Virtual DOM and reconciliation process, React offers a powerful, flexible system to keep the UI synchronized with the application state.

Some more examples

  • Builder: In JavaScript, the Builder pattern is used in the URLSearchParams class to help construct URL query strings.

  • Chain of Responsibility: Middlewares in ASP.NET Core and Express.js, where each middleware function can pass control to the next one.

  • Bridge: In React, JSX can be considered an implementation of the bridge pattern. It's a syntax that gets compiled into vanilla JavaScript, acting as a bridge between the two different ways of expressing UI components.

  • Composite: React uses the Composite pattern to build components from other components.

  • Facade: jQuery uses the Facade pattern, providing a simplified API over the complex browser's DOM API.

  • Command: In JavaScript, Redux actions can be seen as the Command pattern, encapsulating an action as an object.

  • State: In JavaScript, the state of a Promise (pending, fulfilled, rejected) is a representation of the State pattern.

⚠️ While these patterns can make code more flexible and easier to test, they can also lead to an increase in the number of classes and objects, making the code more complex. Tradeoffs...

Discovering Design Patterns

Given the above, anyone who writes, designs, or maintains code, from beginners to senior developers, can greatly benefit from learning more about design patterns. These patterns can largely enrich one's programming skills, improve communication with peers, and even facilitate the maintenance of legacy code.

Curious about where to dive in?

I always recommend Refactoring Guru as an entry point. Refactoring Guru is an educational website that offers a deep dive into the world of refactoring code and design patterns.

They feature a catalog of 23 classic design patterns divided into three categories: creational, structural, and behavioral.

Each pattern comes with a thorough explanation, including its intent, structure, applicability, implementation in various languages, known uses in real software, and related patterns.

The patterns are explained in a language-agnostic way, but the site provides examples in Java, C#, and other popular programming languages.

If you like learning with videos, there's this excellent series by Christopher Okhravi, where he explores each pattern in a dedicated video:

What else, let's see... 🤔

There's this Design Patterns course in, which is part of a Software Design and Architecture Specialization. You can enroll for free as of the time of this writing:

And books, of course! 🙂

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" or "GoF" book). This is THE book, the one that introduced the concept of design patterns.

Head First Design Patterns by Eric Freeman and Elisabeth Robson. This one provides a more accessible introduction to design patterns, using a visually rich format designed for the way your brain works (if anyone can pretend to know that 😅).

🚫 Anti-patterns!

Since we're talking patterns, it's important to be aware of the concept of anti-patterns, the "wolf in sheep's clothing" 😈.

Also known as pitfalls or bad practices, anti-patterns are common responses to recurring problems that may seem helpful in the short term but can lead to poor outcomes in the long run. They are essentially the "what not to do" in various situations in software development. Understanding anti-patterns is as important as understanding design patterns because they help developers avoid common mistakes and traps.

Here are a few examples of common anti-patterns in software development:

  1. God Object: This anti-pattern occurs when a single object or class knows too much or does too much. The object is usually so large that it is hard to maintain and understand. This anti-pattern violates the Single Responsibility Principle, which states that a class should have only one reason to change.

  2. Golden Hammer: This anti-pattern is the assumption that a single solution can solve all problems. It's based on the saying, "if all you have is a hammer, everything looks like a nail." In software development, this could mean using a particular technology or pattern indiscriminately, regardless of whether it's the best choice for the task at hand.

  3. Premature Optimization: This is the practice of trying to make code more efficient at an early stage of development, often at the expense of readability and maintainability. As Donald Knuth famously said, "Premature optimization is the root of all evil." It's usually better to make the code correct and understandable first, then optimize later if necessary.

  4. Overengineering: This anti-pattern involves designing software to be more robust or complicated than it needs to be. This could mean anticipating features that aren't required, adding unnecessary layers of abstraction, or using complex design patterns where a simpler solution would suffice. Overengineering can make the software more difficult to understand, test, and maintain.

✋A word of caution

Design patterns can be extremely powerful tools, but it's very important to remember that they are just tools, not goals in themselves. They should not be applied indiscriminately just for the sake of using them; their purpose is to solve specific types of problems in software design, not to serve as a prescriptive template for how all code should be structured.

Overzealous application of design patterns can lead to unnecessary complexity, making the code harder to understand, maintain, and change.

So, if you're ever in doubt about whether you should use a pattern or not, take a step back and focus on the actual problem you're trying to solve. Understand the problem first, and then decide if a design pattern can provide a solution that's appropriate for the problem and the context you're in. Seek advice from more experienced colleagues, consult reference materials, and prototype different solutions if needed.

Simplicity and clarity should always be the primary goals in software design. As the old adage goes, "do the simplest thing that could possibly work." It's easier to refactor simpler designs as requirements change and as the software grows in complexity.