Pattern 7Replacing Strategy

Intent

To define an algorithm in abstract terms so it can be implemented in several different ways, and to allow it to be injected into clients so it can be used across several different clients

Overview

Strategy has a few parts. The first is an interface that represents some algorithm, such as a bit of validation logic or a sorting routine. The second is one or more implementations of that interface; these are the strategy classes themselves. Finally, one or more clients use the strategy objects.

For instance, we may have several different ways we want to validate a set of data input from a form on a website, and we may want to use that validation code in several places. We could create a Validator interface with a validate method to serve as our strategy object, along with several implementations that could be injected into our code at the appropriate spots.

Also Known As

Policy

Functional Replacement

Strategy is closely related to Pattern 1, Replacing Functional Interface, in that the strategy objects themselves are generally a simple functional interface, but the Strategy pattern contains more moving parts than just a Functional Interface. Still, this suggests a straightforward replacement for Strategy in the functional world.

To replace the strategy classes, we use higher-order functions that implement the needed algorithms. This avoids the need to create and apply interfaces for different strategy implementations. From there, it’s straightforward to pass our strategy functions around and use them where needed.

Sample Code: Person Validation

One common use of Strategy is to create different algorithms that can be used to validate the same set of data. Let’s take a look at an example of using Strategy to do just that.

We’ll implement two different validation strategies for a person that contain a first, middle, and last name. The first strategy will consider the person valid if he or she has a first name, the second will only consider the person valid if all three names are set. On top of that, we’ll look at some simple client code that collects valid people together.

In Java

In Java, we need a PersonValidator interface, which our two validation strategies, FirstNameValidator and FullNameValidator, will implement. The validators themselves are straightforward; they return true if they consider the person valid and false otherwise.

The validators can then be composed in the PersonCollector class, which will collect People objects that pass validation. The class diagram below outlines this solution:

images/PersonValidator.png

Figure 6. Person Validator Strategy. Using Strategy to validate a person

This works fine, but it involves spreading our logic across several classes for no particularly good reason. Let’s see how we can simplify Strategy using functional techniques.

In Scala

In Scala, there’s no need for the PersonValidator interface we saw in the Java examples. Instead, we’ll just use plain old functions to do our validation. To carry a person around, we’ll rely on a case class with attributes for each part of a person’s name. Finally, instead of using a full-on class for the person collector, we’ll use a higher-order function that itself returns another function that’s responsible for collecting people.

Let’s start with the Person case class. This is a pretty standard case class, but notice how we’re using Option[String] to represent the names instead of just String, since this case class represents a person that may have parts of the name missing:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/strategy/PeopleExample.scala
 
case​ ​class​ Person(
 
firstName: ​Option​[​String​],
 
middleName: ​Option​[​String​],
 
lastName: ​Option​[​String​])

Now let’s take a look at our first name validator, a function called isFirstNameValid. As the code below shows, we use the isDefined method on Scala’s Option, which returns true if the Option contains Some and returns false otherwise to see whether the person has a first name:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/strategy/PeopleExample.scala
 
def​ isFirstNameValid(person: Person) = person.firstName.isDefined

Our full name validator is a function, isFullNameValid. Here, we use a Scala match statement to pick apart a Person, and then we ensure that each name is there using isDefined. The code is below:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/strategy/PeopleExample.scala
 
def​ isFullNameValid(person: Person) = person ​match​ {
 
case​ Person(firstName, middleName, lastName) =>
 
firstName.isDefined && middleName.isDefined && lastName.isDefined
 
}

Finally, our person collector, a function aptly named personCollector, takes in a validation function and produces another function that’s responsible for collecting valid people. It does so by running a passed-in person through the validation function. It then appends it to an immutable vector and stores a reference to the new vector in the validPeople var if it passes validation. Finally it returns validPeople, as the code below shows:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/strategy/PeopleExample.scala
 
def​ personCollector(isValid: (Person) => ​Boolean​) = {
 
var​ validPeople = Vector[Person]()
 
(person: Person) => {
 
if​(isValid(person)) validPeople = validPeople :+ person
 
validPeople
 
}
 
}

Let’s take a look at our validators and person-collector at work, starting with creating a person-collector that considers single names valid and one that only considers full names valid:

 
scala>​ val singleNameValidCollector = personCollector(isFirstNameValid)
 
singleNameValidCollector: ...
 
 
scala>​ val fullNameValidCollector = personCollector(isFullNameValid)
 
fullNameValidCollector: ...

We can now define a few test names:

 
scala>​ val p1 = Person(Some("John"), Some("Quincy"), Some("Adams"))
 
p1: com.mblinn.mbfpp.oo.strategy.PeopleExample.Person = ...
 
 
scala>​ val p2 = Person(Some("Mike"), None, Some("Linn"))
 
p2: com.mblinn.mbfpp.oo.strategy.PeopleExample.Person = ...
 
 
scala>​ val p3 = Person(None, None, None)
 
p3: com.mblinn.mbfpp.oo.strategy.PeopleExample.Person = ...

Then we run through our two person-collectors, starting with singleNameValidCollector:

 
scala>​ singleNameValidCollector(p1)
 
res0: scala.collection.immutable.Vector[...] =
 
Vector(Person(Some(John),Some(Quincy),Some(Adams)))
 
 
scala>​ singleNameValidCollector(p2)
 
res1: scala.collection.immutable.Vector[...] =
 
Vector(
 
Person(Some(John),Some(Quincy),Some(Adams)),
 
Person(Some(Mike),None,Some(Linn)))
 
 
scala>​ singleNameValidCollector(p3)
 
res2: scala.collection.immutable.Vector[...] =
 
Vector(
 
Person(Some(John),Some(Quincy),Some(Adams)),
 
Person(Some(Mike),None,Some(Linn)))

And we’ll finish up with fullNameValidCollector:

 
scala>​ fullNameValidCollector(p1)
 
res3: scala.collection.immutable.Vector[...] =
 
Vector(Person(Some(John),Some(Quincy),Some(Adams)))
 
 
scala>​ fullNameValidCollector(p2)
 
res4: scala.collection.immutable.Vector[...] =
 
Vector(Person(Some(John),Some(Quincy),Some(Adams)))
 
 
scala>​ fullNameValidCollector(p3)
 
res5: scala.collection.immutable.Vector[...] =
 
Vector(Person(Some(John),Some(Quincy),Some(Adams)))

As we can see, the two collectors work as they should, delegating to the validation functions that were passed in when they were created.

In Clojure

In Clojure, we’ll solve our person-collecting problem in a similar way to Scala, using functions for the validators and a higher-order function that takes in a validator and produces a person-collecting function. To represent the people, we’ll use good old Clojure maps. Since Clojure is a dynamic language and doesn’t have Scala’s Option typing, we’ll use nil to represent the lack of a name.

Let’s start by looking at first-name-valid?. It checks to see if the :first-name of the person is not nil and returns true if so; otherwise it returns false.

ClojureExamples/src/mbfpp/oo/strategy/people_example.clj
 
(​defn​ first-name-valid? [person]
 
(​not​ (​nil?​ (:first-name person))))

The full-name-valid? function pulls out all three names and returns true only if they’re all not nil:

ClojureExamples/src/mbfpp/oo/strategy/people_example.clj
 
(​defn​ full-name-valid? [person]
 
(​and
 
(​not​ (​nil?​ (:first-name person)))
 
(​not​ (​nil?​ (:middle-name person)))
 
(​not​ (​nil?​ (:last-name person)))))

Finally, let’s take a look at our person-collector, which takes in a validation function and produces a collector function. This works almost exactly like the Scala version, the main difference being that we need to use an atom to store a reference to our immutable vector in an atom.

ClojureExamples/src/mbfpp/oo/strategy/people_example.clj
 
(​defn​ person-collector [valid?]
 
(​let​ [valid-people (​atom​ [])]
 
(​fn​ [person]
 
(​if​ (valid? person)
 
(​swap!​ valid-people ​conj​ person))
 
@valid-people)))

Before we wrap up, let’s see our Clojure person collection in action, starting by defining the collector functions as we do below:

 
=> (def first-name-valid-collector (person-collector first-name-valid?))
 
#'mbfpp.oo.strategy.people-example/first-name-valid-collector
 
=> (def full-name-valid-collector (person-collector full-name-valid?))
 
#'mbfpp.oo.strategy.people-example/full-name-valid-collector

Now we need some test data:

 
=> (def p1 {:first-name "john" :middle-name "quincy" :last-name "adams"})
 
#'mbfpp.oo.strategy.people-example/p1
 
=> (def p2 {:first-name "mike" :middle-name nil :last-name "adams"})
 
#'mbfpp.oo.strategy.people-example/p2
 
=> (def p3 {:first-name nil :middle-name nil :last-name nil})
 
#'mbfpp.oo.strategy.people-example/p3

And we can run it through our collectors, starting with the collector that only requires a first name for the person to be valid:

 
=> (first-name-valid-collector p1)
 
[{:middle-name "quincy", :last-name "adams", :first-name "john"}]
 
=> (first-name-valid-collector p2)
 
[{:middle-name "quincy", :last-name "adams", :first-name "john"}
 
{:middle-name nil, :last-name "adams", :first-name "mike"}]
 
=> (first-name-valid-collector p3)
 
[{:middle-name "quincy", :last-name "adams", :first-name "john"}
 
{:middle-name nil, :last-name "adams", :first-name "mike"}]

Then we finish up with the collector that requires the full name for the person to be valid:

 
=> (full-name-valid-collector p1)
 
[{:middle-name "quincy", :last-name "adams", :first-name "john"}]
 
=> (full-name-valid-collector p2)
 
[{:middle-name "quincy", :last-name "adams", :first-name "john"}]
 
=> (full-name-valid-collector p3)
 
[{:middle-name "quincy", :last-name "adams", :first-name "john"}]

Both work as expected, validating the passed-in name before storing it if valid and then returning the full set of valid names.

Discussion

Strategy and Template Method serve similar ends. Both are ways to inject some bit of custom code into a larger framework or algorithm. Strategy does so using composition, and Template Method does so using inheritance. We replaced both patterns with ones based on functional composition.

Though both Clojure and Scala have language features that allow us to build hierarchies, we’ve replaced both Template Method and Strategy with patterns based on functional composition. This leads to simpler solutions to common problems, mirroring the old object-oriented preference to favor composition over inheritance.

For Further Reading

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

Related Patterns

Pattern 1, Replacing Functional Interface

Pattern 6, Replacing Template Method

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

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