Polymorphism


Overuse of the if statement can quickly turn your code into a high-maintenance legacy that is difficult to follow. A concept known as polymorphism can help structure your code to minimize the need for if statements.

Student grading is a bit more involved than the above example. You need to support grading for honors students. Honors students are graded on a higher scale. They can earn five grade points for an A, 4 points for a B, 3 points for a C, and 2 points for a D.


A highly refactored test:

 public void testCalculateHonorsStudentGpa() {    assertGpa(createHonorsStudent(), 0.0);    assertGpa(createHonorsStudent(Student.Grade.A), 5.0);    assertGpa(createHonorsStudent(Student.Grade.B), 4.0);    assertGpa(createHonorsStudent(Student.Grade.C), 3.0);    assertGpa(createHonorsStudent(Student.Grade.D), 2.0);    assertGpa(createHonorsStudent(Student.Grade.F), 0.0); } private Student createHonorsStudent(Student.Grade grade) {    Student student = createHonorsStudent();    student.addGrade(grade);    return student; } private Student createHonorsStudent() {    Student student = new Student("a");    student.setHonors();    return student; } 

The code to resolve this is trivial.

 private boolean isHonors = false; // ... void setHonors() {    isHonors = true; } // ... int gradePointsFor(Grade grade) {    int points = basicGradePointsFor(grade);    if (isHonors)       if (points > 0)          points += 1;    return points; } private int basicGradePointsFor(Grade grade) {    if (grade == Grade.A) return 4;    if (grade == Grade.B) return 3;    if (grade == Grade.C) return 2;    if (grade == Grade.D) return 1;    return 0; } 

… but things are becoming unwieldy. Next, suppose you must support a different grading scheme:

 double gradePointsFor(Grade grade) {    if (isSenatorsSon) {       if (grade == Grade.A) return 4;       if (grade == Grade.B) return 4;       if (grade == Grade.C) return 4;       if (grade == Grade.D) return 4;       return 3;    }    else {       double points = basicGradePointsFor(grade);       if (isHonors)          if (points > 0)             points += 1;       return points;    } } 

Now the code is getting messy. And the dean has indicated that there are more schemes on the way, in this politically correct age of trying to be everything to everyone. Every time the dean adds a new scheme, you must change the code in the Student class. In changing Student, it is easy to break the class and other classes that depend on it.

You would like to close the Student class to any further changes. You know that the rest of the code in the class works just fine. Instead of changing the Student class each time the dean adds a new scheme, you want to support the new requirement by extending the system.[4]

[4] [Martin2003], p. 99.

Design your system to accommodate changes through extension, not modification.


You can consider the grading scheme to be a strategy that varies based on the type of student. You could create specialized student classes, such as HonorsStudent, RegularStudent, and PrivilegedStudent, but students can change status. Changing an object from being one type to being a different type is difficult.

Instead, you can create a class to represent each grading scheme. You can then assign the appropriate scheme to each student.

You will use an interface, GradingStrategy, to represent the abstract concept of a grading strategy. The Student class will store a reference of the interface type GradingStrategy, as shown in the UML diagram in Figure 5.2. In the diagram, there are three classes that implement the interface: RegularGradingStrategy, HonorsGradingStrategy, and EliteGradingStrategy. The closed-arrow relationship shown with a dashed line is known as the realizes association in UML. In Java terms, RegularGradingStrategy implements the GradingStrategy interface. In UML terms, RegularGradingStrategy realizes the GradingStrategy interface.

Figure 5.2. Strategy


The GradingStrategy interface declares that any class implementing the interface will provide the ability to return the grade points for a given grade. You define GradingStrategy in a separate source file, GradingStrategy.java, as follows:

UML and Interfaces

More often than not, UML class diagrams are intended to show the structure of the systemthe relations and thus the dependencies between its classes. As such, UML diagrams are best left uncluttered with detail. You should rarely show nonpublic methods, and often you might show no methods at all in a particular class. Diagram only interesting and useful things.

UML is best used as a simple semipictorial language for expressing some high-level concepts of a system. If you clutter your diagram with every possible detail, you will obscure the important things that you are trying to express.

Representing interfaces in UML is no different. Often it is valuable to show only that a class implements a particular interface. The methods defined in the interface might be generally understood and are also available in the code. UML gives you a visually terse way of showing that a class implements an interface. As an example, you can express the fact that the String method implements the Comparable interface with this UML diagram:


 package sis.studentinfo; public interface GradingStrategy {    public int getGradePointsFor(Student.Grade grade); } 

You represent each strategy with a separate class. Each strategy class implements the GradingStrategy interface and thus provides appropriate code for the getGradePointsFor method.

An interface defines methods that must be part of the public interface of the implementing class. All interface methods are thus public by definition. As such, you need not specify the public keyword for method declarations in an interface:

 package sis.studentinfo; public interface GradingStrategy {    int getGradePointsFor(Student.Grade grade); } 

HonorsGradingStrategy:

 package sis.studentinfo; public class HonorsGradingStrategy implements GradingStrategy {    public int getGradePointsFor(Student.Grade grade) {     int points = basicGradePointsFor(grade);     if (points > 0)        points += 1;     return points; } int basicGradePointsFor(Student.Grade grade) {    if (grade == Student.Grade.A) return 4;    if (grade == Student.Grade.B) return 3;    if (grade == Student.Grade.C) return 2;    if (grade == Student.Grade.D) return 1;    return 0;   } } 

RegularGradingStrategy:

 package sis.studentinfo; public class RegularGradingStrategy implements GradingStrategy {    public int getGradePointsFor(Student.Grade grade) {       if (grade == Student.Grade.A) return 4;       if (grade == Student.Grade.B) return 3;       if (grade == Student.Grade.C) return 2;       if (grade == Student.Grade.D) return 1;       return 0;    } } 

If you're savvy enough to spot the duplication in this code, take the time to refactor it away. You could introduce a BasicGradingStrategy and have it supply a static method with the common code. Or you can wait (only because I said sootherwise never put off eliminating duplication!): The next lesson introduces another way of eliminating this duplication.

In StudentTest, instead of using setHonors to create an honors student, you send the message setGradingStrategy to the Student object. You pass an HonorsGradingStrategy instance as the parameter.

 private Student createHonorsStudent() {    Student student = new Student("a");    student.setGradingStrategy(new HonorsGradingStrategy());    return student; } 

Note how you were able to make this change in one place by having eliminated all duplication in the test code!

You then need to update the Student class to allow the strategy to be set and stored. Initialize the gradingStrategy instance variable to a RegularGradingStrategy object to represent the default strategy.

 public class Student {    ...    private GradingStrategy gradingStrategy =        new RegularGradingStrategy();       ...    void setGradingStrategy(GradingStrategy gradingStrategy) {       this.gradingStrategy = gradingStrategy;    }    ... 

The next section, Using Interface References, explains why you can declare both the gradingStrategy instance variable and the parameter to setGradingStrategy as the type GradingStrategy.

Modify the Student code to obtain grade points by sending the gradePointsFor message to the object stored in the gradingStrategy reference.

 int gradePointsFor(Grade grade) {    return gradingStrategy.getGradePointsFor(grade); } 

Eliminate the instance variable isHonors, its associated setter method, and the method basicGradePointsFor.

Now that the gradePointsFor method does nothing but a simple delegation, you can inline its code to the getGpa method. In some cases, single-line delegation methods such as gradePointsFor can provide cleaner, clearer code, but in this case, the gradePointsFor method adds neither. Its body and name say the same thing, and no other code uses the method.

Inline the gradePointsFor method by replacing the call to it (from getGPA) with the single line of code in its body. You then can eliminate the gradePointsFor method entirely.

 double getGpa() {    if (grades.isEmpty())       return 0.0;    double total = 0.0;    for (Grade grade: grades)       total += gradingStrategy.getGradePointsFor(grade);    return total / grades.size(); } 

You now have an extensible, closed solution. Adding a third implementation of GradingStrategy, such as EliteGradingStrategy, is a simple exercise of creating a new class and implementing a single method. The code in Student can remain unchangedyou have closed it to any modifications in grading strategy. Also, you can make future changes to a particular grading strategy exclusive of all other grading strategies.



Agile Java. Crafting Code with Test-Driven Development
Agile Javaв„ў: Crafting Code with Test-Driven Development
ISBN: 0131482394
EAN: 2147483647
Year: 2003
Pages: 391
Authors: Jeff Langr

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net