4 minute read

After having rethought the Factory, the Visitor and the Builder paterns in previous posts, let’s look now at another structural one: the Decorator.

The Decorator

This design pattern belongs to the structural category and its purpose is to attach additional responsibilities to an object dynamically, by wrapping it in another object that shares the same interface. It is the flexible alternative to subclassing for extending behavior: rather than a combinatorial explosion of DiscountedTaxedGiftWrappedProduct subclasses, you wrap a product in as many independent decorators as you need, and they stack.

We reuse the same Product domain. Each decorator changes the price() and the description() while leaving everything else untouched. To keep the pattern visibly distinct from the visitor, whose rules varied per product type, the decorators here apply the same rule to every product:

  • Discounted: 10% off the wrapped price.
  • Taxed: adds 20% VAT to the wrapped price.
  • GiftWrapped: adds a flat 5.00 wrapping fee.

Because they stack, a 100.00 book decorated DiscountedTaxedGiftWrapped goes 100.00 → 90.00 → 108.00 → 113.00, and its description reads "A book (discounted) (VAT incl.) (gift-wrapped)".

The object-oriented approach

The figure below shows the class diagram of the object-oriented decorator:

decorator-oop

The classic Gang of Four Decorator is an object that implements the component interface and holds a reference to another component, delegating the untouched operations and overriding the ones it enhances. An abstract ProductDecorator captures the delegation once:

public abstract class ProductDecorator implements Product
{
  protected final Product product;

  protected ProductDecorator(Product product)
  {
    this.product = Objects.requireNonNull(product, "Product is null");
  }

  public String name() { return product.name(); }
  public String description() { return product.description(); }
  public BigDecimal price() { return product.price(); }
  public ProductType type() { return product.type(); }
}

Each concrete decorator then overrides only what it changes:

public class Discounted extends ProductDecorator
{
  private static final BigDecimal RATE = new BigDecimal("0.10");

  public Discounted(Product product) { super(product); }

  public BigDecimal price()
  {
    return product.price().subtract(amount(product.price(), RATE));
  }

  public String description()
  {
    return product.description() + " (discounted)";
  }
}

Since a decorator is a Product, decorators wrap decorators and the enhancements compose by nesting:

Product wrapped = new GiftWrapped(new Taxed(new Discounted(new BaseProduct(book))));
BigDecimal price = wrapped.price();   // 113.00

The leaf being wrapped is a BaseProduct, a small record that adapts a shared common.Product into the decorator’s own interface. This is necessary because common.Product is sealed and so, exactly like the object-oriented visitor, the decorator cannot make the common records implement its interface directly.

The functional approach

Look now at the class diagram of the functional style implementation:

decorator-fp

The functional counterpart of a decorator is simply a function whicg maps a product to an enhanced product and implemented as an UnaryOperator<Product>. Because the common records are immutable, “enhancing” one means rebuilding it through the ProductType factory, already seen at the very beginning, which is why the FP side reuses common directly with no adapter:

public static final UnaryOperator<Product> DISCOUNTED = product ->
  product.type().newInstance(product.name(),
    product.description() + " (discounted)",
    product.price().subtract(amount(product.price(), "0.10")));

Being ordinary values, the decorations compose with andThen, exactly as the factory composed its factory function, the visitor composed its operations and the builder composed its steps:

UnaryOperator<Product> decorate = DISCOUNTED.andThen(TAXED).andThen(GIFT_WRAPPED);
Product wrapped = decorate.apply(book);   // price 113.00

And, just like the functional builder step, a decoration is a reusable first-class value. For example, the same discount could be applied twice:

Product wrapped = DISCOUNTED.andThen(DISCOUNTED).apply(book);   // 100 -> 90 -> 81

The object-oriented Decorator wraps the component in a stack of objects sharing its interface, while the functional one expresses the very same stacking as the composition of pure Product to Product functions. “Turtles all the way down”, and both land on the same enhanced product.

Project structure

The code is organized as a multi-module Maven project. The product domain lives in its own common module: a sealed Product interface, the three product records and the ProductType enumerated which already carries the FP factory function seen above. Everything that can reuse that domain does:

oop-fp-design-patterns        (parent POM)
├── common                    sealed Product, the records, ProductType(+factory)
├── factory   (→ common)      ProductFactory (OOP); the FP factory *is* common.ProductType
├── visitor   (→ common)      FP: operations over the common records (switch + lambda bundle)
│                             OOP: its own element hierarchy (see below)
├── builder   (→ common)      immutable Order over the common records; OOP: fluent
│                             OrderBuilder; FP: composed UnaryOperator<Order> steps
└── decorator (→ common)      FP: composed UnaryOperator<Product> decorations over the
                              common records; OOP: its own Product interface (see below)

The FP factory, the FP visitor and the FP decorator all operate directly on the common records, so nothing is duplicated there. The two exceptions are the object-oriented Visitor and the object-oriented Decorator. The Visitor needs an accept method on every element (double dispatch); the Decorator needs a non-sealed Product interface that its wrappers can implement. In both cases common.Product is sealed and cannot be extended from another module, so each owns its own element/component types and reuses only the ProductType enumerated - the OOP decorator bridges back to common through a small BaseProduct adapter. This asymmetry is not accidental. The classic Visitor requires every element to expose an accept method and the classic Decorator requires every component to share the wrappers’ interface; both couple the elements to the pattern’s abstraction, so they cannot be the sealed records defined in common. The functional approach has no such coupling: it operates over the sealed type from the outside - pattern-matching for the visitor, rebuilding through the factory for the decorator - so the elements know nothing about the operations applied to them and, hence, can be the shared common records.

The full code of these examples, including the associated unit tests, can be found here.