Decorator Pattern

Decorator Pattern

In software development, it's common to start with a simple solution and gradually add more functionality as requirements evolve. However, as we incrementally add logging, error handling, validation, and other cross-cutting concerns to our code, the original logic can become cluttered and difficult to maintain. This is especially true when all of these responsibilities are handled within a single method.

In this article, I’ll walk you through a common scenario in C# where additional functionality is added to a method in stages, and we’ll observe how the code becomes progressively more complex. Once we understand the problem, I’ll introduce the Decorator Pattern, a structural design pattern that allows us to dynamically add responsibilities to objects while keeping our core logic clean and focused.

By the end of this article, you’ll see how the decorator pattern not only makes our code more maintainable but also adheres to fundamental software design principles such as Single Responsibility and Open-Closed principles.

Before diving into the solution, let's look at a simple example of how a method can evolve as we add new functionality. We'll start with a basic division operation and gradually introduce more concerns like logging, error handling, and validation. As you'll see, each new responsibility adds complexity, and the method becomes harder to maintain. This step-by-step progression will highlight the problem that the decorator pattern is designed to solve. Let’s explore how this happens.


Simple Division:

  • In the beginning, the division is straightforward and clean.
  • Problem: No logging, error handling, or validation.

Logging Added:

  • You introduce logging for debugging purposes, adding a Console.WriteLine statement.
  • Problem: You’ve introduced one responsibility (logging) into your division logic, and you might need more logic in the future there.

Validation for Division by Zero:

  • The division now includes a basic check for divisor == 0, throwing an exception if necessary.
  • Problem: The method is doing more than one thing now—validation logic and logging.

Try-Catch Block:

  • Finally, you add proper error handling with a try-catch block to prevent crashing in case of an exception.
  • Problem: The method has become cluttered with multiple responsibilities, including validation, logging, and error handling, which violates the Single Responsibility Principle.

Applying the Decorator Pattern

Now that we've seen how a method can become bloated with multiple responsibilities, let's explore how the Decorator Pattern can cleanly solve this problem. In the decorator pattern, each functionality is encapsulated in its own class, and we dynamically compose these behaviors by "wrapping" one class with another.

Step 1: Defining the Core Service

We begin by defining the IDivideService interface and the base implementation DivideService, which performs the basic division:

Step 2: Adding Logging with a Decorator

Instead of cluttering the core division logic, we create a DivideServiceLogDecorator class to handle logging:

Step 3: Adding Validation

Next, we create a DivideServiceValidationDecorator to handle validation (like ensuring the divisor is not zero):

Step 4: Handling Exceptions

Finally, we wrap the logic with an exception handler using DivideServiceExceptionsDecorator:

Composing the Decorators

To see the decorator pattern in action, let's look at how these classes can be used together. In the Main method, we set up the division service by progressively decorating it with logging, validation, and exception handling:

In this example, the DivideService handles the basic division logic, while each decorator adds a layer of functionality on top. We start with the core service, then wrap it with a logging decorator, followed by validation, and finally exception handling. When the Divide method is called, each responsibility is handled by the appropriate decorator, keeping the code modular and flexible.

The beauty of the decorator pattern lies in its ability to add or remove functionality without changing the underlying service or duplicating code. You can easily customize or extend this setup with more decorators as needed, making the code more maintainable and aligned with the Single Responsibility and Open-Closed principles.


To Sum Up

To wrap things up, the decorator pattern can be extended beyond just logging, validation, and error handling. You can implement a Metrics Decorator to track performance or count calls, a Retry Policy Decorator to handle transient failures, or a Circuit Breaker Decorator to prevent overloading services. Additionally, a Concurrency Lock Decorator can ensure thread safety when accessing shared resources. These decorators allow you to build flexible, scalable solutions while keeping your code clean and adhering to the Open-Closed Principle, making them ideal for enterprise applications.


**Tip

For those using dependency injection frameworks, the AutoFac library provides built-in support for decorators, making it even easier to manage and apply them. With AutoFac, you can elegantly handle decorators using DI, eliminating the need for manual service composition like in our GetDivideService() method. To learn more about how AutoFac supports decorators, check out their official documentation here.

Sunday Benjamin

Software Engineer || Pure & Industrial Chemist

6 个月

Decorator pattern simplified. Thanks for the insight

Dimitris Stefanakis

Software Engineer

6 个月

Excellent article, decorator and composition are two of my favorite patterns. If I may, I would like to add to this beautiful article that decorate can be applied "pre" or "post" depending when you do the manipulation to the value that you pass to the method. A "pre" is when we do some manipulation and then pass the value a "post" is when we first pass the value and then apply a transformation.

要查看或添加评论,请登录

Alkiviadis Skoutaris的更多文章

社区洞察

其他会员也浏览了