6.4 ■ Prototype 1: Representation formats
Sidebar 6.3 Dealing with exceptions
When dealing with exceptions it is important to remember that when an exception is thrown a whole set of statements are skipped between the throw statement and the matching catch state- ment. This is relatively easy to consider if the throw and catch statements are quite close, but it can be easily overlooked, becoming a serious problem, when the exception propagates through several methods.
If a method propagates an exception (generated either by the method itself or by some method it calls), all the statements from the point where it is generated to the end of the method are skipped.
Let’s consider some examples of methods operating on bank accounts.
void withdrawal(double amount){
try{
subtract(amount); // can throw exception
// in the case of an exception the following statement is skipped printConfirmation();
}catch(AmountNotAvailableException e){ //.. } }
void subtract(double amount)() throws AmountNotAvailableException {
if(amount < available)
throw new AmountNotAvailableException(); available -# amount;
//.. }
In the previous example the subtract()method throws an exception if amountis less than avail- able, therefore the subtraction of amountfrom availableis skipped.
The withdrawal() method calls subtract(), if subtract terminates normally then it prints a confirmation, otherwise the call to printConfirmation()is skipped.
This use of exceptions allows us to write the sequence of operations in the ideal case and to separate the handling of exceptional cases. It is important to consider carefully the order of operations. Let’s consider the following example:
void transfer(double amount) throws AmountNotAvailableException { destinationBankAccount.add(amount);
sourceBankAccount.subtract(amount); // can throw exception }
If no exception is thrown, the above code appears to be correct. But the subtract()method can throw an exception that is propagated to the caller of the transfer()method. When this happens the amount is credited to the destination account, but no amount is withdrawn from the source account: some money has magically appeared! The overall coherence of the accounts is lost.
To solve this problem the following code should be used instead:
void transfer(double amount) throws AmountNotAvailableException { sourceBankAccount.subtract(amount); // can throw exception destinationBankAccount.add(amount);
}
In the absence of exceptions the overall effect of the method is the same, but if an exception is thrown the add()method is skipped, preserving the overall coherence of the accounts.
As a general rule, extra attention must be paid when exceptions can be thrown in order to preserve the coherence of the data.
The methods implementing the arithmetic operations are similar: they store in operand_2the result and replace operand_1with the default value of Rational.
public class Calculator {
private Rational operand_1 # new Rational(); private Rational operand_2 # new Rational(); private Format format # new FixedPointFormat(); public void addOperand(String newOperand) throws
FormatException { Rational previous # operand_2;
operand_2 # format.parse(newOperand);
// can throw exception operand_1 # previous;
}
public void add()
operand_2 # operand_1.plus(operand_2); operand_1 # new Rational();
}
public void subtract() {
operand_2 # operand_1.minus(operand_2); operand_1 # new Rational();
}
public void multiply() {
operand_2 # operand_1.mul(operand_2); operand_1 # new Rational();
}
public void divide() {
operand_2 # operand_1.div(operand_2); operand_1 # new Rational();
}
public void delete() { operand_2 # operand_1; operand_1 # new Rational(); }
public String firstOperand(){
return format.toString(operand_1); }
public String secondOperand(){ return format.toString(operand_2); }
public void setFormat(Format newFormat){ format # newFormat;
}
public Format getFormat(){ return format;
} }
Class Command implements the command line interpreter. It has an attribute (calc) that links the interpreter to an instance of the Calculator class. The method nextCommand()reads a command and process it. If a valid command is recognized the corresponding method of the calculator is invoked. The readLine() method of the BufferedReader class can throw an IOException when some problem with the input stream occurs. We handle this exception enclosing all the method’s statements in a try–catch block. When an IOException is thrown we assume that there is no command to process, therefore all the statements are skipped and the method returns. public class Command {
Calculator calc # new Calculator();
BufferedReader lineReader # new BufferedReader( new InputStreamReader ( System.in ) ); boolean nextCommand() { System.out.print("\n[" ! calc.getFormat().getName()!"," ! calc.firstOperand() ! ", " ! calc.secondOperand() ! "] >"); try {
// reads the command from the keyboard String command # lineReader.readLine(); // executes an arithmetical operation if(command.equals("!")) calc.add();
else if(command.equals("-")) calc.subtract(); else if(command.equals("*")) calc.multiply(); else if(command.equals("/")) calc.divide(); else if(command.equals("rat")) calc.setFormat(Format.rational); else if(command.equals("fixed")) calc.setFormat(Format.fixedPoint); else if(command.equals("float")) calc.setFormat(Format.floatingPoint); else if(command.equals("del")) calc.delete(); else if(command.indexOf("op") ># 0) try{ calc.addOperand(command.substring(2).trim()); }
catch(FormatException e){
System.out.println("Wrong operand: " ! e.toString()); }
else if(command.equals("help")) printHelp();
else if(command.equals("exit")) return false;
else
System.out.println("Error! Not a valid command"); }
catch(IOException ioe) { ioe.printStackTrace(); } return true;
}
void printHelp() { System.out.println(); System.out.println
("Insert one of the following commands:"); System.out.println
(" op <number> (store an operand)"); System.out.println
(" ! (sum the last two operands)"); System.out.println
(" - (subtract the last two operands)"); System.out.println
(" * (multiply the last two operands)"); System.out.println
(" / (divide the last two operands)"); System.out.println
(" dec (switch to base 10)"); System.out.println
(" bin (switch to binary base)"); System.out.println
(" hex (switch to hexadecimal base)"); System.out.println
(" fixed (switch to fixed point format)"); System.out.println
(" float (switch to floating point format)"); System.out.println
(" rat (switch to rational format)"); System.out.println
(" del (remove last operand)"); System.out.println
(" help (print this command list)"); System.out.println
(" exit (terminate execution)"); System.out.println();
}
public static void main(String[] args) { Command command # new Command(); while(command.nextCommand()); }
} }
6.4.4 Test
We have two main components in the architecture and we test them sepa- rately. Table 6.4 describes a test case; it presents a list of commands issued to the calculator, the resulting values of the first and second operands and the format being used.
First we test the calculator component invoking the methods of class Calculator directly. The following test method implements the first three lines of Table 6.4.
public class TestCalculator extends TestCase { //...
public void testOperations(){
Calculator calc # new Calculator(); try{ calc.addOperand("3.2"); assertEquals("0.0",calc.firstOperand()); assertEquals("3.2",calc.secondOperand()); calc.addOperand("2.8"); assertEquals("3.2",calc.firstOperand()); assertEquals("2.8",calc.secondOperand()); calc.add(); assertEquals("0.0",calc.firstOperand()); assertEquals("6.0",calc.secondOperand()); }catch(FormatException e){
fail("Unexpected format exception"); }
} }
The same test case is used to test the command line component. There are two significant differences from the previous test: first, the command line component takes the input from the standard input and writes the Table 6.4 Test case
Command First operand Second operand Format
Op 3.2 0.0 3.2 Fixed point Op 2.8 3.2 2.8 Fixed point ! 0.0 6.0 Fixed point Op 1 6.0 1.0 Fixed point 0 0.0 5.0 Fixed point Op 2 5.0 2.0 Fixed point ( 0.0 10.0 Fixed point Op 111 10.0 111.0 Fixed point 0.0 0.09009009009009009 Fixed point Rat 0.01.0 10.0111.0 Rational
output to the standard output; second, the output may contain extra spaces or text in addition to the values of the operands.
The simplest solution is to redirect the standard input and output streams (System.inand System.out). The input can be read from a string by means of class StringBufferInputStream. The output produced by the program can be recorded into a string buffer by means of class ByteArrayOutputStream.
The option of performing an exact string match is not feasible because a minor change in the output (addition of a single white space) would break the test. A better option is to check the output for the presence of specific substrings. In addition it is useful to verify that the substrings appear in a given order within the output. In the test class TestCommandthe purpose of the method assertContainsInOrder()is to verify that the output string contains all the elements of a string array in the correct order.
The following test class implements the first three lines of the test case described in Table 6.4. It is the text-oriented version of the TestCalculator test presented before.
public class TestCommand extends TestCase { //...
public void testCommand(){
String input # "op 3.2\n" ! "op 2.8\n" ! "!\n" ! "exit\n";
// redirect the standard input to read // from the input string
InputStream oldIn # System.in;
System.setIn(new StringBufferInputStream(input)); // redirect the standard output to write
// into a string buffer
PrintStream oldOut # System.out; ByteArrayOutputStream buffer #
new ByteArrayOutputStream(); System.setOut(new PrintStream(buffer));
// create a command object and run it Command c # new Command();
c.run(); Decision point
How is it possible to test a program that uses standard input and output?
Decision point
// restore original standard input and output System.setOut(oldOut);
System.setIn(oldIn);
// copy the output into a string String output # buffer.toString(); String[] expected#{ "0.0","3.2","3.2",
"2.8","0.0","6.0" }; // verify the output contains the expected strings assertContainsInOrder(output,expected);
}
protected void assertContainsInOrder(String output, String[] strings){ int lastIndex # -1;
int currentIndex # -1;
for (int i # 0; i < strings.length; i!!) {
currentIndex # output.indexOf(strings[i],lastIndex!1); assertTrue("Couldn’t find [" ! i ! "]:"! strings[i],
currentIndex>-1); lastIndex # currentIndex; }
} }
6.5
■
Prototype 2: Number bases
The focus of this prototype is on the introduction of different number bases to the calculator.
6.5.1 Analysis
The main feature that is added in this development iteration is the capabil- ity of handling different number bases. Analogous with what we’ve done with the representation formats, we can represent the concepts involved in this issue as a set of related classes. The result of this analysis is presented in Figure 6.7 as a class diagram.
We start from the generic concept of number base; it involves the capa- bility of parsing operands in a given base and printing the result in a given base. This abstract concept can be separated into three concrete number bases: decimal, binary and hexadecimal. Each number base is characterized by the base in the proper sense (e.g. 2 for the binary base) and by the set of digits that can be used (e.g. 0 and 1 for the binary base).
The command line must therefore accommodate new commands: to manage number bases and to read commands from a file. The new commands are described in Table 6.5.
The command line interpreter must provide the user with information about the current base and representation format. This is a simple extension of the information provided by the interpreter in the previous prototype.
6.5.2 Design
The design of this prototype is basically an extension of the previous one with the addition of the classes required to handle multiple number bases.
The structure of the classes identified during analysis is quite similar to the classes identified in the previous prototype, which were related to representation formats. Because of this analogy we decide to adopt a similar approach and to represent the base in use by the calculator through an object of type Base. The resulting class diagram is shown in Figure 6.8.
The class Base defines the interface and basic behaviour, which are common to all the concrete base classes, DecimalBase, BinartyBase and HexBase.
There is an important difference between the number base related class hierarchy and the representation format related one. Class Base can use generic algorithms to convert between a string and a number; therefore the subclasses need only define the parameters for such algorithms. In the case of format the separation is achieved by defining a specific behaviour in the con- crete subclasses. In the case of the base, it is achieved by customizing the generic algorithms defined in the abstract class.
The sequence of calls to add a new operand to the calculator is described in Figure 6.9. The command class calls the addOperand() method to add a Figure 6.7 Analysis of number bases
Base 10 Digits 0123456789 Decimal Base Base 2 Digits 01 Binary Hexadecimal Base 16 Digits 0123456789ABCDEF
Table 6.5 Additional commands Command Description
Dec Switch to base 10
Bin Switch to binary base
new operand (1). The Calculatorinvokes the parse()method on the Format object and passes the Baseobject as a parameter (2). Thus Formatis able to call the parse()method on the base to convert the number using the given number base (3). Finally the method parse()of Formatcreates a new Rational object.
6.5.3 Implementation
Class Baseimplements the generic concept of number base, it provides two essential methods: parse()converts a string representation of a number into a double, toString()does the opposite. The former is based on Equation 6.1, while the latter uses Equation 6.2 and Equation 6.4. These methods can be customized using two parameters: the base and the digits; they are stored in the homonymous attributes and are initialized through the constructor. Figure 6.8 Calculator with format and base classes
FractionalFormat <<abstract>> Format FixedPointFormat FloatingPointFormat DecimalBase <<abstract>> Base BinaryBase HexBase
Calculator operand_1 Rational
operand_2 base
format
Figure 6.9 Collaboration for adding a new operand
1 : addOperand()
command :
Command calc : Calculator : Format
: Rational calc format 2 : parse() 4 : new : Base 3 : parse() operand_1 base
public abstract class Base { private String name; private int base; private String digits;
static final int MAX_PRECISION # 10;
// max number of decimal ciphers double EPSILON; // smallest representable number Base(String name, int base, String digits){
this.name # name; this.base # base; this.digits # digits;
EPSILON # Math.pow(base,-MAX_PRECISION); }
public String getName() { return name; } public int getBase() { return base; } double parse(String number) {
// decodes the sign double sign # 1.0; if(number.charAt(0) ## '-'){ sign # -1.0; number # number.substring(1).trim(); }else if(number.charAt(0) ## '!'){ sign # 1.0; number # number.substring(1).trim(); }
// parses the integer part and the decimal part int power;
int index # number.indexOf('.'); if(index ># 0)
power # index-1; else
power # number.length()-1; double result # 0.0;
double mult # Math.pow(base,power); // decodes the integer part
for(int i # 0; i < number.length(); i!!) if(number.charAt(i)!#'.'){
result !# mult * digits.indexOf(number.charAt(i)); mult /# base;
}
return result * sign; }
String toString(double number) { if(number ## 0.0) return "0";
StringBuffer result#new StringBuffer(); if(number<0){
result.append('-'); number # -number; }
int i;
int power # (int)Math.floor(Math.log(number!EPSILON/2) /Math.log(base)); if(power<0) power # 0;
double divider # Math.pow(base,power); int num_digits # 0;
double divResult, cipher; for(int i # power;
(number>EPSILON && num_digits<MAX_PRECISION) || i ># -1; —i){
divResult # number / divider;
cipher # Math.floor((number!EPSILON/2) / divider); if(divider < 1.0){
num_digits!!;
if(num_digits##1) result.append('.'); }
result.append(digits.charAt((int)cipher)); number -# cipher * divider;
divider /# base; }
return result.toString(); }
}
The Base class provides almost everything required to define a number base. The only thing left to be specified is the base together with the digits. The concrete number bases have only to provide these two parameters in addition to the name of the base.
public class BinaryBase extends Base{ public BinaryBase() {
super("bin", 2 ,"01"); }
}
public class DecimalBase extends Base { public DecimalBase() {
super("dec", 10 ,"0123456789"); }
}
public class HexBase extends Base { public HexBase() {
super("hex", 16 ,"0123456789ABCDEF"); }
In the Commandclass it is sufficient to introduce three new elements in the command selection chain in order to handle the three different bases:
public class Command { boolean nextCommand() {
// ... try {
// reads the command from the keyboard
if(command.equals("!") || command.equals("-") ... ... else if(command.equals("dec")) calc.setBase(new DecimalBase()); else if(command.equals("bin")) calc.setBase(new BinaryBase()); else if(command.equals("hex")) calc.setBase(new HexBase()); }
catch(IOException ioe) { ioe.printStackTrace(); } return true;
} }
6.5.4 Test
The test of the multiple number bases must be conducted together with the test of the representation format. The goal of the test is to check whether numbers are represented correctly using all possible combinations of base and format.
Class TestFormat is a simple test that verifies the conformance of the program with the example presented in Table 6.2.
public class TestFormat extends TestCase { //...
public void testFormatBase(){
Calculator calc # new Calculator(); try { calc.addOperand("0.75"); assertEquals("0.75",calc.secondOperand()); calc.setBase(new BinaryBase()); assertEquals("0.11",calc.secondOperand()); calc.setBase(new HexBase()); assertEquals("0.C",calc.secondOperand()); calc.setFormat(new FloatingPointFormat()); assertEquals("C.0*10^-1.0",calc.secondOperand()); calc.setBase(new BinaryBase()); assertEquals("1.1*10^-1.0",calc.secondOperand()); calc.setBase(new DecimalBase()); assertEquals("7.5*10^-1.0",calc.secondOperand());
calc.setFormat(new RationalFormat()); assertEquals("3.0/4.0",calc.secondOperand()); calc.setBase(new BinaryBase()); assertEquals("11.0/100.0",calc.secondOperand()); calc.setBase(new HexBase()); assertEquals("3.0/4.0",calc.secondOperand()); } catch (FormatException e) { fail("Unexpected exception"); } } }
6.6
■
Extension
The reader can extend the application presented in this chapter in several ways:
■ The use of only two operands in the calculator represents a limitation when complex expressions are needed. A more general solution consists of using a stack: each time an operand is added it is pushed onto the stack; when an operation is selected the operands are popped from the stack and the result is pushed back onto the stack.
■ The calculator described here is able to handle only constant numeric values. A powerful extension consists of adding the capability of dealing with variables. In this case the calculator must manage symbolic expres- sions: expressions must be stored using an internal format (an abstract syntax tree). In addition a table must contain the values of the variables that are used to compute the value of symbolic expressions.
■ A graphical user interface can be developed to provide a user friendly interaction with the calculator.
6.7
■
Assessment
Analysis techniques. We analysed the problem domain and identified the main concepts; since they are correlated we abstracted concepts into more general ones.
Modelling techniques. In this chapter we introduced use case diagrams and collaboration diagrams. In addition we used inheritance to factorize common characteristics into base classes.
Development approach. The development of the multi-format calculator highlighted two important issues:
■ dynamic behaviour and polymorphism; and
■ exception handling.
Handling a dynamic behaviour is a common problem and the solution adopted in this system is largely reusable. Actually it is an instance of the
well-known Strategy pattern (Gamma et al. 1995). In addition, we have seen how exceptions can help in dealing with abnormal situations and how they allow a more readable and clean handling of errors.
Idiom Class generalization
Context Many classes have common features.
Problem The replication of the same features makes classes less understandable and maintainable.
Forces or tradeoffs The same attribute or method is repeated many times in different classes.
Some operations are common to many classes. Solution Create a superclass of all the existing similar classes.
Migrate the common characteristics into the superclass. Force resolution Common features are not replicated. The superclass
becomes the single point of change. Derived classes are simpler. Code reuse is enforced. Gathering common features makes the structure more comprehensible. Design rationale We find commonalities among existing classes and make
them into a single class.
Pattern Strategy
Context A class embeds a behaviour that can be chosen andor changed at run time.
Problem Switching the behaviour dynamically.
Lets the behaviour vary independently from clients that use it.
Forces or tradeoffs The clients need a stable interface. The behaviour must be changeable.
The multiple behaviours must be evident from the structure of the classes.
Solution Define a common interface for all the different behaviours (Strategy). For each behaviour define a class that
implements the interface (ConcreteStrategyA,
ConcreteStrategyB, ...). The behaviours are used by a client that defines the context within which they operate (Context). To select a behaviour an object of one of the behaviour classes can be linked to the Context.
!operation() ConcreteStrategyA !operation() <<interface>> Strategy Context !operation() ConcreteStrategyB !strategy
Examples To implement different representation formats for numbers an abstract interface can be declared. It defines two methods that can be used to convert between String
and double:
interface Format {
String toString(double number);
double parse(String number); }
Then we can define as many concrete behaviours as there are formats. For instance the concrete behaviour for the floating point format can be implemented as:
class FloatingPointFormat implements Format { String toString(double number){
// convert to a floating point // formatted string
}
double toDouble(String number) { // parse a floating point formatted // string
} }
The client that wants to perform a conversion uses the abstract interface:
void printNumber(double number, Format format){ System.out.println
(format.toString(number)); }
The format actually adopted depends on the effective class of the object passed as argument format.
Force resolution The clients always access the behaviour through the same invariable interface.
The behaviour varies according to the effective class of the format object.
The inheritance hierarchy makes the different behaviours explicit.
Design rationale This pattern leverages the polymorphism and dynamic binding. The Contextcan be linked to any class that implements the Strategyinterface; when it invokes one of the operations, the one defined in the actual class of the object is activated.
6.8
■
Reference
Gamma, E., Helm, R., Johnson, R. and Vlissides, J. (1995) Design Patterns: Elements