Gift wrap? Shipping insurance? Optional extras have a habit of turning pricing code into a long chain of if statements. Decorator lets you wrap an object in layers, each adding one thing, while the object still looks exactly the same from the outside.

   
Category Structural
Smell fixed S09: Add-ons are boolean flags and pricing branches
Code patterns/structural/decorator

The Scene

Ava’s order is a gift, so she adds gift wrap and then insurance:

[032] cart.addon customer=cu-1 addon=giftwrap
  totals A$295.44
[033] cart.addon customer=cu-1 addon=insurance
  totals A$300.33
[034] cart.addon customer=cu-1 addon=sparkles
  rejected: unknown add-on sparkles

Gift wrap is a flat A$4.50. Insurance is 2% of the discounted subtotal. And no, CornerStore doesn’t sell sparkles. Yet.

But what happens when it does? Or when it adds a “priority handling” fee, or a “carbon offset”?


The Smell

In the baseline, every add-on is a boolean on the cart:

    // SMELL S09: add-ons are plain boolean flags, one per add-on
    public bool GiftWrap;

    public bool Insurance;

And pricing checks each flag in turn:

        // SMELL S09: each add-on is a boolean flag checked in an if-chain
        var cost = 0m;
        if (cart.GiftWrap)
        {
            cost += 4.50m;
        }

        if (cart.Insurance)
        {
            cost += Math.Round(discountedSubtotal * 0.02m, 2, MidpointRounding.AwayFromZero);
        }

Two add-ons are fine. But every new one means another field on the cart, another if in pricing and another case in the console. The central price calculation has to know about every extra the business will ever dream up.


The Idea

Decorator wraps an object in another object with the same interface. The wrapper does its own bit of work and passes the rest through to whatever it’s wrapping. You can stack wrappers as deep as you like, and the outside world can’t tell how many layers there are.

Think of ordering a coffee. You start with a plain coffee. Add milk: still a coffee, a bit more expensive. Add an extra shot: still a coffee, more expensive again. The barista doesn’t need a separate menu item for every combination. Each extra just adds its cost to whatever’s underneath.


The Refactor

Everything shares one small contract:

internal interface IAddOnPriceComponent
{
    decimal Cost { get; }
}

The innermost layer is a base component that costs nothing:

internal sealed class BaseAddOnPriceComponent : IAddOnPriceComponent
{
    public decimal Cost => 0m;
}

Each add-on is a decorator that wraps another component and adds its own cost on top:

internal sealed class GiftWrapPriceDecorator(IAddOnPriceComponent component, bool enabled) : IAddOnPriceComponent
{
    public decimal Cost => component.Cost + (enabled ? 4.50m : 0m);
}

And the pricing service stacks the layers:

IAddOnPriceComponent component = new BaseAddOnPriceComponent();
component = new GiftWrapPriceDecorator(component, cart.GiftWrap);
component = new InsurancePriceDecorator(component, cart.Insurance, discountedSubtotal);
return component.Cost;

Ask the outermost layer for its Cost, and the question ripples inward: insurance asks gift wrap, gift wrap asks the base, then each layer adds its part on the way back out.

classDiagram
    class IAddOnPriceComponent {
        <<interface>>
        +Cost
    }
    IAddOnPriceComponent <|.. BaseAddOnPriceComponent
    IAddOnPriceComponent <|.. GiftWrapPriceDecorator
    IAddOnPriceComponent <|.. InsurancePriceDecorator
    GiftWrapPriceDecorator o--> IAddOnPriceComponent : wraps
    InsurancePriceDecorator o--> IAddOnPriceComponent : wraps
    PricingService --> IAddOnPriceComponent : composes

A deliberate compromise

You might notice something unusual. Each decorator takes an enabled flag, and both are always applied. In the textbook version, you’d only wrap when the option is chosen:

if (cart.GiftWrap)
{
    component = new GiftWrapPriceDecorator(component);
}

Why the difference? Remember the rule of this series: one pattern, one seam. The cart’s boolean flags are still used by other code in the baseline, so this refactor moves the cost rules into decorators and leaves the flags alone as the on/off switch. A fuller redesign would replace the flags with a list of selected add-ons and wrap only those. It’s a nice example of how real refactors often land in stages, not all at once.


What It Cost

The lightest refactor in the Structural group: 9 files, about 79 lines added and 15 removed. Each decorator is only a few lines long.

What you get: a new add-on is a new decorator class, plus one line where the layers are stacked. The existing cost rules never change.


When Not to Use It

  • Layers start depending on each other’s order. Decorators work best when each layer is independent. Insurance here deliberately receives the discounted subtotal directly, so it doesn’t accidentally charge 2% on the gift wrap too. If your layers keep needing to know what’s above or below them, the stack is hiding real coupling.
  • There are only one or two fixed options. A couple of if statements that never grow are easier to read than a stack of wrappers.
  • Debugging deep stacks. Ten layers of wrappers can make a stack trace hard to follow. Keep each layer small and obvious.

You’ve used decorators in .NET without knowing it: wrapping a FileStream in a GZipStream in a BufferedStream is exactly this pattern.


Easily Confused With

  • Proxy. Both wrap an object behind the same interface. A decorator adds a responsibility. A proxy controls access: caching, permissions or lazy loading. See Decorator vs Proxy.
  • Composite. Both are recursive structures. A decorator wraps one component. A composite holds many children. See Composite vs Decorator.

Try It Yourself

dotnet build patterns/structural/decorator/Decorator.slnx
dotnet test patterns/structural/decorator/Decorator.slnx
dotnet run --project patterns/structural/decorator/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json

Exercise: add a PriorityHandlingPriceDecorator that charges a flat A$7.00. Notice you don’t touch the gift wrap or insurance classes at all.

The Decorator README includes a sequence diagram of the cost calculation. The full change is in DIFF.md.


Previous: Composite · Next: Facade