© Edward Sciore 2019
Edward ScioreJava Program Designhttps://doi.org/10.1007/978-1-4842-4143-1_4

4. Strategies

Edward Sciore1 
(1)
Newton, MA, USA
 

Class hierarchies are a fundamental feature of object-oriented programming languages. Chapter 3 examined their capabilities. This chapter examines their (often significant) limitations and introduces the more flexible concept of a strategy hierarchy. Strategy hierarchies are a central component of several design techniques. This chapter examines two such techniques—the strategy pattern and the command pattern—and their uses.

The Strategy Pattern

Let’s begin by reviewing the template pattern from Chapter 3. In it, an abstract class (known as the template) provides a skeletal implementation of each public method, and relies on nonpublic abstract methods to provide the implementation-specific details. These abstract methods get implemented by the template subclasses. Each subclass is called a strategy class because it provides a particular strategy for implementing the template’s abstract methods.

Listing 4-1 gives a simple example. The class IntProcessor is the template class. It has an abstract method f that computes an output value from a given integer. The method operateOn passes an integer to f and prints its output value. There are two strategy subclasses, AddOne and AddTwo, that provide different implementations of f. The class TestClient demonstrates the use of these classes. It creates an instance of each subclass and calls the operateOn method of each instance.
public abstract class IntProcessor {
   public void operateOn(int x) {
      int y = f(x);
      System.out.println(x + " becomes " + y);
   }
   protected abstract int f(int x);
}
public class AddOne extends IntProcessor {
   protected int f(int x) {
      return x+1;
   }
}
public class AddTwo extends IntProcessor {
   protected int f(int x) {
      return x+2;
   }
}
public class TestClient {
   public static void main(String[] args) {
      IntProcessor p1 = new AddOne();
      IntProcessor p2 = new AddTwo();
      p1.operateOn(6); // prints "6 becomes 7"      p2.operateOn(6); // prints "6 becomes 8"
   }
}
Listing 4-1

Example Template Pattern Classes

Another way to design this program is to not use subclassing. Instead of implementing the strategy classes as subclasses of IntProcessor, you can give them their own hierarchy, called a strategy hierarchy. The hierarchy’s interface is named Operation, and has the method f. The IntProcessor class, which no longer has any subclasses or abstract methods, holds a reference to an Operation object and uses that reference when it needs to call f. The revised code appears in Listing 4-2. The TestClient class creates the desired Operation objects and passes each to IntProcessor via dependency injection.
public class IntProcessor {
   private Operation op;
   public IntProcessor(Operation op) {
      this.op = op;
   }
   public void operateOn(int x) {
      int y = f(x);
      System.out.println(x + " becomes " + y);
   }
   private int f(int x) {
      return op.f(x);
   }
}
interface Operation {
   public int f(int x);
}
class AddOne implements Operation {
   public int f(int x) {
      return x+1;
   }
}
class AddTwo implements Operation {
   public int f(int x) {
      return x+2;
   }
}
public class TestClient {
   public static void main(String[] args) {
      Operation op1 = new AddOne();
      Operation op2 = new AddTwo();
      IntProcessor p1 = new IntProcessor(op1);
      IntProcessor p2 = new IntProcessor(op2);
      p1.operateOn(6); p2.operateOn(6);
   }
}
Listing 4-2

Refactoring Listing 4-1 to Use a Strategy Hierarchy

If you compare the two listings you will see that they are refactorings of each other, with nearly identical code. The primary difference is how the strategy classes are attached to the IntProcessor class. Figure 4-1 shows the corresponding class diagrams for the two different designs.
../images/470600_1_En_4_Chapter/470600_1_En_4_Fig1_HTML.jpg
Figure 4-1

Class diagrams for Listings 4-1 and 4-2

The technique of organizing strategy classes into a hierarchy is called the strategy pattern. The strategy pattern is depicted by the class diagram of Figure 4-2. The strategy interface defines a set of methods. Each class that implements the interface provides a different strategy for performing those methods. The client has a variable that holds an object from one of the strategy classes. Because the variable is of type StrategyInterface, the client has no idea which class the object belongs to and consequently does not know which strategy is being used.
../images/470600_1_En_4_Chapter/470600_1_En_4_Fig2_HTML.jpg
Figure 4-2

The strategy pattern

The class diagram on the right side of Figure 4-1 corresponds to the strategy pattern. IntProcessor is the client, Operation is the strategy interface, and AddOne and AddTwo are the strategy classes.

The Java Thread class provides a real-life example of the duality between the template and strategy patterns. Recall the ThreadTest program from Listing 3-25. The class Thread is the template class, whose public method start calls the abstract method run. Its subclass ReadLine is the strategy class that implements run. Figure 4-3 depicts the relationship between Thread and ReadLine.
../images/470600_1_En_4_Chapter/470600_1_En_4_Fig3_HTML.jpg
Figure 4-3

Using the template pattern to connect Thread to its strategy class

In the corresponding design that uses the strategy pattern, ReadLine will belong to a strategy hierarchy, which will be a dependency of Thread. The strategy interface is called Runnable and has the method run. A Thread object holds a reference to a Runnable object, and its start method will call the Runnable object’s run method. See Figure 4-4.
../images/470600_1_En_4_Chapter/470600_1_En_4_Fig4_HTML.jpg
Figure 4-4

Using the strategy pattern to connect Thread to its strategy class

Compare Figure 4-3 with Figure 4-4. Figure 4-3 requires that ReadLine extend Thread, whereas Figure 4-4 requires that ReadLine implement Runnable. Syntactically, this difference is minor. In fact, revising the code for ReadLine does not involve any change to its code apart from its class header. The revised class appears in Listing 4-3, with differences from Listing 3-24 in bold.
public class ReadLine implements Runnable {
   private boolean done = false;
   public void run() {
      Scanner sc = new Scanner(System.in);
      String s = sc.nextLine();
      sc.close();
      done = true;
   }
   public boolean isDone() {
      return done;
   }
}
Listing 4-3

The Revised ReadLine Class

A Thread object obtains its Runnable object via dependency injection. That is, the client passes the desired Runnable object into the Thread constructor. Listing 4-4 gives code for the client program RunnableThreadTest, which revises the ThreadTest class of Listing 3-25 to use the strategy pattern. Differences are in bold.
public class RunnableThreadTest {
   public static void main(String[] args) {
      ReadLine r = new ReadLine();
      Thread t = new Thread(r);
      t.start();
      int i = 0;
      while(!r.isDone()) {
         System.out.println(i);
         i++;
      }
   }
}
Listing 4-4

The RunnableThreadTest Class

Although ThreadTest and RunnableThreadTest have practically the same code, their designs are conceptually very different. In ThreadTest, the class ReadLine is a subclass of Thread, which means that ReadLine IS-A Thread. In RunnableThreadTest, the ReadLine object is unrelated to Thread and is merely passed into the constructor of a new Thread object.

Current wisdom holds that creating threads using the strategy pattern produces better designs than creating threads using the template pattern. The primary reason is that the strategy pattern creates two objects—in this case, the runnable and the thread—which keeps their concerns separate. In contrast, the template pattern combines both concerns into a single object. A second reason is that the strategy pattern is more flexible, in that a runnable object is able to extend another class. For example, suppose for some reason that you want each SavingsAccount object to run in its own thread. The template pattern approach would not be possible here because Java does not allow SavingsAccount to extend both Thread and AbstractBankAccount.

You may have noticed that the Thread class is depicted differently in Figures 4-3 and 4-4. In Figure 4-3 it is an abstract class, with run as its abstract method. In Figure 4-4 it is a non-abstract class, whose run method calls the run method of its strategy class.

The Thread class was designed so that it can be used either way. Listing 4-5 shows the basic idea. The key issue is how to implement the run method. There are two potential run methods: the method defined in Thread, and the method defined in a subclass of Thread. If the strategy pattern is used (as in Listing 4-4) then the run method defined in Thread is executed, which calls the run method of the Runnable object passed to it. If the template pattern is used (as in Listing 3-25) then the run method defined in the subclass overrides the method defined in Thread, and is executed.
public class Thread {
   private Runnable target;
   public Thread() {
      this(null); // if no Runnable is specified, use null
   }              
   public Thread(Runnable r) {
      target = r;
   }
   public void start() {
      ...         // allocate a new thread
      run();      // and run it
   }
   // This method can be overridden by a subclass.
   public void run() {
      if (target != null)
         target.run();
   }
}
Listing 4-5

A Simplified Implementation of Thread

You might be perplexed by why null is used as a possible value for the variable target, especially since it complicates the code. The reason stems from the need to handle the following statements:
   Thread t1 = new Thread();
   t1.start();
   Runnable r = null;
   Thread t2 = new Thread(r);
   t2.start();

These statements execute two threads, neither of which has a run method. Although the code is pointless, it is legal, and the Thread class must handle it. The solution taken in Listing 4-5 is to store a null value as the target Runnable object in these cases. The run method can then check to see if the target is null; if so, it does nothing.

Comparators

Recall from Chapter 2 how the Comparable interface makes it possible to compare objects. This interface has one method, named compareTo, which specifies an ordering on the objects. If a class implements Comparable then its objects can be compared by calling compareTo. The order defined by CompareTo is called the object’s natural order.

The problem is that Comparable hardcodes a specific ordering, which makes it essentially impossible to compare objects in any other way. For example, the AbstractBankAccount class implements Comparable<BankAccount>, and its compareTo method (given in Listing 3-4) compares bank accounts by their balance from low to high. It does not allow you to compare accounts by account number or by balance from high to low.

How can you specify different comparison orders? Use the strategy pattern! The strategy interface declares the comparison method, and the strategy classes provide specific implementations of that method.

Because object comparison is so common, the Java library provides this strategy interface for you. The interface is called Comparator and the method it declares is called compare. The compare method is similar to compareTo except that it takes two objects as parameters. The call compare(x,y) returns a value greater than 0 if x>y, a value less than 0 if x<y, and 0 if x=y.

The code for the example comparator class AcctByMinBal appears in Listing 4-6. Its compare method compares two BankAccount arguments, using essentially the same code as the compareTo method of Listing 3-4. The primary difference is syntactic: the compare method has two arguments, whereas compareTo has one argument. The other difference is that Listing 4-6 subtracts the account balances in the opposite order from Listing 3-4, meaning that it compares balances from high to low. That is, the account having the smallest balance will be the “maximum.”
class AcctByMinBal implements Comparator<BankAccount> {
   public int compare(BankAccount ba1, BankAccount ba2) {
      int bal1 = ba1.getBalance();
      int bal2 = ba2.getBalance();
      if (bal1 == bal2)
         return ba1.getAcctNum() – ba2.getAcctNum();
      else
         return bal2 – bal1;
   }
}
Listing 4-6

The AcctByMinBal Class

Listing 4-7 gives code for the program ComparatorBankAccounts, which revises the CompareBankAccounts class of Listing 2-9. Unlike CompareBankAccounts, which found the maximum bank account using the natural ordering, ComparatorBankAccounts finds the maximum element according to four specified orderings. Each ordering is represented by a different comparator object. Two of the comparators are passed to the local method findMax. The other two are passed to the Java library method Collections.max.
public class ComparatorBankAccounts {
   public static void main(String[] args) {
      List<BankAccount> accts = initAccts();
      Comparator<BankAccount> minbal = new AcctByMinBal();
      Comparator<BankAccount> maxbal = innerClassComp();
      Comparator<BankAccount> minnum = lambdaExpComp1();
      Comparator<BankAccount> maxnum = lambdaExpComp2();
      BankAccount a1 = findMax(accts, minbal);
      BankAccount a2 = findMax(accts, maxbal);
      BankAccount a3 = Collections.max(accts, minnum);
      BankAccount a4 = Collections.max(accts, maxnum);
      System.out.println("Acct with smallest bal is " + a1);
      System.out.println("Acct with largest bal is "  + a2);
      System.out.println("Acct with smallest num is " + a3);
      System.out.println("Acct with largest num is "  + a4);
   }
   private static BankAccount findMax(List<BankAccount> a,
                              Comparator<BankAccount> cmp) {
      BankAccount max = a.get(0);
      for (int i=1; i<a.size(); i++) {
         if (cmp.compare(a.get(i),max) > 0)
            max = a.get(i);
      }
      return max;
   }
   ... // code for the three comparator methods goes here
}
Listing 4-7

The ComparatorBankAccounts Class

The findMax method of Listing 4-7 revises the corresponding method of Listing 2-9. It now takes two parameters: a list of bank accounts and a comparator. It returns the largest account, where “largest” is determined by the comparator.

The Collections.max method, like other library methods that involve comparison, is able to handle both the Comparable and Comparator interfaces. If you call Collections.max with one argument (as in Listing 2-9) then it will compare elements according to their natural order. On the other hand, if you call Collections.max with a comparator as its second argument (as in Listing 4-7) then the elements will be compared according to the order specified by the comparator.

The main method of Listing 4-7 creates four objects of type Comparable<BankAccount>. The first object is an instance of the AcctByMinBal class of Listing 4-6. The other three objects are created by the methods innerClassComp, lambdaExpComp1, and lambdaExpComp2; the code for these methods will appear in listings 4-8 to 4-10. Each of these methods creates an object from an anonymous inner class; anonymous inner classes are discussed in the next section.

The class diagram for the ComparatorBankAccounts program appears in Figure 4-5. Note how it follows the strategy pattern.
../images/470600_1_En_4_Chapter/470600_1_En_4_Fig5_HTML.jpg
Figure 4-5

The Class Diagram for ComparatorBankAccounts

Anonymous Inner Classes

The rule of Abstraction (from Chapter 2) asserts that the type of a variable should be an interface when possible. In such a case the name of the class that implements the interface is relatively unimportant, as it will only be used when the class constructor is called. This section examines how to create unnamed classes, called anonymous inner classes, and the convenience that they provide.

Explicit Anonymous Classes

An anonymous inner class defines a class without giving it a name. Suppose that T is an interface. The general syntax is:
   T v = new T() { ... };
This statement causes the compiler to do three things:
  • It creates a new class that implements T and has the code appearing within the braces.

  • It creates a new object of that class by calling the class’s default constructor.

  • It saves a reference to that object in variable v.

Note that the client will never need to know the class of the new object, because it interacts with the object only via the variable of type T.

The code for the method innerClassComp in ComparatorBankAccounts appears in Listing 4-8. The bold code highlights the anonymous inner class syntax. The code within the braces implements the compare method, which in this case happens to be the same as the compareTo method of Listing 3-4. This class is named AnonymousA in the class diagram of Figure 4-5, but of course we don’t know (or care) what its name really is.
private static Comparator<BankAccount> innerClassComp() {
   Comparator<BankAccount> result =
      new Comparator<BankAccount>() {
         public int compare(BankAccount ba1,
                            BankAccount ba2) {
            int bal1 = ba1.getBalance();
            int bal2 = ba2.getBalance();
            if (bal1 == bal2)
              return ba1.getAcctNum() - ba2.getAcctNum();
            else
              return bal1 - bal2;
        }
      };
   return result;
}
Listing 4-8

The innerClassComp Method

Lambda Expressions

An anonymous inner class provides a convenient way to define a class and create a single instance of it, as both the class and its instance can be created inline. This section shows how it is often possible to shorten the definitions of anonymous inner classes, making them even more convenient.

An interface is said to be functional if it has only one method, not counting any default or static methods. The interface Comparator<T> is an example of a functional interface. An anonymous inner class for a functional interface can be written very compactly. Since there is only one method to define, its name and return type are determined by the interface, so you don’t need to write them; you only need to write the code for the method. This notation is called a lambda expression . Its syntax is:
   (T1 t1, ..., Tn tn) -> {...}
The method’s parameter list is to the left of the “arrow” and its code is to its right, within braces. The method lambdaExpComp1 in ComparatorBankAccounts uses this syntax; see the bold portion of Listing 4-9. Its compare method compares accounts by their account numbers, from high to low.
private static Comparator<BankAccount> lambdaExpComp1() {
   Comparator<BankAccount> result =
     (BankAccount ba1, BankAccount ba2) -> {
         return ba2.getAcctNum() - ba1.getAcctNum();
     };
   return result;
}
Listing 4-9

The lambdaExpComp1 Method

Although lambda expressions can be written reasonably compactly, Java lets you abbreviate them even further.
  • You don’t have to specify the types of the parameters.

  • If there is only one parameter then you can omit the parentheses around it.

  • If the body of the method consists of a single statement then you can omit the braces; if a single-statement method also returns something then you also omit the “return” keyword.

The method lambdaExpComp2 in ComparatorBankAccounts uses this syntax; see the bold portion of Listing 4-10. The compare method compares accounts by their account numbers, from low to high.
private static Comparator<BankAccount> lambdaExpComp2() {
   Comparator<BankAccount> result =
      (ba1, ba2) -> ba1.getAcctNum() - ba2.getAcctNum();
   return result;
}
Listing 4-10

The lambdaExpComp2 Method

For another example of a lambda expression, consider again the implementation of the Thread class in Listing 4-5. Its variable target held the specified runnable object, with a null value denoting a nonexistent runnable. The run method had to use an if-statement to ensure that it only executed nonnull runnables.

The use of the null value to mean “do nothing” is poor design, as it forces the run method to make the decision to “do something” or “do nothing” each time it is executed. A better idea is to have the class make the decision once, in its constructor. The solution is to use a lambda expression. The code of Listing 4-11 revises Listing 4-5.
public class Thread {
   private static Runnable DO_NOTHING = () -> {};
   private Runnable target;
   public Thread() {
      this(DO_NOTHING); // use the default runnable
   }
   public Thread(Runnable r) {
      target = (r == null) ? DO_NOTHING : r;
   }
   public void start() {
      ...         // allocate a new thread
      run();
   }
   // This method can be overridden by a subclass.
   public void run() {
         target.run(); // no need to check for null!
   }
}
Listing 4-11

A revised Implementation of Thread

The class creates a Runnable object via the lambda expression ()->{}. This lambda expression defines a run method that takes no arguments and does nothing. This Runnable object is saved in the constant DO_NOTHING. If no Runnable object is passed into the Thread constructor then the variable target will receive a reference to DO_NOTHING instead of a null value. Since this object is runnable, the run method can execute it without the need for an if-statement.

The Strategy Pattern as a Design Tool

Let’s return to the design of the banking demo. Chapter 3 introduced version 9 of the demo, which supported three kinds of bank account organized into the class hierarchy of Figure 4-6.
../images/470600_1_En_4_Chapter/470600_1_En_4_Fig6_HTML.jpg
Figure 4-6

The version 9 BankAccount hierarchy

Suppose that the bank wants to add another feature to the design. It already distinguishes between domestic accounts and foreign accounts; it now wants to charge a yearly maintenance fee of $5 for foreign-owned accounts. The BankAccount interface will get a new method named fee, which returns the fee for that account.

A simple way to implement the fee method is from within the class AbstractBankAccount, as shown in Listing 4-12. Although this code is straightforward, its use of the if-statement is bad design. The method will need to be modified each time the bank changes the fee categories—which is a blatant violation of the Open/Closed rule.
public int fee() {
   if (isforeign)
      return 500;   // $5 is 500 cents
   else
      return 0;
}
Listing 4-12

A Naïve Implementation of the Fee method in AbstractBankAccount

A better idea is to use the strategy pattern. The ownership information will be moved to its own strategy hierarchy, whose interface is called OwnerStrategy and whose two strategy classes correspond to the two different fee categories. The AbstractBankAccount class will have a dependency on OwnerStrategy and will obtain all owner-related information from it. This design is version 10 of the banking demo. The relevant portion of its class diagram is in Figure 4-7, with changes from Figure 3-5 in bold.
../images/470600_1_En_4_Chapter/470600_1_En_4_Fig7_HTML.jpg
Figure 4-7

The version 10 bank account classes

This diagram shows the fee method added to the BankAccount interface. The class AbstractBankAccount implements the method by calling the fee method of its OwnerStrategy object. The OwnerStrategy classes also implement the additional method isForeign.

The code for the OwnerStrategy interface appears in Listing 4-13. Listing 4-14 gives the code for the Foreign class; the Domestic class is similar.
public interface OwnerStrategy {
   boolean isForeign();
   int fee();
}
Listing 4-13

The OwnerStrategy Interface

public class Foreign implements OwnerStrategy {
   public boolean isForeign() {
      return true;
   }
   public int fee() {
      return 500;  // $5 is 500 cents
   }
   public String toString() {
      return "foreign";
   }
}
Listing 4-14

The Foreign Class

The code for the version 10 AbstractBankAccount class appears in Listing 4-15, with changes in bold. Its boolean variable isforeign has been replaced by the strategy variable owner. Its isForeign and fee methods call the isForeign and fee strategy methods of owner. Its toString method calls the toString method of the strategy object to obtain the string indicating that the account is “domestic” or “foreign.” Initially, the owner variable is bound to a Domestic strategy object. The setForeign method rebinds that variable to the OwnerStrategy object determined by its argument value.
public abstract class AbstractBankAccount
                      implements BankAccount {
   protected int acctnum;
   protected int balance = 0;
   private OwnerStrategy owner = new Domestic();
   protected AbstractBankAccount(int acctnum) {
      this.acctnum = acctnum;
   }
   public boolean isForeign() {
      return owner.isForeign();
   }
   public int fee() {
      return owner.fee();
   }
   public void setForeign(boolean b) {
      owner = b ? new Foreign() : new Domestic();
   }
   public String toString() {
      String accttype = accountType();
         return accttype + " account " + acctnum
             + ": balance=" + balance + ", is "
             + owner.toString() + ", fee=" + fee();
   }
   ...
}
Listing 4-15

The Version 10 AbstractBankAccount Class

The Command Pattern

The OwnerStrategy strategy hierarchy arose from the problem of how to implement multiple ways to calculate the fee of a bank account. The initial solution, given in Listing 4-12, used an if-statement to determine which calculation to perform. This use of an if-statement was problematic: not only was it inefficient but it would need to be modified each time a new type of fee was added. Replacing the if-statement with a strategy hierarchy elegantly resolved both issues.

A similar situation exists in the BankClient class. It assigns a number to eight different input commands and its processCommand method uses an if-statement to determine which code to execute for a given command number. The code for the method appears in Listing 4-16.
private void processCommand(int cnum) {
   if      (cnum == 0) quit();
   else if (cnum == 1) newAccount();
   else if (cnum == 2) select();
   else if (cnum == 3) deposit();
   else if (cnum == 4) authorizeLoan();
   else if (cnum == 5) showAll();
   else if (cnum == 6) addInterest();
   else if (cnum == 7) setForeign();
   else
      System.out.println("illegal command");
}
Listing 4-16

The Version 9 processCommand Method

A better design for this method is to create a strategy interface InputCommand and an implementing strategy class for each command type. BankClient can then hold a polymorphic array of type InputCommand, containing one object from each strategy class. The command number passed to processCommand becomes an index into that array. The revised processCommand method appears in Listing 4-17. Note how the indexed array access replaces the if-statement.
private void processCommand(int cnum) {
   InputCommand cmd = commands[cnum];
   current = cmd.execute(scanner, bank, current);
   if (current < 0)
      done = true;
}
Listing 4-17

The Version 10 processCommand Method

The strategy interface InputCommand has eight implementing classes—one class for each type of command. These classes are named QuitCmd, NewCmd, DepositCmd, and so on. Figure 4-8 shows their class diagram.
../images/470600_1_En_4_Chapter/470600_1_En_4_Fig8_HTML.jpg
Figure 4-8

The InputCommand Strategy Hierarchy

The strategy method declared by InputCommand is named execute. The execute method for each strategy class contains the code required to perform its designated command. The code for these strategy classes is taken from the methods referenced in Listing 4-16. For example, the execute method of DepositCmd contains the same code as the version 9 deposit method.

One complicating issue is that the version 9 methods are able to modify the global variables of BankClient; in particular, the version 9 newAccount and select commands change the value of the variable current, and the quit command changes the value of done. However, the strategy classes have no such access to the BankClient variables. The solution taken in version 10 is for the execute method to return the new value of current (or the old value, if it did not change). A value of -1 indicates that done should be set to true. The code of Listing 4-17 reflects this decision: the return value of execute is assigned to current, and if current is negative, the value of done gets set to true.

The code for the InputCommand interface appears in Listing 4-18. The code for DepositCmd appears in Listing 4-19. The code for the other strategy classes is analogous and are omitted here.
public interface InputCommand {
   int execute(Scanner sc, Bank bank, int current);
}
Listing 4-18

The Version 10 InputCommand Interface

public class DepositCmd implements InputCommand {
   public int execute(Scanner sc, Bank bank, int current) {
      System.out.print("Enter deposit amt: ");
      int amt = sc.nextInt();
      bank.deposit(current, amt);
      return current;
   }
   public String toString() {
      return "deposit";
   }
}
Listing 4-19

The Version 10 DepositCmd Class

The use of command objects also solves another problem of the version 9 BankClient class, which is related to its run method. The code in question is the string beginning “Enter command...” in Listing 4-20. This string explicitly assigns numbers to commands and must be kept in synch with the processCommand method. If new commands are added to processCommand or if the numbers assigned to existing commands change, then this string will need to be rewritten.
public void run() {
   while (!done) {
      System.out.print("Enter command (0=quit, 1=new,
                        2=select, 3=deposit, 4=loan,
                        5=show, 6=interest, 7=setforeign): ");
      int cnum = scanner.nextInt();
      processCommand(cnum);
   }
}
Listing 4-20

The Version 9 BankClient Run Method

The version 10 BankClient class has a better design, which appears in Listing 4-21. It takes advantage of the fact that the commands array contains an object for each command. When the run method is called, it calls the method constructMessage to traverse that array and construct the “Enter command...” string. Consequently, that string will always be accurate no matter how the commands change.
public class BankClient {
   private Scanner scanner;
   private boolean done = false;
   private Bank bank;
   private int current = 0;
   private InputCommand[] commands = {
         new QuitCmd(),
         new NewCmd(),
         new SelectCmd(),
         new DepositCmd(),
         new LoanCmd(),
         new ShowCmd(),
         new InterestCmd(),
         new SetForeignCmd() };
   public BankClient(Scanner scanner, Bank bank) {
      this.scanner = scanner;
      this.bank = bank;
   }
   public void run() {
      String usermessage = constructMessage();
      while (!done) {
         System.out.print(usermessage);
         int cnum = scanner.nextInt();
         processCommand(cnum);
      }
   }
   private String constructMessage() {
      int last = commands.length-1;
      String result = "Enter Account Type (";
      for (int i=0; i<last; i++)
         result += i + "=" + commands[i] + ", ";
      result += last + "=" + commands[last] + "): ";
      return result;
   }
   private void processCommand(int cnum) {
      InputCommand cmd = commands[cnum];
      current = cmd.execute(scanner, bank, current);
      if (current < 0)
         done = true;
   }
}
Listing 4-21

The Version 10 BankClient Class

The method constructMessage creates the user message. In doing so, it appends each InputCommand object to the string. Java interprets this as implicitly appending the result of the object’s toString method. That is, the following statements are equivalent:
   result += i + "=" + commands[i] + ", ";
   result += i + "=" + commands[i].toString() + ", ";

The use of a strategy hierarchy shown in Figure 4-8 is called the command pattern. The structure of the command pattern is the same as for the strategy pattern. For example, in Figure 4-8, BankClient is the client and depends on the strategy hierarchy headed by InputCommand. The only difference between the two patterns is the purpose of their strategies. In the strategy pattern the strategies are computational—they provide alternative ways to compute a value. In the command pattern the strategies are procedural—they provide alternative tasks that can be performed.

Eliminating the Class Hierarchy

The duality between the template pattern and the strategy pattern implies that any design using the template pattern can be redesigned to use the strategy pattern. This section shows how to redesign the banking demo so that its BankAccount class hierarchy is replaced by a strategy hierarchy. This redesign is version 11 of the banking demo.

The idea of the redesign is to implement SavingsAccount, RegularChecking, and InterestChecking as strategy classes, headed by a strategy interface named TypeStrategy. The interface declares the three methods collateralRatio, accountType, and interestRate. Consequently, AbstractBankAccount will no longer need subclasses. Instead, it will implement these three methods via its reference to a TypeStrategy object.

Figure 4-9 shows the version 11 class diagram. In this design, AbstractBankAccount has two strategy hierarchies. The OwnerStrategy hierarchy is the same as in version 10. The TypeStrategy hierarchy contains the code for the methods of AbstractBankAccount that were previously abstract.
../images/470600_1_En_4_Chapter/470600_1_En_4_Fig9_HTML.jpg
Figure 4-9

Version 11 of the Banking Demo

The TypeStrategy interface appears in Listing 4-22.
public interface TypeStrategy {
   double collateralRatio();
   String accountType();
   double interestRate();
}
Listing 4-22

The TypeStrategy Interface

The classes SavingsAccount, RegularChecking, and InterestChecking implement TypeStrategy. These classes are essentially unchanged from version 10; the primary difference is that they now implement TypeStrategy instead of extending AbstractBankAccount. Listing 4-23 gives the code for SavingsAccount; the code for the other two classes is similar.
public class SavingsAccount implements TypeStrategy {
   public double collateralRatio() {
      return 1.0 / 2.0;
   }
   public String accountType() {
      return "Savings";
   }
   public double interestRate() {
      return 0.01;
   }
}
Listing 4-23

The Version 11 SavingsAccount Class

In version 10, the newAccount method of class Bank used the type number entered by the user to determine the subclass of the new account. The newAccount method in version 11 uses the type number to determine the TypeStrategy of the new account. It then passes the TypeStrategy object to the AbstractBankAccount constructor, as shown in Listing 4-24. Analogous changes are needed to the class SavedBankInfo, but are not shown here.
public int newAccount(int type, boolean isforeign) {
   int acctnum = nextacct++;
   TypeStrategy ts;
   if (type==1)
      ts = new SavingsAccount();
   else if (type==2)
      ts = new RegularChecking();
   else
      ts = new InterestChecking();
   BankAccount ba = new AbstractBankAccount(acctnum, ts);
   ba.setForeign(isforeign);
   accounts.put(acctnum, ba);
   return acctnum;
}
Listing 4-24

The Version 11 newAccount Method of Bank

The code for AbstractBankAccount appears in Listing 4-25, with changes in bold. The primary difference from version 10 is that the class is no longer abstract and implements the previously abstract methods collateralRatio, accountType, and interestRate. The code of these methods simply calls the corresponding methods of the TypeStrategy variable ts.
public class AbstractBankAccount implements BankAccount {
   private int acctnum;
   private int balance = 0;
   private OwnerStrategy owner = new Domestic();
   private TypeStrategy ts;
   public AbstractBankAccount(int acctnum, TypeStrategy ts) {
      this.acctnum = acctnum;
      this.ts = ts;
   }
   ...
   private double collateralRatio() {
      return ts.collateralRatio();
   }
   private String accountType() {
      return ts.accountType();
   }
   private double interestRate() {
      return ts.interestRate();
   }
}
Listing 4-25

The AbstractBankAccount Class

Templates vs. Strategies

The template pattern and the strategy pattern use different mechanisms to accomplish similar goals—the template pattern uses the class hierarchy, whereas the strategy pattern uses a separate strategy hierarchy. Can we derive any insight into when one technique is preferable over the other?

In the template pattern the class hierarchy forms a structure that organizes the different strategy classes. A class hierarchy takes a general concept (such as “bank account”) and divides it into increasingly narrower concepts (such as “savings account,” “regular checking,” and “interest checking”). Such an organization is known as a taxonomy.

A taxonomy is a useful organizational concept. For example, this book’s table of contents is a taxonomy of its information. One characteristic of a taxonomy is that membership in a category is permanent. For example in the version 10 banking demo, a savings account cannot become a checking account. The only way to “convert” a savings account into a checking account is to create a new checking account, transfer the savings account’s balance to it, and then delete the savings account. But this conversion is not exact—in particular, the checking account will have a different account number from the savings account.

Another characteristic of a taxonomy is that it can only represent a hierarchical relationship between its members. For example, the banking demo organizes accounts according to “savings” vs. “checking.” That organization cannot handle the added distinction of “foreign” vs. “domestic.”

On the other hand, the strategy pattern is much more fluid. Each strategy hierarchy corresponds to a completely independent way of organizing the objects. Moreover, the strategy pattern allows an object to change its choice of strategies. For example, the setForeign method in BankAccount changes that object’s membership in the OwnerStrategy hierarchy.

Version 11 of the banking demo demonstrated how strategies can even subsume the functionality of subclasses. In that version, every bank account belongs to the same class, namely AbstractBankAccount. The concept of “checking account” or “savings account” is no longer embedded in a class hierarchy. Instead, a savings account is merely a bank account that has a specific implementation of the TypeStrategy methods (namely, savings accounts pay interest, have a low collateral ratio, and have the name “Savings”). Similarly, the two kinds of checking accounts are just bank accounts with their own TypeStrategy implementations. Such a design is tremendously flexible. It is possible to create various combinations of checking-savings accounts simply by mixing and matching their strategy implementations.

Is this a good idea? Not necessarily. A class hierarchy provides a structure that helps tame the unbridled complexity that strategies enable. The decision of how to mix strategy hierarchies and template subclasses requires careful consideration, and will depend on the situation being modeled. For example, my sense is that version 10 of the banking demo is a better design. The division into checking and savings accounts seems reasonable and corresponds to how banks operate. Version 11, by giving up the hierarchy, seems to be less realistic and less easily understood.

In this book I take the position that the version 11 demo is interesting and educational, but ultimately a dead end. The revisions of Chapter 5 will be based on version 10.

Summary

In the template pattern, each subclass of the template defines a different strategy for implementing the template’s abstract methods, and is called a strategy class. This chapter investigated the technique of organizing these strategy classes into their own strategy hierarchy. When the strategy classes perform computation this technique is called the strategy pattern; when they denote tasks it is called the command pattern.

These two design patterns model situations where a class can have multiple ways to perform a computation or a task. A common example is object comparison. The Java library has the interface Comparator for precisely this purpose. A client can implement customized comparison code by writing an appropriate class that implements Comparator.

Strategy classes are often written as anonymous inner classes. If the strategy interface is functional then a strategy class for it can be written compactly and elegantly as a lambda expression.

The strategy pattern is more flexible than the template pattern, and this flexibility can lead to better designs. An example is the problem of how to calculate fees based on account ownership. Since the class hierarchy is organized by account type, the fee calculations do not neatly fit into the existing class structure. Instead, the creation of a separate OwnerStrategy hierarchy was easy and elegant, and did not impact the existing class hierarchy.

The strategy pattern can in fact be used to eliminate class hierarchies altogether, but this is not necessarily a good idea. As a class designer, you need to understand your options. It is then up to you to weigh their tradeoffs for a given situation.

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

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