Hands on Testing Java: Lab #11

Inheritance

Introduction

There are three basic capabilities that a modern object-oriented programming (OOP) language (like Java) must provide:

First, an OOP language must provide abstract data types---the ability to create new data types (i.e., classes) that hides data (i.e., private instance variables) and provides a public interface (i.e., public methods). We've used this all along (e.g., the String class), and we've implemented our own since Lab #6 (e.g., Fraction and Coordinate classes).

The second and third capabilities revolve around the similarities found between related, but different, classes. For example, in this lab you will implement classes to represent different types of bank accounts. One of the problems we'll have to solve is computing interest. Interest might be calculated monthly or yearly; it might be simple or compound. There's a similarity and a difference between the solutions for computing interest. How can a programmer take advantage of the similarities and still implement different behaviors?

So the second capability is providing some way to describe the common attributes and abilities of related class. For example, the interest rate is a common attribute for any "yearly interest calculator" and any "monthly interest calculator". It would be useful if we could describe this attribute once and have it apply to all of the different types.

The third capability is providing some way to override common abilities. For example, any interest calculator must be able to compute interest, but how this is done varies from calculator to calculator. In OOP terminology, this is known as polymorphism.

The second and third capabilities come from inheritance. That's the subject of this lab.

Getting Prepared

Do this...

Do this...
The code you get should be good to go. Run the unit tests for a green bar.

The Problem Description

Suppose we have been given the task of creating a program that will keep track of the accounts for a bank. There are a number of different kinds of accounts that the bank provides:

Account Type Transaction
charge
Interest Minimum balance
& penalty
Special
Regular account none none $2 if <$50 none
Interest account none 7%, monthly none none
Checking account $0.10 7%, monthly $6 if <$100 none
CD account none 15%, yearly none 20% of balance penalty
if withdrawal before 12 months

Your initial tempation might be to implement four different classes for these four different types of accounts. But this misses the commonalities. Another initial tempation might be to implement just one class for all four accounts, and use instance variables and if statements to handle the different requirements. This misses the simplifying power of inheritance.

Let's first just concentrate on the simplest part of all four accounts, their basic attributes.

Attributes of an Account

Perhaps the most obvious attribute for an account is the balance.

An account is owned by someone, so we should keep track of the account holder's name.

The CD account needs to know how old the account is in order to time its interest payment just right. It's not hard to imagine that other accounts might need this age as well. So we'll also keep track of the number of months that the account has been around.

So that's three solid attributes: a balance, a name, and an age.

The fuzzy part is implementing the differences. Consider them at a very high level in terms of when and why the charges are applied. There are two possibilities:

  1. Every account needs to react to individual transactions in different ways (i.e., transaction charge and minimum balance).
  2. Every account needs to react at the end of the month in different ways (i.e., account charge and interest).

We're going to be very abstract about these computations. Switch to internal perspective: imagine that as an Account object, I have a list of "calculators" which compute interest and transaction fees and the like for you. I will call these calculators "modifiers" because they will modify my balance.

So we end up with these attributes for an Account class:

Attributes of Account
Description Type Name
the balance of the account double myBalance
the name of the account holder String myName
the age of the account (in months) int myAge
transaction modifiers ???? myTransactionModifiers
monthly modifiers ???? myMonthlyModifiers

First off, notice the plurals in the descriptions: "transaction modifiers" and "monthly modifiers". Storing multiple objects should be done with an array or list; since it's unclear how many of these modifiers we will want or need, a list seems like a better approach.

But lists of what? We need to know the element type (like the List of Students in Lab #9), but we don't have element types for the "transaction modifiers" and "monthly modifiers". It looks like we'll need to design some more classes!

Let's call these new classes TransactionModifier and MonthlyModifier. So now the blanks can be filled in:

Attributes of Account
Description Type Name
the balance of the account double myBalance
the name of the account holder String myName
the age of the account (in months) int myAge
transaction modifiers List<TransactionModifier> myTransactionModifiers
monthly modifiers List<MonthlyModifier> myMonthlyModifiers

Do this...

  1. Add these instance variables to the Account class.
  2. You will get compilation errors because you haven't declared the MonthlyModifier class yet; comment out the declaration of myMonthlyModifiers for now.
  3. Compile your code, and run the tests for a green bar.

You need a constructor to initialize the instance variables. You need to receive values for the balance, name, and age. The lists can be initialized to empty ArrayLists.

Do this...
Create an explicit-value constructor that receives a balance, name, and age.

Do this...
Write an accessor method for Account#myBalance.

Do this...
Add this method to Account:

public void addTransactionModifier(TransactionModifier modifier) {
  myTransactionModifiers.add(modifier);
}

Add a similar method named addMonthlyModifier(MonthlyModifier) altered appropriately; comment out this method.

Do this...
Your code should still compile, and the unit tests should run for a green bar.

Transaction Modifiers

A transaction modifier needs to be triggered after every transaction (withdraw or deposit money). For example, suppose I'm a bank account, and I have two transaction modifiers. What they do doesn't matter to me because it's their responsibility to do what they do. They might charge for each transaction; they might charge an extra fee if the withdrawal is too large; they might charge a fee going below the minimum; they might even credit the balance if a deposit gets the balance above a minimum.

The key is that as an account, I do not care exactly what the transaction modifiers do. I care only that they modify my balance after a withdrawal or deposit.

So you need code that will ask each transaction modifier what should be done with the balance after a transaction. It's as if the transaction modifiers are an assembly line working on my balance. The first modifier tweaks it a bit and passes it on to the second; the second tweaks the balance some more and passes it on to the third; and so on until the last modifier is done. Each time I pass the current balance (as modified by the previous transaction-modifier) to the current transaction modifier; at the end, I return the balance as modified by the last transaction-modifier.

Inheritance gives us this ability to specify that an object has an ability without actually defining the ability.

It's tempting to get caught up considering all of the transaction modifiers of an account all at once, but it's much better for us to look at individual transaction-modifiers first.

Transaction Modifiers Inheritance Hierarchy

An inheritance hierarchy is a collection of classes that are related through inheritance. You've already imported one inheritance hierarchy: TransactionModifier, MinimumBalancePenalizer, and PerTransactionCharger. Here's a UML diagram wikipedia for the relationship between the classes:

transaction modifier inheritance hierarchy

TransactionModifier is known as the superclass because it is drawn in this diagram above the others. It is also known as the base class because it serves as the base of all of the other classes. That it, it's the most basic version of the other classes.

PerTransactionCharger and MinimumBalancePenalizer are known as subclasses because they are drawn under the superclass. So the diagram allows us to categorize our classes, but what do these categories mean?

Inheritance captures "is a" relationships. A "minimum-balance penalizer" is a "transaction modifier"; a "per-transaction charger" is a "transaction modifier". By saying that PerTransactionCharger or any subclass extends (or inherits from) TransactionModifier, we're saying that anything a TransactionModifier can do, any subclass charger can do as well. How these things are done might be very different in each subclass, but the fact that they can be done is the same for all subclasses.

So you'll see in these classes two methods, one for reacting to a withdrawal of money, the other to a deposit. All transaction modifiers must be able to react to these actions, but how they do it differs.

Let's start from the bottom up.

Per-Transaction Charges

Consider the PerTransactionCharger class. If I am a per-transaction charger, then I need to charge a fee for each transaction. I'll need to keep track of that fee, so I have one attribute:

Attributes of PerTransactionCharger
Description Type Name
the amount to charge per transaction double myPerTransactionCharge

Now when a withdrawal or deposit is made, I just need to subtract this per-transaction charge from the given balance.

Behavior of PerTransactionCharger#modifyBalanceAfterWithdrawal(double)

I am a per-transaction charger. I will receive a balance, and I will return a new balance computed from the received balance minus my per-transaction charge.

Objects of PerTransactionCharger#modifyBalanceAfterWithdrawal(double)
Description Type Kind Movement Name
a balance double variable in balance
a new balance double variable out newBalance
my per-transaction charge double instance variable instance myPerTransactionCharge
Operations of PerTransactionCharger#modifyBalanceAfterWithdrawal(double)
Description Predefined? Name Library
receive the balance yes parameter built-in
subtraction yes - built-in
return the new value yes return statement built-in
Algorithm of PerTransactionCharger#modifyBalanceAfterWithdrawal(double)
  1. Receive balance.
  2. Let newBalance be balance minus myPerTransactionCharge.
  3. Return newBalance.

Do this...
Look at how this was tested in PerTransactionChargerTest#testModifyBalanceAfterWithdrawal(). No matter what the balance is, a PerTransactionCharger always charges its specified amount for all withdrawals. Look at the definition of PerTransactionCharger#modifyBalanceAfterWithdrawal(double) to see the implementation.

Question #11.01 What parts of the OCD for PerTransactionCharger#modifyBalanceAfterWithdrawal(double) have to change for the OCD for PerTransactionCharger#modifyBalanceAfterDeposit(double)?
To be answered in Exercise Questions/lab11.txt.

Do this...
Read over PerTransactionChargerTest#testModifyBalanceAfterDeposit() and PerTransactionCharger#modifyBalanceAfterDeposit(double).

Minimum-Balance Penalizer

There are many ways to penalize an account for going below a minimum balance. For a little variety, we'll assume that depositing into an account is never penalized, even if the balance is below the minimum. So the algorithm for a modifying the balance after a deposit is simple:

Algorithm of MinimumBalancePenalizer#modifyBalanceAfterDeposit(double)
  1. Receive balance.
  2. Return balance.

Do this...
Read over MinimumBalancePenalizerTest#testModifyBalanceAfterDeposit() and MinimumBalancePenalizer#modifyBalanceAfterDeposit(double).

It's processing a withdrawal that's more interesting:

Behavior of MinimumBalancePenalizer#modifyBalanceAfterWithdrawal(double)

I am a minimum-balance penalizer. I will receive the current balance. If this balance is less that my minimum, then I will return the balance minus my minimum-balance penalty; otherwise, I will return the old balance.

This behavior actually suggests the attributes for the class:

Attributes of MinimumBalancePenalizer
Description Type Name
my minimum balance double myMinimumBalance
my penalty double myPenalty

So back to the method:

Objects of MinimumBalancePenalizer#modifyBalanceAfterWithdrawal(double)
Description Type Kind Movement Name
the current balance double variable in balance
my minimum balance double instance variable instance myMinimumBalance
my penalty double instance variable instance myPenalty
Operations of MinimumBalancePenalizer#modifyBalanceAfterWithdrawal(double)
Description Predefined? Name Library
receive the balance yes parameter built-in
if... then... otherwise yes if statement built-in
balance is less than my minimum balance yes < built-in
subtraction yes - built-in
return a value yes return statement built-in
Algorithm of MinimumBalancePenalizer#modifyBalanceAfterWithdrawal(double)
  1. Receive balance.
  2. If balance is less than myMinimumBalance, then
        return balance - myPenalty,
    otherwise
        return balance.

The Inhertance Hierarchy

Previously, without inheritance, you would have to create two instance variables in the Account class, one for a per-transaction charger and another one for a minimum-balance penalizer even if the account doesn't need them.

With inheritance, we've abstracted away the needs of an account; an account onlyneeds something to modify its balance after each transaction; it does not need to know how it will be done. So the Account class does not need to explicitly worry about PerTransactionChargers and MinimumBalancePenalizers; it will worry about the abstraction: TransactionModifier.

Externally, when I create an Account for a particular type of account, then I provide PerTransactionChargers and MinimumBalancePenalizers.

In order for this to work, the superclass must promise the common methods.

Implementing the Inheritance Hierarchy

To implement this, we're using an interface for the superclass. A class with no instance variables and with only promised methods (i.e., abstract methods, see below) can be declared as a Java interface. These interfaces are still often called "superclasses".

interface-declaration pattern
public interface InterfaceName {
  ConstantDeclarations
  AbstractMethodDeclarations
}
  • InterfaceName is the name of the new data type.
  • ConstantDeclarations is a list of constant declarations.
  • AbstractMethodDeclarations is a list of abstract methods.
  • No variables; only abstract instance methods.
  • The name of an interface follows the same conventions as the name of a class.

Do this...
Read TransactionModifier.

The methods in an interface are only promises. TransactionModifier promises that a TransactionModifier object will have modifyBalanceAfterWithdrawal(double) and modifyBalanceAfterDeposit(double) methods. These are written as abstract methods:

abstract-method-declaration pattern
public abstract returnType methodName(parameters);
  • returnType is the data type of the value that the method returns.
  • methodName is the name of the method.
  • parameters is a list of declarations of parameters.

Question #11.02 The pattern actually does not match perfectly with the abstract methods in TransactionModifier. What's missing in the actual code?
To be answered in Exercise Questions/lab11.txt.

Do this...
Change the abstract methods of TransactionModifier to match the pattern better, and compile the code.

Question #11.03 Does the compiler care?
To be answered in Exercise Questions/lab11.txt.

You were given the conventional code; your compiler might even complain after you make the change.

Do this...
Undo your change, and compile your code.

Once the methods have been promised, we want to implement subclasses in this inheritance hiearchy. They have to implement the interface:

class-declaration-implementing-interfaces pattern
public class ClassName implements InterfaceList {
}
  • ClassName is the name of the new data type.
  • InterfaceList is a comma-separate list of interface names.

Do this...
Find the implements clauses in PerTransactionCharger and MinimumBalancePenalizer.

By saying PerTransactionCharger implements TransactionModifier, we're saying that PerTransactionCharger must implement the methods promised in TransactionModifier. And, in fact, they do. This is known as overriding a method. Unlike overloading a method which allows you to reuse a method name with a different signature, overriding insists that you use the exact same signature as the method in the superclass: the same method name, the same return type, and the same parameter list.

Do this...
Change the name of the modifyBalanceAfterWithdrawal(double) method in PerTransactionCharger. Compile your code for an error.

Question #11.04 What error message does the compiler give you for the PerTransactionCharger class?
To be answered in Exercise Questions/lab11.txt.

Do this...
Change the name back.

The compiler forces you to override all of the abstract methods from the interface. This is to ensure that you actually provide behaviors for all of those promised actions.

So now the Account class can go to any of its TransactionModifiers, and ask it to modify the balance after a withdrawal. The Account class does not need to know how this is done; it just needs to know that it can and will be done.

Since you're using TransactionModifier as the element type for the myTransactionModifiers list, we call it a base class. It's the declared type of the elements; we'll actually allocate instances of the subclasses since they can actually do the computations.

Using the Inheritance Hiearchy

You need to be able to withdraw money from an account. Besides the obvious subtraction of the withdrawal amount, you also have to ask each transaction modifier to modify the balance.

Behavior of Account#withdraw(double)

I am an account. I will receive an amount to withdraw. I will set my balance to be my (old) balance minus this amount to withdraw. Then for each of my transaction modifiers, I will set my balance to be whatever the current transaction-modifier modifies it to be.

Objects of Account#withdraw(double)
Description Type Kind Movement Name
the amount to withdraw double variable in amount
my balance double instance variable instance myBalance
my transaction modifiers List<TransactionModifier> instance variable instance myTransactionModifiers
the current transaction modifier TransactionModifier variable local modifier

Specification of Account#withdraw(double):

receive: the amount to withdraw
return: nothing

Do this...
Write a method stub for Account#withdraw(double).

Operations of Account#withdraw(double)
Description Predefined? Name Library
receive the amount to withdraw yes parameter built-in
subtraction yes - built-in
loop through my transaction modifiers yes foreach loop built-in
modifying the balance after the withdrawal yes modifyBalanceAfterWithdrawal(double) TransactionModifier
Algorithm of Account#withdraw(double)
  1. Receive amount.
  2. Set myBalance to be myBalance minus amount.
  3. Foreach modifier in myTransactionModifiers:
        Set myBalance to be the withdraw of modifier and myBalance.
    End foreach.

To test this method, you will create integration tests. You will still use JUnit just as you have in the past, but you'll end up testing at least three classes (Account, PerTransactionCharger, and MinimumBalancePenalizer) and possibly two or more methods. Strictly speaking, a unit test should just test one unit (i.e., one method from one class). This is tricky to do with the Account class without getting into some advanced JUnit techniques. Integration testing will still give us good feedback, the only problem is that if one thing goes wrong with, say, the PerTransactionCharger class, you'll get failed tests in PerTransactionChargerTest and AccountTest.

Do this...

To make an account behave like a regular account (as specified in the chart at the beginning of this lab exercise), all you have to do is add the proper modifiers. You haven't written any of the monthly modifiers, and a regular account needs only one transaction modifier. So here's some likely code:

theAccount.addTransactionModifier(new MinimumBalancePenalizer(100, 8));

Incidentally, the numbers here do not match the numbers in the chart of bank accounts.

To make a withdrawal or deposit, the code would look like this:

theAccount.withdraw(500);
theAccount.deposit(25);
theAccount.withdraw(87);

To assert anything about theAccount, I would have to use theAccount.getBalance().

Do this...
Write an AccountTest#testRegularAccount() method using the example above as a starting point. Start by adding a transaction modifier so that myAccount behaves like a regular account as described in the table at the beginning of this lab. Then write statements that withdraws money out of this account. Assert the balance after each withdrawal. Be sure to be thorough in your testing (some withdrawals trigger the penalty, others don't). Compile the code, and run the tests for a red bar.

You'll be able to do more extensive testing when you can also deposit money into the account.

Do this...
Write an AccountTest#testCheckingAccount() the same way. Compile the code, and run the tests for a red bar.

Do this...
Implement Account#withdraw(double). Compile your code, and run the tests for a red bar.

Do this...
Now do all of this again for Account#deposit(double). Add deposits to the AccountTest#testRegularAccount() and AccountTest#testCheckingAccount() methods, and assert the balance of the account after each deposit.

The Bigger Picture

Now that you have this inheritance hiearchy plugged into your Account class, you can create new subclasses of TransactionModifier to add to an Account, and you won't have to recompile any of the code that now exists. If you want a per-transaction charger to charge a percentage of the balance (i.e., PercentagePerTransactionCharger), write the class, and start using it. Strictly speaking, you wouldn't even have to add new tests to AccountTest because you'll create a test-case class for the new subclass (i.e., PercentagePerTransactionChargerTest), so you'll know that that class works fine. AccountTest is really just testing to make sure that the modifiers are being activated.

Let's move on to the inheritance hierarchy for monthly fees. This hierarchy will actually have two levels.

Monthly Modifiers

At the end of each month, the age of an account needs to be incremented, and the monthly modifiers must be executed. This is so that account charges and interest can be applied.

Behavior of Account#nextMonth()

I am an account. I will increment my age. I will loop through my monthly modifiers, setting my balance to be the result of telling the current modifier that it's a new month (providing it with my age and my balance).

Objects of Account#nextMonth()
Description Type Kind Movement Name
my age int instance variable instance myAge
my monthly modifiers List<MonthlyModifier> instance variable instance myMonthlyModifiers
the current monthly modifier MonthlyModifier variable local modifier
my balance double instance variable instance myBalance
Operations of Account#nextMonth()
Description Predefined? Name Library
increment my age yes ++ built-in
loop through my monthly modifiers yes foreach loop built-in
notify the modifier of a new month no modifyBalanceForNewMonth(int age,double balance) MonthlyModifier
Algorithm of Account#nextMonth()
  1. Increment myAge.
  2. Foreach modifier in myMonthlyModifiers:
        set myBalance to be the result of using modifier to modify the balance for the new month.
    End foreach.

Do this...
Uncomment any statements in Account that relates to MonthlyModifiers. Implement Account#nextMonth().

You'll get compilation errors for MonthlyModifiers.

Do this...
Create a new interface named MonthlyModifiers.

You should now get compilation errors that MonthlyModifier does not implement the modifyBalanceForNewMonth(int,double) method that it should.

Do this...
Write an abstract method MonthlyModifier#modifyBalanceForNewMonth(int,double) which receives an age (an int) and a balance (a double). Now the code should compile, and the tests run for a green bar.

Interest Calculators

Interest is calculated at the end of a month, so these interest calculators will eventually be monthly modifiers, but let's first create the calculators themselves.

Let's compare the two types of interest calculators:

Attributes of MonthlyInterestCalculator
Description Type Name
the yearly interest rate double myInterestRate

   

Attributes of YearlyInterestCalculator
Description Type Name
the yearly interest rate double myInterestRate

Most banks will tell you the interest rate for the whole year, even if it's calculated each month, so calling this attribute "yearly interest rate" for both interest calculators isn't too much of a stretch.

We'll also consider the interest rate to be in "percentage form". That is, the yearly interest rate will be 4.7 if we want a rate of 4.7%; for computations, this will mean dividing by 100.

So the two interest calculators have a common attribute: the yearly interest rate. In order to share a common attribute, we'll have to use an actual class (as opposed to using an interface).

Let's consider this class:

Attributes of InterestCalculator
Description Type Name
the yearly interest rate double myInterestRate

Now with inheritance, we can tell Java that the MonthlyInterestCalculator and YearlyInterestCalculator should both have this attribute as well. First, as a design:

Attributes of MonthlyInterestCalculator
Description Type Name
all attributes from InterestCalculator

   

Attributes of YearlyInterestCalculator
Description Type Name
all attributes from InterestCalculator

All of the attributes from InterestCalculator should used in the MonthlyInterestCalculator and YearlyInterestCalculator.

As a UML diagram, it would look like this:

interest calculator inheritance hierarchy

Notice how the instance variable myInterestRate is specified once in the superclass; the subclasses get this instance variable for free. The accessor getInterestRate() you will also get for free in the subclasses!

The syntax for this in Java is actually quite straightforward.

Do this...

Now the declarations for the other two classes:

Do this...
Declare computation classes named MonthlyInterestCalculator and YearlyInterestCalculator.

This isn't any different from declaring classes in the past. Inheritance with classes (i.e., without an interface) is accomplished with two extra words in the declaration:

class-declaration-with-inheritance pattern
public class ClassName extends SuperclassName {
}
  • ClassName is the name of the new data type.
  • SuperclassName is the name of the class to inherit from.

The SuperclassName is the name of the class which has the common attributes in it.

Do this...
Modify both MonthlyInterestCalculator and YearlyInterestCalculator so that they both extend InterestCalculator. Your code should compile without errors.

Now, whenever you create an instance of either subclass, that instance will have an myInterestRate instance variable in it. If we think of a class as a blueprint, inheritance tells the subclass blueprints to re-use the superclass blueprint. Each subclass is now free to go beyond this common design to declare peculiar attributes if needed. For these interest calculators, there's no need, so we can move on to the abilities of these objects.

Creating Instances of a Subclass

First, we need to be able to create these calculators. You'll want to be able to create calculators like this:

new MonthlyInterestCalculator(3.0)
new YearlyInterestCalculator(12.0)

Do this...

That's one; now for the second:

Do this...

You'll get compilation errors for calling constructors which don't exist.

Do this...

Now the code should compile.

What do you normally do in a constructor? You initialize the instance variables.

Do this...
In the MonthlyInterestCalculator constructor, initialize myInterestRate to be the value of the constructor's argument.

This is not as crazy as it sounds. Yes, this instance variable is not declared in MonthlyInterestCalculator, but MonthlyInterestCalculator extends a class which does declare this instance variable.

Question #11.05 What error message does the compiler give you about the assignment statement? Write down the message exactly.
To be answered in Exercise Questions/lab11.txt.

Do this...
Delete the offending assignment statement.

Remember back to Lab #6 when we talked about private instance variables. By declaring an instance variable as private, no other class gets access to the instance variable. Not even subclasses!

This may sound really strange, because I keep on saying that the subclasses do have the myInterestRate instance variable. They do! The subclasses just don't have direct access to it. This is a good thing because we may want to enforce a variety of invariants on the variable, and maintaining invariants is much easier to do if the superclass has complete control over the variable.

But then, how can the subclasses access this instance variable which is hidden from them? Indirectly, through the superclass!

Start by declaring a constructor for the superclass:

Do this...
Declare InterestCalculator#InterestCalculator(double) to receive a yearly interest rate. Now use an assignment statement in this constructor to assign a value to myInterestRate.

There's an annoying assumption Java makes about constructors. It assumes that at the beginning of a constructor, it first invokes the default constructor from the superclass. Let's make this assumption explicit:

Do this...
Add this code as the first statement in the constructors of both subclasses:

super();

The super keyword, used in a constructor, indicates that you want to invoke a constructor from the superclass. In this case, without any arguments, you're invoking the default constructor.

Question #11.06 Write down the exact error message that the super(); statements generate.
To be answered in Exercise Questions/lab11.txt.

You've only made explicit what Java assumes; the error messages are just a bit more confusing when the compiler tries to complain about the assumptions.

The problem is that super(); invokes the default constructor of the superclass, but InterestCalculator (the superclass) doesn't have a default constructor! Instead, you should invoke the explicit-value constructor of InterestCalculator that receives the interest rate.

Do this...
Use super(interestRate) (or whatever you're parameter is called) instead of super() in the constructors. Now your code should compile fine.

Invoking superclass constructors must be the first statement in a subclass's constructor because the instance variables from the superclass must be initialized first. Otherwise bad things can happen.

There's still a question whether things are actually working out the way that they should. Do instances of the subclasses actually have their own myInterestRate instance variables? Are these instance variables actually being initialized? Test to find out!

Do this...
Write MonthlyInterestCalculatorTest#testGetInterest() and YearlyInterestCalculatorTest#testGetInterestRate() methods, and test the getInterestRate() accessor method on all four instances.

For example,

assertEquals("interest rate should be 33.0%", 33.0, myCalculator33.getInterestRate());

You don't even have to declare getInterestRate() in the subclasses, so long as you have it in the superclass.

These three classes demonstrate the two key capabilities that inheritance provides:

  1. sharing common definitions of attributes (i.e., InterestCalculator#myInterestRate) and
  2. sharing common definitions of abilities (i.e., InterestCalculator#getInterestRate()).

In both cases, the component was declared once in the superclass, and both subclasses get them for free.

Interesting Computation

This perhaps begs the question: why do we have three separate classes? So far, all you've done are the very basics for any class: constructors and accessors. The really interesting components of a class are the computational methods. It's in the the computational methods that the inheritance hierarchy will really pay off.

Let's start with YearlyInterestCalculator#modifyBalanceForNewMonth(int,double).

Behavior of YearlyInterestCalculator#modifyBalanceForNewMonth(int,double)

I am a yearly-interest calculator. I will receive the age of the account and the account's balance. If the age is a multiple of 12, I will return the balance plus (the balance multiplied by my interest divided by 100.0); otherwise, I will return the account's balance.

I divide by 100.0 because the interest rate is given in percentage form (i.e., 3.0 for 3%).

Objects of YearlyInterestCalculator#modifyBalanceForNewMonth(int,double)
Description Type Kind Movement Name
the age of the account int parameter in age
the account's balance double parameter in balance
my interest rate double instance variable instance getInterestRate()

Specification of YearlyInterestCalculator#modifyBalanceForNewMonth(int,double):

receive: the age of the account, the account's balance
return: the accrued interest

Do this...
Create a YearlyInterestCalculator#modifyBalanceForNewMonth(int,double) method stub.

Operations of YearlyInterestCalculator#modifyBalanceForNewMonth(int,double)
Description Predefined? Name Library
receive values yes parameters built-in
if... then... otherwise yes if statement built-in
age is a multiple of 12 yes mod 12 is 0 built-in
return a value yes return statement built-in
add balance and interest yes + built-in
multiply balance by my interest rate yes * built-in
divide by 100.0 yes / 100.0 built-in
Algorithm of YearlyInterestCalculator#modifyBalanceForNewMonth(int,double)
  1. Receive age and balance.
  2. If age mod 12 equals 0, then
        return balance + balance * getInterestRate() / 100.0;
    otherwise
        return balance.

Before coding this, write some tests.

Do this...
Code up YearlyInterestCalculatorTest#testModifyBalanceForNewMonth():

public void testModifyBalanceForNewMonth() {
    assertEquals("no interest on 3rd month", 1000.0,
      myCalculator4.modifyBalanceForNewMonth(3, 1000.00), 1e-3);
    assertEquals("no interest on 7th month", 1000.0,
      myCalculator4.modifyBalanceForNewMonth(7, 1000.00), 1e-3);
    assertEquals("4.0% interest on 12th month", 1040.0,
      myCalculator4.modifyBalanceForNewMonth(12, 1000.00), 1e-3);
    assertEquals("6.0% interest on 12th month", 106.0,
      myCalculator6.modifyBalanceForNewMonth(12, 100.00), 1e-3);
    assertEquals("no interest on 13th month", 100.0,
      myCalculator6.modifyBalanceForNewMonth(13, 100.00), 1e-3);
    assertEquals("6.0% interest on 24th month", 106.0,
      myCalculator6.modifyBalanceForNewMonth(24, 100.00), 1e-3);
    assertEquals("no interest on 35th month", 100.0,
      myCalculator6.modifyBalanceForNewMonth(35, 100.00), 1e-3);
    assertEquals("6.0% interest on 36th month", 106.0,
      myCalculator6.modifyBalanceForNewMonth(36, 100.00), 1e-3);
}

Compile the code, and run the tests for a red bar.

Keep in mind that the calculator keeps no history. That is, it computes a new balance based on the balance it's given. So even though myCalculator6 triggers an interest payment, it does not accumulate because that's not the responsibility of the calculator. Its responsibility is to compute a new balance based on the balance it's given in the method and based on its logic.

Accumulating the interest is the responsibility of an Account object (as computed by an interest calculator).

This test method gives a pretty thorough coverage of a variety of cases: different ages, different balances, different interests.

Do this...
Implement the YearlyInterestCalculator#modifyBalanceForNewMonth(int,double) algorithm properly. Compile the code, and run the unit tests for a green bar.

Plugging the Method into the Inheritance Hierarchy

Using the signature modifyBalanceForNewMonth(int,double) for this method was no coincidence. We'll eventually plug this inheritance hiearchy into the MonthlyModifier hierarchy.

YearlyInterestCalculator#modifyBalanceForNewMonth(int,double) explains why we need both the age and balance passed into the method. The process established in Account#nextMonth() is that all monthly modifiers are told to modify the balance each month. They have the option of not modifying the balance at all.

Interest is computed from a balance, so clearly that's information needed by a compute-interest method. Hence, the balance parameter.

The age is necessary for the modifiers that do not react every month (like the YearlyInterestCalculator).

What's lacking right now is a promise from the superclass that all interest calculators will have this method. Unlike other classes and their methods, all that can be done is give a promise for the method because we don't know what algorithm to implement for InterestCalculator#modifyBalanceForNewMonth(int,double). The issue here is responsibility. It is not the responsibility of a plain InterestCalculator to implement an algorithm; inheritance allows that responsibility to be passed on to the subclasses which can better implement the algorithms (plural!).

This is the same issue we had with TransactionModifier; we needed to promise some behaviors.

Promising a method in a superclass is done by declaring an abstract method, whether the superclass is implemented as an actual class or interface:

abstract-method-declaration pattern
public abstract returnType methodName(parameters);
  • returnType is the data type of the value that the method returns.
  • methodName is the name of the method.
  • parameters is a list of declarations of parameters.

Unlike an interface, where the abstract keyword is optional, abstract methods in an abstract class must use the abstract keyword since they're a special case in a class.

Do this...
Declare InterestCalculator#modifyBalanceForNewMonth(int,double) as an abstract method.

Question #11.07 What error message does the compiler give you because of this abstract method?
To be answered in Exercise Questions/lab11.txt.

Consider this code:

InterestCalculator calculator = new InterestCalculator(33);
assertEquals(0.0, calculator.modifyBalanceForNewMonth(5, 100.0), 1e-3);

What code should be executed for calculator.modifyBalanceForNewMonth(5, 100.0)? It depends, of course. If it were a yearly calculator, it could use the computation you just implemented; if it were a monthly calculator, it would use a different algorithm. But the code above doesn't commit itself to one of these specific interest calculators.

This is useless to us. We cannot have a general interest calculator trying to calculate interest.

To prevent this problem, Java requires that a class with an abstract method be declared as an abstract class. An abstract class cannot be constructed directly (i.e., with new). Subclasses are welcome (and still required) to invoke the superclass constructors, but you can never invoke a constructor of an abstract class directly. So the first line of this code would generate a compiler error.

abstract-class-declaration pattern
public abstract class ClassName {
}
  • ClassName is the name of the abstract class.

Do this...
Turn InterestCalculator into an abstract class.

As an abstract class, no one can create an instance of InterestCalculator, and so the problematic code above generates a compiler error on the constructor call. If one cannot create an problematic object, one cannot even try to call the problematic method.

Question #11.08 What compilation error do you get from MonthlyInterestCalculator?
To be answered in Exercise Questions/lab11.txt.

Since the subclasses want to be concrete (as opposed to abstract), you have to provide actual definitions for the method.

Do this...
Write a stub for MonthlyInterestCalculator#modifyBalanceForNewMonth(int, double). Now the code should compile, and the tests run for a green bar.

So the next trick is to write real code for this stub.

Algorithm of MonthlyInterestCalculator#modifyBalanceForNewMonth(int,double)
  1. Return balance + balance * getInterestRate() / 100.0 / 12.0.

Do this...
Write a test method MonthlyInterestCalculatorTest#testModifyBalanceForNewMonth(). Invoke the method many, many times on your instance variables in the test-case class. (Hint: you might be able to tweak YearlyInterestCalculatorTest#testModifyBalanceForNewMonth().) Compile, and run your tests for a red bar.

Do this...
Implement MonthlyInterestCalculator#modifyBalanceForNewMonth(int,double) so that the code compiles, and the unit tests run for a green bar.

Integrating It All

The last step to plugging the interest calculators into the account classes is to add the interest calculators to the monthly-modifiers hierarchy.

Do this...
Change InterestCalculator so that it implements the MonthlyModifier interface.

This will work immediately because we're already implementing the necessary method in InterestCalculator that MonthlyModifier requires.

Do this...
Uncomment Account#addMonthlyModifier(MonthlyModifier) (if you haven't already). Use this method in the test methods of AccountTest so that the regular and checking accounts have the proper interest calculators in them. Compile, and run your code for a green bar.

You haven't actually tested the interest calculators in the accounts yet. This is just to make sure that adding the monthly modifiers don't break anything you've already tested.

But the next step is to test the interest calculators:

Do this...
Add more assertions to the AccountTest#testRegularAccount() and AccountTest#testChecking() methods. Be sure to invoke Account#nextMonth() several times, and add withdraws and deposits between the nextMonth() calls.

Write a method stub for Account#nextMonth() so that the code compiles and runs for a red bar.

Do this...
Implement Account#nextMonth() so that the code compiles and runs for a green bar.

Submit

Submit copies of your code (including classes you didn't modify) and the answers to the questions. Include a sample execution of your test-case classes.

Terminology

abstract class, abstract data type, base class, base class, extends, implement (an interface), inheritance, inheritance hierarchy, inherit from, integration test, interface, override (a method), subclass, superclass