Lecture
Measuring the quality of an abstraction
In Ingalls' opinion, "a system should be built with a minimum set of unchangeable parts; those parts should be as general as possible and considered within a unified model" [51]. As applied to object-oriented design, such components are the classes and objects that reflect the key abstractions of the system, while unity is provided by the corresponding implementation mechanisms.
Experience shows that the process of identifying classes and objects is successive and iterative. Except for the very simplest problems, one never manages to identify and describe classes definitively on the first attempt. Chapters 4 and 7 show how, in the course of the work, the contradictions that arise in the initial definition of abstractions are smoothed out. Obviously, such a process entails additional costs of recompilation, reconciliation and the making of changes to the design of the system. It is therefore very important to come as close as possible to the right decisions from the very beginning, in order to reduce the number of subsequent steps of approximation to the truth. To assess the quality of the classes and objects identified in a system, the following five criteria may be proposed:
The term coupling is taken from structured design, but in a freer interpretation it is also used in object-oriented design. Stevens, Myers and Constantine define coupling as "the measure of the strength of association established by a connection from one module to another. Strong coupling complicates a system since a module is harder to understand, change, or correct by itself. Complexity can be reduced by designing systems with the weakest possible coupling between modules" [52]. An example of the wrong approach to the problem of coupling was given by Page-Jones, who described a modular stereo system in which the power supply was placed in one of the loudspeakers [53].
Besides coupling between modules, in object-oriented analysis coupling between classes and objects is significant. There is a certain tension between the phenomena of coupling and inheritance. On the one hand, it is desirable to avoid strong coupling of classes; on the other hand, the mechanism of inheritance, which tightly binds subclasses to superclasses, helps to exploit the similarity of abstractions to advantage.
The notion of cohesion is also borrowed from structured design. Cohesion is the degree of interaction between the elements of an individual module (and, for OOD, also of an individual class or object), a characteristic of its density. The least desirable is coincidental cohesion, when completely unrelated abstractions are gathered into a single class or module. As an example, one may imagine a class combining the abstractions of dogs and spacecraft. The most desirable is functional cohesion, in which all the elements of a class or module interact closely in achieving a definite goal. Thus, for example, the class Dog will be functionally cohesive if it describes the behavior of a dog, the whole dog, and nothing but the dog.
Closely adjoining the ideas of coupling and cohesion are the notions of sufficiency, completeness and primitiveness. By sufficiency is meant the presence in a class or module of everything necessary for the realization of logical and efficient behavior. In other words, the components must be fully usable. As an example, consider the class set. The operation of removing an element from a set in this class is obviously necessary, but it would be a mistake not to include in this class the operation of adding an element as well. A violation of the requirement of sufficiency is discovered very quickly, as soon as a client is created that uses the abstraction. By completeness is meant the presence in the interface part of a class of all the characteristics of the abstraction. The idea of sufficiency imposes minimal requirements on the interface, while the idea of completeness embraces all aspects of the abstraction. A class or module is characterized as complete if its interface guarantees everything for interaction with users. Completeness is a subjective factor, and developers often abuse it, elevating to the top such operations as can be implemented at a lower level. From this follows the requirement of primitiveness. Primitive operations are only those that require access to the internal implementation of the abstraction. Thus, in the example with the class set the operation Add (adding an element to the set) is primitive, whereas the operation of adding four elements will not be primitive, since it is quite efficiently implemented through the operation of adding one element. Of course, efficiency is also a subjective thing. An operation that requires direct access to the data structure is primitive by definition. An operation that can be described in terms of existing primitive operations, but at the cost of significantly greater computational expense, is also a candidate for inclusion in the category of primitives [An example might be the operation of adding an arbitrary number of elements to a set (and not necessarily four). - Ed. note].
How to choose operations?
Functionality. Describing the interface of a class or module is difficult work. Usually the first approximation is made proceeding from the structural meaning of the class, and then, as clients of the class appear, the interface is refined, modified and supplemented. In particular, a need may arise to create new classes or to change the interaction of existing ones.
Within each class it is customary to have only primitive operations that reflect individual aspects of behavior. Such methods are called precise. It is also customary to separate methods that are unrelated to one another. This makes it easier to form subclasses with redefined behavior. The decision about the number of methods may be governed by two considerations: describing the behavior in a single method simplifies the interface, but complicates and increases the size of the method itself; splitting a method complicates the interface, but makes each of the methods simpler. As Meyer observes, "a good designer knows how to find a compromise between a large number of connections (breaking the system into fragments) and a large size of modules (which may lead to a loss of manageability)" [54].
In object-oriented design it is customary to regard the methods of a class as a single whole, since they all interact with one another to implement the protocol of the abstraction. Thus, having defined a behavior, one must decide in which of the classes this behavior is implemented. Halbert and O'Brien have proposed the following criteria for making such a decision:
| ® Reusability | Will this behavior be useful in more than one context? |
| ® Complexity | How difficult is it to implement such behavior? |
| ® Applicability | How characteristic is the given behavior of the class into which we want to include the behavior? |
| ® Implementation knowledge | Is it necessary, in order to implement the given behavior, to know the secrets of the class? |
Usually operations are declared as methods of the class to whose objects the given actions relate. However, in the languages Object Pascal, C++, CLOS and Ada it is permissible to describe operations in the form of free subprograms (class utilities). A free subprogram, in C++ terminology, is a function that is not a member of a class. Free subprograms cannot be redefined like ordinary methods; there is no such generality in them. The presence of utilities makes it possible to satisfy the requirement of primitiveness and to reduce coupling between classes, especially if these high-level operations involve objects of many different classes.
Aspects of memory and time consumption. After we have decided on the necessity of a particular function and have determined its semantics, a decision must be taken about its use of time and memory. To express such decisions it is customary to use the notion of the best, average and worst cases, where the worst is the upper permissible limit of expenditure.
We noted earlier that since one object sends a message to another, these two objects must be synchronized in some way. In the case of many threads of control this means that the passing of messages is more complex than the control of subprogram calls. For most programming languages synchronization is simply not needed, since in them programs are single-threaded and all objects act sequentially. In such cases we speak of simple message passing, since its semantics is more like a simple subprogram call. However, in languages that support concurrency [Ada and Smalltalk have direct support for concurrency. Languages such as C++ do not possess such support, but in them one can often provide concurrency semantics by means of extensions with (platform-dependent) classes: an example is the AT&T library for C++], one must take care of more sophisticated message passing systems, in order to avoid cases where two threads work simultaneously and inconsistently with one and the same object. Objects whose semantics is preserved under multithreading are either synchronous or guarded.
In some circumstances it is useful to mark concurrency both for individual operations and for the object as a whole, since different operations may require different forms of synchronization. The following forms of message passing are distinguished:
| ® Synchronous | The operation is activated only when both the sending and the receiving objects are ready; the wait for mutual readiness may be indefinitely long. |
| ® Balking | The same as synchronous, however, if the receiver is not ready, the sender does not perform the operation. |
| ® Timeout | The same as synchronous, however, the sender will wait for the receiver's readiness no longer than a certain time. |
| ® Asynchronous | The operation is performed regardless of the receiver's readiness. |
The required form is chosen for each operation separately, but only after its functional semantics has been defined.
How to choose relationships
Collaboration. The relationships between classes and objects are connected with concrete actions. If we want object X to send object Y a message M, then directly or indirectly class X must have access to class Y, otherwise it is impossible to invoke the operation M in class X. By accessibility we mean the ability of one abstraction to see another and to refer to its public resources. Abstractions are accessible to one another only when their scopes overlap and the necessary access rights are granted (thus, the private part of a class is accessible only to the class itself and to its friends). Coupling is therefore connected with visibility.
One useful rule is the Law of Demeter, which asserts that "the methods of any class should not depend in any way on the structure of any other classes, but only on the (top-level) structure of the class itself. In each method messages are sent only to objects from a strictly limited set of classes" [56]. Following this law makes it possible to create loosely coupled classes whose implementation is hidden. Such classes are sufficiently autonomous, and to understand their logic there is no need to know the structure of other classes.
In analyzing the structure of the classes of a system as a whole, one may find that the inheritance hierarchy is either wide and shallow, or narrow and deep, or balanced. In the first case the structure of classes looks like a forest of free-standing trees. Classes may be freely mixed and enter into relationships [57]. In the second case the structure of classes resembles a single tree with branches of classes having a common ancestor [58]. Each of these variants has its own merits and drawbacks. Classes making up a forest are independent of one another, but probably do not make the best use of the possibilities of specialization and code sharing. In the case of a tree of classes this "communality" is used to the maximum, and therefore each of the classes is smaller in size. However, in the latter case the classes cannot be understood without the context of all their ancestors.
Sometimes one has to choose between the relationships of inheritance, aggregation and using. For example, should the class Car inherit, contain or use the classes Engine and wheel? In this case the using relationships are more appropriate. In Meyer's opinion, between classes A and B "inheritance relationships are more suitable when any object of class B may at the same time be regarded as an object of A" [59]. On the other hand, if an object is something more than the sum of its parts, then the aggregation relationship is not entirely apt.
Mechanisms and visibility. The relationships between objects are determined mainly by the mechanisms of their interaction. The question is only who must know about what. For example, at a weaving factory materials (lots) arrive at workstations for processing. As soon as they reach a workstation, the manager must be notified of this. Is the arrival of material at a workstation an operation on the workstation, on the material, or on both at once? If it is an operation on the workstation, then the workstation class must be visible to the material. If it is an operation on the material, then the material class must be visible to the workstation, since a lot of material must distinguish between workstations. In the case of an operation on the room and the workstation, mutual visibility must be provided. Similarly, the relationship between the manager and the workstation (but not between the material and the manager) must be defined: either the manager must know about the workstation, or the workstation about the manager.
Sometimes in the design process it is useful to define the visibility of objects explicitly. There are four basic ways of making object X (the client) see object Y (the server):
These variants may be combined. Y may be part of X and at the same time be visible to other objects. In the Smalltalk language such a way usually implies a dependency between two objects. A shared visibility zone leads to a structural dependency, that is, one object does not have exclusive access rights to another: the state of that other object may be changed in several ways.
Choosing an implementation
The internal structure (implementation) of classes and objects is developed only after the design of their external behavior has been completed. In doing so, two design decisions must be made: to choose the way of representing the class or object and the way of placing them in a module.
Representation. The representation of classes and objects must almost always be encapsulated (hidden). This makes it possible to introduce changes (for example, a redistribution of memory and time resources) without disrupting the functional links with other classes and objects. As Wirth wisely observed: "the choice of representation is often a fairly difficult one, and it is not uniquely determined by the facilities available. It must always be considered from the point of view of the operations on the data" [60]. Consider, for example, a class corresponding to aircraft flight schedules. How should it be optimized - for search efficiency or for the speed of adding/removing a flight? Since it is impossible to implement both at once, a choice must be made based on the goals of the system. Sometimes such a choice is not easy, and then a family of classes is created with the same interface but with fundamentally different implementations, in order to provide variability of behavior.
One of the most difficult decisions is the choice between computing elements of an object's state and storing them in the form of data fields. Consider, for example, the class Cone with its corresponding method volume. This method returns the value of the object's volume. In the structure of the cone, data about its height and the radius of its base are stored as separate fields. Should one also create a data field for the volume, or should it be computed as needed inside the method volume [60]? If we want to obtain the volume value as fast as possible, the corresponding data field must be created. If saving memory is more important, it is better to compute this value. The optimal way of representing an object is always determined by the nature of the problem being solved. In any case, this choice must not affect the interface of the class.
Module structure. Similar questions arise in distributing the declarations of classes and objects among modules. In the Smalltalk language this problem is absent; here no module mechanism is implemented. In the languages Object Pascal, C++, CLOS and Ada there exists the notion of a module as a separate language construct. The decision about where to declare classes and objects in these languages is a compromise between the requirements of visibility and information hiding. In the general case, modules must be functionally cohesive inside and loosely coupled with one another. In doing so, a number of non-technical factors must be taken into account, such as reuse, security, documentation. Designing modules is no simpler a process than designing classes and objects. About information hiding Parnas, Clements and Weiss say the following: "The application of this principle is not always obvious. The principle is aimed at minimizing the cost of software (over its whole lifetime), for which the designer is required to be able to estimate the likelihood of changes. Such estimates are based on practical experience and knowledge of the problem domain, including an understanding of programming technology and hardware peculiarities" [61].
Summary
Comments