Lesson 6. Inheritance

In this lesson you will learn about:

• switch statements

• maps

• lazy initialization

• inheritance

• extending methods

• calling superclass constructors

• the principle of subcontracting

The switch Statement

In the last chapter, you coded methods in HonorsGradingStrategy and RegularGradingStrategy to return the proper GPA for a given letter grade. To do so, you used a succession of if statements. As a reminder, here is what some of the relevant code looks like (taken from HonorsGradingStrategy):

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;
}

Each of the conditionals in basicGradePointsFor compares a value to a single variable, grade. Another construct in Java that allows you to represent related comparisons is the switch statement:

int basicGradePointsFor(Student.Grade grade) {
   switch (grade) {
      case A: return 4;
      case B: return 3;
      case C: return 2;
      case D: return 1;
      default: return 0;
   }
}

As the target of the switch statement, you specify a variable or expression to compare against. In this example, the target is the grade parameter:

switch (grade) {

You then specify any number of case labels. Each case label specifies a single enum value. When the Java VM executes the switch statement, it determines the value of the target expression. It compares this value to each case label in turn.

If the target value matches a case label, the Java VM transfers control to the statement immediately following that case label. It skips statements between the switch target and the case label. If the target value matches no case label, the Java VM transfers control to the default case label, if one exists. The default case label is optional. If no default case label exists, the VM transfers control to the statement immediately following the switch statement.

Thus, if the value of the grade variable is Student.Grade.B, Java transfers control to the line that reads:

case B: return 3;

The Java VM executes this return statement, which immediately transfers control out of the method.

Case Labels Are Just Labels

A case label is just a label. The Java VM uses the case labels only when the Java VM initially encounters the switch statement. Once Java has located the matching case label and transferred control to it, it ignores the remainder of the case labels. Java executes the remainder of the code in the switch statement in top-to-bottom order. Java blissfully ignores the case labels and the default label, through to the end of the switch statement (or until another statement such as return transfers control.)1

1 You'll learn more about statements to transfer control in Lesson 7.

The following example demonstrates how the switch statement flows:

image

The test fails:

junit.framework.AssertionFailedError: expected<6> but was<11>

Since score was set to Score.touchdown, the Java VM transferred control to the line following the corresponding case label:

case touchdown:
   totalPoints += 6;

Once Java executed that line, it then executed the next three statements (and ignored the case labels):

case extraPoint:  // ignored
   totalPoints += 1;
case twoPointConversion:  // ignored
   totalPoints += 2;
case safety:    // ignored
   totalPoints += 2;

Total points thus is 6 plus 1 plus 2 plus 2, or 11. Test failure!

You obtain the desired behavior by inserting a break statement at the end of each section of code following a case label:

image

The break statement is another statement that transfers control flow. It transfers control out of the switch statement to the next statement following. In the example, executing any break statement results in control being transferred to the assertEquals statement.

Multiple case labels can precede a statement appearing within the switch statement:

switch (score) {
   case fieldGoal:
      totalPoints += 3;
      break;
   case touchdown:
      totalPoints += 6;
      break;
   case extraPoint:
      totalPoints += 1;
      break;
   case twoPointConversion:
   case safety:
      totalPoints += 2;
      break;
}

If score matches either Score.twoPointConversion or Score.safety, the code adds two points to the total.

In addition to switching on enum values, you can switch on char, byte, short, or int values. You cannot, unfortunately, switch on String values.

Your code should not contain a lot of switch statements. Also, it should not contain lots of multiple if statements that accomplish the same goal. Often, you can eliminate switch statements in favor of polymorphic solutions, as in the previous lesson. Your main guides as to whether you should replace switch statements with polymorphism are duplication and frequency/ease of maintenance.

Before continuing, refactor RegularGradingStrategy to use a switch statement instead of multiple if statements.

Maps

Yet another alternative to using a switch statement is to use a map. A map is a collection that provides fast insertion and retrieval of values associated with specific keys. An example is an online dictionary that stores a definition (a value) for each word (key) that appears in it.

Java supplies the interface java.util.Map to define the common behavior for all map implementations. For the report card messages, you will use the EnumMap implementation. An EnumMap is a Map with the additional constraint that all keys must be enum objects.

To add a key-value pair to a Map, you use the put method, which takes the key and value as parameters. To retrieve a value stored at a specific key, you use the get method, which takes the key as a parameter and returns the associated value.

Suppose you need to print an appropriate message on a report card for each student, based on their grade. Add a new class named ReportCardTest to the sis.reports package.

image

Change the Grade enum defined in Student to public to make this code compile.

The ReportCard class:

image

The ReportCard class defines an instance variable messages as a parameterized Map type. The type parameters are Student.Grade for the key and String for the value. When you construct an EnumMap, you must also pass in the class that represents the enum type of its keys.

The getMessages method uses lazy initialization (see the sidebar) to load the appropriate message strings, which you define as static constants, into a new instance of the EnumMap.

The getMessage method is a single line of code that uses the Map method get to retrieve the appropriate string value for the grade key.

Maps are extremely powerful, fast data structures that you might use frequently in your applications. Refer to Lesson 9 for a detailed discussion of their use.

Inheritance

The code for RegularGradingStrategy and HonorsGradingStrategy contains duplicate logic. The code that you used to derive a basic grade is the same in both classes—both classes contain switch statements with the exact same logic:

switch (grade) {
   case A:  return 4;
   case B:  return 3;
   case C:  return 2;
   case D:  return 1;
   default: return 0;
}

One solution for eliminating this duplication is to use inheritance. I discussed inheritance briefly in the Agile Java Overview chapter. You can factor the commonality between RegularGradingStrategy and HonorsGradingStrategy into a common class called BasicGradingStrategy. You can then declare the classes RegularGradingStrategy and HonorsGradingStrategy as subclasses of BasicGradingStrategy. As a subclass, each of RegularGradingStrategy and BasicGradingStrategy obtain all of the behaviors of Basic-GradingStrategy.

You will refactor to an inheritance-based solution in very small increments. The first step is to create a BasicGradingStrategy class:

package sis.studentinfo;

public class BasicGradingStrategy {
}

Then declare both RegularGradingStrategy and HonorsGradingStrategy as subclasses of BasicGradingStrategy by using the extends keyword. The extends clause must appear before any implements clause.

image

image

You can now move the common code to a method on the BasicGradingStrategy superclass (also known as the base class). First, move the method basicGradePointsFor from HonorsGradingStrategy to BasicGradingStrategy:

image

Recompile and test.

Even though HonorsGradingStrategy no longer contains the definition for basicGradePointsFor, it can call it just as if it were defined in the same class. This is akin to your ability to call assertion methods in your test classes, because they extend from junit.framework.TestCase. Conceptually, HonorsGradingStrategy is a BasicGradingStrategy, much as a class that implements an interface is of that interface type. Thus HonorsGradingStrategy can use any (nonprivate) method defined in BasicGradingStrategy. (We'll discuss the nonprivate distinction shortly.)

The UML representation of this relationship appears in Figure 6.1.

Figure 6.1. Inheriting from BasicGradingStrategy

image

Next, modify RegularGradingStrategy to reuse the method basicGradePointsFor.

package sis.studentinfo;

public class RegularGradingStrategy
      extends BasicGradingStrategy
      implements GradingStrategy {
   public int getGradePointsFor(Student.Grade grade) {
      return basicGradePointsFor(grade);
   }
}

Abstract Classes

You could stop here and have a sufficient solution. You could also go one step further and eliminate a bit more duplication by having the superclass BasicGradingStrategy implement the GradingStrategy interface directly. The only problem is, you don't necessarily have an implementation for getGradePointsFor.

Java allows you to define a method as abstract, meaning that you cannot or do not wish to supply an implementation for it. Once a class contains at least one abstract method, the class itself must similarly be defined as abstract. You cannot create new instances of an abstract class, much as you cannot directly instantiate an interface.

Any class extending from an abstract class must either implement all inherited abstract methods, otherwise it too must be declared as abstract.

Change the declaration of BasicGradingStrategy to implement the GradingStrategy interface. Then supply an abstract declaration for the getGradePointsFor method:

image

The subclasses no longer need to declare that they implement the GradingStrategy interface:

image

Extending Methods

A third solution2 is to recognize that the RegularGradingStrategy is virtually the same as BasicGradingStrategy. The method getGradePointsFor as defined in HonorsGradingStrategy is basically an extension of the method in RegularGradingStrategy—it does what the base class method does, plus a little more. In other words, you can supply a default definition for getGradePointsFor in BasicGradingStrategy that will be extended in HonorsGradingStrategy.

2 Are you getting the picture that there are many ways to skin a cat in Java? All these options allow you to make your code as expressive as possible. It also means that there is no one “perfect” way to implement anything of complexity.

Move the definition for getGradePointsFor from RegularGradingStrategy to BasicGradingStrategy. You should no longer declare BasicGradingStrategy as abstract, since all of its methods supply definitions.

image

RegularGradingStrategy no longer defines any methods!

(Compile, test.)

Next, change the code in HonorsGradingStrategy to extend the superclass method getGradePointsFor. To extend a method, you define a method with the same name in a subclass. In that subclass method, you call the superclass method to effect its behavior. You supply additional, specialized behavior in the remainder of the subclass method.

In comparison to extending, Java will let you fully supplant a method's definition with a completely new one that has nothing to do with the original. This is known as overriding a method. This is a semantic definition: The Java compiler knows no distinction between overriding and extending; it's all overriding to the compiler. You, on the other hand, recognize an extending method by its call to the superclass method of the same name.

image

Prefer extending to overriding.

Your subclass definition should specialize the behavior of the superclass method in some manner, not change it completely.

To explicitly call a method in a superclass, use the super keyword to scope the message send, as shown in bold:

image

When you use the super keyword, the Java VM looks in the superclass to find the corresponding method definition.

Refactoring

The method basicGradePointsFor in BasicGradingStrategy is now superfluous. You can inline it and eliminate the method basicGradePointsFor.

image

Is there any more need for the separate subclass RegularGradingStrategy? There's not much left to it:

package studentinfo;

public class RegularGradingStrategy
   extends BasicGradingStrategy {
}

You can eliminate this class by changing the gradeStrategy reference in Student to use BasicGradingStrategy as the default.

public class Student {
   ...
   private GradingStrategy gradingStrategy =
      new BasicGradingStrategy();
   ...

All the work you have done since learning about inheritance has been on the code side only. You have not changed any tests. (I hope you have been running your tests with each incremental change.) You have not changed any behavior; you have only changed the way in which the existing behavior is implemented. The tests in Student for GPA adequately cover the behavior.

However, the tests in Student cover behavior within the context of managing students. You want to additionally provide tests at the unit level. The general rule is to have at least one test class for each production class. You should test each of HonorsGradingStrategy and BasicGradingStrategy individually.

Here is a test for BasicGradingStrategy:

image

A corresponding test for HonorsGradingStrategy:

image

Don't forget to add these tests to the suite defined in AllTests.

Enhancing the Grade Enum

The number of basic grade points is information that can be associated directly with the Grade enum. It is possible to enhance the definition of an enum to include instance variables, constructors, and methods, just like any other class type. The main restriction is that you cannot extend an enum from another enum.

Modify the declaration of the Grade enum in the Student class:

image

You have now associated each named instance of the enum with a parameter. Further, you terminated the list of enum instances with a semicolon. After that semicolon, code in the Grade enum declaration looks just like that in any class type. The parameter associated with each named enum instance is passed to the constructor of Grade. This points parameter is stored in an instance variable named points. You retrieve the points via the getPoints method.

You can now simplify the code in BasicGradingStrategy to:

image

No more switch statement!

Summer Course Sessions

image

The CourseSession class currently supports only 15-week sessions (with a one-week break) that occur in the spring and fall. The university also needs support for summer sessions. Summer sessions will begin in early June. They last eight weeks with no break.

One solution would be to pass the session length into the CourseSession class, store it, and use it as a multiplier when calculating the session end date.

However, there are many other differences between summer sessions and spring/fall sessions, including differences in maximum class session, credit values, and so on. For this reason, you have chosen to create a new class named SummerCourseSession.

The simplest approach for implementing SummerCourseSession, given what you know, is to have it extend from CourseSession. A SummerCourseSession is a CourseSession with some refinement on the details (see Figure 6.2).

Figure 6.2. SummerCourseSession Specialization

image

You want to create the SummerCourseSession classes (test and production) in a new package, summer, for organizational purposes.

The test you initially code verifies that SummerCourseSession calculates the course end date correctly:

image

Calling Superclass Constructors

An initial noncompiling attempt at an implementation for SummerCourseSession follows.

image

The key things to note are in bold.

First, you want to declare SummerCourseSession as a subclass of CourseSession, so you use the extends keyword in the class declaration.

Second, the constructor for SummerCourseSession makes a call to the superclass constructor that takes the department, course number, and start date as parameters. It does this by using the super keyword. This is similar to a super method invocation.

Third, in order to quickly get things working, you copied the getEndDate method from CourseSession and altered the value of the sessionLength temporary to be 8 instead of 16.

This code will not compile. You should see three compiler errors. The first one you know how to fix:

image

Change the definition of getEndDate in CourseSession to public and recompile:

public Date getEndDate() {

You now receive a different error related to getEndDate:

image

A subclass method that overrides a superclass method must have equivalent or looser access privileges than a superclass method. In other words, CourseSession's control over the getEndDate method must be tighter than the control of any of its subclasses. If a superclass marks a method as package, a subclass can mark the overridden method as package or public. If a superclass marks a method as public, a subclass must mark the overridden method as public.

The solution is to change getEndDate in SummerCourseSession to be public:

public Date getEndDate() {

After compiling, you should have two remaining errors that appear similar:

image

Sure enough, the CourseSession constructor that takes a Date as a parameter is marked private:

private CourseSession(
   String department, String number, Date startDate) {
   // ...

You made the constructor private in order to force clients to construct CourseSession instances using the class creation method. The problem is, the private access modifier restricts access to the constructor. Only code defined within CourseSession itself can invoke its constructor.

Your intent is to expose the CourseSession constructor to code only in CourseSession itself or code in any of its subclasses. Even if you define a CourseSession subclass in a different package, it should have access to the CourseSession constructor. You don't want to expose the constructor to other classes.

Specifying public access would allow too much access. Non-subclasses in different packages would be able to directly construct CourseSession objects. Package access would not allow subclasses in different packages to access the CourseSession constructor.

Java provides a fourth (and final) access modifier, protected. The protected keyword indicates that a method or field may be accessed by the class in which it is defined, by any other class in the same package, or by any of its subclasses. A subclass has access to anything in its superclass marked as protected, even if you define the subclass in a package other than the superclass.

Change the CourseSession constructor from private to protected:

protected CourseSession(
   String department, String number, Date startDate) {
   // ...

The compilation error regarding the constructor goes away. You have one remaining error: getEndDate references the private superclass field startDate directly.

image

You could solve the problem by changing startDate to be a protected field. A better solution is to require that subclasses, like any other class, access the field by using a method.

Change the getStartDate method from package to protected access.

image

You must change the code in both CourseSession and SummerCourseSession to use this getter:

image

Since I told you to be lazy and copy the code for getEndDate, note that you had the additional effort of making the change in two places.

In SummerCourseSession, getEndDate makes a call to getStartDate, but getStartDate is not defined in SummerCourseSession. Since the super. scope modifier was not used, Java first looks to see if getStartDate is defined in SummerCourseSession. It is not, so Java starts searching up the inheritance chain until it finds an accessible definition for getStartDate.

Protected-level access is one step “looser” than package. One minor downside is that it is possible for other classes in the same package to access a protected element (method or field). Usually this is not what you want. There are few good design reasons to make a field accessible to other classes in the same package and to subclasses in different packages but not to other classes.

In the case of CourseSession, you want the constructor to be accessible only to subclasses, regardless of package, and not to any other classes. There is no way in Java to enforce this restriction.

Refactoring

Time to eliminate the duplicate code you hastily introduced.

The only change between getEndDate as defined in CourseSession and SummerCourseSession is the session length. Define a protected method in CourseSession to return this value:

image

Change getEndDate to call getSessionLength:

image

In SummerCourseSession, override the method getSessionLength. Summer sessions are 8 weeks, not 16. In this case, overriding is a fine alternative to extending, since you are not changing behavior, only data-related details of the behavior.

image

When you override a method, you should precede it with an @Override annotation. You use annotations to mark, or annotate, specific portions of code. Other tools, such as compilers, IDEs, and testing tools, can read and interpret these annotations. The Java compiler is the tool that reads the @Override annotation.

The compiler ensures that for a method marked as @Override, a method with the same name and arguments exists in a superclass. If you screwed up somehow—perhaps you mistyped the method as getSessionLentgh instead of getSessionLength—the compile fails. You see the message:

method does not override a method from its superclass

Java does not require you to add @Override annotations to your code. However, they provide valuable documentation for your code as well as protections from simple mistakes. You can read more about annotations in Lesson 15.

You may now remove getEndDate from SummerCourseSession. In Figure 6.3, the getSessionLength operation appears twice, an indication that the subclass (SummerCourseSession) overrides the superclass (CourseSession) definition. Note that I have preceded each method with access privilege information, something I rarely do. Since normally I only display public information in a UML sketch, I prefer to omit the + (plus sign) that indicates a publicly accessible operation.

Figure 6.3. Overriding a Protected Method

image

In this circumstance, however, getSessionLength is protected, not public. Adding the access privilege indicators to the diagram in Figure 6. differentiates access between the methods and highlights the relevancy of getSessionLength. The UML indicator for protected is the pound sign (#).3

3 The UML access indicator for private is (-). The indicator for package-level access is (~).

Most of the logic for determining the end date of a session is fixed. The only variance in the logic between CourseSession and SummerCourseSession is the session length. This variance is represented by the abstraction of a separate method, getSessionLength. The CourseSession class can supply one implementation for getSessionLength, the SummerCourseSession another.

The method getEndDate in CourseSession acts as a template. It supplies the bulk of the algorithm for calculating a session end date. Certain pieces of the algorithm are “pluggable,” meaning that the details are provided elsewhere. Subclasses can vary these details. The template algorithm in getEndDate is an example of a design pattern known as Template Method.4

4 [Gamma1995].

More on Constructors

You call a superclass constructor from a subclass constructor by using the super keyword, as demonstrated earlier. A call to a superclass constructor must appear as the first line in a subclass constructor.

A subclass extends, or builds upon, a superclass. You can think of a subclass object as an outer layer, or shell, around an object of the base class. Before the subclass object can exist, Java must build and properly initialize an object of the superclass. The following language test demonstrates how classes are constructed, using a demo class called SuperClass and its subclass SubClass.

image

The Java virtual machine requires every class to have at least one constructor. However, you can code a class without explicitly defining a constructor. If you don't define a constructor Java will automatically generate a default, no-argument constructor.

If you do not provide a super call in a subclass constructor, it is as if Java inserted a call to the no-argument superclass constructor. Even constructors that take parameters call the no-argument superclass constructor by default.

image

The test constructs a new SubClass instance, passing in a String as a parameter. The parameter information appears in bold. The test then demonstrates that creating a SubClass instance results the execution of the SuperClass no-argument constructor.

Remember, if you do supply a constructor with arguments in a class, Java does not automatically supply a no-argument constructor. In this situation, any subclass constructors must explicitly call a superclass constructor, otherwise you receive a compilation error.

image

The above code generates the following compiler error:

image

The subclass constructor should probably be:

class SubClass extends SuperClass {
   SubClass(String parm) {
      super(parm);
   }
}

Inheritance and Polymorphism

The class SummerCourseSession inherits from CourseSession. Just as a class that implements an interface is of that interface type, a class that inherits from another is of that superclass type. Therefore, SummerCourseSession is a CourseSession.

Just as you were taught to prefer interface reference types, you should also prefer base class for the reference type. Assign a new SummerCourseSession to a CourseSession reference:

CourseSession session = new SummerCourseSession();

Even though the session variable is of the type CourseSession, it still refers to a SummerCourseSession in memory.

You defined the method getEndDate in both CourseSession and SummerCourseSession. When you send the getEndDate message using the session variable, the message is received by a SummerCourseSession object. Java executes the version of getEndDate defined in SummerCourseSession.

The client thinks it is sending a message to a CourseSession, but an object of a class derived from CourseSession receives and interprets the message. This is another example of polymorphism.

The Principle of Subcontracting

The client that sends a message to an object using a variable of CourseSession type expects it to be interpreted and acted upon in a certain manner. In test-driven development, unit tests define this “manner.” A unit test describes the behavior supported by interacting with a class through its interface. It describes the behavior by first actually effecting that behavior, then by ensuring that any number of postconditions hold true upon completion.

For a class to properly derive from CourseSession, it must not change the expectations of behavior. Any postcondition that held true for a test of CourseSession should also hold true for a test executing against a subclass of CourseSession. In general, subclasses should keep or strengthen postconditions. Subclasses should extend the behavior of their superclasses, meaning that postconditions should be added to tests for the subclass.

What this means for TDD is that you should specify contracts (in the form of unit tests) at the level of the superclass. All subclasses will need to conform to these unit tests. You will build these contracts using a pattern known as Abstract Test.5

5 [George2002].

Technically, there is no compelling reason to make this refactoring. The tests for CourseSession class could retain the contract for both CourseSession and SummerCourseSession. Minor differences do exist, though. The CourseSession class explicitly tracks the number of instances created; this is something the SummerCourseSession class need not do.

Building the Session superclass results in a slightly cleaner and easier to understand hierarchy. Conceptually, a CourseSession seems to be an object at the same hierarchical “level” as a SummerCourseSession.

It's up to you to decide if such a refactoring is worthwhile. If it gives you a simpler solution, go for it. If it creates artificial classes in the hierarchy without any benefit, it's a waste of time.

For the CourseSession example, you will refactor such that both CourseSession and SummerCourseSession inherit from a common abstract superclass, instead of SummerCourseSession inheriting from CourseSession. See Figure 6.4.

Figure 6.4. The Session Hierarchy

image

Create an abstract test class, SessionTest, that specifies the base unit tests for Session and its subclasses. Move testCreate, testComparable, and test-EnrollStudents from CourseSessionTest into SessionTest. These tests represent the contracts that should apply to all Session subclasses.

SessionTest contains an abstract factory method, createSession. Subclasses of SessionTest will provide definitions for createSession that return an object of the appropriate type (CourseSession or SummerCourseSession).

image

image

You must change the test subclasses, CourseSessionTest and SummerCourseSessionTest, so they extend from SessionTest. As you work through the example, you may refer to Figure 6.5 as the UML basis for the test reorganization.

Figure 6.5. Abstract Test

image

Each test subclass will also need to provide the implementation for createSession.

image

In CourseSessionTest, you'll need to change any usages of the createCourseSession method to createSession.

image

The method testCourseDates creates its own local instance of a CourseSession. The alternative would be to use the CourseSession object created by the setUp method in SessionTest. Creating the CourseSession directly in the subclass test method makes it easier to read the test, however. It would be difficult to understand the meaning of the test were the instantiation in another class. The assertion is closely related to the setup of the CourseSession.

Both CourseSessionTest and SummerCourseSessionTest now provide an implementation for the abstract createSession method.

The most important thing to note is that CourseSessionTest and SummerCourseSessionTest share all tests defined in SessionTest, since both classes extend from SessionTest. JUnit executes each test defined in SessionTest twice—once for a CourseSessionTest instance and once for a SummerCourseSessionTest instance. This ensures that both subclasses of Session behave appropriately.

When JUnit executes a method defined in SessionTest, the code results in a call to createSession. Since you defined createSession as abstract, there is no definition for the method in SessionTest. The Java VM calls the createSession method in the appropriate test subclass instead. Code in createSession creates an appropriate subtype of Session, either CourseSession or SummerCourseSession.

The refactoring of the production classes (I've removed the Java API documentation):

image

image

image

The bulk of code in Session comes directly from CourseSession. The method getSessionLength becomes an abstract method, forcing subclasses to each implement it to provide the session length in weeks. Here are CourseSession and SummerCourseSession, each having been changed to inherit from Session:

image

image

image

All that is left in the subclasses are constructors, static methods, and template method overrides. Constructors are not inherited. Common methods, such as enroll and getAllStudents, now appear only in the Session superclass. All of the fields are common to both subclasses and thus now appear in Session. Also important is that the return type for the create method in SummerCourseSession and CourseSession is the abstract supertype Session.

The test method testCourseDates appears in both SessionTest subclasses. You could define a common assertion for this test in SessionTest, but what would the assertion be? One simple assertion would be that the end date must be later than the start date. Subclasses would strengthen that assertion by verifying against a specific date.

Another solution would be to assert that the end date is exactly n weeks, minus the appropriate number of weekend days, after the start date. The resultant code would have you duplicate the logic from getEndDate directly in the test itself.

Of perhaps more value is adding assertions against the method getSessionLength in the abstract test:

public void testSessionLength() {
   Session session = createSession(new Date());
   assertTrue(session.getSessionLength() > 0);
}

By expressing postconditions clearly in the abstract class, you are establishing a subcontract that all subclasses must adhere to.

This refactoring was a considerable amount of work. I hope you took the time to incrementally apply the changes.

Exercises

  1. In the exercises for Lesson 5, you created a method to calculate the strength of a piece based on its type and board position. Modify this method to use a switch statement instead of an if statement.
  2. Modify the strength calculation method to use a Map that associates piece types with their base strength value. In order to place double values in Map, you will need to declare the map as Map<Piece.Type,Double>.6

    6 Lesson 7 includes a section on wrapper types that explains why this is so.

  3. Use lazy initialization to preload the Map of piece type to base-strength value. Note the effect on readability this change has on the code. What factors would drive you to either leave this change in the code or to remove it?
  4. Move the point values for the pieces into the piece type enumeration and use a method on the enumeration to get the point value.
  5. Move the character representation for a piece onto the Piece.Type enum. Code in Board will still need to manage uppercasing this character for a black piece.
  6. You must implement the various moves that a king can make. The basic rule is that a king can move one square in any direction. (For now, we will ignore the fact that neighboring pieces can either restrict such a move or can turn it into a capture.)
  7. The code in Board is getting complex. It now manages the mathematics of a grid, it stores pieces, it determines valid moves for pieces (the queen and king, so far), it gathers pieces and determines their strength. By the time you add move logic for the remaining pieces (queen, pawn, rook, bishop, and knight), the class will be very cluttered with conditional logic.

    There are a few ways to break things up. From my viewpoint, there are two main functions of code in Board class. One goal is to implement the 8x8 board using a data structure—a list of lists of pieces. A lot of the code in Board is geared toward navigating this data structure, without respect to the rules of chess. The rest of the code in Board defines the rules of the chess game.

    Break the Board class into two classes: Game, which represents the logic of a chess game, and Board, a simple data structure that stores pieces and understands the grid layout of the board.

    This will be a significant refactoring. Move methods as incrementally as possible, making sure tests remain green with each move. If necessary, create parallel code as you refactor, then eliminate the unnecessary code once everything is working. Make sure you add tests as needed.

    Take this opportunity to begin to encapsulate the implementation details of the data structure of Board. Instead of asking for ranks and adding pieces, have the Game code use the Board method put.

  8. A queen can move any number of squares as long as it forms a straight line. Implement this capability on Piece. Note that your initial implementation will require an if statement to determine whether the piece is a queen or a king.

    The solution may involve recursion—the ability of a method to call itself. A recursive call is no big deal; it is a method call like any other. But you must be careful to provide a way for the recursion to stop, otherwise you might get stuck in an infinite loop of the method calling itself.

  9. Your code to manage moves for kings and queens includes an if statement. You can imagine that supporting further moves for pieces will involve a bunch of conditional statements. One way of cleaning things up is to create a Piece hierarchy, with separate subtypes for King, Queen, Bishop, and so on.

    In preparation for creating Piece subclasses, move the method getPossibleMoves to the Piece class.

  10. Now you are ready to create Piece subclasses. You will note that as you create subclasses for Queen and King, the Piece.Type enum seems redundant. In the next exercise, you'll eliminate this enum. Create Queen and King classes to extend Piece. Move the relevant tests from PieceTest into QueenTest and KingTest. This will force you to move code as well into the new subclasses. Also, create subclasses for the remaining pieces and alter the factory methods accordingly on Piece. Don't worry about coding up the move rules for the other pieces yet.

    Refactor at will. Eliminate duplication. Add tests where tests were missing due to moving methods onto a new class.

  11. The final step is to eliminate the need for the Type enum. Since you now have a subclass for each piece, you can move code depending on the type into that sublcass.

    You may need to test the type of the subclass created. Instead of asking each piece for its Piece.Type value, you can simply ask for its class:

    piece.getClass()

    You can then compare this to a Class literal:

    Class expectedClass = Queen.class;
    assertEquals(expectedClass, piece.getClass();

    You also want to eliminate as much code as possible that asks for the type of an object—that's one of the main reasons for creating the subclasses in the first place. See if you can move any code that looks like if (piece.getClass().equals(Pawn.class) into the appropriate class.

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

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