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.
Do this...
edu.institution.username.hotj.lab11a
Exercise Questions/lab11a.txt.Design/lab11a.txt.Account.TransactionModifier,
PerTransactionCharger,
PerTransactionChargerTest,
MinimumBalancePenalizer,
MinimumBalancePenalizerTest.Do this...
The code you get should be good to go. Run the unit tests for a green bar.
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 | Account Charge | Transaction charge |
Interest | Minimum balance & penalty |
Special |
|---|---|---|---|---|---|
| Regular account | $10 or 10% | none | none | $2 if <$50 | none |
| Interest account | $5 or 5% | none | 7%, monthly | none | none |
| Checking account | $10 or 10% | $0.10 | 7%, monthly | $6 if <$100 | none |
| CD account | 0 | 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.
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:
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 AccountDescription Type Name the balance of the account doublemyBalancethe name of the account holder StringmyNamethe age of the account (in months) intmyAgetransaction modifiers ???? myTransactionModifiersmonthly 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 AccountDescription Type Name the balance of the account doublemyBalancethe name of the account holder StringmyNamethe age of the account (in months) intmyAgetransaction modifiers List<TransactionModifier> myTransactionModifiersmonthly modifiers List<MonthlyModifier> myMonthlyModifiers
Do this...
Account
class.MonthlyModifier class yet; comment out the declaration
of myMonthlyModifiers for now.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.
myTransactionModifiers to be an empty
ArrayList<TransactionModifier>; do the same for
myMonthlyModifiers, but comment this out.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.
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.
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
for the relationship between the
classes:
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.
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 PerTransactionChargerDescription Type Name the amount to charge per transaction doublemyPerTransactionCharge
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 doublevariable in balancea new balance doublevariable out newBalancemy per-transaction charge doubleinstance 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 returnstatementbuilt-in
Algorithm of PerTransactionCharger#modifyBalanceAfterWithdrawal(double)
- Receive
balance.- Let
newBalancebebalanceminusmyPerTransactionCharge.- 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).
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)
- Receive
balance.- 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 MinimumBalancePenalizerDescription Type Name my minimum balance doublemyMinimumBalancemy penalty doublemyPenalty
So back to the method:
Objects of MinimumBalancePenalizer#modifyBalanceAfterWithdrawal(double)Description Type Kind Movement Name the current balance doublevariable in balancemy minimum balance doubleinstance variable instance myMinimumBalancemy penalty doubleinstance variable instance myPenalty
Operations of MinimumBalancePenalizer#modifyBalanceAfterWithdrawal(double)Description Predefined? Name Library receive the balance yes parameter built-in if... then... otherwise yes ifstatementbuilt-in balance is less than my minimum balance yes <built-in subtraction yes -built-in return a value yes returnstatementbuilt-in
Algorithm of MinimumBalancePenalizer#modifyBalanceAfterWithdrawal(double)
- Receive
balance.- If
balanceis less thanmyMinimumBalance, then
returnbalance-myPenalty,
otherwise
returnbalance.
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.
PerTransactionCharger is a
TransactionModifier. So if the Account
asks for a TransactionModifier and I give it a
PerTransactionCharger, it will be happy.MinimumBalancePenalizer is a
TransactionModifier. So if the Account
asks for a TransactionModifier and I give it a
MinimumBalancePenalizer, it will be happy.In order for this to work, the superclass must promise the common methods.
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
|
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
|
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
|
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.
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 doublevariable in amountmy balance doubleinstance variable instance myBalancemy transaction modifiers List<TransactionModifier>instance variable instance myTransactionModifiersthe current transaction modifier TransactionModifiervariable 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)
- Receive
amount.- Set
myBalanceto bemyBalanceminusamount.- Foreach
modifierinmyTransactionModifiers:
SetmyBalanceto be thewithdrawofmodifierandmyBalance.
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...
AccountTest.myAccount.setUp() method which creates a instance
for the instance variable; use whatever values you like for the
balance, name, and age.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 usetheAccount.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...
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.
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.
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 intinstance variable instance myAgemy monthly modifiers List<MonthlyModifier>instance variable instance myMonthlyModifiersthe current monthly modifier MonthlyModifiervariable local modifiermy balance doubleinstance 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()
- Increment
myAge.- Foreach
modifierinmyMonthlyModifiers:
setmyBalanceto be the result of usingmodifierto 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 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:
|
|
|
||||||||||||||||||
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 InterestCalculatorDescription Type Name the yearly interest rate doublemyInterestRate
Now with inheritance, we can tell Java that the
MonthlyInterestCalculator and
YearlyInterestCalculator should both have this
attribute as well. First, as a design:
|
|
|
||||||||||||||||||
All of the attributes from InterestCalculator
should used in the MonthlyInterestCalculator and
YearlyInterestCalculator.
As a UML diagram, it would look like this:
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...
InterestCalculator with its one instance
variable.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
|
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.
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...
MonthlyInterestCalculatorTest which has two
MonthlyInterestCalculator instance variables:
myCalculator3 for a 3% interest rate, andmyCalculator5 for a 5% interest rate.MonthlyInterestCalculatorTest#setUp()
method which initializes the two instance variables
appropriately.That's one; now for the second:
Do this...
YearlyInterestCalculatorTest which has two
YearlyInterestCalculator instance variables:
myCalculator4 for a 4% interest rate, andmyCalculator6 for a 6% interest rate.YearlyInterestCalculatorTest#setUp()
method which initializes the two instance variables
appropriately.You'll get compilation errors for calling constructors which don't exist.
Do this...
MonthlyInterestCalculator#MonthlyInterestCalculator(double)
to receive a yearly interest.YearlyInterestCalculator#YearlyInterestCalculator(double)
to receive a yearly interest.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( (or
whatever you're parameter is called) instead of
interestRate)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:
InterestCalculator#myInterestRate) andInterestCalculator#getInterestRate()).In both cases, the component was declared once in the superclass, and both subclasses get them for free.
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 intparameter in agethe account's balance doubleparameter in balancemy interest rate doubleinstance 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 ifstatementbuilt-in age is a multiple of 12 yes mod 12 is 0 built-in return a value yes returnstatementbuilt-in add balance and interest yes +built-in multiply balance by my interest rate yes *built-in divide by 100.0 yes / 100.0built-in
Algorithm of YearlyInterestCalculator#modifyBalanceForNewMonth(int,double)
- Receive
ageandbalance.- If
agemod 12 equals 0, then
returnbalance+balance*getInterestRate()/ 100.0;
otherwise
returnbalance.
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);
}
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.
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
|
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
|
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)
- 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.
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.
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.
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