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 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
Student
s 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...
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 ArrayList
s.
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 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
statementbuilt-in
Algorithm of PerTransactionCharger#modifyBalanceAfterWithdrawal(double)
- Receive
balance
.- Let
newBalance
bebalance
minusmyPerTransactionCharge
.- 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 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
statementbuilt-in balance is less than my minimum balance yes <
built-in subtraction yes -
built-in return a value yes return
statementbuilt-in
Algorithm of MinimumBalancePenalizer#modifyBalanceAfterWithdrawal(double)
- Receive
balance
.- If
balance
is 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 PerTransactionCharger
s and
MinimumBalancePenalizer
s; it will worry about the
abstraction: TransactionModifier
.
Externally, when I create an Account
for a
particular type of account, then I provide
PerTransactionCharger
s and
MinimumBalancePenalizer
s.
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
TransactionModifier
s, 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 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)
- Receive
amount
.- Set
myBalance
to bemyBalance
minusamount
.- Foreach
modifier
inmyTransactionModifiers
:
SetmyBalance
to be thewithdraw
ofmodifier
andmyBalance
.
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 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()
- Increment
myAge
.- Foreach
modifier
inmyMonthlyModifiers
:
setmyBalance
to be the result of usingmodifier
to modify the balance for the new month.
End foreach.
Do this...
Uncomment any statements in Account
that relates to
MonthlyModifier
s. 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 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:
|
|
|
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 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
statementbuilt-in age is a multiple of 12 yes mod 12 is 0 built-in return a value yes return
statementbuilt-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)
- Receive
age
andbalance
.- If
age
mod 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