Patterns 101: The Strategy Pattern

By Joe Hubert
Page 3 of 4

Extending the Application

Now the restaurant is doing well, and my friend has decided to add a couple of new positions:

  • A host/hostess
  • A bartender

The host/hostess must include a name, phone number, and one reference (the number of years of experience is not required). The bartender must include a name, phone number, the number of years of experience, two references, and must be of legal drinking age.

In order to support the new functionality, I have to:

  • Add a field to the JobApplicantForm class to indicate whether or not each applicant is of legal drinking age
  • Expand the switch statement and write the routines to support the validation rules for the new positions.

Instead of making the changes as described above, I decide that I'm going to try to take a more object-oriented approach. This could be one approach:

  • Modify the class, JobApplicantForm, so that it still contains all the form properties but the validate() method is abstract (making this class abstract)
  • Create a class for each job type that extends the JobApplicantForm base class and overrides the validate() method

In other words, based on the job position selected in the form, I would create a specific subclass of JobApplicantForm (such as ManagerApplicantForm), and call validate() on the subclass. The validate() method within ManagerApplicantForm would contain validation logic specific to the requirements rules for the manager position.

I know that as this application evolves, I may include more common methods in the base class. It's conceivable that my subclasses may contain the validate() method and not much more. Something doesn't seem right about this approach.

Incoporating Design Pattern Prinicples and Concepts

Two statements come to mind from the Gang of Four book, Design Patterns: Elements of Reusable Object Oriented Software:

  • Consider what should be variable in your design.
  • Favor object composition over class inheritance.

The first bullet was well-paraphrased by Alan Shalloway in his book, Design Patterns Explained, as "Find what varies and encapsulate it." In our case, it is the validation rule that varies. So I will encapsulate it by creating a class or interface whose sole purpose is to provide the validation. These validation classes won't extend the JobApplicantForm class at all. And that leads us to the second bullet...

"Favor object composition over class inheritance."

I was very confused the first time I read this principle bullet, especially in a text based on object oriented design. I mean, what's so cool and object oriented about aggregation? Abstraction and polymorphism don't really come into play when objects are contained in other objects. The instruction that this principle means to convey is this: don't create deep class hierarchies for incremental variations in functionality. Instead we create a separate class hierarchy that focuses on our validation requirements.

Note/Disclaimer/Plea for the reader to hang in there:

Our "better" design obviously contains a lot more classes than the original approach. And it might seem that we complicated the validation process. As with many examples, the real value of the approach may not be that evident in a simple case. But imagine if you had a team of developers working on an application with similar requirements. Since the form class is decoupled from the validation class, development can proceed in parallel. Imagine that the application dealt with orders, some of which required different validation rules depending on the customer, the product, or other factors. Imagine if you had to maintain several instances of the same application, and the validation rules per instance varied but the form information was consistent. The importance of proper separation of code, although seemingly trivial at times, should not be underestimated.

Combining the intentions of these two guidelines listed at the beginning of this section, I am going to take this approach in refactoring my design (read these bullets carefully):

  • I will create a FormValidator base class that will encapsulate the application validation functionality. I will make this an abstract base class as opposed to an interface. I am making this decision because the FormValidator base class will contain some basic logic that may be shared by the subclasses. An interface doesn't support method content, only definition. By keeping validate() abstract, I'll be forced to implement it in all my subclasses based on FormValidator (or I won't be able to compile my code).
  • The JobApplicantForm class will contain a FormValidator object. This object will be set to the appropriate FormValidator subclass when validation is called.
  • I will create subclasses of FormValidator for all specific validation rules (one class per job position in this case). See Figure 1.
  • I will create a class that contains a factory method. This method will return the appropriate FormValidator subclass based on the value of the position property of the instance of JobApplicantForm.
  • I will modify the validate() method in JobApplicantForm. It will retrieve the appropriate FormValidator subclass from the factory method and delegate its responsibilities to the validate() method of the FormValidator subclass. It will pass itself as an argument to that method.

This is the essence of the Strategy pattern: declare an interface or base class that performs an algorithm needed in all instances, and implement the specifics of that algorithm in the subclasses of that interface or base class. Note: an implication here is that the FormValidator subclasses have knowledge of the properties of the JobApplicantForm class. Their validation logic refers to these properties directly. Since these Strategy classes are intended to validate the form, I decide that this level of coupling is acceptable and necessary in the design. The process of delegation is common in implementations of the Strategy (and other) patterns.

class diagram Figure 1

The client class doesn't change, and this is what we should expect. Although we've refactored our design to be easier to extend and maintain, we haven't affected the clients that will use our code. The only change to our new client class, JobApplicantTestClient2.java, is the addition of test cases for the host/hostess and bartender positions.

Listing 4: JobApplicantTestClient2.java
package strategy101.common;

import strategy101.patterns.JobApplicantForm;

public class JobApplicantTestClient2 {

    public static void main(String args[]) {

        JobApplicantForm form;
        FormSuccess result;

        //-- Negative test form for busboy/busgirl
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_BUSSER);
        form.setPhone("111 222-3333");
        result = form.validate();
        runTest("Busboy/girl fail", result);

        //-- Positive test form for busboy/busgirl
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_BUSSER);
        form.setName("Bobby Busser");
        form.setPhone("111 222-3333");
        form.setYearsExp(new Double(1));
        form.setReference1("Mr. Smith, 222-555-1234");
        result = form.validate();
        runTest("Busboy/girl pass", result);

        //-- Negative test form for wait staff
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_WAIT_STAFF);
        form.setName("Wendy Waittress");
        form.setYearsExp(new Double(3));
        result = form.validate();
        runTest("Wait staff fail", result);

        //-- Positive test form for wait staff
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_WAIT_STAFF);
        form.setName("Wendy Waittress");
        form.setPhone("111 222-4444");
        form.setYearsExp(new Double(3));
        form.setReference2("Mr. Jones, 222-555-2345");
        result = form.validate();
        runTest("Wait staff pass", result);

        //-- Negative test form for manager
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_MANAGER);
        form.setName("Maxwell T. Manager");
        form.setReference1("Mr. Pink, 222-555-4567");
        result = form.validate();
        runTest("Manager fail", result);

        //-- Positive test form for manager
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_MANAGER);
        form.setName("Maxwell T. Manager");
        form.setPhone("111 222-7777");
        form.setYearsExp(new Double(8));
        form.setReference1("Mr. Pink, 222-555-4567");
        form.setReference2("Mr. Bojangles, 222-555-2345");
        form.setReference3("Mr. Anderson, 222-555-6789");
        result = form.validate();
        runTest("Manager pass", result);


        //-- Negative test form for host/hostess
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_HOSTER);
        form.setName("Hank Hostwell");
        form.setReference1("Mr. Pink, 222-555-4567");
        result = form.validate();
        runTest("Host/Hostess fail", result);

        //-- Positive test form for host/hostess
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_HOSTER);
        form.setName("Hank Hostwell");
        form.setPhone("111 222-7777");
        form.setYearsExp(new Double(8));
        form.setReference3("Mr. Anderson, 222-555-6789");
        result = form.validate();
        runTest("Host/Hostess pass", result);

        //-- Negative test form for bartender
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_BARTENDER);
        form.setName("Isaac Washington");
        form.setReference1("Capt. Merrill Stubing, 222-555-7890");
        result = form.validate();
        runTest("Bartender fail", result);

        //-- Positive test form for bartender
        form = new JobApplicantForm();
        form.setPosition(JobApplicantForm.JOB_BARTENDER);
        form.setName("Isaac Washington");
        form.setPhone("111 222-7777");
        form.setYearsExp(new Double(20));
        form.setReference1("Capt. Merrill Stubing, 222-555-7890");
        form.setReference3("Dr. Adam Bricker, 222-555-8901");
        form.setLegal(true);
        result = form.validate();
        runTest("Bartender pass", result);

    }

    private static void runTest(String testName, FormSuccess result) {
        System.out.println("======= Running test: " + testName + " =======");
        if (result.isSuccess()) {
            System.out.println("Submit successful.");
        }
        else {
            System.out.println("Form did not pass validation. See message:");
        }
        System.out.println(result.getResultMessage());
        System.out.println("=======================\n");
    }
}

The classes for the refactored design are in the strategy101/common and strategy101/patterns packages. If you compile the code and run the class, JobApplicantTestClient2, you will see the output. You can also view them.

Feedback

Articles
Factory 101

Strategy 101