Using Design Patterns to Make Better Models: Decorator Pattern
So far in our design pattern series, we've looked at the Strategy Pattern and the Observer Pattern. We find both useful when building simulations and often use them in some form or another in our simulation projects.
The Decorator Pattern is a little different.
It is a useful pattern to understand, but we don't use it very often in our day-to-day simulations. In many cases, we find simpler approaches, such as configuration variables or composition, easier to work with.
That doesn't make Decorator a bad pattern, though. It solves a specific problem particularly well: adding behavior to an object dynamically without modifying the original class or creating a subclass for every possible combination.
That's what we'll demonstrate with our vehicle-rental simulation.
The problem: too many combinations
Imagine a vehicle-rental company with two types of vehicles:
Economy Car
SUV
Customers can also add optional extras:
GPS
Premium Insurance
At first, this seems simple enough.
But if we represent every combination using inheritance, we quickly end up with classes such as:
EconomyCar
EconomyCarWithGPS
EconomyCarWithInsurance
EconomyCarWithGPSAndInsurance
SUV
SUVWithGPS
SUVWithInsurance
SUVWithGPSAndInsuranceThis approach works. The problem is what happens when the business grows.
Add more vehicles and more optional extras, and the number of combinations grows rapidly. With five optional features, each vehicle type already has 32 possible combinations.
We don't actually create all those classes in our model, the point is to show how quickly this approach becomes difficult to maintain.

The underlying problem is that the optional features have become tied to specific vehicle classes.
GPS isn't really a type of SUV.
It is an additional responsibility that can be applied to an SUV, an Economy Car, or potentially any future vehicle.
So perhaps it shouldn't be represented through inheritance at all.
Enter the Decorator Pattern
The Decorator Pattern takes a different approach.
Instead of creating a new class for every combination, we start with a basic object and wrap it with additional objects that add behavior.
Our model has an abstract Rental class that defines the common interface:
public abstract class RentalEvery rental has a:
description
price
preparation time
The simulation can therefore work with a Rental without needing to know exactly what type of rental it has.
We then introduce an abstract RentalDecorator:
public abstract class RentalDecorator extends Rental {
protected Rental rental;
public RentalDecorator(Rental rental) {
this.rental = rental;
}
}This gives us one of the key ideas behind the Decorator Pattern:
The decorator is both a Rental and contains a Rental.
In other words:
RentalDecorator IS-A Rental
RentalDecorator HAS-A Rental
Adding the decorations
We can now create individual decorators for our optional extras.
For example, our GPS decorator can add $10 to the price and two minutes to the preparation time:
public double price() {
return rental.price() + 10.00;
}
public double preparationTime() {
return rental.preparationTime() + 2.0;
}It can also add " + GPS" to the description.
The important part is that GPS doesn't need to know what it is decorating.
It could wrap:
EconomyCaror:
SUVor even another decorator.
The Insurance decorator works in exactly the same way.
Building a rental dynamically
This is where the difference becomes most obvious.
With the original design, an SUV with GPS and insurance requires:
new SUVWithGPSAndInsurance()That class has to exist.
With Decorator, we can build the same rental dynamically:
new Insurance(
new GPS(
new SUV()
)
)We start with an SUV, wrap it with GPS, and then wrap that with Insurance.
There is no SUVWithGPSAndInsurance class.

Each layer is still a Rental, so the rest of the simulation doesn't need to know how the object was assembled.
Delegation: how the layers work
Suppose the simulation asks for:
rental.price()For:
Insurance
↓
GPS
↓
SUVthe call effectively travels through each layer:
Insurance.price()
↓
GPS.price()
↓
SUV.price()The SUV returns its base price.
GPS adds its cost.
Insurance adds its cost.
The same process happens for preparation time and description.
So if an SUV costs $80, GPS costs $10, and insurance costs $20:
SUV $80
+ GPS $10
+ Premium Insurance $20
-------------------------------
$110The important thing is that each decorator only needs to understand its own contribution.

What changes in the simulation?
Interestingly, almost nothing.
This is an important part of our model.
We have two process flows:
Non Decorator Design
Customer Source
↓
Rental Service
↓
Customer Sinkand:
Decorator Design
Customer Source
↓
Rental Service
↓
Customer SinkBoth services ultimately ask:
agent.rental.preparationTime()The process doesn't need to know whether the customer has an SUV, an SUV with GPS, or an SUV with GPS and insurance.
The simulation behavior stays essentially the same. The object design changes underneath it.

But do we actually use Decorator?
Not very often.
And this is an important point: design patterns don't automatically make code better.
For a simple example, we could just have:
boolean hasGPS;
boolean hasInsurance;and calculate the price with a couple of if statements:
double getPrice() {
double price = 80;
if (hasGPS) {
price += 10;
}
if (hasInsurance) {
price += 20;
}
return price;
}For two or three optional features, we'd argue that this is probably cleaner and easier to understand than creating several decorator classes.
Decorator becomes more attractive when the additional features represent independent pieces of behavior and the number of possible combinations starts becoming difficult to manage.
For example:
Object
↓
Logging
↓
Caching
↓
Authentication
↓
CompressionThose aren't simply attributes describing the object. They are behaviors being layered around it.
The Decorator Pattern is particularly useful for this kind of dynamically composable behavior. A real-world example is with Java I/O classes, where decorators such as buffering and line-number functionality can be layered around an input stream.
So, when should you use Decorator?
Decorator is worth considering when:
you have many optional behaviors
those behaviors are relatively independent
combinations need to be created dynamically
you don't want the base class to know about every possible addition
the additions genuinely represent behavior, rather than just data
For a handful of simple options, it can be overengineering.
The real lesson isn't:
Decorator is better than using booleans and if statements.
It's:
Decorator is one tool for managing a particular type of complexity.
Conclusion
The Decorator Pattern solves the problem of adding responsibilities to an object dynamically without creating a new subclass for every possible combination.
Our vehicle-rental model demonstrates the difference clearly.
The non-Decorator design represents every combination as a subclass:
SUVWithGPSAndInsuranceThe Decorator design composes the same rental dynamically:
Insurance
↓
GPS
↓
SUVThe result is more flexible and easier to extend when the number of independent behaviors grows.
But that flexibility adds classes and complexity.
That's why we don't reach for Decorator first in our own simulations. If a few variables and some straightforward logic solve the problem, that is often the better design.
The goal of design patterns isn't to use as many patterns as possible.
It's knowing when the problem is complicated enough to justify one.
And with that, we've covered the Decorator Pattern. Next, we'll look at the Factory Pattern and how it can help us manage object creation more cleanly.
The example model can be downloaded below:
Devon Cowling is a simulation engineer and data scientist working with The AnyLogic Modeler. To contact him, use LinkedIn.
With contributions from Selaelo Kgoale
What next?
If you liked this post, you could read more posts by following the links above. Why not subscribe to our blog or follow us on any of the social media accounts for future updates? The links are in the Menu bar at the top, or the footer at the bottom. You can also join the mobile app here!
If you really want to make a difference in supporting us please consider joining our Patreon community here.
If you want to contact us for some advice, maybe a potential partnership or project or just to say "Hi!", feel free to get in touch here, and we will get back to you soon!




Comments