Pattern 9 | Replacing Decorator |
To add behavior to an individual object rather than to an entire class of objects—this allows us to change the behavior of an existing class.
Decorator is useful when we’ve got an existing class that we need to add some behavior to but we can’t change the existing class. We may want to introduce a breaking change, but we can’t change every other part of the system where the class is used. Or the class may be part of a library that we can’t, or don’t want to, modify.
Decorator uses a combination of inheritance and composition. It starts with an interface with at least one concrete implementation. This implementation is the class that we can’t or don’t want to change.
We then implement the interface with an abstract decorator class, which gets an instance of our existing, concrete class composed into it. Our abstract decorator class can itself have several implementations, which tweak the behavior of the existing class using composition, as shown in this figure:
This gives us some ability to add or modify behavior on existing classes, but we’re mostly limited to small tweaks since we rely on the base behavior of the composed class.
Wrapper
The essence of Decorator is wrapping an existing class with a new one so that the new class can tweak the behavior of the existing one. In the functional world, one simple replacement is to create a higher-order function that takes in the existing function and returns a new, wrapped function.
The wrapped function does its job and then delegates to the existing function. For instance, we could create a wrapWithLogger function that wraps up an existing function with a bit of logging, returning a new function.
Let’s take a look at using Decorator with a basic four-function calculator. The calculator has four operations, add, subtract, multiply, and divide. To demonstrate Decorator, we’ll take a basic calculator and decorate it so that it logs out the calculation it’s performing to the console.
In Java, our solution consists of an interface and two concrete classes. The Calculator interface is implemented by both CalculatorImpl and LoggingCalculator. The LoggingCalculator class serves as our decorator and needs a CalculatorImpl composed into it to do its job. An outline of this approach can be found in the following image:
The LoggingCalculator class delegates to the composed CalculatorImpl and then logs the calculation to the console.
In Scala, our calculator is just a collection of four functions. To keep things simple, we’ll constrain ourselves to integer operations, since implementing generic numeric functions in Scala is a bit involved. The code for our Scala calculator follows:
ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/decorator/Examples.scala | |
| def add(a: Int, b: Int) = a + b |
| def subtract(a: Int, b: Int) = a - b |
| def multiply(a: Int, b: Int) = a * b |
| def divide(a: Int, b: Int) = a / b |
To wrap our calculator functions in logging code, we use makeLogger. This is a higher-order function that takes in a calculator function and returns a new function that runs the original calculator function and prints the result to the console before returning it.
ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/decorator/Examples.scala | |
| def makeLogger(calcFn: (Int, Int) => Int) = |
| (a: Int, b: Int) => { |
| val result = calcFn(a, b) |
| println("Result is: " + result) |
| result |
| } |
To use makeLogger, we run our original calculator functions through it and assign the results into new vals, as the following code shows:
ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/decorator/Examples.scala | |
| val loggingAdd = makeLogger(add) |
| val loggingSubtract = makeLogger(subtract) |
| val loggingMultiply = makeLogger(multiply) |
| val loggingDivide = makeLogger(divide) |
Now we can use our printing calculator function to do some arithmetic and print the results:
| scala> loggingAdd(2, 3) |
| Result is: 5 |
| res0: Int = 5 |
| |
| scala> loggingSubtract(2, 3) |
| Result is: -1 |
| res1: Int = -1 |
Let’s take a look at our calculator solution in Clojure.
The structure of our Clojure solution is similar to the Scala one, the main difference being that our Clojure solution isn’t constrained to integers since Clojure is dynamically typed. The following code defines our calculator functions:
ClojureExamples/src/mbfpp/oo/decorator/examples.clj | |
| (defn add [a b] (+ a b)) |
| (defn subtract [a b] (- a b)) |
| (defn multiply [a b] (* a b)) |
| (defn divide [a b] (/ a b)) |
Next we need a make-logger higher-order function to wrap our calculator functions up with logging code:
ClojureExamples/src/mbfpp/oo/decorator/examples.clj | |
| (defn make-logger [calc-fn] |
| (fn [a b] |
| (let [result (calc-fn a b)] |
| (println (str "Result is: " result)) |
| result))) |
Finally, we can create some logging calculator functions and use them to do some logging math:
ClojureExamples/src/mbfpp/oo/decorator/examples.clj | |
| (def logging-add (make-logger add)) |
| (def logging-subtract (make-logger subtract)) |
| (def logging-multiply (make-logger multiply)) |
| (def logging-divide (make-logger divide)) |
| => (logging-add 2 3) |
| Result is: 5 |
| 5 |
| => (logging-subtract 2 3) |
| Result is: -1 |
| -1 |
It’s no accident that the Scala and Clojure solutions to the calculator problem are so similar: they both rely only on basic higher-order functions, which are similar across both languages.
Design Patterns: Elements of Reusable Object-Oriented Software [GHJV95]—Decorator
Pattern 7, Replacing Strategy
Pattern 16, Function Builder
3.133.133.233