Pattern 6 | Replacing Template Method |
To specify the outline of an algorithm, letting callers plug in some of the specifics
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.
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.
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.
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:
To get template implementations with different behaviors, the user of the Template class creates different subclasses with different implementations of numToLetter and printGradeReport.
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.
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.
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.
Design Patterns: Elements of Reusable Object-Oriented Software [GHJV95]–Template Method
Pattern 1, Replacing Functional Interface
Pattern 7, Replacing Strategy
Pattern 16, Function Builder
18.225.234.24