Rethinking Java Design Patterns: from OOP to FP (5)
After having rethought the Factory, the Visitor, the Builder and the Decorator paterns in previous posts, let’s look now at a behavioral one: the Strategy.
The Strategy
This design pattern belongs to the behavioral category and its purpose is to define a family of algorithms, encapsulate each one of them and make them interchangeable, such that the algorithm may vary independently of its consumer. Where the decorator asked what else should happen to this object ?, the strategy asks which one of these algorithms should be applied ?.
We keep the same Product domain and we compute a shipping cost for it. Three
interchangeable algorithms are provided:
- Standard: a flat
4.99fee. - Express:
9.99plus 2% of the product price. - FreeOver: the familiar “free delivery over 50.00” commercial rule. It is parameterized by a price threshold and by the strategy to apply when the threshold isn’t reached. Should the product price be greater than or equal to the threshold, the shipping is free, otherwise the product doesn’t qualify and the cost is the one computed by that other strategy.
For our 100.00 book, the standard shipping costs 4.99, the express one costs
11.99 and, with a 50.00 threshold, the free-over one costs 0.00, since 100.00
is above the threshold. Raising that same threshold to 150.00 falls back to the
standard shipping and, hence, to 4.99.
Notice that, unlike the visitor, nothing here varies per product type: what varies is the
algorithm and it is the caller that picks it.
The object-oriented approach
The figure below shows the class diagram of the object-oriented strategy:

The classic Gang of Four Strategy declares an interface for the family of the algorithms and one class per algorithm:
public interface ShippingStrategy
{
BigDecimal cost(Product product);
}
public class ExpressShipping implements ShippingStrategy
{
private static final BigDecimal FEE = new BigDecimal("9.99");
private static final BigDecimal RATE = new BigDecimal("0.02");
public BigDecimal cost(Product product)
{
return FEE.add(product.price().multiply(RATE).setScale(2, RoundingMode.HALF_UP));
}
}
An algorithm that needs to be parameterized becomes a class with state. Here
FreeOverShipping holds its threshold and the strategy to fall back to:
public FreeOverShipping(BigDecimal threshold, ShippingStrategy otherwise) { ... }
public BigDecimal cost(Product product)
{
return product.price().compareTo(threshold) >= 0 ? FREE : otherwise.cost(product);
}
Last but not least, the ShippingCalculator class, as the context, is the object that uses the algorithm without
knowing which one it is. It only holds a reference to the interface, which is
what allows the algorithm to be replaced at runtime:
ShippingCalculator calculator = new ShippingCalculator(new StandardShipping());
BigDecimal cost = calculator.cost(book); // 4.99
calculator.setStrategy(new ExpressShipping());
cost = calculator.cost(book); // 11.99
Contrary to the visitor and to the decorator, the strategy doesn’t require
anything at all from the elements it processes: no accept method and no shared
component interface. Consequently, and this is the first time it happens on the
object-oriented side, the module reuses the sealed common.Product directly, with
neither its own hierarchy, nor any adapter.
The functional approach
Look now at the class diagram of the functional style implementation:

Of all the patterns seen so far, this is the one where the functional answer is the
most radical. The interface ShippingStrategy in the OO implementation declares one single
method and, like any interface, holds no state, such that everything it tells us is
a Product comes in, a BigDecimal comes out.
In functional terms, this is nothing more than a Function<Product, BigDecimal> type.
So each algorithm becomes a plain value of the function type, for example:
public static final Function<Product, BigDecimal> EXPRESS = product ->
EXPRESS_FEE.add(product.price().multiply(EXPRESS_RATE).setScale(2, RoundingMode.HALF_UP));
As opposed to the OO side which required the FreeOverShipping class holding
the threshold and the shipping strategy, the FP side captures them in a closure.
So this class on the OO side becomes on the FP side a higher-order function,
i.e. a function returning the strategy itself:
public static Function<Product, BigDecimal> freeOver(BigDecimal threshold,
Function<Product, BigDecimal> otherwise)
{
return product -> product.price().compareTo(threshold) >= 0 ? FREE : otherwise.apply(product);
}
The very same happens to ShippingCalculator, the context class on the OOP side.
Its whole reason to exist was to hold a strategy in a field, such that its cost()
and total() operations could delegate to it. But a context is just an operation
parameterized by an algorithm and this, once again, is precisely a higher-order
function. Hence, the ShippingCalculator.total() method on the OO side becomes on the FP one:
public static Function<Product, BigDecimal> totalWith(Function<Product, BigDecimal> strategy)
{
return product -> product.price().add(strategy.apply(product));
}
such that the following call on the OO side, shown above:
ShippingCalculator calculator = new ShippingCalculator(new StandardShipping());
...
BigDecimal total = calculator.total(book);
becomes on the FP side:
BigDecimal total = totalWith(STANDARD).apply(book);
There is no property to hold the strategy anymore and, consequently, no setStrategy()
method either. Here the strategy is an argument which doesn’t need to be stored
in the context, just call the function with the right value and that’s all.
But the real advantage of the strategies as ordinary values is that they can be combined. Picking the cheapest of several shipping options requires yet another class on the OO side, while here it’s a simple combinator:
Function<Product, BigDecimal> best = cheapest(STANDARD, EXPRESS); // 4.99
And as usual, they compose with andThen, for example to apply
a promotion to whatever cost has been computed:
Function<Product, BigDecimal> promo =
EXPRESS.andThen(cost -> cost.divide(TWO, 2, RoundingMode.HALF_UP)); // 6.00
The OO Strategy encapsulates each algorithm in a class implementing a common interface and injects the chosen one into a context object, while the functional one observes that such an interface describes nothing but a function type which the JDK already provides and, consequently, keeps only the algorithms themselves. “Turtles all the way down”, and both compute the same cost.
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)
└── strategy (→ common) shipping algorithms over the common records; OOP: the
ShippingStrategy hierarchy + context; FP: plain
Function<Product, BigDecimal> values
The FP factory, the FP visitor and the FP decorator all operate directly on the
common records, so nothing is duplicated there, and the Strategy does so on
both of its sides. 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 Strategy confirms the rule the other
way around: it doesn’t couple the elements to its abstraction either, only the
client to it, and this is precisely why it is the only pattern here whose
object-oriented implementation reuses common as freely as its functional one.
The full code of these examples, including the associated unit tests, can be found here.