Lecture
Programming paradigms
Jenkins and Glasgow hold that "most programmers use one programming language in their work and follow one style. They program in the paradigm imposed by the language they use. Often they leave aside alternative approaches to the goal, and consequently it is difficult for them to see the advantages of a style more suited to the problem being solved" [40]. Bobrow and Stefik defined the concept of programming style thus: "It is a way of building programs based on definite programming principles, together with the choice of a suitable language that makes programs written in this style understandable" [41]. These same authors identified five basic varieties of programming style, which are listed below along with the kinds of abstractions inherent in them:
| ® procedure-oriented | algorithms |
| ® object-oriented | classes and objects |
| ® logic-oriented | goals, often expressed in terms of the predicate calculus |
| ® rule-oriented | "if-then" rules |
| ® constraint-oriented | invariant relationships |
It is impossible to declare any one programming style the best in all areas of practical application. For example, for designing knowledge bases the rule-oriented style is more suitable, while for computational problems the procedure-oriented one is. In our experience the object-oriented style is the most acceptable for the widest range of applications; indeed, this paradigm often serves as the architectural foundation on which we base other paradigms.
Every programming style has its own conceptual base. Every style requires its own frame of mind and way of perceiving the problem being solved. For the object-oriented style the conceptual base is the object model. It has four major elements:
These elements are major in the sense that without any one of them the model will not be object-oriented. Besides the major ones, there are three further minor elements:
In calling them minor, we mean that they are useful in the object model but not obligatory.
Without such a conceptual foundation you may program in a language such as Smalltalk, Object Pascal, C++, CLOS, Eiffel, or Ada, but from beneath the outward beauty the style of FORTRAN, Pascal, or C will peep out. The expressive power of an object-oriented language will be either lost or distorted. But even more importantly, there will then be little chance of coping with the complexity of the problems being solved.
Abstraction
The meaning of abstraction. Abstraction is one of the basic methods used for solving complex problems. Hoare holds that "abstraction manifests itself in finding similarities among certain objects, situations, or processes of the real world, and in making decisions on the basis of these similarities, abstracting for the time being from the differences that exist" [42]. Shaw defined this concept thus: "A simplified description or account of a system in which some properties and details are singled out while others are omitted. A good abstraction is one that emphasizes the details that are essential for consideration and use, and omits those that are inessential at the moment" [43]. Berzins, Gray, and Naumann recommended that "an idea should qualify as an abstraction only if it can be set out, understood, and analyzed independently of the mechanism that will subsequently be adopted for its implementation" [44]. Summing up these differing points of view, we obtain the following definition of abstraction:
An abstraction singles out the essential characteristics of some object that distinguish it from all other kinds of objects and thus clearly defines its conceptual boundaries from the observer's point of view.
Abstraction concentrates attention on the external features of an object and makes it possible to separate the most essential features of behavior from the inessential ones. Abelson and Sussman called such a separation of meaning and implementation an abstraction barrier [45], which is based on the principle of minimizing connections, whereby the interface of an object contains only the essential aspects of behavior and nothing more [46]. We consider one further additional principle useful, called the principle of least astonishment, according to which an abstraction must encompass all of an object's behavior, but no more and no less, and must not introduce surprises or side effects lying outside its sphere of applicability.
The choice of the right set of abstractions for a given problem domain constitutes the chief task of object-oriented design. In view of the importance of this topic, Chapter 4 is devoted entirely to it.
In the opinion of Seidewitz and Stark, "there exists a whole spectrum of abstractions, beginning with objects that correspond almost exactly to the realities of the problem domain, and ending with objects that have no right to exist" [47]. Here are these abstractions, from the most useful to the least useful:
| ® Entity abstraction | An object represents a useful model of some entity in the problem domain |
| ® Action abstraction | An object consists of a generalized set of operations |
| ® Virtual machine abstraction | An object groups operations that are either used together by a higher level of control or themselves use some set of lower-level operations |
| ® Coincidental abstraction | An object includes a set of operations that have nothing in common with one another |
We try to build entity abstractions, since they correspond directly to the entities of the problem domain.
A client is the name given to any object that uses the resources of another object (called a server). We shall characterize the behavior of an object by the services it renders to other objects and by the operations it performs on other objects. Such an approach concentrates attention on the external manifestations of an object and leads to the idea that Meyer called the contract model of programming [48]: the external manifestation of an object is regarded from the standpoint of its contract with other objects, and in accordance with this its internal structure must also be carried out (often in interaction with other objects). The contract records all the obligations that the server object has toward the client object. In other words, this contract defines the responsibility of the object - the behavior for which it answers [49].
Every operation provided for by this contract is uniquely defined by its formal parameters and the type of the returned value. The complete set of operations that a client may carry out on another object, together with the correct order in which these operations are invoked, is called a protocol. The protocol reflects all the possible ways in which an object may act or be acted upon, thereby completely defining the external behavior of the abstraction from the static and dynamic points of view.

An abstraction focuses on the characteristics of an object that are essential from the observer's point of view.
Central to the idea of abstraction is the notion of the invariant. An invariant is some logical condition whose value (true or false) must be preserved. For each operation of an object one can specify preconditions (invariants assumed by the operation) and postconditions (invariants that the operation satisfies). A change in an invariant violates the contract associated with the abstraction. In particular, if a precondition is violated, then the client is not observing its obligations and the server cannot perform its task correctly. If, on the other hand, a postcondition is violated, then it is the server that has violated its obligations, and the client can no longer trust it. In the event that some condition is violated, an exception is raised. As we shall see later, some languages have facilities for working with exceptions: objects may raise exceptions in order to halt further processing and warn other objects of the problem, which in turn may take upon themselves the catching of the exception and cope with the problem.
Note that the concepts operation, method, and member function come from different programming traditions (Ada, Smalltalk, and C++ respectively). In fact they denote one and the same thing and will henceforth be used interchangeably.
All abstractions possess both static and dynamic properties. For example, a file as an object requires a certain amount of memory on a particular device, has a name and contents. These attributes are static properties. The particular values of each of the properties listed, however, are dynamic and change in the course of using the object: a file can be enlarged or reduced, its name and contents can be changed. In the procedural style of programming, the actions that change the dynamic characteristics of objects constitute the essence of the program. All events are connected with the calling of subroutines and with the execution of statements. The rule-oriented style of programming is characterized by the fact that, under the influence of certain conditions, certain rules are activated, which in turn invoke other rules, and so on. The object-oriented style of programming is connected with acting upon objects (in Smalltalk terms, with sending messages to objects). Thus, an operation on an object gives rise to some reaction of that object. The operations that can be performed with respect to a given object, and the object's reaction to external influences, determine the behavior of that object.
Examples of abstractions. To illustrate what has been said above, let us give several examples. In this case we shall concentrate attention not so much on identifying abstractions for a particular problem (this is examined in detail in Chapter 4) as on the way of expressing abstractions.
In a greenhouse operation using hydroponics, plants are grown in a nutrient solution without sand, gravel, or other soil. Managing the operating regime of a greenhouse installation is a very responsible business, depending both on the kind of crops being grown and on the stage of growing. A whole range of factors must be monitored: temperature, humidity, lighting, acidity (the pH value), and the concentration of nutrients. In large operations, automatic systems are often used to solve this problem, which monitor and regulate the factors indicated. Simply put, the goal of automation here is to achieve observance of the growing regime with minimal human intervention.
One of the key abstractions in such a problem is the sensor. Several varieties of sensors are known. Everything that affects the harvest must be measured, so we must have sensors for water and air temperature, humidity, pH, lighting, and nutrient concentration. From an external point of view, a temperature sensor is an object capable of measuring the temperature where it is located. What is temperature? It is a numeric parameter having a limited range of values and a definite precision, denoting a number of degrees Fahrenheit, Celsius, or Kelvin. What is the sensor's location? It is some identifiable place in the greenhouse where we need to know the temperature; there are probably not many such places. For a temperature sensor, what matters is not so much the location itself as the fact that this particular sensor is situated in this particular place, and that distinguishes it from other sensors. Now one may ask the question: what are the responsibilities of a temperature sensor? We decide that the sensor must know the temperature at its location and report it on request. And what actions may a client perform with respect to the sensor? We make the decision that the client may calibrate the sensor and obtain from it the current temperature value.
To demonstrate design decisions, the C++ language will be used. Readers insufficiently familiar with this language, as well as those wishing to refresh their knowledge of the other object-based and object-oriented languages mentioned in this book, may find brief descriptions of them with examples in the appendix. And so, here are the declarations that specify an abstract temperature sensor in C++.
// Temperature in degrees Fahrenheit
typedef float Temperature;
// A number uniquely identifying the location of the sensor
typedef unsigned int Location;
class TemperatureSensor {
public:
TemperatureSensor (Location);
~TemperatureSensor();
void calibrate(Temperature actualTemperature);
Temperature currentTemperature() const;
private:
...
};
Here the two type definition statements Temperature and Location introduce convenient aliases for the simplest types, and this allows us to express our abstractions in the language of the problem domain [Unfortunately, the typedef construct does not define a new data type and does not provide protection for it. For example, the following declaration in C++: "typedef int Count;" simply introduces a synonym for the primitive type int. As we shall see in the next section, other languages, such as Ada and Eiffel, have more sophisticated semantics with regard to the strong typing of base types]. Temperature is a numeric data type in floating-point format for recording temperatures on the Fahrenheit scale. Values of type Location denote places on the farm where temperature sensors may be situated.
The class TemperatureSensor is only the specification of a sensor; its actual innards are hidden in its private part. The class TemperatureSensor is not yet an object. The sensors themselves are its instances, and they must be created before one can operate with them. For example, one may write as follows:
Temperature temperature;
TemperatureSensor greenhouse1Sensor(1);
TemperatureSensor greenhouse2Sensor(2);
temperature = greenhouse1Sensor.currentTemperature();
Let us consider the invariants associated with the operation currentTemperature. The precondition includes the assumption that the sensor is installed in the correct place in the greenhouse, and the postcondition that the sensor returns the temperature value in degrees Fahrenheit.
Up to now we have considered the sensor passive: someone must request the temperature from it, and then it will answer. However, there is another, equally legitimate approach. The sensor could actively monitor the temperature and notify other objects when its deviation from a given value exceeds a given level. The abstraction changes little as a result: only the object's responsibility is formulated somewhat differently. What new operations does it need in connection with this? The usual idiom for such cases is the callback. The client provides the server with a function (a callback function), and the server calls it when it sees fit. Here one needs to write something like this:
class ActiveTemperatureSensor {
public:
ActiveTemperatureSensor (Location,
void (*f)(Location, Temperature));
~ActiveTemperatureSensor();
void calibrate(Temperature actualTemperature);
void establishSetpoint(Temperature setpoint,
Temperature delta);
Temperature currentTemperature() const;private:
...
};
The new class ActiveTemperatureSensor has become only slightly more complex, but it expresses the new abstraction quite adequately. In creating an instance of the sensor, we pass it on initialization not only the location but also a pointer to a callback function whose parameters define the place of installation and the temperature. The new setting function establishSetpoint allows the client to change the trigger threshold of the temperature sensor, and the sensor's responsibility consists in calling the callback function each time the current temperature actualTemperature deviates from setpoint by more than delta. The client thereby learns the place where the trigger occurred and the temperature there, and beyond that it must itself know what to do about it.
Note that the client can still request the temperature on its own initiative. But what if the client does not perform the initialization, for example does not specify the permissible temperature? In designing we must certainly resolve this question by adopting some reasonable assumption: let it be considered that the interval of permissible temperature variations is infinitely wide.
Exactly how the class ActiveTemperatureSensor fulfills its obligations depends on its internal representation and should not concern external clients. This is determined by the implementation of its private part and its member functions.
Let us now consider another example of abstraction. For each crop being grown there must be specified a growing plan describing the variation over time of temperature, lighting, feeding, and a number of other factors ensuring a high yield. Since such a plan is part of the problem domain, its implementation as an abstraction is entirely justified.
For each crop being grown there is its own separate plan, but the general form of the plans is the same for all crops. The basis of a growing plan is a table matching moments in time with a list of necessary actions. For example, for some crop on the 15th day of growth the plan calls for maintaining a temperature of 78ЂF for 16 hours, of which 14 hours are with lighting, and then lowering the temperature to 65ЂF for the remainder of the day. In addition, the application of fertilizer may be required in the middle of the day in order to maintain the specified acidity value.
Thus, the growing plan is responsible for coordinating in time all the actions necessary in growing a crop. Our decision consists in not entrusting the plan abstraction with the execution of the plan itself - that will be the responsibility of another abstraction. In this way we clearly separate the concepts among the various parts of the system and limit the conceptual size of each individual abstraction.
From the standpoint of the interface of the plan object, the client must have the ability to set the details of the plan, change the plan, and query it. For example, an object may be implemented with a "human-computer" interface and manual modification of the plan. The object that contains the details of the growing plan must be able to change itself. In addition, there must exist a plan-executing object capable of reading the plan. As is evident from the further description, no object stands apart; rather, all of them interact to ensure a common goal. Proceeding from such an approach, the boundaries of each abstraction object and the protocols of their communication are defined.
In C++ the growing plan will look as follows. First let us introduce new data types, bringing our abstractions closer to the vocabulary of the problem domain (day, hour, lighting, acidity, concentration):
// A number denoting the day of the year
typedef unsigned int Day;
// A number denoting the hour of the day
typedef unsigned int Hour;
// Boolean type
enum Lights {OFF, ON};
// A number denoting the acidity index in the range from 1 to 14
typedef float pH;
// A number denoting the concentration as a percentage: from 0 to 100
typedef float Concentration;
Next, for tactical purposes, let us describe the following structure:
// Structure defining the conditions in the greenhouse
struct Condition {
Temperature temperature;
Lights lighting;
pH acidity;
Concentration concentration;
};
We used a structure rather than a class, since Condition is merely a mechanical grouping of parameters, without any internal behavior, and the richer semantics of a class are not needed here.
Finally, here is the growing plan:
class GrowingPlan (
public:
GrowingPlan (char *name);
virtual ~GrowingPlan();
void clear();
virtual void establish(Day, Hour, const Condition&);
const char* name() const;
const Condition& desiredConditions(Day, Hour) const;
protected:
...
};
Note that we have provided for one new responsibility: every plan has a name, and that name can be set and queried. Note also that the operation establish is declared virtual so that subclasses can override it.
The public part of the declaration contains the object's constructor and destructor (which define the procedures for creating and destroying it), two modifier operations (clear, which erases the entire plan, and establish, which defines the elements of the plan) and two selectors that report state (the functions name and desiredCondition). We have omitted the private part of the class from the declaration, replacing it with an ellipsis, since what matters to us at the moment is the external responsibilities, not the internal representation of the class.
Encapsulation
What is it? Although we described our abstraction GrowingPlan as a mapping of actions to moments in time, it need not be implemented literally as a table of data. Indeed, a client has no business knowing the implementation of the class that serves it, so long as that class honors its obligations. In fact, the abstraction of an object always precedes its implementation. And once the implementation decision has been made, it must be treated as a secret of the abstraction, hidden from most clients. As Ingalls wisely observes: "No part of a complex system should depend on the internal details of any other part" [50]. Whereas abstraction "helps people to think about what they are doing", encapsulation "allows program changes to be reliably made" [51].
Abstraction and encapsulation complement each other: abstraction is directed at the observable behavior of an object, while encapsulation deals with its internal workings. Most often encapsulation is achieved through information hiding, that is, by masking all internal details that do not affect external behavior. Usually both the internal structure of an object and the implementation of its methods are hidden.
Encapsulation thus establishes clear boundaries between different abstractions. Take the structure of a plant as an example: in order to understand the action of photosynthesis at the top level, it is entirely acceptable to ignore such details as the functions of the plant's roots or the chemistry of the cell walls. In much the same way, when designing a database it is customary to write programs so that they do not depend on the physical representation of the data; instead one concentrates on the schema, which reflects the logical structure of the data [52]. In both cases objects are protected from the implementation details of lower-level objects.
Liskov states outright that "abstraction works only in conjunction with encapsulation" [53]. In practice this means that a class has two parts: an interface and an implementation. The interface reflects the external behavior of an object, describing the abstraction of the behavior of all objects of the given class. The internal implementation describes the representation of that abstraction and the mechanisms for achieving the desired behavior of the object. The principle of separating interface from implementation corresponds to the essence of things: the interface part gathers everything that concerns the interaction of the given object with any other objects; the implementation hides from other objects all details that have no bearing on the process of object interaction. Britton and Parnas called such details "the secrets of an abstraction" [54].

Encapsulation hides the implementation details of an object.
Thus, encapsulation may be defined as follows:
Encapsulation is the process of separating from one another those elements of an object that define its structure and its behavior; encapsulation serves to isolate the contractual obligations of an abstraction from their implementation.
Examples of encapsulation. Let us return to the example of the hydroponic greenhouse. Another key abstraction of this problem domain is the heater, which maintains a given temperature in the enclosure. The heater is a low-level abstraction, and so we can confine ourselves to just three actions on this object: turning it on, turning it off, and querying its state. The heater must not be responsible for maintaining the temperature; that would be a higher-level behavior, realized jointly by the heater, the temperature sensor and one more object. We speak of higher-level behavior because it builds upon the simple behavior of the heater and the sensor, adding something more to them, namely hysteresis (or lag), thanks to which one can avoid frequent switching of the heater on and off in states close to the boundary. By making this decision about the division of responsibility, we make each abstraction more cohesive.
As always, we begin with the types.
// Boolean type
enum Boolean {FALSE, TRUE};
In addition to the three operations proposed above, we need the usual meta-operations for creating and destroying an object (a constructor and a destructor). Since there may be several heaters in the system, when creating each of them we will tell it the location where it is installed, as we did with the temperature sensor class TemperatureSensor. And so, here is the class Heater for abstract heaters, written in C++:
class Heater {
public:
Heater(Location);
~Heater();
void turnOn();
void tum0ff();
Boolean is0n() const;
private:
};
That is all that outsiders need to know about the class Heater. The inside of the class is quite another matter. Suppose the hardware designers decided to place the controlling computers outside the greenhouse (where it is too hot and humid), and to connect them to the sensors and actuators by means of serial interfaces. It is reasonable to expect that the heaters will be switched by means of a relay block, and that the block will be driven by commands arriving over the serial interface. Let us say that to turn on a heater one transmits the textual name of the command, the number of the heater's location, and one more number used as the signal to switch the heater on.
Here is a class expressing an abstract serial port.
class SerialPort {
public:
SerialPort();
~SerialPort();
void write(char*);
void write(int);
static SerialPort ports[10];
private:
};
Instances of this class will be real serial ports, to which strings and numbers can be output.
Let us add three more parameters to the class Heater.
class Heater {
public:
...
protected:
const Location repLocation;
Boolean repIsOn;
SerialPort* repPort;
};
These parameters repLocation, repIsOn, repPort form its encapsulated state. The rules of C++ are such that when the program is compiled, if a client attempts to access these parameters directly, an error message will be issued.
Let us now define the implementations of all the operations of this class.
Heater::Heater(Location 1)
: repLocation(1),
repIsOn(FALSE),
repPort(&SerialPort::ports[l]) {}
Heater::Heater() {}
void Heater::turnOn()
{
if (!repls0n) {
repPort->write("*");
repPort->write(repLocation);
repPort->write(1);
repIsOn = TRUE;
}}
void Heater::turn0ff()
{
if (repIsOn) {
repPort->write("*");
repPort->write(repLocation);
repPort->write(0);
repIsOn = FALSE;
}}
Boolean Heater::is0n() const
{
return repIsOn;
}
This style of implementation is typical of well-structured object-oriented systems: classes are written sparingly, since their specialization is carried out through subclasses.
Suppose that for some reason the hardware architecture of the system has changed, and instead of a serial port the control must be exercised through a fixed area of memory. There is no need to change the interface part of the class - it is enough to rewrite the implementation. According to the rules of C++, the modified class will then have to be recompiled, but not the other objects, unless they depend on the timing and space characteristics of the previous code (which is extremely undesirable and utterly unnecessary).
Let us now turn to the implementation of the class GrowingPlan. As has been said, it is in essence a time schedule of actions. Probably the best implementation of it would be a dictionary of time-action pairs with an open hash table. There is no point in remembering actions hour by hour; they do not occur that often, and in the intervals between them the system can interpolate the course of the process.
Encapsulation will hide two secrets from prying eyes: the fact that the schedule actually uses an open hash table, and the fact that intermediate values are interpolated. Clients are free to think that they are obtaining data from an hourly array of parameter values.
Sensible encapsulation localizes those features of the design that are liable to change. As the system evolves, the developers may decide that some operations take somewhat longer than is permissible, and that some objects occupy more memory than is acceptable. In such situations the internal representation of an object is often changed in order to implement more efficient algorithms, or to optimize the algorithm by the memory criterion, replacing the storage of data with computation. An important advantage of restricting access is the ability to introduce changes into an object without changing other objects.
Ideally, attempts to access data that is closed to access ought to be detected at program compilation time. The question of how these conditions are realized in particular programming languages is a subject of continual discussion. Thus, Smalltalk provides protection against direct access to the instances of another class, detecting such attempts at compile time. At the same time, Object Pascal does not encapsulate the representation of a class, so nothing in this language keeps a client from referring directly to the internal fields of another object. The CLOS language occupies an intermediate position on this question, placing all responsibility for restricting access on the programmer. In this language all slots may be accompanied by the attributes :reader, :writer and :accessor, which permit, respectively, reading, writing or full access to the data (that is, both reading and writing). In the absence of attributes a slot is fully encapsulated. By convention, acknowledging that some value is stored in a slot is regarded as a violation of the abstraction, so good CLOS programming style requires that, when publishing the interface of a class, only the names of its functions be documented, while the fact that a slot has full-access functions must be hidden [55]. In C++ the control of access and visibility is more flexible. The members of a class may be assigned to the public, private or protected part. The public part is accessible to all objects; the private part is completely closed to other objects; the protected part is visible only to instances of the given class and its subclasses. In addition, C++ has the notion of "friends", to whom the private part is open.
Information hiding is a relative notion: what is hidden at one level of abstraction is revealed at another level. It is possible to get inside objects; true, it usually requires that the developer of the server class take special care about this, and that the developers of the client classes not be too lazy to work it out. Encapsulation is no protection against stupidity; as Stroustrup noted, it "protects against accident, not against fraud" [56]. Of course, the programming language has nothing to do with it here; except perhaps that the operating system may restrict access to the files in which the implementations of classes are described. In practice, however, it is sometimes simply necessary to acquaint oneself with the implementation of a class in order to understand its purpose, especially if there is no external documentation.
Modularity
The notion of modularity. In Myers's opinion, "Dividing a program into modules to some extent makes it possible to reduce its complexity... However, a far more important fact is that within a modular program sets of well-defined and documented interfaces are created. These interfaces are invaluable for a thorough understanding of the program as a whole" [57]. In some programming languages, for instance in Smalltalk, there are no modules, and classes constitute the only physical basis of decomposition. In other languages, including Object Pascal, C++, Ada and CLOS, a module is an independent language construct. In these languages classes and objects make up the logical structure of the system, and they are placed in modules, which form the physical structure of the system. This property becomes especially useful when a system consists of many hundreds of classes.
According to Barbara Liskov, "modularity is the division of a program into fragments that are compiled separately, but that can establish connections with other modules". We shall use Parnas's definition: "The connections between modules are their assumptions about each other" [58]. In most languages that support the principle of modularity as an independent concept, the interface of a module is separated from its implementation. Thus, modularity and encapsulation go hand in hand. Different programming languages support modularity in different ways. For example, in C++ modules are separately compiled files. It is traditional for C/C++ to place the interface part of modules in separate files with the extension .h (so-called header files). The implementation, that is, the body of the module, is stored in files with the extension .c (in C++ programs the extensions .ee, .cp and .cpp are often used). The connection between files is declared by the macro processor directive #include. This approach rests entirely on convention and is not a strict requirement of the language itself. In Object Pascal the principle of modularity is formalized somewhat more strictly. This language defines a special syntax for the interface part and the implementation of a module (unit). Ada goes one step further: a module (called a package) also has two parts - a specification and a body. But, unlike Object Pascal, it permits the connections with modules to be defined separately for the specification and for the body of the package. Thus, it is permissible for the body of a module to have connections with modules that are invisible to its specification.
Dividing a program into modules correctly is almost as difficult a task as choosing the right set of abstractions. Zelkowitz is absolutely right when he asserts: "since at the beginning of work on a project the solutions may be unclear, decomposition into modules can cause difficulties. For well-known applications (for instance, building compilers) this process can be standardized, but for new problems (military systems or spacecraft control) the task can be very hard" [59].
Modules play the role of physical containers into which the definitions of classes and objects are placed during the logical design of a system. The same situation arises for the designers of on-board computers. The logic of electronic equipment can be built on the basis of elementary circuits of the NOT, NAND, NOR type, but such circuits can be combined into standard integrated circuits (modules), for example of the 7400, 7402 or 7404 series.
For small problems it is acceptable to describe all the classes and objects in a single module. However, for most programs (apart from the most trivial ones) the best solution will be to group logically related classes and objects into a separate module, leaving open those elements that other modules absolutely must see. This way of breaking a program into modules is good, but it can be carried to the point of absurdity. Consider, for example, a task that runs on multiprocessor equipment and requires a message-passing mechanism to coordinate its work. In large systems such as those described in chapter 12, it is quite normal to have several hundred and even thousands of kinds of messages. It would be naive to define each message class in a separate module. Not only does this create a documentation nightmare, but even simply finding the needed fragments of the description becomes extremely difficult for the user. When changes are introduced into the design, hundreds of modules will have to be modified and recompiled. This example shows that information hiding has its downside too [60]. Dividing a program into modules in an unsystematic way is sometimes far worse than having no modularity at all.

Modularity allows abstractions to be kept separate.
In traditional structured design, modularity is the art of sorting subprograms into piles in such a way that subprograms that use one another or that change together end up in the same pile. In object-oriented programming the situation is somewhat different: it is necessary to separate physically the classes and objects that make up the logical structure of the design.
On the basis of accumulated experience one can list the techniques and rules that make it possible to compose modules out of classes and objects in the most effective way. Britton and Parnas hold that "the ultimate goal of decomposing a program into modules is to reduce programming costs through independent development and testing. The structure of a module must be simple enough to comprehend; the implementation of each module must not depend on the implementation of other modules; measures must be taken to ease the process of introducing changes where they are most likely" [61]. Pragmatic considerations set a limit to these guidelines. In practice, recompiling the body of a module is not a laborious operation: only the given module is compiled anew, and the program is relinked. Recompiling the interface part of a module, on the contrary, is more laborious. In strongly typed languages one has to recompile the interface and the body of the changed module itself, then all the modules connected with it, then the modules connected with those, and so on down the chain. In the end, very large programs may require many hours for recompilation (unless the development environment supports fragmentary compilation), which is clearly undesirable. Therefore one should strive to keep the interface part of modules as narrow as possible (within the limits of providing the necessary connections). Our programming style requires that everything that can possibly be hidden be hidden in the implementation of a module. Gradually moving declarations from the implementation into the interface part is far less dangerous than "cleaning out" superfluous interface code.
Thus, the programmer must find a balance between two opposing tendencies: the desire to hide information and the need to make certain abstractions visible in several modules. Parnas, Clements and Weiss proposed the following rule: "Those features of a system that are subject to change should be hidden in separate modules; only those elements whose probability of change is small may be used as intermodule elements. All data structures must be isolated within a module; access to them will be possible for all the procedures of that module and closed to all others. Access to data from a module must be carried out only through the procedures of that module" [62]. In other words, one should strive to build modules so as to group together logically related abstractions and to minimize the mutual connections between modules. Proceeding from this, let us give a definition of modularity:
Modularity is the property of a system that has been decomposed into internally cohesive but loosely coupled modules.
Thus, the principles of abstraction, encapsulation and modularity are mutually complementary. An object logically defines the boundaries of a particular abstraction, while encapsulation and modularity make those boundaries physically inviolable.
In the process of dividing a system into modules, two rules may be useful. First, since modules serve as the elementary and indivisible blocks of a program that can be reused within a system, the distribution of classes and objects among modules must take this into account. Second, many compilers create a separate code segment for each module. Therefore restrictions on the size of a module may appear. The dynamics of subprogram calls and the placement of declarations within modules can strongly affect the locality of references and the management of virtual memory pages. If procedures are poorly divided among modules, cross-segment calls become more frequent, which leads to a loss of cache efficiency and frequent page swapping.
The choice of a division into modules may also be influenced by certain external circumstances. In collective program development the distribution of work is carried out, as a rule, on a modular principle, and a correct division of the project minimizes the connections between the participants. Here the more experienced programmers are usually responsible for the interfaces of the modules, and the less experienced ones for the implementation. At a larger scale, the same relationships hold for the relations between subcontractors. Abstractions can be distributed so that the module interfaces can be quickly established by agreement between the companies participating in the work. Changes to an interface cause much shouting and gnashing of teeth, to say nothing of the enormous expenditure of paper - all these factors make the interface extremely conservative. As for documenting the project, that too is built, as a rule, on a modular principle - the module serves as the unit of description and administration. Ten modules instead of one will require ten times as many descriptions, and therefore, unfortunately, documentation requirements sometimes influence the decomposition of a project (in most cases negatively). Security requirements may also make themselves felt: part of the code may be unclassified and another part classified; the latter is then implemented as a separate module (or modules).
Bringing together such contradictory requirements is rather difficult, but the main thing is to understand: identifying the classes and objects in a design and organizing the module structure are independent activities. The process of identifying classes and objects forms part of the logical design of a system, while the division into modules is a stage of physical design. Of course, it is sometimes impossible to complete the logical design of a system without completing the physical design, and vice versa. These two processes are carried out iteratively.
Examples of modularity. Let us see how modularity is realized in the hydroponic gardening system. Suppose that, instead of purchasing specialized hardware, it has been decided to use a standard workstation with a graphical user interface, GUI (Graphical User Interface). By means of the workstation the operator can form new growing plans, modify existing plans and observe their execution. Since the abstraction of a growing plan is one of the key ones, let us create a module containing everything relating to the growing plan. In C++ we would need roughly the following header file (let us call it gplan.h).
// gplan.h
#ifndef _GPLAN_H
#define _GPLAN_H 1
#include "gtypes.h"
#include "except.h"
#include "actions.h"
class GrowingPlan ...
class FruitGrowingPlan ...
class GrainGrowingPlan ...
#endif
Here we import into the file three other header files with the definitions of the interfaces to which we shall refer: gtypes.h, except .h and actions.h. The actual code of the classes we shall place in an implementation module, in a file named gplan.cpp.
We could also gather into one module all the programs relating to the dialog windows specific to this application. This module will certainly depend on the classes declared in gplan.h, and on other header files with the descriptions of the GUI classes.
There will probably be many other modules importing lower-level interfaces. Finally we shall get to the main function - the point at which our program is launched by the operating system. In object-oriented design this will most likely be the least significant and least interesting part of the system, whereas in the traditional structured approach the top-level function is the cornerstone that holds up the whole edifice. We consider the object-oriented approach to be more natural, since, as Meyer observes, "in practice software systems offer a certain set of services. Reducing them to a single function is possible, but unnatural... Real systems have no top level" [63].
Hierarchy
What is hierarchy? Abstraction is a useful thing, but always, except in the very simplest situations, the number of abstractions in a system far exceeds our mental capacity. Encapsulation makes it possible to remove this obstacle to some degree, by taking the internal content of abstractions out of view. Modularity also simplifies the task, by combining logically related abstractions into groups. But this turns out to be insufficient.
A significant simplification in the understanding of complex problems is achieved by forming a hierarchical structure out of abstractions. Let us define hierarchy as follows:
Hierarchy is a ranking or ordering of abstractions by levels.
The principal kinds of hierarchical structures as applied to complex systems are the class structure (the "is-a" hierarchy) and the object structure (the "part of" hierarchy).
Examples of hierarchy: single inheritance. An important element of object-oriented systems and the principal kind of "is-a" hierarchy is the concept of inheritance mentioned above. Inheritance denotes a relationship between classes (a parent/child relationship) in which one class borrows the structural or functional part of one or several other classes (respectively, single and multiple inheritance). In other words, inheritance creates a hierarchy of abstractions in which subclasses inherit their structure from one or several superclasses. Often a subclass extends or rewrites the components of the class above it.
Semantically, inheritance describes a relationship of the "is-a" type. For example, a bear is a mammal, a house is real estate and "quicksort" is a sorting algorithm. Thus, inheritance gives rise to a "generalization-specialization" hierarchy, in which a subclass represents a specialized particular case of its superclass. The "litmus test" of inheritance is the reverse check; thus, if B is not an A, then B should not be derived from A.
Let us now consider the various kinds of plants grown in our gardening system. We have already introduced a generalized representation of an abstract plan for growing plants. However, different crops require different plans. Moreover, the plans for fruits resemble one another, but differ from the plans for vegetables or flowers. It makes sense to introduce, at a new level of abstraction, a generalized "fruit" plan, including instructions on pollination and harvesting. Here is what the definition of a plan for fruits, as a descendant of the general growing plan, will look like in C++.
// The Yield type
typedef unsigned int Yield;
class FruitGrowingPlan : public GrowingPlan {
public:
FruitGrowingPlan(char* name);
virtual ~FruitGrowingPlan();
virtual void establish(Day, Hour, Condition&);
void scheduleHarvest(Day, Hour);
Boolean isHarvested() const;
unsigned daysUntilHarvest() const;
Yield estimatedYield() const;
protected:
Boolean repHarvested;
Yield repYield;

Abstractions form a hierarchy.
This means that the fruit growing plan FruitGrowingPlan is a kind of growing plan GrowingPlan. The parameters repHarvested and repYield have been added to it, four new functions have been defined and the function establish has been overridden. We could now continue specializing - for example, define on the basis of the "fruit" plan an "apple" class AppleGrowingPlan.
In an inheritance hierarchy the common part of the structure and behavior is concentrated in the most general superclass. For this reason inheritance is spoken of as a generalization-specialization hierarchy. Superclasses here reflect the most general abstractions, and subclasses the more specialized ones, in which the members of the superclass may be supplemented, modified and even hidden. The principle of inheritance makes it possible to simplify the expression of abstractions, and makes a design less cumbersome and more expressive. Cox writes: "In the absence of inheritance, every class becomes an independent block and must be developed "from scratch". Classes lose their commonality, since every programmer implements them in his own way. The coherence of a system is then achieved only through the discipline of the programmers. Inheritance makes it possible to introduce new programs into circulation the way we teach beginners new concepts - by comparing the new with something already known" [64].
The principles of abstraction, encapsulation and hierarchy stand in a certain healthy conflict with one another. Danforth and Tomlinson assert: "Data abstraction creates an opaque barrier hiding the state and the functions of an object; the principle of inheritance requires that access to both the state and the functions of an object be opened up to derived objects" [65]. For any class there are usually two kinds of clients: objects that manipulate instances of the given class, and inheriting subclasses. Liskov therefore notes that there are three ways of violating encapsulation through inheritance: "a subclass may gain access to the instance variables of its superclass, call a private function, and finally, refer directly to the superclass of its superclass" [66]. Different programming languages strike the compromise between inheritance and encapsulation differently; the most flexible in this respect is C++. In it the interface of a class may be divided into three parts: the private part, visible only to the class itself; the protected part, visible also to subclasses; and the public part, visible to everyone.
Examples of hierarchy: multiple inheritance. In the previous example we considered single inheritance, where the subclass FruitGrowingPlan was created from just one superclass GrowingPlan. In a number of cases it is useful to implement inheritance from several superclasses. Suppose we need to define a class representing kinds of plants.
class Plant {
public:
Plant(char* name, char* species);
virtual ~Plant();
void setDatePlanted(Day);
virtual establishGrowingConditions(const Condition&);
const char* name() const;
const char* species() const;
Day datePlantedt) const;
protected:
char* repName;
char* repSpecies;
Day repPlanted;
private:
...
};
Every instance of the class plant will contain a name, a species and a planting date. In addition, for each kind of plant one can specify particular optimal growing conditions. We want this function to be overridden by subclasses, and so it is declared virtual in the C++ implementation. Three parameters are declared as protected, that is, they will be accessible both to the class and to its subclasses (the private part of the specification is accessible only to the class itself).
Studying the problem domain, we come to the conclusion that different groups of cultivated plants - flowers, fruits and vegetables - have their own particular properties that are essential to the technology of growing them. For example, for flowers it is important to know the times of flowering and of seed ripening. Similarly, the harvest time is important for the abstractions of fruits and vegetables. Let us create two new classes - flowers (Flower) and fruit-vegetables (FruitVegetable); both of them inherit from the class Plant. However, some flowering plants bear fruit! For this abstraction we shall have to create a third class, FlowerFruitVegetable, which will inherit from the classes Flower and FruitVegetablePlant.
To avoid redundancy, multiple inheritance comes in very handy in this case. First let us describe flowers and fruit-vegetables separately.
class FlowerMixin {
public:
FlowerMixin(Day timeToFlower, Day timeToSeed);
virtual ~FlowerMixin();
Day timeToFlower() const;
Day timeToSeed() const;
protected:
...
};
class FruitVegetableMixin {
public:
FruitVegetableMixin(Day timeToHarvest);
virtual ~FruitVegetableMixin();
Day timeToHarvest() const;
protected:
...
};
We deliberately described these two classes without inheritance. They inherit from no one and are specially intended to be mixed in (whence the name Mixin) to other classes. For example, let us describe a rose:
class Rose : public Plant, public FlowerMixin...
And here is a carrot:
class Carrot : public
продолжение следует...
Часть 1 2.2. The Constituent Parts of the Object Approach
Часть 2 - 2.2. The Constituent Parts of the Object Approach
Comments