Chapter 8 – Designing Classes
Chapter Goals
-
To learn how to discover appropriate classes for a given problem
-
To understand the concepts of cohesion and coupling
-
To minimize the use of side effects
-
To document the responsibilities of methods and their callers with preconditions
and postconditions
-
To understand static methods and variables
-
To understand the scope rules for local variables and instance variables
-
To learn about packages
-
To learn about unit testing frameworks
Discovering Classes
-
A class represents a single concept from the problem domain
-
Name for a class should be a noun that describes concept
-
Concepts from mathematics:
Point
Rectangle
Ellipse
-
Concepts from real life:
BankAccount
CashRegister
Self Check 8.1
What is the rule of thumb for finding classes?
- Answer:
Look for nouns in the problem description.
Self Check 8.2
Your job is to write a program that plays chess. Might
ChessBoard be an
appropriate class? How about
MovePiece?
- Answer:
Yes (ChessBoard) and no (MovePiece).
Cohesion
-
A class should represent a single concept
-
The public interface of a class is cohesive if all of its features are related
to the concept that the class represents
-
This class lacks cohesion:
public class CashRegister
{
public void enterPayment(int dollars, int quarters, int dimes,
int nickels, int pennies)
. . .
public static final double NICKEL_VALUE = 0.05;
public static final double DIME_VALUE = 0.1;
public static final double QUARTER_VALUE = 0.25;
. . .
}
Cohesion
-
CashRegister, as described above, involves two concepts: cash
register and coin
-
Solution: Make two classes:
public class Coin
{
public Coin(double aValue, String aName) { . . . }
public double getValue() { . . . }
. . .
}
public class CashRegister
{
public void enterPayment(int coinCount, Coin coinType) { . . . }
. . .
}
Coupling
-
A class depends on another if it uses objects of that class
-
CashRegister depends on Coin to determine the value of the
payment
-
Coin does not depend on CashRegister
-
High Coupling = many class dependencies
-
Minimize coupling to minimize the impact of interface changes
-
To visualize relationships, draw class diagrams
-
UML: Unified Modeling Language
-
Notation for object-oriented analysis and design
Dependency
High and Low Coupling Between Classes
Self Check 8.3
Why is the
CashRegister class from Chapter 4 not cohesive?
- Answer:
Some of its features deal with payments, others with coin values.
Self Check 8.4
Why does the
Coin class not depend on the
CashRegister class?
- Answer:
None of the coin operations require the CashRegister class.
Self Check 8.5
Why should coupling be minimized between classes?
- Answer:
If a class doesn't depend on another, it is not affected by interface changes in
the other class.
Self Check 8.6
Is the
substring method of the
String class an accessor or a
mutator?
- Answer:
It is an accessor – calling substring doesn't modify the string
on which the method is invoked. In fact, all methods of the String
class are accessors.
Self Check 8.7
Is the
Rectangle class immutable?
- Answer:
No – translate is a mutator.
Self Check 8.8
If
a refers to a bank account, then the call
a.deposit(100)
modifies the bank account object. Is that a side effect?
- Answer:
It is a side effect; this kind of side effect is common in object-oriented programming.
Self Check 8.9
Consider the
DataSet class of Chapter 6. Suppose we add a method
void read(Scanner in)
{
while (in.hasNextDouble())
add(in.nextDouble());
}
Does this method have a side effect other than mutating the data set?
- Answer:
Yes – the method affects the state of the Scanner parameter.
Common Error: Trying to Modify Primitive Type Parameters
-
void transfer(double amount, double otherBalance)
{
balance = balance - amount;
otherBalance = otherBalance + amount;
}
-
Won't work
-
Scenario:
double savingsBalance = 1000;
harrysChecking.transfer(500, savingsBalance);
System.out.println(savingsBalance);
-
In Java, a method can never change parameters of primitive type
Common Error: Trying to Modify Primitive Type Parameters
double savingsBalance = 1000;
harrysChecking.transfer(500, savingsBalance);
System.out.println(savingsBalance);
...
void transfer(double amount, double otherBalance)
{
balance = balance - amount;
otherBalance = otherBalance + amount;
}
Common Error: Trying to Modify Primitive Type Parameters
double savingsBalance = 1000;
harrysChecking.transfer(500, savingsBalance);
System.out.println(savingsBalance);
...
void transfer(double amount, double otherBalance)
{
balance = balance - amount;
otherBalance = otherBalance + amount;
}
Common Error: Trying to Modify Primitive Type Parameters
double savingsBalance = 1000;
harrysChecking.transfer(500, savingsBalance);
System.out.println(savingsBalance);
...
void transfer(double amount, double otherBalance)
{
balance = balance - amount;
otherBalance = otherBalance + amount;
}
Common Error: Trying to Modify Primitive Type Parameters
double savingsBalance = 1000;
harrysChecking.transfer(500, savingsBalance);
System.out.println(savingsBalance);
...
void transfer(double amount, double otherBalance)
{
balance = balance - amount;
otherBalance = otherBalance + amount;
}
Call by Value and Call by Reference
Call by Value Example
harrysChecking.transfer(500, savingsAccount);
Preconditions
-
If precondition is violated, method is not responsible for computing the correct
result. It is free to do anything
-
Method may throw exception if precondition violated — more in Chapter 11:
if (amount < 0) throw new IllegalArgumentException();
balance = balance + amount;
-
Method doesn't have to test for precondition (Test may be costly):
// if this makes the balance negative, it's the caller's fault
balance = balance + amount;
Preconditions
-
Method can do an assertion check:
assert amount >= 0;
balance = balance + amount;
-
To enable assertion checking:
java -enableassertions MainClass
-
You can turn assertions off after you have tested your program, so that it runs
at maximum speed
-
Many beginning programmers silently return to the caller:
if (amount < 0)
return; // Not recommended; hard to debug
balance = balance + amount;
Syntax 8.1 Assertion
Postconditions
-
Postcondition: requirement that is true after a method has completed
-
If method call is in accordance with preconditions, it must ensure that
postconditions are valid
-
Two kinds of postconditions:
-
The return value is computed correctly
-
The object is in a certain state after the method call is completed
-
/**
Deposits money into this account.
(Postcondition: getBalance() >= 0)
@param amount the amount of money to deposit
(Precondition: amount >= 0)
*/
-
Don't document trivial postconditions that repeat the @return clause
-
Formulate pre- and postconditions only in terms of the interface of the class:
amount <= getBalance() // this is the way to state a postcondition
amount <= balance // wrong postcondition formulation
-
Contract: If caller fulfills preconditions, method must fulfill postconditions
Self Check 8.10
Why might you want to add a precondition to a method that you provide for other
programmers?
- Answer:
Then you don't have to worry about checking for invalid values – it
becomes the caller's responsibility.
Self Check 8.11
When you implement a method with a precondition and you notice that the caller
did not fulfill the precondition, do you have to notify the caller?
- Answer:
No — you can take any action that is convenient for you.
Static Methods
-
Every method must be in a class
-
A static method is not invoked on an object
-
Why write a method that does not operate on an object?
-
Common reason: encapsulate some computation that involves only numbers.
-
Numbers aren't objects, you can't invoke methods on them. E.g., x.sqrt() can never be legal in Java
-
Example:
public class Financial
{
public static double percentOf(double p, double a)
{
return (p / 100) * a;
}
// More financial methods can be added here.
}
-
Call with class name instead of object:
double tax = Financial.percentOf(taxRate, total);
Self Check 8.12
Suppose Java had no static methods. How would you use the
Math.sqrt method for computing the square root of a number
x?
Self Check 8.13
The following method computes the average of an array list of numbers:
public static double average(ArrayList<Double> values)
Why must it be a static method?
- Answer:
You cannot add a method to the ArrayList class — it is a class in the standard Java library that you cannot modify.
Static Variables
-
A static variable belongs to the class, not to any object of the class:
public class BankAccount
{
private double balance;
private int accountNumber;
private static int lastAssignedNumber = 1000;
...
}
If lastAssignedNumber was not static, each instance of BankAccount
would have its own value of lastAssignedNumber
-
public BankAccount()
{
lastAssignedNumber++; // Updates the static variable
accountNumber = lastAssignedNumber; // Sets the instance variable
}
A Static Variable and Instance Variables
Self Check 8.14
Name two static variables of the
System class.
- Answer:
System.in and System.out.
Self Check 8.15
Harry tells you that he has found a great way to avoid those pesky objects: Put
all code into a single class and declare all methods and variables
static.
Then
main can call the other static methods, and all of them can access
the static variables. Will Harry's plan work? Is it a good idea?
- Answer:
Yes, it works. Static methods can access static variables of the same class. But it
is a terrible idea. As your programming tasks get more complex, you will want to
use objects and classes to organize your programs.
Scope of Local Variables
-
Scope of variable: Region of program in which the variable can be accessed
-
Scope of a local variable extends from its declaration to end of the block that encloses it
-
Sometimes the same variable name is used in two methods:
public class RectangleTester
{
public static double area(Rectangle rect)
{
double r = rect.getWidth() * rect.getHeight();
return r;
}
public static void main(String[] args)
{
Rectangle r = new Rectangle(5, 10, 20, 30);
double a = area(r);
System.out.println(r);
}
}
-
These variables are independent from each other; their scopes are disjoint
Scope of Local Variables
-
Scope of a local variable cannot contain the definition of another variable with
the same name:
Rectangle r = new Rectangle(5, 10, 20, 30);
if (x >= 0)
{
double r = Math.sqrt(x);
// Error - can't declare another variable called r here
. . .
}
-
However, can have local variables with identical names if scopes do not overlap:
if (x >= 0)
{
double r = Math.sqrt(x);
. . .
} // Scope of r ends here
else
{
Rectangle r = new Rectangle(5, 10, 20, 30);
// OK - it is legal to declare another r here
. . .
}
Overlapping Scope
-
A local variable can shadow a variable with the same name:
-
Local scope wins over class scope:
public class Coin
{
private String name;
private double value; // Instance variable
. . .
public double getExchangeValue(double exchangeRate)
{
double value; // Local variable with the same name
. . .
return value;
}
}
-
Access shadowed variables by qualifying them with the this reference:
value = this.value * exchangeRate;
Self Check 8.16
Consider the following program that uses two variables named
r. Is this legal?
public class RectangleTester
{
public static double area(Rectangle rect)
{
double r = rect.getWidth() * rect.getHeight();
return r;
}
public static void main(String[] args)
{
Rectangle r = new Rectangle(5, 10, 20, 30);
double a = area(r);
System.out.println(r);
}
}
- Answer:
Yes. The scopes are disjoint.
Self Check 8.17
What is the scope of the
balance variable of the
BankAccount
class?
- Answer:
It starts at the beginning of the class and ends at the end of the class.
Packages
-
Package: Set of related classes
- Important packages in the Java library:
| Package |
Purpose |
Sample Class |
| java.lang |
Language support |
Math |
| java.util |
Utilities |
Random |
| java.io |
Input and output |
PrintStream |
| java.awt |
Abstract Windowing Toolkit |
Color |
| java.applet |
Applets |
Applet |
| java.net |
Networking |
Socket |
| java.sql |
Database Access |
ResultSet |
| javax.swing |
Swing user interface |
JButton |
| omg.w3c.dom |
Document Object Model for XML documents |
Document |
Organizing Related Classes into Packages
Syntax 8.2 Package Specification
Package Names
-
Use packages to avoid name clashes
java.util.Timer
vs.
javax.swing.Timer
-
Package names should be unambiguous
-
Recommendation: start with reversed domain name:
com.horstmann.bigjava
-
edu.sjsu.cs.walters for Britney Walters' classes (walters@cs.sjsu.edu)
Packages and Source Files
Self Check 8.18
Which of the following are packages?
-
java
-
java.lang
-
java.util
-
java.lang.Math
Self Check 8.19
Is a Java program without
import
statements limited to using the default and
java.lang packages?
-
Answer: No — you simply use fully qualified names for all
other classes, such as java.util.Random and java.awt.Rectangle.
Self Check 8.20
Suppose your homework assignments are located in the directory
/home/me/cs101
(
c:\Users\me\cs101 on Windows). Your instructor tells you to place your
homework into packages. In which directory do you place the class
hw1.problem1.TicTacToeTester?
- Answer:
/home/me/cs101/hw1/problem1 or, on Windows, c:\Users\me\cs101\hw1\problem1
The Explosive Growth of Personal Computers
Unit Testing Frameworks
-
Unit test frameworks simplify the task of writing classes that contain many test
cases
-
JUnit: http://junit.org
- Built into some IDEs like BlueJ and Eclipse
-
Philosophy: whenever you implement a class, also make a companion test class.
Run all tests whenever you change your code
Unit Testing Frameworks
-
Customary that name of the test class ends in Test:
import org.junit.Test;
import org.junit.Assert;
public class CashRegisterTest
{
@Test public void twoPurchases()
{
CashRegister register = new CashRegister();
register.recordPurchase(0.75);
register.recordPurchase(1.50);
register.enterPayment(2, 0, 5, 0, 0);
double expected = 0.25;
Assert.assertEquals(expected, register.giveChange(), EPSILON);
}
// More test cases
. . .
}
-
If all test cases pass, the JUnit tool shows a green bar:
Self Check 8.21
Provide a JUnit test class with one test case for the
Earthquake class
in Chapter 5.
Self Check 8.22
What is the significance of the
EPSILON parameter in the
assertEquals
method?
- Answer:
It is a tolerance threshold for comparing floating-point numbers. We want the
equality test to pass if there is a small roundoff error.