๐Ÿšš Understanding the Decorator Design Pattern in Ruby


author

Author: David Martinez

Created: January 7, 2025 - Updated: January 8, 2025

Read Time: 16 min

Ruby

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. ๐Ÿ“ฆ

What Is the Decorator Pattern? ๐Ÿค”

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.

When to Use the Decorator Pattern?

๐Ÿ”น 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.

Problem Statement

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.

Naive Solution (Without Pattern)

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.

UML Diagram

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 ShippingCostCalculator
def initialize(base_cost)
@base_cost = base_cost
end
def calculate(express: false, insurance: false, region: nil)
cost = @base_cost
cost += 10 if express
cost += 20 if insurance
cost += tax(region) if region
cost
end
private
def tax(region)
case region
when :us then 5
when :eu then 10
else 0
end
end
end
def main
calculator = ShippingCostCalculator.new(100)
puts calculator.calculate # 100
puts calculator.calculate(express: true) # 110
puts calculator.calculate(insurance: true) # 120
puts calculator.calculate(region: :us) # 105
puts calculator.calculate(express: true, insurance: true, region: :eu) # 140
end

Why This Approach Is Not Scalable? ๐Ÿค”

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.

Applying the Decorator Pattern ๐ŸŽจ

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.

UML Diagram

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.

How does it Work?

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.

Advantages of Using the Decorator Pattern ๐Ÿš€

๐Ÿ”น 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.

Show me the Code

# Base class for calculating shipping cost
class BaseShipping
def initialize(base_cost)
@base_cost = base_cost
end
def calculate
@base_cost
end
end
# Base decorator class
class ShippingDecorator
def initialize(component)
@component = component
end
def calculate
@component.calculate
end
end
# Decorator for express shipping
class ExpressShipping < ShippingDecorator
def calculate
super + 10
end
end
# Decorator for insurance
class InsuredShipping < ShippingDecorator
def calculate
super + 20
end
end
# Decorator for regional taxes
class RegionalTaxShipping < ShippingDecorator
def initialize(component, region)
super(component)
@region = region
end
def calculate
super + tax
end
private
def tax
case @region
when :us then 5
when :eu then 10
else 0
end
end
end
base_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)

Conclusion ๐Ÿ”–

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! ๐ŸŽ‰

Join the Quest!

๐Ÿ’ป You can find this and other design patterns here ๐Ÿ“š