Pattern 6Replacing Template Method

Intent

To specify the outline of an algorithm, letting callers plug in some of the specifics

Overview

The Template Method pattern consists of an abstract class that defines some operation, or set of operations, in terms of abstract suboperations. Users of Template Method implement the abstract template class to provide implementation of the substeps. A template class looks like this code snippet:

 
public​ ​abstract​ ​class​ TemplateExample{
 
 
public​ ​void​ anOperation(){
 
subOperationOne();
 
subOperationTwo();
 
}
 
 
protected​ ​abstract​ ​void​ subOperationOne();
 
 
protected​ ​abstract​ ​void​ subOperationTwo();
 
}

To use it, extend the TemplateExample and implement the abstract suboperations.

For instance, to use Template Method for board games, create a Game template that defines the abstract set of steps it takes to play a board game (setUpBoard, makeMove, declareWinner, and so on). To implement any particular board game, extend the abstract Game class and implement the substeps as appropriate for a particular game.

Functional Replacement

Our functional replacement for Template Method will satisfy its intent, which is to create a skeleton for some algorithm and let callers plug in the details. Instead of using classes to implement our suboperations, we’ll use higher-order functions; and instead of relying on subclassing, we’ll rely on function composition. We’ll do so by passing the suboperations into a Pattern 16, Function Builder and having it return a new function that does the full operation.

An outline of this approach in Scala looks like so:

 
def​ makeAnOperation(
 
subOperationOne: () => ​Unit​,
 
subOperationTwo: () => ​Unit​) =
 
() => {
 
subOperationOne()
 
subOperationTwo()
 
}

This lets us program more directly, since we no longer need to define suboperations and subclasses.

Sample Code: Grade Reporter

As an example, let’s take a look at a template method that prints grade reports. It does this in two steps. The first takes a list of grades in numeric form and translates them into letter form, and the second formats and prints the report.

Since those two steps can be done in many different ways, we’ll just specify the skeleton required to create a grade report, translate the grades first, and then format and print the report, and we’ll leave it up to individual implementations to specify exactly how the grades are translated and the report is printed.

We’ll also go over two such implementations. The first translates to the full letter grades A, B, C, D, and F and prints a simple histogram. The second adds plus and minus grades to some of the letters and prints a full list of grades.

Classic Java

A sketch of using Template Method to solve this problem in classic Java uses the following: a GradeReporterTemplate that has a single fully implemented method, reportGrades, and two abstract methods, numToLetter and printGradeReport.

The numToLetter method specifies how to convert a single numeric grade into a letter grade, and printGradeReport specifies how to format and print a grade report. Both methods must be implemented by users of the template. The class diagram provides an outline:

images/GradeReporter.png

Figure 5. Grade Reporter Template. Using Template Method to report grades

To get template implementations with different behaviors, the user of the Template class creates different subclasses with different implementations of numToLetter and printGradeReport.

In Scala

Instead of relying on inheritance, the Scala replacement for Template Method uses Pattern 16, Function Builder to compose together suboperations.

The core of the solution is the function makeGradeReporter, which takes a numToLetter function to translate numeric grades to letter grades and a printGradeReport to print the report. The makeGradeReporter function returns a new function that composes its input functions together.

We’ll also need a couple of different implementations of the numToLetter and printGradeReport functions so we can see this solution in action.

Let’s start by looking at makeGradeReporter. It takes numToLetter and printGradeReport as arguments and produces a new function that takes a Seq[Double] to represent a list of grades. It then uses map to convert each grade to a letter grade and passes the new list into printGradeReport. Here’s the code:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/tm/GradeReporter.scala
 
def​ makeGradeReporter(
 
numToLetter: (Double) => ​String​,
 
printGradeReport: (Seq[​String​]) => ​Unit​) = (grades: Seq[Double]) => {
 
printGradeReport(grades.map(numToLetter))
 
}

Now let’s take a look at the functions we’ll need to convert to full letter grades and to print a histogram. The first, fullGradeConverter, just uses a big if-else statement to do the grade conversion:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/tm/GradeReporter.scala
 
def​ fullGradeConverter(grade: Double) =
 
if​(grade <= 5.0 && grade > 4.0) ​"A"
 
else​ ​if​(grade <= 4.0 && grade > 3.0) ​"B"
 
else​ ​if​(grade <= 3.0 && grade > 2.0) ​"C"
 
else​ ​if​(grade <= 2.0 && grade > 0.0) ​"D"
 
else​ ​if​(grade == 0.0) ​"F"
 
else​ ​"N/A"

The next, printHistogram, is a bit more involved. It uses a method named groupBy to group grades together into a Map, which it then turns into a list of tuples of counts using the map method. Finally, it uses a for comprehension to print the histogram, as the code below shows:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/tm/GradeReporter.scala
 
def​ printHistogram(grades: Seq[​String​]) = {
 
val​ grouped = grades.groupBy(identity)
 
val​ counts = grouped.map((kv) => (kv._1, kv._2.size)).toSeq.sorted
 
for​(count <- counts) {
 
val​ stars = ​"*"​ * count._2
 
println(​"%s: %s"​.format(count._1, stars))
 
}
 
}

Let’s take a look at this sample line by line, starting with the first line of printHistogram’s body:

 
val​ grouped = grades.groupBy(identity)

The groupBy method takes in a function and uses it to group together all the elements of a sequence for which the function returns the same value. Here we pass in the identity function, which just returns whatever was passed in so we can group together all grades that are the same. The REPL output below shows us using this snippet to group together a vector of grades:

 
scala>​ val grades = Vector("A", "B", "A", "B", "B")
 
grades: scala.collection.immutable.Vector[java.lang.String] = Vector(A, B, A, B, B)
 
 
scala>​ val grouped = grades.groupBy(identity)
 
grouped: scala.collection.immutable.Map[...] =
 
Map(A -> Vector(A, A), B -> Vector(B, B, B))

Next we take the map of grouped grades and use map and toSeq to turn it into a sequence of tuples, where the first element is the grade and the second element is the grade count. Then we sort that sequence. By default, Scala sorts sequences of tuples by their first element, so this gives us a sorted sequence of grade counts.

 
val​ counts = grouped.map((kv) => (kv._1, kv._2.size)).toSeq.sorted

The REPL output below shows us using this code snippet to get our sequence of grade counts:

 
scala>​ val counts = grouped.map((kv) => (kv._1, kv._2.size)).toSeq.sorted
 
counts: Seq[(java.lang.String, Int)] = ArrayBuffer((A,2), (B,3))

Finally we use a for comprehension over the sequence of tuples to print up a histogram of grades, as the snippet below shows:

 
for​(count <- counts) {
 
val​ stars = ​"*"​ * count._2
 
 
println(​"%s: %s"​.format(count._1, stars))
 
}

This highlights an interesting use of Scala’s * operator. It can be used to repeat a string, as the following REPL output demonstrates:

 
scala>​ "*" * 5
 
res0: String = *****

Now we just need to use makeGradeReporter to compose our two functions together to create fullGradeReporter, as the following code does:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/tm/GradeReporter.scala
 
val​ fullGradeReporter = makeGradeReporter(fullGradeConverter, printHistogram)

Then we can define some sample data and run fullGradeReporter to print a histogram:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/tm/GradeReporter.scala
 
val​ sampleGrades = Vector(5.0, 4.0, 4.4, 2.2, 3.3, 3.5)
 
scala>​ fullGradeReporter(sampleGrades)
 
A: **
 
B: ***
 
C: *

Now if we want to change the way we do our grade conversion and report printing, we only need to create additional conversion and reporting functions. We can use makeGradeReporter to compose them together.

Let’s see how to rewrite the Template Method example that converts to plus/minus grades and prints up a full list of them. As before, we’ll need two functions. The first is plusMinusGradeConverter, for our grade conversions. The second is the printAllGrades method, which just prints a simple list of converted grades.

Here’s the code for our plusMinusGradeConverter function:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/tm/GradeReporter.scala
 
def​ plusMinusGradeConverter(grade: Double) =
 
if​(grade <= 5.0 && grade > 4.7) ​"A"
 
else​ ​if​(grade <= 4.7 && grade > 4.3) ​"A-"
 
else​ ​if​(grade <= 4.3 && grade > 4.0) ​"B+"
 
else​ ​if​(grade <= 4.0 && grade > 3.7) ​"B"
 
else​ ​if​(grade <= 3.7 && grade > 3.3) ​"B-"
 
else​ ​if​(grade <= 3.3 && grade > 3.0) ​"C+"
 
else​ ​if​(grade <= 3.0 && grade > 2.7) ​"C"
 
else​ ​if​(grade <= 2.7 && grade > 2.3) ​"C-"
 
else​ ​if​(grade <= 2.3 && grade > 0.0) ​"D"
 
else​ ​if​(grade == 0.0) ​"F"
 
else​ ​"N/A"

And here’s the code for printAllGrades:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/tm/GradeReporter.scala
 
def​ printAllGrades(grades: Seq[​String​]) =
 
for​(grade <- grades) println(​"Grade is: "​ + grade)

Now we just need to compose them together using makeGradeReporter, and we can use it to create a full grade report, as the code below shows:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/tm/GradeReporter.scala
 
val​ plusMinusGradeReporter =
 
makeGradeReporter(plusMinusGradeConverter, printAllGrades)
 
scala>​ plusMinusGradeReporter(sampleGrades)
 
Grade is: A
 
Grade is: B
 
Grade is: A-
 
Grade is: D
 
Grade is: C+
 
Grade is: B-

That wraps up our replacement for Template Method in Scala. Next up, let’s take a look at how things look in Clojure.

In Clojure

The Clojure replacement for Template Method is similar to the Scala one. Just as in Scala, we’ll use Pattern 16, Function Builder, named make-grade-reporter, to compose together a function that converts numeric grades to letter grades and a function that prints a report. The make-grade-reporter returns a function that maps num-to-letter over a sequence of numeric grades. Let’s take a look at the code for it first:

ClojureExamples/src/mbfpp/oo/tm/grade_reporter.clj
 
(​defn​ make-grade-reporter [num-to-letter print-grade-report]
 
(​fn​ [grades]
 
(print-grade-report (​map​ num-to-letter grades))))

Converting a numeric grade to a full letter grade is just a matter of a simple cond expression, as we can see below:

ClojureExamples/src/mbfpp/oo/tm/grade_reporter.clj
 
(​defn​ full-grade-converter [grade]
 
(​cond
 
(​and​ (​<​​=​ grade 5.0) (​>​ grade 4.0)) ​"A"
 
(​and​ (​<​​=​ grade 4.0) (​>​ grade 3.0)) ​"B"
 
(​and​ (​<​​=​ grade 3.0) (​>​ grade 2.0)) ​"C"
 
(​and​ (​<​​=​ grade 2.0) (​>​ grade 0)) ​"D"
 
(​=​ grade 0) ​"F"
 
:else ​"N/A"​))

Printing a histogram can be done much the way we did it in Scala, using group-by to group grades together, mapping a function over the grouped grades to get counts, and then using a sequence comprehension to print the final histogram. Here’s the code to print a histogram:

ClojureExamples/src/mbfpp/oo/tm/grade_reporter.clj
 
(​defn​ print-histogram [grades]
 
(​let​ [grouped (group-by ​identity​ grades)
 
counts (​sort​ (​map
 
(​fn​ [[grade grades]] [grade (​count​ grades)])
 
grouped))]
 
(​doseq​ [[grade ​num​] counts]
 
(​println​ (​str​ grade ​":"​ (​apply​ ​str​ (​repeat​ ​num​ ​"*"​)))))))

Now we can use make-grade-reporter to combine full-grade-converter and print-histogram into a new function, full-grade-reporter. The code to do so is below:

ClojureExamples/src/mbfpp/oo/tm/grade_reporter.clj
 
(​def​ full-grade-reporter (make-grade-reporter full-grade-converter print-histogram))

Here we’re running it on some sample data:

ClojureExamples/src/mbfpp/oo/tm/grade_reporter.clj
 
(​def​ sample-grades [5.0 4.0 4.4 2.2 3.3 3.5])
 
=> (full-grade-reporter sample-grades)
 
A:**
 
B:***
 
C:*

To change the way we convert grades and print the report, we just create new functions to compose with make-grade-reporter. Let’s create plus-minus-grade-converter and print-all-grades functions and then compose them together into a plus-minus-grade-reporter.

The plus-minus-grade-reporter function is straightforward; it’s just a simple cond expression:

ClojureExamples/src/mbfpp/oo/tm/grade_reporter.clj
 
(​defn​ plus-minus-grade-converter [grade]
 
(​cond
 
(​and​ (​<​​=​ grade 5.0) (​>​ grade 4.7)) ​"A"
 
(​and​ (​<​​=​ grade 4.7) (​>​ grade 4.3)) ​"A-"
 
(​and​ (​<​​=​ grade 4.3) (​>​ grade 4.0)) ​"B+"
 
(​and​ (​<​​=​ grade 4.0) (​>​ grade 3.7)) ​"B"
 
(​and​ (​<​​=​ grade 3.7) (​>​ grade 3.3)) ​"B-"
 
(​and​ (​<​​=​ grade 3.3) (​>​ grade 3.0)) ​"C+"
 
(​and​ (​<​​=​ grade 3.0) (​>​ grade 2.7)) ​"C"
 
(​and​ (​<​​=​ grade 2.7) (​>​ grade 2.3)) ​"C"
 
(​and​ (​<​​=​ grade 2.3) (​>​ grade 0)) ​"D"
 
(​=​ grade 0) ​"F"
 
:else ​"N/A"​))

The print-all-grades function simply uses a sequence comprehension to print each grade:

ClojureExamples/src/mbfpp/oo/tm/grade_reporter.clj
 
(​defn​ print-all-grades [grades]
 
(​doseq​ [grade grades]
 
(​println​ ​"Grade is:"​ grade)))

Now we can compose them together with make-grade-reporter and run them on our sample data to print a grade report:

ClojureExamples/src/mbfpp/oo/tm/grade_reporter.clj
 
(​def​ plus-minus-grade-reporter
 
(make-grade-reporter plus-minus-grade-converter print-all-grades))
 
=> (plus-minus-grade-reporter sample-grades)
 
Grade is: A
 
Grade is: B
 
Grade is: A-
 
Grade is: D
 
Grade is: C+
 
Grade is: B-

That’s it for our Clojure version of Template Method replacement. Let’s wrap up with some discussion on how the Template Method compares to its functional replacement.

Discussion

Our functional replacement for Template Method fulfills the same intent but operates quite differently. Instead of using subtypes to implement specific suboperations, we use functional composition and higher-order functions.

This mirrors the old object-oriented preference of composition over inheritance. Even in the object-oriented world, I prefer to use the pattern described in Pattern 11, Replacing Dependency Injection to inject suboperations into a class, rather than using Template Method and subclassing.

The biggest reason for this is that it helps to prevent code duplication. For instance, in the example we used in this chapter, if we wanted a class that printed a histogram of plus/minus grades, we would have to either create a deeper inheritance hierarchy or cut and paste code from the existing implementations. In a real system, this can get fragile very quickly.

Composition also does a better job of making an API explicit. The Template Method class may expose protected helper methods that are used by framework code but shouldn’t be used by a client. The only way to indicate this is with comments in the API documentation.

For Further Reading

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

Related Patterns

Pattern 1, Replacing Functional Interface

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
18.225.234.24