Keywords: signature, export, assertion, STRING, debugging
Reusable software is based on the idea that you never re-write code: you just use it in different ways, and add code as necessary. The most important technique for designing reusable code is to design for reuse from the very beginning, and write a solution for future users, not just for the current use. Reuse will fail if you design a minimal solution to the current problem. Some basic principles for designing reusable software are presented in this chapter, and illustrated with the Eiffel library class STRING. The advantages of reuse principles to avoid bugs in the first place, and recover from any bugs that do arise, are then discussed.
Reuse is made possible by the definition of a clear, precise object interface. You do not need to know how an object works to use that object, all you need to know is how it behaves. To use a television set, for example, you need to know how to turn the set on, change channels, change volume, and so on. To use a computer you type on the keyboard and see the effects on the screen. To use a car you need to know how to turn it on, how to steer, and how to accelerate and brake. In no case do you actually need to know what happens "inside the box". Few of us understand the technology of signal transmission, reception, and transformation but we all know how to watch TV.
Any discipline develops a set of standard solutions, and over time one of these standards comes to predominate: driving on the left versus right, VHS versus BetaMax, IBM clone versus Macintosh, and so on. You can buy any CD player and it will play any CD. The player and the CD were probably made by different organisations, probably in different countries, but that is not a problem because their interaction has been standardised. The definition of a standard interface allows us to "plug and play", and any differences are hidden behind the interface.
Eiffel was designed to support reuse, so it has a set of strategies for defining a standard interface. The interface to, or external appearance of, a routine is defined by five things:
The interface to a class is the set of features that can be used by a client of that class; formally, by the set of exported features. The external features of a class can be found by running a system tool named short on a class, to show the standard interface to that class.
The signature of a feature is a list of the types that are passed to, and returned by, a feature. The signature of a routine can be read directly from the routine header, because the header lists the received types in its argument list, and the returned type for a function. A routine can be identified as a function or a procedure purely by its signature, because a function returns a value and a procedure does not. The routine header and the routine signature contain the same type information, but the header also contains other information.
A signature is usually written as a set of input types, followed by a semi-colon, followed by the output type if any, so it has the form < input types; output type>. The signature of the routine sqrt, for example, is <REAL; REAL>. In system design, much of the code can be written without thinking much about the implementation, by defining the routine headers and leaving the bodies empty. This defines the interaction between routines in the system, with no internal detail.
The class ACCOUNT in the first part of the case study contains
12 features. Two of the features are attributes: one variable attribute
(balance) and one constant attributes (the interest rate).
Six of the features are basic procedures, to set and show the balance,
to show the rate, to deposit and withdraw money, and to add interest. Two
high level routines are defined to make and show the account; these call
the basic routines. and to deposit, withdraw, and add interest to the account.
The remaining features are two functions, that return the daily interest
rate and the interest to be added to the account each day. The header for
each feature is shown to the left below, and the signature is shown to
the right:
balance: REAL is | < - ; REAL > |
set_balance is | < - ; - > |
show_balance is | < - ; - > |
rate: REAL is | < - ; REAL > |
show_rate is | < - ; - > |
make is | < - ; - > |
show is | < - ; - > |
deposit (amount: REAL) is | < REAL ; - > |
withdraw (amount: REAL) is | < REAL ; - > |
add_interest is | < - : - > |
interest: REAL is | < - ; REAL > |
day_rate: REAL is | < - ; REAL > |
The feature header and the signature show the types of the input and output values for the feature. The signature may be defined before the routine is coded and thus define the external behaviour of the routine; design then consists of implementing the behaviour in code. If the correct way to divide a task into parts is not known initially, the code may be written first and then wrapped in a routine to define the signature and support reuse. In either case, the signature defines the precise, external appearance of the routine, and provides the clear division between internal and external that is essential for the design of reusable software.
5.3 Behaviour versus implementation
There are really only two types of behaviour, queries and commands. A query returns information about the state of the object, where a command changes the state of an object. The behaviour of an object is defined by the commands and queries that the object provides or supplies. This approach can be illustrated by thinking of an object as a big, black box with two sets of buttons, "query buttons" and "command buttons". If you push a query button, an indicator lights up on the button and gives you some information about the internal state of the machine. Pushing the button does not change the value, so if you push the button ten times in a row, you'll get the same answer each time (unless a command has changed the state in between queries). On the other hand, when you push a command button, the machine starts screeching and clicking but you do not get any information about what is happening inside the box. When the machine stops and you push a query button, the answer you get will usually be different from the answer you had before the command was done. The machine has changed state.
A procedure changes one or more attribute values, and returns nothing. The attribute values define the state of an object, so a command is implemented by a procedure.
A function returns a value and changes nothing, so it is a query. An attribute also returns a value and nothing is changed by getting an attribute value, so an attribute also behaves like a query. A query can thus be implemented by a function or by an attribute. When a value is returned from a query, the value may have been stored in the object as an attribute, or computed by a function. An attribute behaves identically to a function with no arguments; from the outside, it is impossible to tell if a returned value was stored or computed. In the ACCOUNT class, for example, the interest rate was stored as a constant and the daily interest rate was calculated. The daily interest rate could have been stored as an attribute, and the code would behave identically. A function was used because there is a data dependency between the yearly and daily interest rates, so storing two separate figures is error-prone; if the yearly rate changes, the daily rate should change accordingly.
It is interesting to compare the two views of a class, from the outside and from the inside. From the outside, all we see is behaviour and the distinction is between command and query; this view is shown to the left below. From the inside, we see that behaviour is implemented as data or routines; this view is shown to the right below.
A feature of a class is either a command or a query. It can be implemented as an attribute, a function, or a procedure. The behaviour of the class is more important than its implementation, so all features are indented equally in the class listing.
Any feature of an object can always be called from inside that object.
Only some features can be seen and used outside of their class, however;
we call these exported features. Each feature in a class has an export
policy that defines whether it can be seen by a client of the class.
The export policies of all the features thus define the external behaviour
of the class. The export policy is defined when the feature is defined,
by writing one or more class names in curly brackets after the feature
keyword preceding the feature definition. The named client classes can
then use the exported features. If no names are placed after the feature
keyword, then any class can use the features. The main export policies
that can be set on a feature are
Export clause | Meaning |
feature | exported to all classes |
feature {ANY} | exported to all classes |
feature {X, Y, Z} | exported to classes X, Y, Z |
feature {} | exported to no class |
feature {NONE} | exported to no class |
Common error: feature is not exported to client
Error code: VUEX (2)
Error: feature of qualified call is not available to client class.
What to do: make sure feature after dot is exported to caller.
The creation clause may also contain an export policy to list the classes that can call the routine as a creation routine. The format of the export policy is the same as for feature:
A class can specify several creation routines under the single creation keyword. There may be several creation clauses, if the designer wants a different export policy for each creation routine. The creation and the export status of a routine are independent, so a routine can be called as a creator (using !!) or as a normal feature (no !!). It is even possible (though unusual) to export the creation status (creation) to one class and the non-creation status (feature) to another.creation {A, B} make
Common error: creation routine is not available to client
Error code: VGCC (6)
Error: Creation instruction uses call to improper feature.
What to do: Make sure that feature of call is a creation procedure, is not 'once', and is available for creation to enclosing class.
A class is designed around the data that it contains, but is defined by its behaviour. The actual way that the data is stored or implemented is usually hidden, and only the behaviour is exported. This allows us to change the way that the data is stored, without affecting the behaviour of the class. If the attributes of the class are exported, then every time an attribute changes, the class and its users have to be changed; a simple change can thus have large effects on the system. In order to insulate as much of the system as possible from change, the attributes are normally hidden. It is possible to export an attribute, and in fact it is often necessary to export an attribute, but this should be avoided; it makes the code less reusable. It forces the client to know about the exact form of the data, and the client as well as the supplier has to be amended when the representation changes.
If you absolutely have to export an attribute, then simple export it. Do not write a function that returns the attribute value and export the function. Seen from the outside, there is no difference between these methods. Seen from the inside, the function takes more code, is less easy to read, and is more likely to trip you up if the attribute ever changes.
Exported features provide a way to group the feature definitions in a class. Exported features define the class interface, so some programmers prefer to make the interface clearly visible in the code by listing all the exported features first, then all the hidden or private features. This view of the system is provided by a class diagram (see section 5.6) or a short listing (see section 5.9). Dividing the listing into public and private features makes the calling structure of the system very hard to follow, however; the reader cannot predict where a routine is likely to occur in the class listing, and has to search through pages of code to find a feature definition.
The call structure provides another way to organise a code listing. If the designer decides that calling order is most important, then each exported routine is followed by its hidden routines, in calling order. The location of a called feature definition is then easily predicted, and easily found by looking down the listing from the call. The listing tends to look cluttered and the public features are hard to find, however, because each change (from exported to private and back) needs a new feature keyword and policy.
No convention on listing order has yet become standard. This book places the attributes and their get, set, use, and show routines at the top of the class; both the attributes and their basic routines are usually private, and it is clear where to search for code that gets and shows a value. Often, the remaining features are all exported so both the class interface and the called routines are easy to find in the listing. Where an exported routine calls a private routine, they are listed in calling order to help the reader (and designer) of the code. Utility routines, that are called in many places in a class, can be listed at the end of a class because they are usually simple and are tested (called) often, so they should be correct and are seldom examined.
The classes in a system are listed in client order.
The basic relation between two classes is that of client-supplier. A supplier class provides a set of services that are used by the client class. The client declares, creates, and uses objects of the supplier type. Formally, a client relation is defined by a declaration of type SUPPLIER in the client class. A client chart is used to make the structure of the system clear, without a great amount of detail. Each class in the chart is shown by the name of the class enclosed in an oval. A line is drawn from the client to the supplier, from left to right on the page, to show the client links and reveal the overall structure of the system. Note that this chart does not show the objects in the system, just the classes. The code in one class may create 10,000 objects of another class, but this defines only a single client-supplier relationship.
Showing every declared type on a diagram would make the diagram hard to read and thus defeat the purpose of the diagram, so three things are not shown in a client chart:
A class diagram is used to describe the structure of a class. The standard form of a class diagram (Booch, 1994; Coad and Yourdon, 1990; Rumbaugh et al., 1991) is a box with round corners, divided into three parts. At the top of the box is the class name. The name of each attribute in the class is then listed, followed by the name of each routine. This notation can be extended to provide more detail by including the type of each attribute, and the argument list (if any) and the returned type (if any) for each routine.
A class diagram does not quite match the way Eiffel works, because it shows all the attributes and all the routines of the class. First, Eiffel makes a strong distinction between behaviour and implementation, so strong that from outside the class you cannot tell if a query is implemented as an attribute or as a function. Second, it is good software engineering practice to hide as many of the attributes as possible. A standard class diagram does not show the interface to the class; it shows the implementation.
I have adapted the standard notation to be slightly more Eiffel-like, by showing the signatures and only the exported features. A class diagram for each class in the simple banking system is shown below. A class diagram is not drawn for an Eiffel library class, because such a class often has a large number of features, and is described in the Eiffel Library Manual.
The two types of diagram can be combined to form a system diagram, that shows a client chart for the system and a class diagram for each user-defined class. A system diagram does not show the fine control or calling structure of the system, nor does it show the data flow in the system. It shows the client (and will later show the inheritance) structure of the system, and a partial implementation of each class.
Designing and using notations is a huge industry, and no notation for OO systems has yet emerged as standard. Pure Eiffel notations for system design and documentation are described in Jézéquel (1996) and Waldén and Nerson (1995).
An explicit feature interface allows a feature to be used without worrying about the implemetation; this is just what a programmer wants. A feature is defined once, and then reused forever. A caller must pass the right arguments to the feature, because actual and formal arguments must agree in order and type. The definition of arguments says nothing about the values of those arguments, however, it just defines their type. In most cases, however, there are also restrictions on the values that are passed to and received back from a routine. To define these restrictions, a contract is established between client and supplier by setting preconditions and postconditions on a routine.
A precondition is a condition that must be true before the routine is executed; we say the condition is asserted to be true on entry to the routine. If I wish to calculate the average, for example, the appropriate code is average := sum / count. Before this code is executed, I must be assured that the value of count is non-zero, because a divide by zero is undefined and will crash the system. Thus, a precondition on this routine is that count be non-zero. If this precondition is satisfied (if it is true), then the routine guarantees to return the correct value for the average. The correct value of the average is is a postcondition on the routine; it is an assertion that must be true when the routine exits. Pre- and postconditions can be explicitly defined, and checked when the routine is entered and exits. If the precondition is violated, then the software contract becomes void, and no result is guaranteed. If the precondition is true and the postcondition is violated, then the code in the routine is incorrect. Assertions enforce the software contract. For two assertions pre and post, the contract is defined by testing the pre assertions on entry to, and the post assertions on exit from, the routine.
The general form of an assertion is a statement that evaluates to true or false; it may be implemented as a value, expression, or function call. An assertion may be preceded by a name, that is used to make the meaning of the test clear. The general form of an assertion is thus
name: logical expressionMultiple assertions in a pre- or postcondition are separated by semi-colons.
The format for a routine with assertions is shown below, where an optional TYPE is shown for functions.
When a routine with assertions is called, the following sequence of events occurs:name (arguments) <:TYPE> is -- header comment local declarations require pre-condition(s) do code ensure post-condition(s) end -- name
The old operator can be used to check the value(s) changed by a procedure. The keyword old before an attribute in a postcondition refers to the value of that attribute on entry to the procedure, so the change made by the procedure can be defined and checked by the post-condition. Functions change nothing, so functions never use this keyword.withdraw (amount: REAL) is -- deduct this amount of money from the balance require positive: amount > 0 funds: amount <= balance do balance := balance - amount ensure changed: balance = old balance - amount end -- withdraw
The routine header shows the signature: a single REAL value is passed to the routine from its caller, and no value is returned. The precondition defines what must be true when this routine is called: the amount to withdraw must be positive, and the withdrawal must succeed. The postcondition defines what must be true when the routine exits: the current balance must equal the old balance plus the amount withdrawn. These facts completely define the behaviour or action of the routine, and can be used to design the routine, to check that it is correct, and to describe the routine to a user.
A function that is only a simple expression usually does not contain a post-condition. The post-condition in this case would simply repeat the expression, so nothing is gained by repeating the expression as a post-condition. Local variables can only be used in a routine body or in the post-condition of their routine. The special local variable Result can only be used in the body or post-condition of a function.
A routine does not contain code in the routine body to test if it has been called in the right way. In Eiffel, design by contract says: "If you call me in the right way, I guarantee to return the correct answer". If the client calls the routine incorrectly and violates the contract, the contract is broken by the client and the supplier routine no longer guarantees anything. This is an important principle in the design of an Eiffel system: it is the responsibility of the client to call the routine in the right way.
The signature and the assertions on a routine define the formal behaviour of a routine. They can be used to specify the behaviour of a feature before any code is written, and thus be used to design the system. Assertions may be be used while the system is running, to check that the code really does do what was specified. Finally, the assertions can be used to communicate with a programmer who is looking for a routine to accomplish a task, and scans through the class to find if such a routine already exists.
You turn assertion testing on and off by setting the assertion
status in the Ace file. Assertion testing can be set to one of six levels,
where each level adds to the previous one:
1. assertion (no) | no assertion checking |
2. assertion (require) | test pre-conditions |
3. assertion (ensure) | also test post-conditions |
4. assertion (invariant) | also test class invariant |
5. assertion (loop) | also test loop variants and invariants |
6. assertion (check) | also test check instructions |
7. assertion (all) | same as assertion (check) |
An assertion can be defined on a class, as well as on a routine. The class invariant defines what must be true of any object of that type. The class invariant is checked on entry to, and on exit from, any feature of the class except the creation routine; the object does not exist until after a creation routine has returned control to the caller. The invariant may be false within a routine, though this is unusual.
The format of a class invariant is one or more assertions placed under the invariant keyword; multiple assertions are separated by semi-colons. The keyword is placed by convention at the end of a class, after the last feature definition.
The case study uses a class ACCOUNT. Consider the situation where the balance of an account should never be less than zero. This is part of the definition of the class, so it is an assertion on the class and not on a feature of the class, so it is defined as a class invariant. The invariant is added by writing the two lines of code
in the class listing after the last feature definition, and before the end of the class. The balance is set to zero when an object is created, and should never become negative. If the code in a routine of the class does make the balance negative, then Eiffel flags a class assertion violation on exit from the routine and generates a run-time error.invariant not_negative: balance >= 0.0
5.9 Documentation: the short form of a class
Many forms of notation and documentation are used to describe a computer system at a higher level than the code. It is a commonplace in computing that the documentation never keeps pace with the system itself. When the system changes, any existing documentation has to be changed to reflect the new system. If this is done, it is a constant drain on resources for the company. If it is not done, the documentation is out of date. Eiffel solves this problem by storing the documentation in the code, and provides a set of tools to derive class and feature definitions directly from the code.
The short tool is an executable program that takes a class as an argument and produces a document from the class definition. The tool reads the text file, and records the external interface of every exported feature in the class. Non-exported features are not listed, because they are not part of the external interface. To find the useful features in a class, I simply type
short <className>and the system displays a short listing of the class. You can produce this document for any class that you define, or for any class in the Eiffel library. Eiffel uses your Ace file to find the location of the class, and scans the text file for that class. short will only work if you have an executable system file in your current directory.
To illustrate how useful such documentation can be, the short definitions (output by running the tool short on the class ANY) for the routines copy, clone, and equal are given below. The routine header gives the type of the input data passed to the routine, and output data passed back from a function; this defines the signature of the feature. The interface provides all the information needed to use these routines, with no knowledge of the implementation. The interface definitions are:
The three routine definitions use two Eiffel features that have not yet been covered. The keyword like allows the type of an argument to be defined as "like this one". The class ANY matches a class of any type. Because the routines can be applied to an object of any type, the object received as an argument can be of any type. Defining one argument to be like another (to be of the same type) means that the routine can declare and use an argument of the appropriate type, no matter what type of object was passed to it.copy (other: like Current) is -- Copy every field of other onto -- corresponding field of current object require other_not_void: other /= Void ensure is_equal (other) end -- copy clone (other: ANY): like other is -- Void if other is void. -- Otherwise, new object is field-by-field identical -- to object attached to other ensure equal (Result, other) end -- clone equal (some: ANY; other: like some): BOOLEAN is -- Are some and other either both void -- or attached to field-by-field identical objects? ensure Result = (some = Void and other = Void) or (some /= Void and other /=Void and then some.is_equal (other)) end -- equal
A short listing does not list all the features offered by a class, just the features that are defined in the class and exported. A class may also inherit features, discussed in Chapter 10; a listing of all the available features can be genrated by riunning flat on the class, with a command of the form
flat <className>Common error: No executable in the current directory; short produces no output.
What to do: Compile an Eiffel system so that you have an executable in your current directory.
5.10 The Eiffel library class STRING
An abbreviated version of the short form of the ISE Eiffel Version 3.3.7 library class STRING is shown below. The class offers many more features than these; the full list may be found by looking in the Eiffel Version 3 Library Manual, that lists the services of over 100 library classes, or by running short on the class in your system. A sequence of selected features are shown below, then some of them are described after the short listing.
The first feature in the listing is the creation routine for a string, that allocates enough storage to store n characters, where n is the single input argument. The precondition states that n must be non-negative, and the postcondition states that the string can now contain n characters.-- Character strings class interface STRING creation procedures make (n: INTEGER) -- Allocate space for at least n characters. require non_negative_size: n >= 0 ensure capacity = n exported features infix "<=" (other: like Current): BOOLEAN -- Is current string less then or equal to other? ensure Result implies not (Current > other) infix "<" (other: STRING ): BOOLEAN -- Is current string lexicographically lower than other? infix ">=" (other: like Current): BOOLEAN -- Is current string greater then or equal to other? ensure Result implies not (Current < other) infix ">" (other: STRING ): BOOLEAN -- Is current string greater than other? ensure Result implies not (Current <=other) append (s: STRING) -- Append a copy of s at end of current string. require argument_not_void: s /= Void ensure count = old count + s.count capacity: INTEGER -- Number of characters guaranteed to fit in space -- currently allocated for string copy (other: STRING) -- Reinitialise with copy of other. require other /= Void ensure count = other.count -- For all i: 1 .. count, item (i) = other.item (i) count: INTEGER -- Actual number of characters making up the string ensure Result >= 0 empty: BOOLEAN --Is string empty? fill_blank -- Fill with blanks ensure -- For all i: 1 .. capacity, item (i) = Blank is_equal (other: STRING): BOOLEAN -- Is current string made of the same character sequence as other? item, infix "@" (i: INTEGER): CHARACTER -- Character at position i require index_large_enough: i >= 1; index_small_enough: i <= count put (c: CHARACTER, i: INTEGER) -- Replace by c character at position i. require index_large_enough: i >= 1; index_small_enough: i <= count ensure item (c) = c remove (i: INTEGER) -- Remove i-th character. require index_large_enough: i >= 1; index_small_enough: i <= count ensure count = old count - 1 resize (newsize: INTEGER) -- Reallocate if needed to accommodate at least newsize characters -- Do not lose any characters in the existing string require new_size_non_negative: newsize >= 0 ensure count >= newsize; count >= old count substring (n1: INTEGER, n2: INTEGER): STRING -- Copy of a substring of current string containing all characters -- at indices between n1 and n2 require meaningful_origin: 1 <= n1; meaningful_interval: n1 <= n2; meaningful_end: n2 <= count ensure Result.count = n2 - n1+ 1 -- For all i: 1 .. n2 - n1, Result.item (i) = item (n1+ i - 1) to_lower -- Convert string to lower case. to_upper -- Convert string to upper case. end interface -- class STRING
The operator "<=" is an infix operator that compares the content of two strings on the basis of lexicographic order. Lexicographic order compares characters in the two strings from the first (leftmost) to the last, stopping when the characters are not equal. The value used for comparison is the ASCII value of the character, so 'a' < 'b' < ... 'A' < 'B', and so on. Because it is an infix operator, it is called by writing "s1 <= other" instead of using a normal feature call of the form "s1.<= (other)". The feature is called on a string, and takes a string as argument (the argument is like the current object). There is no restriction on the value of the input argument. If a value of true is returned, then the current string is not greater than the string passed as an argument.
The append operator takes a string as an arguemnt, and appends the argument to the end of the current string. It requires that the argument not be Void, so the passed reference must actually point to an object of type STRING. If this precondition is satisfied, then the routine guarantees that the new value of the current string has a total length of the old string and the string passed as an argument; the old operator shown in the listing refers to the value of the current object on entry to the routine.
The feature substring takes two integers as arguments. It returns a value of type STRING, that is the part of the current string between positions n1 and n2 in the string. Consider the strings s1 and s2 shown below, where a substring of s1 ("der") is assigned to the string s2:
The function returns a string of length 3, guaranteed by the postcondition Result.count = n2 - n1 + 1). The returned string is a copy of the string containing all the characters from the third to the fifth position, as stated in the header comment and the commented postconditions1: STRING is "Wonderful time" s2: STRINGs2:= s1.substring (4, 6) io.putstring (s2) ==> "der"
-- For all i: 1 .. n2 - n1, Result.item (i) = item (n1+ i - 1)This is a comment rather than an executable postcondition because the Eiffel proof machinery to process assertions cannot handle the logical quantifier "for all"; a discussion of this topic would take us far beyond Eiffel, however, into the field of automatic proof checking.
The preconditions of each feature are used to check that the feature has been called in the right way; this is part of the assertion checking that enforces programming by contract. If a precondition is violated, then the label to the left of that precondition is displayed as an error message. If a client called the feature substring to get a part of the current string and passed values that were invalid, then the name of the violated precondition (meaningful_origin, meaningful_interval, or meaningful_end) would be displayed as part of the error message to tell the user exactly what went wrong.
There are two names for the function that returns a character from a string, given the position of the character. The first name is item, a function that is called using the normal dot notation, such as s.item (3). The second name for the function is the infix operator @, that is called using the normal infix format, such as s @ 3. Both names refer to the same function body, and are thus names of the same feature.
The class interface illustrates how reusable software is reused. A programmer need not write code for any of these STRING features, simply find the appropriate feature in the class, examine the interface to see how it is used, and then use it.
Errors can occur at three levels in the design and coding of a computer system; errors in a program are commonly called bugs. The first and simplest level is that of syntactic errors, where the syntax of a statement is incorrect; an error in the syntax is flagged when you try to compile the system. The second level includes type and interface errors, where each part of a system is correct in isolation, but the pieces don't fit together; in Eiffel, this level is also discovered and flagged at compile time. The third level includes semantic errors, where the system compiles and executes but prodcues the wrong result. A system may also be stylistically wrong or badly designed, but no compiler can yet catch this type of error. Eiffel tries to catch as many errors as possible at compile time, because it is better to fix bugs as soon as possible in system development.
The best solution to the problem of errors is to avoid them in the first place (antibugging). All the design and layout rules given in this book are antibugging tools; they are the products of long experience in making code easier to write and understand. If, after designing the system in parts, using classes and routines to define small modules, and coding and testing each part as it is added, your system is still buggy, then it is time for debugging; this is the most infuriating, unpleasant aspect of programming.
Antibugging is the use of tools and techniques to avoid the generation of bugs. The most common novice error is to forget an end statement. Every routine, every if statement, every inspect statement, and every loop statement requires an end. Because the end is not the focus of attention during coding, it is easy to forget it. The compiler will discover that an end is missing, and generate an error message that specifies a line number, but the error may not be in that line.
The Eiffel compiler generates an error message when it can't parse the current statement; this is what the compiler means by an error. For this reason, the actual error will be at or before the flagged line. If an end is omitted from an if statement inside a loop, the compiler will probably not notice the error at that point, because the code can still be parsed. The statements following the 'missing' end are simply added to the code under the control of the if statement, as if they were part of the if statement. The end of the loop is then interpreted as the end of the if, and the end of the routine is interpreted as the end of the loop. The first time the compiler realizes that something is wrong is when it sees the next routine header inside the 'current' routine. At that point, the compiler gives up and generates an error message, but the real error may be long before the line flagged by the compiler.
The best way to avoid this error is to check that every compound statement is terminated by an end. The best way to ensure this is to indent the code to show flow of control; a missing end then visually 'jumps out' at the programmer. Indenting should be done when the code is written, not as an afterthought; it is a simple, powerful aid to the programmer.
The second level of errors involves inconsistent code, where two pieces of code are correct by themselves, but do not fit together. This type of error is normally found by the compiler as a result of Eiffel's strict type checking. One example of type checking is that actual and formal arguments must agree in number, order and type; if they do not, then the calling and called pieces of code don't fit together. Eiffel catches a large number of errors during the compilation stage, errors that in a less strict system would not be found until run time. Finding errors at the compilation stage is a great advantage, because the compiler tells you where the error is, and sometimes what the error is; you don't have to track it down.
The third and most difficult type of error is one that occurs at run time, when the compiled code is executed. The error may cause the system to crash with a run time error, or the system may run to completion but produce the wrong output. This last type of bug is the hardest to find, because there is no obvious place to look for it; the system executes and terminates normally. The hardest part of debugging is finding the bug; once found, it is comparatively easy to fix.
When Eiffel detects an error at run time, the system terminates with an exception message. The message says that an exception was generated because an assertion was violated; the name of the assertion is usually shown, to indicate what was wrong. Eiffel also shows the location of the error by displaying the calling stack at the time the error occurred. When a routine is called, it is placed on a stack; more formally, a record of the routine call is placed on the call stack. When the routine exits, the record is taken off the stack. The current feature is thus at the top of the stack, the feature that called it is second on the stack, and so on down to the creation routine for the root class at the bottom of the stack. By looking at the routines on the stack, it is easy to find which routine contained the run time error. By looking at the assertion that was violated, it is easy to find what the error was in that routine.
In the Case Study, Part 2), the make routine in BANK calls
the creation routine in CUSTOMER. The make routine in CUSTOMER
creates an account, then executes one transaction of each type on the account.
In the scenario used here, assume that an error occurs when the withdraw
routine is executed in class ACCOUNT, because the funds precondition
is violated. At that point, Eiffel will halt the system and show the current
state of the calling stack. While each version of Eiffel uses a different
format, the run-time error output should look something like
CLASS | ROUTINE | ERROR |
ACCOUNT | withdraw | funds: balance>= 0 |
CUSTOMER | withdraw | Assertion violation |
BANK | make | Assertion violation |
If the system runs but produces the wrong output, your code is syntactically correct but it solves the wrong problem. In this case, the location of the bug can be very hard to find. Few things are more frustrating than staring at code for minutes ... hours ... days, and then realizing that the bug was in a different part of the system entirely! Don't stare at code for more than a couple minutes; if you don't find the bug in that period, it is time to work smarter, not harder.
There are two standard debugging tools; be the computer, and see the data. The first tool requires you to play the role of the computer, and see what the code actually does, as opposed to what you think it does. Pretend that you are really dumb, as dumb as a computer, and that you can understand nothing except very simple instructions; but you know exactly what to do with each instruction. Go through the code using actual data values, and see if the code behaves the way you expected. This technique is known as hand execution of the code, because you execute the code "by hand" and use paper and pen to write down the values made by the code.
The second standard debugging tool is the use of debugging output to locate the bug. If your system is hundreds or thousands of lines long, then the first task is to work out where the bug is not, so you can narrow down the location of where the bug must be. The technique of writing a series of small routines is the antibugging solution to this problem, because the bug must be located in the small amount of code inside the routine. If your routine is large, then you should think strongly about making it more modular by breaking it into a number of simpler pieces. This technique also makes the code reusable, and allows you to test a set of small, easy to understand routines. If you have examined the code in the small routine and still can't find the bug, then collect more information by placing output statements in the code. If the output is correct, then the error must occur after that position in the code; if the output is incorrect, then the error must occur before that statement. When you don't know the answer to a question, seek more information, don't stare at the code; debugging output gives you that information.
Eiffel supplies a special debug keyword that allows you turn on and off debugging output. In your code, you write a debug clause of the form
and the instructions in this clause (probably some form of output) are executed when debugging is turned on. To control debugging, set the debug status in your Ace file to:debug instruction instruction ... end
1. debug (no) | no debugging |
2. debug (yes) | execute debug clause |
3. debug (all) | same as debug (yes) |
In your Ace file, you turn on this labelled debug clause with a statement of the formdebug ("Hard stuff"). instruction instruction ... end
debug ("Hard stuff")You can have multiple occurrences of a labelled debug clause, and multiple labels. Execution of each labelled clause is turned on by including a debug line with that label in your Ace file.
To be able to use debugging output, you must know the correct or expected value of the output, so you can compare the actual to the predicted value. For this reason, test values should be as simple as possible so you can easily calculate the correct answer; values of 0 and 1 are good candidates. In testing the gross_pay routine above, for example, you might enter 40 for the number of hours and 1 for the pay rate; if you entered 53.72 for the hours and 12.346 for the rate, then it is hard to even calculate the correct answer, and thus hard to see if your code is correct. A good place to check that the code is correct is at the boundaries of a routine; Eiffel uses pre- and postconditions for just this purpose.
5.12 Case study: export and assertions
Part Five of the case study shows the class and feature assertions for the system of three classes, and makes most of their features private.
Main points covered in this chapter
• The interface to a routine is defined by its name, signature, comment, and assertions. The signature constrains the type that can be used, and the assertions constrain the value that can be used. An assertion is a statement that evaluates to true or false.
• A pre-condition defines what must be true on entry to the routine, and is listed under the require keyword before the routine body. If a pre-condition fails, then the caller is wrong.
• A post-condition defines what must be true on exit from the routine, and is listed under the ensure keyword after the routine code. If a post-condition fails, then the routine is wrong.
• It is the responsibility of the caller to call a routine in the right way. Design by contract says "If you call me in the right way, I guarantee to produce the right results. If not, not."
• The behaviour of a class is defined by its exported features. The class interface can be seen by running the short or flat tool on a class.
• Eiffel uses assertions to check that a routine has been called correctly. If a call is incorrect, then the system dies at run-time and shows the state of the call stack at that time. The failing assertion is shown at the top of the stack.
• Antibugging is the prevention of errors by careful design and good habits. The best habit is to define a set of small, simple, reusable routines that are easy to understand and check.
• The hardest part of debugging is finding the error. The most powerful debugging tools are hand execution and the use of debugging output. Eiffel supplies the debug clause to control debugging output.
1. What is a precondition? What keyword precedes the precondition in a feature?
2. What is a postcondition? What keyword precedes the postcondition in a feature?
3. What happens when a routine with assertions is called?
4. How does Eiffel use preconditions to generate error messages?
5. What is the complete interface definition for a feature?
6. Run short on class REAL. What is the ouput from running short on a class?
7. Run flat on class REAL. What is the ouput from running flat on a class?
8. Run short on each class of the current case study to show the class interfaces. Run the system, using data that will crash the system with an assertion violation.
9. How is a creation policy specified? What does a creation policy control?
10. How is an export policy specified? What does an export policy control? Can a feature have two export policies?
11. Why must an equality function be exported to its own class?
12. Consider the following specification:
Bill the builder has come to you for help. He has a job to convert a tool shed into a shrine to Elvis Presley, and needs to know how much he should charge for the building job. Write a system that prompts him for input, and shows him the amount of material he needs, and the amount of money he should charge for the job.
The tool shed consists of one large, rectangular room. It is 3 meters high, 2.8 meters wide, and 5.6 meters long. The owner, Mr. Prince, wants to cover the walls in an expensive wallpaper made of crushed red velvet, with silver outlines of Elvis on it. The windows he wants are specially glazed with frosted outlines of angels. The doors are covered with mats of Kentucky blue grass.
Show Bill how much wallpaper, glass, and matting he should buy, and how much to charge Mr. Prince. Wallpaper comes in rolls of 50 square meters, and costs $299.99 per roll; you cannot buy partial rolls. Glass is cut to size, so Bill can buy exactly as much as he needs; glass costs $89.99 per square metre. Matting is bought in units of a square metre, and costs $312.00 per square metre; you cannot buy it in smaller pieces. Bill charges $45 an hour for his labor.
Mr. Prince keeps changing his mind about how many windows and doors he wants, and how large they are. Thus, you have to read in all the relevant data as input, since the plans can change without notice. When he took the job, Bill insisted that all the doors in a wall were the same size, and all the windows in a wall were the same size. Read in the number of windows in each wall, and their dimensions, read in the number of doors in each wall and their dimensions, and calculate the amount of material that Bill has to buy. After all the room details have been input, ask Bill how many hours he thinks he will need to do the job. Show the amount and price of each material needed, the amount and price of labor needed, and the total price for the job.
Program details
The program reads in, for each wall, the number and size of the windows, and the number and size of the doors. It also reads the estimated number of hours needed for labour. The program calculates the amount of each material needed, and the cost of buying the materials. Bill can cut the wallpaper and matting to size, so you need not worry about whether a window cuts Elvis in half, or not. The program also finds the cost of labour, and the total cost of the job. Assume that all measurements are accurate to two decimal places. A sample output from the program looks like this:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
b) Define the classes in the system by placing each of the variables in a class.
c) For each class, define the signature for each feature.