You get a bonus - 1 coin for daily activity. Now you have 1 coin

- 2.2. The Constituent Parts of the Object Approach

Lecture



Это окончание невероятной информации про .

...

Plant, public FruiteVegetableMixin {};

In both cases the classes inherit from two superclasses: instances of the subclass Rose include structure and behavior both from the class Plant and from the class FlowerMixin. And now let us define a cherry, whose marketable product is both flowers and fruit:

class Cherry : public Plant, public FlowerMixin, FruitVegetableMixin...

Multiple inheritance is a simple enough thing, but it complicates the implementation of programming languages. There are two problems - name conflicts between different superclasses, and repeated inheritance. The first case is when a field or an operation with the same name is defined in two or more superclasses. In C++ this kind of conflict must be explicitly resolved by hand, while in Smalltalk the one that occurs first is taken. Repeated inheritance is when a class inherits from two classes, and they separately inherit from one and the same fourth class. A diamond-shaped inheritance structure results, and it must be decided whether the lowest class should receive one or two separate copies of the topmost class. In some languages repeated inheritance is forbidden, in others the conflict is resolved "by fiat", and in C++ it is left to the programmer's discretion. Virtual base classes are used to forbid the duplication of repeated structures; otherwise copies of the fields and functions will appear in the subclass, and the origin of each of the copies will have to be indicated explicitly.

Multiple inheritance is often abused. For example, cotton candy is a particular case of candy, but by no means of cotton. Apply the same "litmus test": if B is not an A, then it should not inherit from A. Often badly formed multiple-inheritance structures can be reduced to a single superclass plus aggregation of the other classes by the subclass.

Examples of hierarchy: aggregation. If the "is a" hierarchy defines a "generalization/specialization" relationship, then the "part of" relationship introduces an aggregation hierarchy. Here is an example.

class Garden {
public:

Garden();
virtual ~Garden();

protected:

Plant* repPlants[100];
GrowingPlan repPlan;

};

This is the abstraction of a garden, consisting of an array of plants and a growing plan.

Dealing with such hierarchies, we often speak of levels of abstraction, which were first proposed by Dijkstra [67]. In a class hierarchy the higher abstraction is a generalization, and the lower one a specialization. We therefore say that the class Flower is at a higher level of abstraction than the class Plant. In a "part of" hierarchy a class is at a higher level of abstraction than any of those used in its implementation. Thus the class Garden stands at a higher level than the class Plant.

Aggregation exists in all languages that use structures or records made up of data of different types. But in object-oriented programming it acquires new power: aggregation makes it possible to group logically related structures physically, and inheritance easily copies these common groups into different abstractions.

In connection with aggregation the problem of ownership, or belonging, of objects arises. In our abstract garden many plants grow at the same time, and removing or replacing one of them does not make the garden a different garden. If we destroy the garden, the plants remain (they can, after all, be transplanted). In other words, the garden and the plants have their own separate and independent lifetimes; we achieved this thanks to the fact that the garden contains not the Plant objects themselves, but pointers to them. On the contrary, we decided that the object GrowingPlan is internally connected with the object Garden and does not exist independently. The growing plan is physically contained in every instance of a garden and perishes together with it. We shall speak in more detail about the semantics of ownership in the next chapter.

Typing

What is typing? The notion of a type is taken from the theory of abstract data types. Deutsch defines a type as "a precise characterization of properties, including structure and behavior, applying to some collection of objects" [68]. For our purposes it is enough to regard the terms type and class as interchangeable [Type and class are not quite the same thing; in some languages they are distinguished. For example, early versions of the Trellis/Owl language allowed an object to have both a class and a type. Even in Smalltalk objects of the classes SmallInteger, LargeNegativeInteger, LargePositiveInteger belong to a single type Integer, although to different classes [69]. To most mortals, distinguishing types from classes is simply distasteful and pointless. It is enough to say that a class implements the notion of a type]. Nevertheless, types are worth discussing separately, since they cast the meaning of abstraction in a completely different light. In particular, we assert that:

Typing is a way of protecting oneself against using objects of one class in place of another, or at least of controlling such use.

Typing forces us to express our abstractions in such a way that the programming language used in the implementation supports the enforcement of the design decisions taken. Wegner notes that such a means of control is essential to programming "in the large" [70].

The idea of type conformance occupies a central place in the notion of typing. Take, for example, physical units of measurement [71]. Dividing distance by time, we expect to obtain velocity, not weight. Multiplying temperature by force makes no sense, whereas multiplying distance by force does. All these are examples of strong typing, where the application domain imposes rules and restrictions on the use and combination of abstractions.

Examples of strong and weak typing. A particular programming language may have a strong or a weak typing mechanism, or even have none at all while remaining object-oriented. For instance, in Eiffel adherence to the rules of type usage is monitored inflexibly - an operation cannot be applied to an object if it is not registered in its class or superclass. In strongly typed languages a violation of type conformance can be detected while the program is being translated. On the other hand, Smalltalk has no types: at run time any message can be sent to any object, and if the class of the object (or its superclass) does not understand the message, an error message is generated. A violation of type conformance may not be detected at translation time and usually manifests itself as a run-time error. C++ tends toward strong typing, but in this language the typing rules can be ignored or suppressed entirely.

Let us consider the abstraction of the various types of tanks that may be used in our greenhouse. There are probably tanks for water and for mineral fertilizers; although the former are intended for liquids and the latter for granular substances, they have quite enough in common to arrange a hierarchy of classes. Let us begin with the types.

// A number denoting a level from 0 to 100 percent
typedef float Level;

The typedef operators in C++ do not introduce new types. In particular, both Level and Concentration are in fact other names for float, and they can be freely mixed in computations. In this sense C++ has weak typing: values of primitive types such as int or float are indistinguishable within the given type. By contrast, Ada and Object Pascal provide strong typing for primitive types. In Ada an interval of values or a subset with limited precision can be declared as an independent type.

2.2. The Constituent Parts of the Object Approach

Strict typing prevents the mixing of abstractions.

Let us now build a hierarchy of classes for tanks:

class StorageTank {
public:

StorageTank();
virtual ~StorageTank();
virtual void fill();
virtual void startDraining();
virtual void stopDraining();
Boolean isEmpty() const;
Level level() const;

protected:
...
};

class WaterTank : public StorageTank{
public:

WaterTank();
virtual ~WaterTank();
virtual void fill();
virtual void startDraining();
virtual void stopDraining();
void startHeating();
void stopHeating();
Temperature currentTemperature() const;

protected:
...
};

class NutrientTank : public StorageTank {
public:

NutrientTank();
virtual ~NutrientTank();
virtual void startDrainingt();
virtual void stopDraining();

protected:
...
};

The class StorageTank is the base class of the hierarchy. It provides the structure and behavior common to all tanks: the ability to fill them or to empty them. The classes WaterTank (a tank for water) and NutrientTank (for fertilizers) inherit the properties of StorageTank, partially override them and add something of their own: for example, the class WaterTank introduces new behavior connected with temperature.

Suppose that we have the following declarations:

StorageTank s1, s2;
WaterTank w;
NutrientTank n;

Note that variables such as s1, s2, w or n are not instances of the corresponding classes. In fact, they are simply names by which we denote objects of the corresponding classes: when we say "the object s1" we actually mean the instance of StorageTank denoted by the variable s1. We shall return to this subtle question in the next chapter.

In checking the types of classes, C++ is typed far more strictly. By this is meant that expressions containing calls to operations are checked for type conformance at compile time. For example, the following is correct:

Level l = s1.level();
w.startDrainingt();
n.stopDraining();

Indeed, such selectors exist in the classes to which the corresponding variables belong. By contrast, the following is incorrect and will cause a compilation error:

s1.startHeating(); // Incorrect
n.stopHeating(); // Incorrect

There are no such functions either in the classes themselves or in their superclasses. But the following

n.fill();

is perfectly correct: there is no fill function in the definition of NutrientTank, but it is present in the class above it.

Thus, strong typing forces us to observe the rules of using abstractions, and so it is all the more useful the larger the project. However, it has its dark side as well. Namely, even small changes in the interface of a class require the recompilation of all its subclasses. Moreover, without parameterized classes, which will be discussed in chapters 3 and 9, it is hard to imagine how one could create a collection of heterogeneous objects. Suppose we want to introduce the abstraction of an inventory list, in which all the property associated with the greenhouse is gathered. The idiom usual for C is applicable in C++ as well: one must use a container class holding pointers to void, that is, to objects of arbitrary type.

class Inventory {
public:

Inventory();
~Inventory();
void add(void*);
void remove(void*);
void* mostRecent() const;
void apply(Boolean (*)(void*));

private:
...
};

The apply operation is a so-called iterator, which makes it possible to apply some operation to all the objects in the list. For more about iterators see the next chapter.

Having an instance of the class Inventory, we can add and destroy pointers to objects of any classes. But these actions are not type-safe - the list may end up containing both tangible objects (tanks) and intangible ones (a temperature or a growing plan), which violates our abstraction of material accounting. Moreover, we could enter into the list objects of the classes WaterTank and TemperatureSensor, and, carelessly expecting an object of the class WaterTank from the mostRecent function, receive a StorageTank.

Generally speaking, there are two general solutions to this problem. First, one can make the container class type-safe. In order not to manipulate untyped void pointers, we could define an inventory class that manipulates only objects of the class TangibleAsset (tangible property), and this class will be mixed into all the classes representing such property, for example into WaterTank, but not into GrowingPlan. This makes it possible to cut off the first kind of problem, where objects of different types are improperly mixed. Second, one can introduce type checking at run time, in order to know what type of object we are dealing with at a given moment. For example, in Smalltalk one can ask objects for their class. In C++ such a facility was not part of the standard until recently, although in practice one can, of course, introduce into the base class an operation returning a class code (a string or a value of an enumerated type). However, one must have very serious reasons for doing this, since type checking at run time weakens encapsulation. As will be shown in the next section, the need for type checking can be mitigated by using polymorphic operations.

In languages with strong typing it is guaranteed that all expressions will be type-conformant. What this means is best explained by an example. The following assignments are permissible:

s1 = s2;
s1 = w;

The first assignment is permissible because the variables belong to one and the same class, and the second because the assignment proceeds upward through the types. In the second case, however, a loss of information occurs (known in C++ as the "slicing problem"), since the class of the variable w, WaterTank, is semantically richer than the class of the variable s1, that is, StorageTank.

The following assignments are incorrect:

w = s1; // Incorrect
w = n; // Incorrect

In the first case the error is that the assignment proceeds downward through the hierarchy, and in the second the classes are not even in a subordinate relationship.

Sometimes it is necessary to convert types. For example, look at the following function:

void checkLevel(const StorageTank& s);

We may cast a value of a higher class to a subclass if and only if the actual parameter in the call turned out to be an object of the class WaterTank. Or here is another case:

if (((WaterTank&)s).currentTemperature() < 32.0) ...

This expression is type-conformant, but not safe. If during the execution of the program it suddenly turns out that the variable s denoted an object of the class NutrientTank, the type cast will give an unpredictable result at run time. Generally speaking, type conversions should be avoided, since they often constitute a violation of the adopted system of abstractions.

Tesler noted the following important advantages of strongly typed languages:

  • "The absence of type checking can lead to mysterious failures in programs during their execution.
  • In most systems the edit-compile-debug process is tedious, and early detection of errors is simply irreplaceable.
  • Type declarations improve the documentation of programs.
  • Many compilers generate more efficient object code if the types are explicitly known to them" [72].

Languages in which typing is absent possess greater flexibility, but even in such languages, in the opinion of Borning and Ingalls: "Programmers usually know what objects are expected as arguments and what will be returned" [73]. In practice, especially when programming "in the large", the reliability of strongly typed languages amply compensates for a certain loss of flexibility in comparison with untyped languages.

Examples of typing: static and dynamic binding. Strong typing and static typing are different things. Strict typing watches over the conformance of types, while static typing (otherwise called static or early binding) determines the moment at which names are bound to types. Static binding means that the types of all variables and expressions are known at compile time; dynamic binding (also called late binding) means that the types are unknown until the moment the program is executed. The concepts of typing and binding are independent, and so a programming language may have: strong typing with static binding (Ada), strong typing with dynamic binding (C++, Object Pascal), or no types at all and dynamic binding (Smalltalk). The CLOS language occupies an intermediate position between C++ and Smalltalk: the type definitions made by the programmer may either be taken into account or not.

Let us comment on this notion once again with an example in C++. Here is a "free", that is, not belonging to the definition of any class, function [A free function is a function that does not belong to any class. In purely object-oriented languages, such as Smalltalk, there are no free procedures; every operation is connected with some class]:

void balanceLevels(StorageTank& s1, StorageTank& s2);

A call to this function with instances of the class StorageTank or any of its subclasses as parameters will be type-conformant, since the type of each actual parameter descends in the inheritance hierarchy from the base class StorageTank.

In implementing this function we may have something like:

if (s1.level()> s2.level()) s2.fill();

What is peculiar about the semantics when the selector level is used? It is defined only in the class StorageTank, and therefore, regardless of the classes of the objects denoted by the variables at the moment of execution, one and the same function inherited by them will be used. The call to this function is statically bound at compile time - we know exactly which operation will be launched.

It is another matter with fill. This selector is defined in StorageTank and overridden in WaterTank, and therefore it will have to be bound dynamically. If at execution time the variable s2 is of the class WaterTank, then the function will be taken from that class, and if it is NutrientTank, then from StorageTank. C++ has a special syntax for explicitly indicating the source; in our example the call to fill will be resolved, respectively, as WaterTank::fill or StorageTank::fill [This is how C++ syntax specifies explicit qualification of a name].

This feature is called polymorphism: one and the same name may denote objects of different types, but, having a common ancestor, they all also have a common subset of operations that can be performed on them [74]. The opposite of polymorphism is called monomorphism; it is characteristic of languages with strong typing and static binding (Ada).

Polymorphism arises where inheritance and dynamic binding interact. This is one of the most attractive properties of object-oriented languages (after their support for abstraction), distinguishing them from traditional languages with abstract data types. And, as we shall see in the following chapters, polymorphism plays a very important role in object-oriented design.

Concurrency

What is concurrency? There are tasks in which automated systems must process many events simultaneously. In other cases the need for computing power exceeds the resources of a single processor. In each of these situations it is natural to use several computers to solve the problem, or to employ multitasking on a multiprocessor computer. A process (a thread of control) is the fundamental unit of action in a system. Every program has at least one thread of control; a concurrent system has many such threads: some are short-lived, while others live throughout the entire session of the system's work. Real concurrency is achieved only on multiprocessor systems, while single-processor systems imitate concurrency by means of time-sharing algorithms.

Apart from this "hardware" distinction, we shall distinguish "heavyweight" and "lightweight" concurrency according to the demand for resources. "Heavyweight" processes are managed by the operating system independently of others, and a separate protected address space is allocated for them. "Lightweight" ones coexist in a single address space. "Heavyweight" processes communicate with one another through the operating system, which is usually slow and costly. Communication between "lightweight" processes is carried out far more simply; often they use the very same data.

Many modern operating systems provide direct support for concurrency, and this circumstance has a very favorable effect on the ability to provide concurrency in object-oriented systems. For example, UNIX systems provide the system call fork, which spawns a new process. Windows NT and OS/2 are multithreaded; in addition they provide programming interfaces for creating processes and manipulating them.

Lim and Johnson note that "the possibilities for designing concurrency in object-oriented languages do not differ greatly from any others; at the lower levels of abstraction concurrency and OOP develop completely independently. With OOP or without it, all the traditional problems of concurrent programming remain" [75]. Indeed, creating large programs is hard enough as it is, and if they are also concurrent, then one must think about the possible idling of one of the threads, failure to receive data, deadlock, and so on.

Fortunately, as the same authors go on to note: "at the upper levels OOP simplifies concurrent programming for the rank-and-file developer, by hiding it in reusable abstractions" [76]. Black et al. drew the following conclusion: "the object model is good for distributed systems, since it implicitly divides a program into (1) distributed units and (2) communicating actors" [77].

Whereas object-oriented programming is based on abstraction, encapsulation and inheritance, concurrency gives its main attention to the abstraction and synchronization of processes [78]. The object is the notion at which these two points of view converge: every object (obtained from an abstraction of the real world) may represent a separate thread of control (an abstraction of a process). Such an object is called active. For systems built on the basis of OOD, the world may be represented as a collection of interacting objects, some of which are active and act as independent computing centers. On this basis let us give the following definition of concurrency:

2.2. The Constituent Parts of the Object Approach

Concurrency allows different objects to act at the same time.

Concurrency is the property that distinguishes active objects from passive ones.

Examples of concurrency. Earlier we acquired the class ActiveTemperatureSensor, whose behavior requires it to measure the temperature periodically and to invoke a callback function known to it whenever the temperature deviates by some amount from the established value. How it would do this we did not explain at the time. Whatever the secrets of the implementation, it is clear that this is an active object and that, consequently, concurrency cannot be dispensed with here. In object-oriented design there are three approaches to concurrency.

First, concurrency is an intrinsic property of some programming languages. Thus, for the Ada language the mechanism of concurrent processes is realized as a task. Smalltalk has a class process, from which all active objects inherit. There are many other languages with built-in mechanisms for the parallel execution and synchronization of processes - Actors, Orient 84/K, ABCL/1, which provide similar mechanisms of concurrency and synchronization. In all these languages one can create active objects whose code is constantly executed in parallel with other active objects.

Second, one can use a class library implementing some variety of "lightweight" concurrency. For example, the AT&T library for C++ contains the classes Shed, Timer, Task, and so on. Its implementation naturally depends on the platform, although the interface is quite portable. With this approach the mechanisms of parallel execution are not built into the language (and hence do not affect systems without concurrency), but at the same time they are practically perceived as built-in.

Finally, third, one can create the illusion of multitasking by means of interrupts. For this one must know something about the hardware. For example, in our implementation of the class ActiveTemperatureSensor we could have a hardware timer periodically interrupting the application, after which all the sensors would measure the temperature and would invoke their callback functions if necessary.

As soon as concurrency is introduced into a system, the question immediately arises of how to synchronize the relations of active objects with one another, and also with the remaining objects that act sequentially. For example, if two objects send messages to a third, there must be some mechanism guaranteeing that the object at which the action is directed will not be destroyed when two active objects attempt to change its state at the same time. In this question abstraction, encapsulation and concurrency come together. In concurrent systems it is not enough to define the behavior of an object; measures must also be taken to guarantee that it will not be torn to pieces by several independent processes.

Persistence

Every software object exists in memory and lives in time. Atkinson et al. proposed that there is a continuous spectrum of object lifetimes: there are objects that are present only during the evaluation of an expression, but there are also such objects as databases, which exist independently of the program. This spectrum of object persistence encompasses:

  • "Intermediate results of the evaluation of expressions.
  • Local variables in procedure calls.
  • Own variables (as in ALGOL-60), global variables and dynamically created data.
  • Data preserved between sessions of program execution.
  • Data preserved on transition to a new version of a program.
  • Data that outlive the program altogether" [79].

Traditionally, the first three levels are the concern of programming languages, and the last ones of databases. This clash of cultures leads to unexpected solutions: programmers devise special schemes for preserving objects in the period between runs of a program, while database designers refashion their technology for short-lived objects [80].

The unification of the principles of concurrency for objects made it possible to create concurrent programming languages. In a similar way, the introduction of persistence as a normal constituent part of the object approach leads us to object-oriented databases (OODB, object-oriented databases). In practice such databases are built on the basis of time-tested models - sequential, indexed, hierarchical, network or relational - but the programmer can introduce an abstraction of an object-oriented interface, through which queries to the database and other operations are performed in terms of objects whose lifetime exceeds the lifetime of an individual program. As we shall see in chapter 10, this unification considerably simplifies the development of certain kinds of applications, allowing, in particular, a single approach to be applied to different segments of a program, some of which are connected with databases and others of which have no such connection.

Programming languages, as a rule, do not support the notion of persistence; a notable exception is Smalltalk, which has protocols for saving objects to disk and loading them from disk. However, writing objects into unstructured files is after all a naive approach, suitable only for small systems. As a rule, persistence is achieved by using the (few) commercial OODBs [81]. Another option is to create an object-oriented shell for relational DBMSs; this is better, in particular, for those who have already invested funds in a relational system. We shall examine such a situation in chapter 10.

Persistence is not only the problem of preserving data. In an OODB it makes sense to preserve classes as well, so that programs can interpret the data correctly. This creates great difficulties as the volume of data grows, especially if the class of an object suddenly has to be changed.

So far we have spoken of preserving objects in time. In most systems objects, when they are created, are allotted a place in memory that does not change and in which the object resides all its life. However, for distributed systems it is desirable to provide the possibility of transferring objects in space, so that they can be moved from machine to machine and even, if necessary, the form of representation of the object in memory can be changed. We shall take up these questions in chapter 12.

In conclusion let us define persistence as follows:

Persistence is the ability of an object to exist in time, outliving the process that gave rise to it, and (or) in space, moving out of its original address space.

Продолжение:


Часть 1 2.2. The Constituent Parts of the Object Approach
Часть 2 - 2.2. The Constituent Parts of the Object Approach

created: 2020-12-19
updated: 2026-03-10
343



Was this answer useful?
Choose a quick rating so we can improve the next answer for you.
How satisfied are you?


Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Object Oriented Analysis and Design"

Terms: Object Oriented Analysis and Design