It's almost impossible to invent something new.

Design patterns are the well-known, time-tested solutions to common software engineering problems in the form of a blueprint. In other words, it’s not a finished design that can be transformed directly into machine code. It is a description or template for how to solve a problem that can be used in many different situations.

Design patterns are formalized best practices that the programmer can use to solve common problems when designing an application or system. They can make your code understandable, predictable, and reusable, increasing developer productivity.

Their definitions can go from naming conventions to architectural suggestions (not to be confused with architectural patterns). Things such as choosing a semantic variable/class name like CashPaymentAdapter or PasswordBroker, the semantic organization of files, and the terminology used in the team.

Why should I learn patterns?

To understand the relevance, imagine you just joined a team and get assigned to a project. You surely hope to understand the existing code and expect it to be as stable as possible. Clean code helps, of course, but what if the solution is not understandable? What if the class files are the pure representation of the S in SOLID but you don’t understand their purpose?

You might manage to work as a developer for a while without knowing about a single pattern. And even then, you might be implementing some patterns without even knowing it (that was my case), so why would you spend time learning them?

Design patterns define a common language that you and your teammates can use to communicate more efficiently. You can say, “Oh, just use a Chain of Responsibility for that,” and everyone will understand the idea behind your suggestion. No need to explain what a singleton is if you know the pattern and its name.

Actually, let’s use that Chain of Responsibility as an example, but in a real-world scenario.

Real-World Example: Chain of Responsibility

The Chain of Responsibility is a behavioral design pattern (we will discuss categories in a bit) that lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process and stop the request or to pass it to the next handler in the chain.

Our study case will be an API endpoint to change the application settings. Of course, we need to be authenticated and have permission to change the settings to proceed.

First, the non-pattern solution:

class SettingController
{
    public function update(Request $request)
    {
        if (!Auth::check()) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        if (!$request->user()->can('update', Setting::class)) {
            return response()->json(['error' => 'Forbidden'], 403);
        }

        Setting::query()
            ->findOrFail($request->id)
            ->update(['value' => $request->value]);

        return response()->json(['success' => 'Setting updated']);
    }
}

Looks clean and simple 🤔 what’s wrong with it? Let’s take a few steps back: Understandable, Predictable, and Reusable.

Is it understandable? Yes. Is it predictable? Maybe. Is it reusable? Nope. With the last one, things may get a bit trickier, since everywhere where this kind of validation is made, the responses may vary, make it less consistent, and at some point will become incomprehensible and unpredictable.

What’s the pattern-based way? We send the request through separate modules (classes) until one of them either breaks the chain or reaches the end.

First, let’s decouple the authentication and permission check by creating separate classes for each of them, but abstracting some behavior:

abstract class Middleware {
    protected Middleware $successor;

    public function setSuccessor(Middleware $middleware)
    {
        $this->successor = $middleware;
    }

    abstract public function handle(Request $request);

    public function next(Request $request)
    {
        if ($this->successor) {
            $this->successor->handle($request);
        }
    }
}

This abstract class will allow us to reuse some behavior and focus on the specifics of each element of the chain (the middleware).

Authentication:

class AuthenticationMiddleware extends Middleware
{
    public function handle(Request $request)
    {
        if (!Auth::check()) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->next()->handle($request);
    }
}

Authorization:

class SettingPolicyMiddleware extends Middleware
{
    public function handle(Request $request)
    {
        if ($request->user()->can('update', Setting::class)) {
            return response()->json(['error' => 'Forbidden'], 403);
        }

        return $this->next()->handle($request);
    }
}

This is an over-simplified example, don't judge me 🤪

Then, the HttpKernel (who instantiates the controller and sends the request object to the appropriate method):

class HttpKernel
{
    public function handle(Request $request)
    {
        $authenticationMiddleware = new AuthenticationMiddleware();
        $settingPolicyMiddleware = new SettingPolicyMiddleware();

        $authenticationMiddleware->setSuccessor($settingPolicyMiddleware);

        return $authenticationMiddleware->handle($request);
    }
}
But wait a minute, this is way more code! And looks more complicated than the single-file approach.

- You, right now

You might be right, sometimes applying design patterns results in over-engineered solutions. But our purpose is to scale. Like we said earlier, what if you have more controllers making the same validations over and over, either with or without behavior variations? It will become hard to maintain, understand, predict, refactor... or even love 😬.

Going back to our example, when implementing this pattern we are:

  • Reusing validations
  • Returning consistent responses
  • Predicting the behavior of a passing and non-passing request
  • Decoupling classes

Since it’s a pattern, anyone who is aware of design patterns can easily understand it because it’s a common language

And this is just one of many design patterns! 😱 There’s a chance, primarily if you haven’t heard about design patterns before, that you feel overwhelmed by the amount and variety of design patterns and their specifications, and the problems they solve. Even if you never encounter these problems, knowing patterns is still useful because it teaches you how to solve all sorts of problems using principles of object-oriented design.

Classification

​​Design patterns can vary in their complexity, level of detail, and scale of applicability to the entire system being designed. The low-level patterns are often called idioms and usually apply only to a single programming language (or a set of them). While the most universal and high-level patterns are architectural patterns and can be implemented in virtually any language.

But most commonly, design patterns are organized into 3 main groups:

Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. To mention a few:

  • Abstract Factory
  • Builder
  • Singleton

Structural patterns ease the design by identifying a simple way to create relationships among entities. Examples:

  • Adapter
  • Decorator
  • Extensibility (a.k.a. Framework)
  • Facade
  • Pipes and filters
  • Proxy pattern

Behavioral patterns take care of effective communication and the assignment of responsibilities between objects. The most common are:

  • Chain of responsibility (our real-world example from earlier!)
  • Command
  • Iterator
  • Memento
  • Null object
  • Observer (often confused with Publish/Subscribe, but comment down what you think is the difference)
  • Publish/Subscribe
  • Scheduled-task
  • Single-serving visitor
  • State
  • Strategy
  • Template method

Conclusion

To summarize here are some benefits and importance of design patterns:

  • It makes code reusable, predictable, understandable, and clean
  • It does not make it bug-free, that’s virtually impossible, but it’s closer to perfection 🤌🏻
  • Speeds up the development process and changes or modifications become easier
  • It reduces common problems that developers face during the development process
  • Improves object-oriented skills, and somehow your problem-solving skills 🤔
  • Easy to understand the flow of code because there is less code so it's easy to maintain

Design patterns provide a way to solve problems related to software development using a proven solution in a common language. The solution makes it easy to develop highly cohesive modules with minimal coupling. They isolate the variability that may exist in the system requirements, making the overall system easier to understand and maintain. And more importantly, design patterns make communication between designers more efficient.