The Decorator pattern
is handy tool in every developer toolbox. Probably, everyone occasionally uses this pattern and knows when and how to apply it. Nevertheless, in this post I will show how to implement decorator in functional style, and contrast it with object-oriented way.
1. Why might you use Decorator pattern
?
Let’s start by highlighting the reasons Decorator pattern
might be useful in your everyday programming practice.
Shortly put, in Object-Oriented Design (OOD) Decorator pattern
is used:
To attach additional responsibilities to an object dynamically.
It enables fine granularity in distributing functionality between objects, which basically is very good example of applying Single Responsibility Principle. To extend behavior of an object, without changing collaborating objects.
Decorator is one of the patterns, that demonstrates the Open/Closed Principle very clearly.
2. Toy problem
To get better grasp of the pattern let’s use it for the following task:
Imagine, that we’re required to develop a monthly net salary calculator, given gross annual salary, after applying series of taxes.
Of course, the task is very simple, and one may argue, that we are over-engineering. However, this is deliberate decision made for demonstration purposes.
3. Decorator in Object-Oriented Light
We will start by designing an interface for our calculator:
package com.oxymorus.decorator.object_oriented;
public interface SalaryCalculator {
double calculate(double grossAnnual);
}
The next step, is to implement default calculator, that just finds average monthly salary:
package com.oxymorus.decorator.object_oriented;
public class DefaultSalaryCalculator implements SalaryCalculator {
@Override
public double calculate(double grossAnnual) {
return grossAnnual / 12;
}
}
Then, following classical definition, we will introduce AbstractTaxDecorator
, that will allow us to apply different taxes as decorations of DefaultSalaryCalculator
.
package com.oxymorus.decorator.object_oriented;
public abstract class AbstractTaxDecorator implements SalaryCalculator {
private final SalaryCalculator salaryCalculator;
public AbstractTaxDecorator(SalaryCalculator salaryCalculator) {
this.salaryCalculator = salaryCalculator;
}
protected abstract double applyTax(double salary);
@Override
public final double calculate(double gross) {
double salary = salaryCalculator.calculate(gross);
return applyTax(salary);
}
}
Next, let’s add several tax decorators:
Social security tax:
package com.oxymorus.decorator.object_oriented; public class SocialSecurityTaxDecorator extends AbstractTaxDecorator { public SocialSecurityTaxDecorator(SalaryCalculator salaryCalculator) { super(salaryCalculator); } @Override protected double applyTax(double salary) { return salary * 0.20; } }
Fixed tax:
package com.oxymorus.decorator.object_oriented; public class FixedTaxDecorator extends AbstractTaxDecorator { public FixedTaxDecorator(SalaryCalculator salaryCalculator) { super(salaryCalculator); } @Override protected double applyTax(double salary) { return salary - 100.0; } }
Finally, we can use everything together:
package com.oxymorus.decorator.object_oriented;
public class Demo {
public static void main(String[] args) {
double netSalary = new FixedTaxDecorator(
new SocialSecurityTaxDecorator(
new DefaultSalaryCalculator()
)
).calculate(60000.00);
}
}
Ok, this is well and good, let’s now take a look at a little bit different approach.
4. Decorator in Functional Light
As with OOD approach, we will start with defining the calculator interface:
package com.oxymorus.decorator.functional;
import java.util.Objects;
@FunctionalInterface
public interface SalaryCalculator {
double calculate(double grossAnnual);
default SalaryCalculator andThen(SalaryCalculator after) {
Objects.requireNonNull(after);
return (double amount) -> after.calculate(calculate(amount));
}
}
As you already noticed, the interface differs a bit from object-oriented counterpart:
we have added
andThen
method, that actually gives us ability to dynamically compose instances ofSalaryCalculator
.moreover,
SalaryCalculator
is marked as @FunctionalInterface, which simply means that we’ll be able to use lambdas instead of decorator classes
We can leave DefaultSalaryCalculator
without any changes. However, let’s define Taxes
class, that encapsulates all possible taxes:
package com.oxymorus.decorator.functional;
public final class Taxes {
private Taxes() {}
public static double socialSecurityTax(double salary) {
return salary * 0.20;
}
public static double fixedTax(double salary) {
return salary - 100;
}
}
And, finally, let’s put everything together:
package com.oxymorus.decorator.functional;
public class Demo {
public static void main(String[] args) {
double netSalary = new DefaultSalaryCalculator()
.andThen(Taxes::socialSecurityTax)
.andThen(Taxes::fixedTax)
.calculate(60000.00);
}
}
5. Comparison and Results
Comparing two implementations, we can conclude:
Both approaches solve the problem
Functional is far more concise
Object-Oriented is verbose and adds accidental complexity, but emphasizes Single Responsibility and Open/Closed Principles