Pattern 9Replacing Decorator

Intent

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.

Overview

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:

images/Decorator.png

Figure 7. Decorator Diagram. A class diagram for the Decorator pattern

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.

Also Known As

Wrapper

Functional Replacement

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.

Sample Code: Logging Calculator

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.

Classic Java

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:

images/Calculator.png

The LoggingCalculator class delegates to the composed CalculatorImpl and then logs the calculation to the console.

In Scala

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.

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.

For Further Reading

Design Patterns: Elements of Reusable Object-Oriented Software [GHJV95]Decorator

Related Patterns

Pattern 7, Replacing Strategy

Pattern 16, Function Builder

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.133.233