Author: David Martinez
Created: January 7, 2025 - Updated: January 8, 2025
Read Time: 16 min
In this article, we dive into the Decorator Design Pattern, a structural pattern that allows you to dynamically add new behaviors to objects without altering their structure. ๐ฆ
The Decorator Pattern is a structural design pattern that lets you add new functionality to an object at runtime. Instead of creating subclass hierarchies to extend behavior, decorators "wrap" objects to enhance their functionality. This helps to keep code flexible and adheres to the Open-Closed Principle.
This pattern is perfect when you need to extend an objectโs behavior in a way that is cleaner and more flexible than subclassing.
๐น Dynamic Behavior Extension: Use this pattern when you need to add new behaviors to objects at runtime without altering their structure.
๐น Avoiding Subclass Explosion: When subclassing becomes impractical due to the sheer number of variations needed.
๐น Open-Closed Principle: Itโs ideal when you want to extend behavior without modifying existing code.
Consider a scenario where you need to calculate shipping costs based on different criteria such as express shipping, insurance, and regional taxes. Without the Decorator pattern, you would need to modify the original class to accommodate new requirements.
In the naive approach, you handle shipping cost calculation by adding logic directly to the ShippingCostCalculator class.
However, this leads to a massive increase in complexity as you introduce more variations of the calculation logic.
In this example:
๐น ShippingCostCalculator class is responsable of all the logic related with shipping, so if the requierements change all of this logic will be affected.
class ShippingCostCalculatordef initialize(base_cost)@base_cost = base_costenddef calculate(express: false, insurance: false, region: nil)cost = @base_costcost += 10 if expresscost += 20 if insurancecost += tax(region) if regioncostendprivatedef tax(region)case regionwhen :us then 5when :eu then 10else 0endendenddef maincalculator = ShippingCostCalculator.new(100)puts calculator.calculate # 100puts calculator.calculate(express: true) # 110puts calculator.calculate(insurance: true) # 120puts calculator.calculate(region: :us) # 105puts calculator.calculate(express: true, insurance: true, region: :eu) # 140end
1๏ธโฃ Rigidity: Adding new features like additional services or conditions requires modifying the core ShippingCostCalculator, which violates the Open-Closed Principle.
2๏ธโฃ Complexity: The method calculate becomes overly complicated with each additional option.
3๏ธโฃ Code Duplication: The logic for each option is embedded in the same class, leading to redundancy and harder maintenance.
4๏ธโฃ Lack of Flexibility: It's hard to mix and match different behaviors dynamically.
The Decorator Pattern solves these issues by allowing you to "wrap" different behaviors in separate decorator classes that build on the base object.
These decorators can be combined as needed to create complex behavior without modifying the base class.
In this improved approach:
๐น Component defines the interface for objects that can have responsibilities added to them.
๐น ConcreteComponent is the base object to which decorators can be added.
๐น Decorator is the base class for all decorators, implementing the Component interface.
๐น ConcreteDecorator classes add new responsibilities to the base object.
1๏ธโฃ Component: This is the base object whose behavior you are extending.
2๏ธโฃ Decorator: A class that wraps the component and adds additional functionality to it.
3๏ธโฃ ConcreteDecorator: Specific decorators (e.g., ExpressShipping, InsuredShipping) that implement new behaviors.
4๏ธโฃ Client: The code that interacts with the decorated objects.
๐น Scalability: You can extend the behavior of objects without modifying their core logic, allowing for a scalable and flexible design.
๐น Mantainability: Each new functionality is encapsulated in its own class, which makes the code easier to maintain and extend.
๐น Composability: You can mix and match decorators to form different combinations of behaviors, promoting code reuse.
๐น Clean Separation of Concerns: Each decorator focuses on a specific responsibility, keeping your codebase clean and organized.
๐น Adheres to Open-Closed Principle: You can extend behavior without modifying the existing code.
# Base class for calculating shipping costclass BaseShippingdef initialize(base_cost)@base_cost = base_costenddef calculate@base_costendend# Base decorator classclass ShippingDecoratordef initialize(component)@component = componentenddef calculate@component.calculateendend# Decorator for express shippingclass ExpressShipping < ShippingDecoratordef calculatesuper + 10endend# Decorator for insuranceclass InsuredShipping < ShippingDecoratordef calculatesuper + 20endend# Decorator for regional taxesclass RegionalTaxShipping < ShippingDecoratordef initialize(component, region)super(component)@region = regionenddef calculatesuper + taxendprivatedef taxcase @regionwhen :us then 5when :eu then 10else 0endendendbase_shipping = BaseShipping.new(50)insured_shipping = InsuredShipping.new(base_shipping)express_insured_shipping = ExpressShipping.new(insured_shipping)final_shipping = RegionalTaxShipping.new(express_insured_shipping, :us)puts final_shipping.calculate # 135 (50 + 10 for express + 20 for insurance + 5 for US tax)
The Decorator Design Pattern provides a flexible and elegant solution for dynamically adding new behaviors to objects. It avoids the pitfalls of class inheritance by allowing you to compose behaviors in a clean and maintainable way.
With decorators, you can easily extend functionality in a way that is both flexible and scalable, making it ideal for scenarios like pricing, logging, or adding features without altering the underlying structure.
By using the Decorator pattern, you can create a more modular and maintainable codebase that is easier to extend and adapt to changing requirements. ๐
If you enjoyed this article, consider sharing it with your friends and colleagues! ๐
Happy coding! ๐
๐ป You can find this and other design patterns here ๐