
Now the restaurant is doing well, and my friend has decided to add a couple of new positions:
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:
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:
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.
Two statements come to mind from the Gang of Four book, Design Patterns: Elements of Reusable Object Oriented Software:
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.
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):
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.
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.
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.