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:
Logging Added:
Validation for Division by Zero:
Try-Catch Block:
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.
Software Engineer || Pure & Industrial Chemist
6 个月Decorator pattern simplified. Thanks for the insight
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.