Joe Hollingsworth
Indiana University Southeast
PS105-4201 Grant Line Road
New Albany, Indiana 47150
Tel: (812)941-2425
Email:
jholly@ius.indiana.edu
URL:
http://www.cs.ius.indiana.edu/FACULTY/jholly/web_docs/homepage.htm
In this paper we examine traditional answers given for two fundamental dilemmas faced by software component engineers. We discuss the problems caused by continuing to accept these traditional answers, and follow with a discussion of how and why we have decided to break with tradition. Our position is: software systems are needlessly more difficult to design and develop in the first place and are more difficult to maintain in the long run when we continue to mindlessly accept these traditional answers to fundamental questions.
Keywords: design-for-reuse, technology transfer,
Workshop Goals: interact; advance software engineering and reuse technology
Working Groups: object technology, component certification, behavioral specification
Our recent research has concentrated on design principles that lead to the engineering of high-quality software components in C++, We call this discipline RESOLVE/C++[1, 3]. The design principles focus on the technical details of component engineering, not on the higher level reuse problems (e.g., library organization, and searching). Since 1994, we have augmented our research by adding a technology transfer phase; we have been applying the results of our research to the development of commercial software applications.
Our position is based on the following observations:
Observation 1 - There are at least two fundamental dilemmas faced by software component engineers:
* How should object values within a program be moved?
* Who is responsible for determining that an operation's precondition is met?
Observation 2 - Traditional/conventional reasoning answers these questions as follows:
* Objects are moved by the assignment operator.
* Both the caller and the called operation are responsible for determining if the called operation's precondition is met.
Based on these observations it is our position that: software systems are needlessly harder to designed and develop in the first place and harder to maintain in the long run because of these traditional component engineering standards. Ultimately, this is because software researchers and engineers are unwilling to question traditional answers to these fundamental dilemmas.
Traditional Object Movement
The traditional method of implementing object value movement is through the assignment operator, i.e., assign a copy of the object's value. Assignment is a fine choice for small values: it is cheap, and conceptually easy to understand. Unfortunately assignment of larger values does not provide the performance that we often seek. The need for large value assignment (along with better performance) has led us to the advent of shallow copying. The term shallow copying is really misleading, because this kind of copying only makes a copy of the address to the object's value, not a copy of the value itself. Shallow copying can lead to slightly better performance (under some circumstances), but it is not conceptually easy to understand. Program understanding becomes much more difficult because shallow copying introduces the possibility of aliasing, and with aliasing comes unwanted side-effects, storage leaks, dangling references, and ultimately the loss of program correctness. You must agree that these problems are frequently faced by software developers, otherwise there would be no market for the expensive software developer tools that exist today, and are used for detecting the source of these kind of problems. All of these problems, i.e., harder to understand programs, increased opportunity for storage leaks, etc., result from the decision to continue with the time-worn tradition (developed back in the 1950's or earlier) of implementing object value movement through assignment.
Object Movement by Swapping - Breaking with Tradition
In [2], Harms and Weide introduced value swapping as an alternative to assignment/copying. This implementation of value movement is conceptually easy to understand, provides excellent performance for large as well as small values, and does not introduce aliasing. Since introducing this alternative, the RESOLVE group embarked on an effort to transfer this technological advance to others through conference and journal papers, conference tutorials, workshop participation, and face to face encounters. We have found uniformly strong resistance to this nontraditional implementation of value movement. After being exposed to the swapping alternative, most engineers see in theory why it is better than assignment/copying, but cannot believe that "real" software can be developed using swapping. We suspect old FORTRAN programmers said the same thing when told that they should eliminate "goto" statements from their programs.
Traditional Defensive Programming
When an operation is about to be called, who should be responsible for making sure that the called operation's preconditions are satisfied? In [5], Meyer outlined the notion of programming by contract, which more or less means that it is the caller's responsibility to put the machine in a state that satisfies the called operation's preconditions, and if that is done, then it is the called operation's responsibility to successfully perform its required computation. Most engineers agree with this in principle, but implement their software otherwise. The following is a table that breaks down the different approaches for assigning the responsibility for an operation's preconditions:
Table: Burden of Preconditions - Who is Responsible?
Approach Calling Operation Called Operation #1 Not Responsible Responsible #2 Responsible Responsible #3 Responsible Not Responsible #4 Not Responsible Not Responsible
Examine almost any computer science textbook and you will find that the authors place at least some of the responsibility for checking preconditions on the shoulders of the called operation. They call this defensive programming. If you subscribe to this engineering approach, then you either follow Approach #1 or Approach #2 listed in the Burden of Preconditions table. But I'll guess that you really follow Approach #2 where the calling operation also makes sure that the preconditions are satisfied prior to making the call, and the called operation then rechecks the preconditions in order to be defensive.
For example, suppose you wrote some code to remove while (q.Size () > 0) { each item from a queue object by calling the q.Dequeue (x); // do queue's dequeue operation. Your code might look something with x } // end while like the code to the right, where q is the queue object:
Dequeue of course has a precondition, which states that the queue object must be non-empty prior to the call. The calling operation is written so that it will not violate Dequeue's preconditions. Since Approach #1 or #2 is being followed, Dequeue is defensive, and when it is called it then verifies that the precondition is satisfied. Thus, when engineers make their operations defensive (e.g., Dequeue) and when they write the client operation (e.g., the client code provided above), they usually create a situation where the precondition is verified more than once. This is Approach #2 and is the traditional approach.
Software engineered according to Approach #2 (the traditional approach) suffers from the following problems:
* It is the most costly approach, with respect to performance, because the precondition gets checked at least twice every time the defensive operation is called.
* The defensive operation is more complex, because there are more statements, branches, and paths.
* Systematic testing becomes more difficult since there are more statements, branches and paths.
* The real computation being performed by the defensive operation can be obscured by the clutter of the code that makes it defensive. This makes maintenance harder.
* Finding and removing defects from the calling operation is more difficult if the defensive operation does something innocuous (e.g., by just doing nothing and returning to the caller) when it detects that the calling operation violated the precondition.
Non-Defensive Programming - Breaking with Tradition
In [4] we break with tradition, adopting Approach #3 which is basically Meyer's programming by contract. We augment Approach #3 with a technology call checking components which are used only during testing and debugging. These checking components alert us when a calling operation violates the precondition of a called operation. These calling operations are the ones that are defective and are the ones that need fixing prior to release. Checking components are removed from the system after testing and debugging has finished.
Software engineered according to Approach #3 enjoys the following benefits:
* Has the best performance, because the called operation is not defensive and does not unnecessarily waste time rechecking the precondition.
* The called operation is less complex. There are less statements, branches and paths. The code is only concerned with performing its required computation.
* Is easier to maintain because there is no defensive code obscuring the true purpose of the operation.
* Because we make use of checking components, detecting when a calling operation violates the precondition of a called operation is automatic and easy. This leads to defect removal occurring earlier on in the life-cycle.
As you might guess, we have been met with strong resistance to this approach, even when the benefits of the programming by contract approach and the problems with the traditional approach are made clear.
It is one thing to tell people to break with tradition, it is another to actually do it. One question that is always of concern is: Does this approach work for "real" software, i.e., does it scale up [6]? >From June 1994 to June 1995, I successfully developed a commercial PC Windows application using object swapping, and programming by contract (Approach #3). I followed these nontraditional approaches and other principles found in RESOLVE and the RESOLVE/C++ discipline [1, 3, 4].
This PC Windows application has been an engineering and commercial success. Here are some facts:
* It is written in C++ using the RESOLVE/C++ discipline and comprises over 200 components and only one C function, (i.e., WinMain).
* Each component is a C++ class template.
* Swapping is used to move all objects' values.
* Only one object needs to be copied. It contains the data entered by the user. A special Copy operation was written to provide this functionality, and it makes a true copy.
* Approach #3 is used to govern all calls, i.e., the calling operation is responsible for making sure that the called operation's preconditions are met.
* Since its release in June 1995, users (in the U.S. and abroad) have uncovered three defects. None of them were "show stoppers" and all were easily located and fixed.
* Since June 1996, I have added significant additional functionality to the original application with very little difficulty.
We appreciate healthy skepticism. However, we believe at times, one must reexamine with an open mind the answers to age-old questions. We have found that the traditional answers were often "right" for the current time, but were not necessarily "right" for the long run. Our research and our work with "real" commercial software has borne this out, time and time again.
[1] Bucci, P., Hollingsworth, J., Krone, J., and Weide, B., "Implementing Components in RESOLVE," in Software Engineering Notes, Vol. 19, No. 4, pp 40 - 51, October, 1994.
[2] Harms, D.E., and Weide, B.W., "Copying and Swapping: Influences on the Design of Reusable Components," in IEEE Transactions on Software Engineering, Vol. 17, No. 5, pp 424-435, May 1991.
[3] Hollingsworth, J., Sreerama, S., Weide, B., and Zhupanov, S., "RESOLVE Components in Ada and C++," in Software Engineering Notes, Vol. 19, No. 4, pp 52 - 63, October, 1994.
[4] Hollingsworth, J., Software Component Design-for-Reuse: A Language Independent Discipline Applied to Ada, Ph.D. thesis, Dept. of Computer and Information Science, The Ohio State University, Columbus, Ohio, 1992. (A PostScript version of the dissertation is available via anonymous ftp from http://www.cis.ohio-state.edu/hypertext/rsrg/RSRG.html)
[5] Meyer, B., Object-oriented Software Construction, Prentice-Hall International, Cambridge, U.K., 1988.
[6] Weide, B., Hollingsworth, J., Scalability of Reuse Technology to Large Systems Requires Local Certifiability, in Larry Latour, Steve Philbrick, Mark Stevens, editors, Proceedings of the WISR 92 Fifth Annual Workshop on Software Reuse, October 1992.
Hollingsworth is an assistant professor in computer science at Indiana University Southeast. He has a Ph.D. in software engineering from the Reusable Software Research Group (RSRG) at The Ohio State University (1992). His research focuses on various aspects of software engineering and reuse including design-for-reuse, formal methods, testing and education, to name a few. He has authored several technical papers on related topics in software engineering. Recently he has also been a consultant to Hollingsworth Solutions Software Company where he has used the RESOLVE discipline in the development of commercial software packages.