Lecture
Classes, Not Objects, Are the Point
What is the central concept of object technology? You need to think twice before answering "object". Objects are useful, but there is nothing new about them.
Ever since structures have been used in Cobol, ever since records have existed in Pascal, ever since a programmer wrote the first struct definition in C, humanity has had objects.
Objects are important for describing the execution of O-O systems. But the basic notion of object technology is the class. Let us return to its definition. (A detailed discussion of objects is given in the next lecture.)
A class is an abstract data type equipped with a possibly partial implementation.
Abstract data types (ADTs) are a mathematical notion, suitable at the specification stage — during analysis. The notion of class, by providing for a partial or complete implementation, supplies the necessary link to software development at the design and programming stages. Recall that a class is called effective if its implementation is complete, and deferred if the implementation is partial.
Like an ADT, a class is a type describing a set of possible data structures called instances of the class. Instances of an ADT are abstractions — elements of a mathematical set. An instance of a class is concrete — it is a data structure stored in computer memory and processed by a program.
For example, if we define a class STACK, based on the ADT specification from the previous lecture and adding the information needed for an adequate representation, the instances of the class will be data structures — concrete stacks. Another example is the class POINT, modeling a point in the plane. If a Cartesian coordinate system is chosen to represent the point, each instance of POINT is a record with fields x and y — the point's abscissa and ordinate.
The term "object" appears as a by-product of the definition of "class". An object is simply an instance of some class.
The program texts describing the system being built contain class definitions. Objects are created only during program execution.
This lecture is devoted to the basic techniques for creating program elements and combining them into systems, which is why classes are the focus of attention. The next lecture will examine the run-time structures produced by an O-O system, which will require studying certain implementation details and a more detailed look at the nature of objects.
A class is a model, and an object is an instance of such a model. This distinction is so obvious that it usually needs no further comment. Nevertheless, a certain category of specialized literature is rather careless about these notions, mixing up the concept of an individual object with the concept of objects in general, which is characterized by a class. This confusion has two sources. One arises from the broad interpretation of the term "object" in natural language. The other source of misunderstanding is related to metaclasses — situations in which classes themselves act as objects. A classic example is a translator for an object-oriented language, for which the classes of the language are the objects being translated.
Some O-O languages, Smalltalk in particular, use the notion of metaclass to resolve this situation. A metaclass is a class whose instances are themselves classes. In "The Name of the Rose", an excerpt from which is given as the epigraph to this lecture, we find the notion of "signs of signs". This is, in essence, an informal definition of metaclasses.
We shall avoid introducing metaclasses, since they create more problems than they solve. In particular, introducing metaclasses causes difficulties for static type checking, which is a necessary condition for developing reliable software. The main functions of metaclasses can be implemented much better through other means:
This book does not use an independent concept of metaclass. The presence of metaclasses in a given language or development environment does not justify blurring the distinction between models and their instances — classes and objects.
Having spent a little time dispelling absurd but widespread and harmful misconceptions, we can return to examining the central properties of classes and find out, in particular, why they are so important in object technology.
To understand the O-O approach, it is essential to clearly realize that classes perform two functions which, before the advent of O-O technologies, were always kept separate. A class is simultaneously a module and a type.
The tools used in software development — programming, design and specification languages, graphical notation systems for analysis — have always included both the possibility of using modules and a type system.
Modules are the structural units of which a program is composed. Various kinds of modules, such as subprograms and packages, were discussed in one of the previous lectures (see ). Regardless of the particular choice of modular structure, a module is always regarded as a syntactic concept. It follows that division into modules affects only the form in which the source text of a program is written, but does not determine its functionality. Indeed, in principle one could write an Ada program as a single package, or a Pascal program as a single main program. Of course, such an approach is not recommended, and any competent programmer will use the modular facilities of the language to divide a program into manageable, comprehensible parts. But given an existing program, for example in Pascal, it is always possible to gather all the modules back together and obtain a working program with equivalent semantics. (The presence of recursive subprograms makes this process less trivial, but does not affect the discussion in any essential way.) Thus, division into modules is dictated by the principles of project management, not by any inherent necessity.
The concept of types is, at first sight, completely different. A type is a static description of well-defined dynamic objects — data elements that are processed during the execution of the software system. A set of types usually contains predefined types, such as INTEGER or CHARACTER, as well as user-defined types: records (structures), pointers, sets (in Pascal), arrays, and others. The notion of type is a semantic concept, and every type directly affects the execution of the software system, since it describes the shape of the objects that the system creates and manipulates.
In non-O-O approaches, the concepts of module and type exist independently of each other. The most remarkable property of the class is that it uses both concepts simultaneously within a single linguistic construct. A class is a module, or a unit of program decomposition, but at the same time a class is a type (or a type template, in cases where parameterization is supported).
The power of the O-O method is, to a large extent, a consequence of this identification. Inheritance, in particular, can only be fully understood when viewed as both a modular extension and, simultaneously, a refinement of type specialization.
How can two concepts that at first sight appear so different be joined in practice? The discussion and examples that follow will answer this question.
An important aspect of the O-O approach is the simplicity and universality of the type system, which is built on a fundamental principle.
Every object is an instance of some class
The object principle will apply not only to composite objects defined by developers (such as data structures containing several fields), but also to basic objects — integer and real numbers, boolean values and characters, which will be regarded as instances of predefined library classes (INTEGER, REAL, DOUBLE, BOOLEAN, CHARACTER).
At first sight, this drive to turn every value, however simple, into an instance of some class may seem exaggerated and even extravagant. After all, mathematicians and engineers have successfully used integer and real numbers for many years without suspecting that they were working with instances of classes. However, this insistence on unification pays off for a number of reasons.
| An example of inheritance — the classes INTEGER, REAL and DOUBLE could be descendants of two more general classes: NUMERIC, in which the basic arithmetic operations ("+", "-", "*") are defined; COMPARABLE, representing the comparison operations ("<" and others). As an example of the use of genericity, consider the generic class MATRIX, whose generic parameter determines the type of the matrix elements. Instances of the class MATRIX [INTEGER] will be integer matrices, while instances of MATRIX [REAL] will contain real numbers as elements. As a combined example of the simultaneous use of inheritance and generic classes, one can use the class MATRIX [NUMERIC], whose instances may contain elements of type INTEGER or REAL, or of any new type T defined by the developer as a descendant of the class NUMERIC. |
Given a good implementation, there is no need to fear any negative consequences from deciding to define all types as classes. Nothing prevents the compiler from being given special information about the basic classes. In that case, the generated code for operations on values of classes INTEGER and BOOLEAN can be just as efficient as if they were built-in types of the language.
Building a consistent and universal type system requires the combined use of a number of important O-O techniques, which will be discussed later. These include expanded classes, which guarantee the correct representation of simple values; infix and prefix operations, which make it possible to use familiar syntax (a < b or -a instead of the clumsy constructs a.less_than (b) or a.negated); and constrained genericity, needed to describe classes that adapt to types with specific operations. For example, the class MATRIX can represent integer matrices as well as matrices whose elements are numbers of other types.
What a class actually is can be discovered by studying a simple but typical example that demonstrates the fundamental properties applicable to practically all classes.
The example uses the representation of a point in a two-dimensional graphics system:

Fig. 7.1. A point and its coordinates
To define the type POINT as an abstract data type, four query functions are needed: x, y, ρ, θ. (In the program texts, the names rho and theta will be used for the last two functions.) The function x returns the abscissa of the point (the horizontal coordinate), y — the ordinate (the vertical coordinate), ρ — the distance from the origin, θ — the polar angle, measured from the horizontal axis. The values x and y are the point's Cartesian coordinates, while ρ and θ are its polar coordinates. Another useful function is distance, which returns the distance between two points.
The ADT specification will further contain such commands as translate (moving the point a given distance horizontally and vertically), rotate (rotating it by a given angle about the origin) and scale (decreasing or increasing the distance to the origin by a given factor).
It is not hard to write a complete ADT specification including the functions listed above and some associated axioms. Below, as an example, are two of the functions listed:
x: POINT REAL
translate: POINT × REAL × REAL POINT
and one of the axioms:
x (translate (p1, a, b)) = x (p1) + a
which states that for an arbitrary point p1 and real values a and b, translating the point causes the abscissa to increase by a.
The reader, if so inclined, may complete the ADT specification independently. In the discussion that follows, it will be assumed that you understand how this ADT is structured, whether or not you have written out its complete formalization. Let us now focus on the implementation of the ADT — the class.
Any abstract data type, and POINT in particular, is characterized by a set of functions describing the operations applicable to instances of the ADT. In a class implementing the ADT, the functions become features — operations applicable to instances of the class.
It was shown that an ADT has functions of three kinds: queries, commands and creators. For class features, an additional classification is needed, based on how a given feature is implemented — by space or by time. (See "Categories of Functions", )
The example of point coordinates clearly demonstrates this difference. Two common representations are available for points — Cartesian or polar coordinates. If a Cartesian coordinate system is chosen for the representation, each instance of the class contains two fields representing the x and y coordinates of the corresponding point:

Fig. 7.2. Representation of a point in Cartesian coordinates
If p1 is such a point, obtaining the values of x and y amounts simply to reading the corresponding fields of this structure. However, determining the values of ρ and θ requires computing the expression √(x 2 + y 2 ) for ρ and arctan (y/x) for θ (provided x is nonzero).
Using a polar coordinate system () leads to the opposite situation. Now ρ and θ are available simply as field values, while determining x and y is possible after simple computations (ρ cosθ, ρ sinθ, respectively).

Fig. 7.3. Representation of a point in polar coordinates
The example given points to the need to consider two kinds of features:
The second category — routines — requires further additional classification. Some routines return a result, and these are called functions. In the example given, x and y are functions in the polar-coordinate representation, while rho and theta are functions in the Cartesian representation; all of them return a result of type REAL. Routines that do not return a result correspond to the commands of the ADT specification and are called procedures. For instance, the class POINT contains the procedures translate, rotate and scale.
| One should not confuse the notion of "function", denoting in classes a routine that returns a result, with the earlier use of the term function as a mathematical description of ADT operations. This unfortunate ambiguity of terms is due to the established terminology in mathematics and programming. |
Below is the classification discussed above, presented as a tree:

Fig. 7.4. Classification of class features by their role
This classification is external, based on how a given feature appears to the client that uses it.
One can propose another, internal classification, using as its main criterion the way a feature is implemented within the class:

Fig. 7.5. Classification of class features by implementation method
At first sight, one aspect of the classification given above may seem troubling. In many cases it is necessary to be able to work with an object, for example with a point p1, without worrying about which internal representation is used for p1 — Cartesian, polar, or something else. Is it necessary, for this, to distinguish attributes from functions?
The answer depends on the point of view from which the problem is considered — that of the developer, the author of the class POINT, or that of the client, who has created a class that uses POINT. For the developer, the difference between attributes and functions is fundamentally important and meaningful. He must decide which features will be implemented as data in memory and which will be obtained as the result of a computation. But forcing the client to be aware of this difference would be a serious mistake. The client must be able to access the values of x or ρ for the point p1 without worrying about, or having any information on, how the corresponding queries are implemented.
The solution to this problem is provided by the Uniform Access principle, introduced in the discussion of modularity (). The principle states that a client must be able to access an object's properties using the same notation, regardless of how that property is implemented — in memory or as the result of a computation (in space or in time, as an attribute or as a routine). This important principle must be followed when designing the notation for referring to a class's features. Thus the expression denoting the value of the feature x of object p1 will always be written as:
p1.x
regardless of whether this accesses a data field of the object or executes a routine.
| With this notation, ambiguity can arise only for argument-free queries, which can be implemented either as functions or as attributes. A command must be a procedure, and a query with arguments must be a function, since attributes cannot take arguments. |
The Uniform Access principle is necessary to guarantee the autonomy of software components. It protects the right of a class's author to freely experiment with different implementation approaches without inconveniencing clients. (See "Using assertions for documentation: the short form of a class", )
| Pascal, C and Ada violate this principle by providing different notations for calling functions and for accessing attributes. This is understandable for such non-O-O languages (although as far back as 1966 the syntax of Algol W, the predecessor of Pascal, satisfied this principle). More recent languages, such as C++ and Java, also fail to follow this principle. Departing from this principle can cause changes made to internal representations (for example, switching from a polar coordinate system to a Cartesian one, or other such changes) to break many client classes. This is one of the causes of instability in software development. |
The Uniform Access principle is also the source of certain requirements for preparing documentation. Consistent application of this principle must guarantee, for example, that the official documentation contains no indication of whether a given argument-free query is a function or an attribute. This is one of the requirements of the standard technique for documenting classes known as the short form of a class.
Below is a version of the source text of the class POINT. Fragments beginning with a double dash "--" are comments, continuing to the end of the line. The comments contain explanations that make the text easier to understand and have no effect on the semantics of the class.
indexing description: "Point in the plane" class POINT feature x, y: REAL -- Abscissa and ordinate rho: REAL is -- Distance to the origin (0, 0) do Result := sqrt (x^2 + y^2) end theta: REAL is -- Polar angle do -- Left as an exercise (exercise E7.3) end distance (p: POINT): REAL is -- Distance to point p do Result := sqrt ((x - p.x)^2 + (y- p.y)^2) end translate (a, b: REAL) is -- Move by a horizontally, b vertically do x := x + a y := y + b end scale (factor: REAL) is -- Change the distance to the origin by a factor of factor do x := factor * x y := factor * y end rotate (p: POINT; angle: REAL) is -- Rotate about p by angle angle do -- Left as an exercise (exercise E7.3) end end
Some aspects of the text above are not obvious and require further explanation.
A class mainly consists of a clause listing its various features, introduced by the keyword feature. In addition, there is an indexing clause giving a general description, useful for understanding the functionality of the class, but having no effect whatsoever on execution semantics. Three additional clauses will be discussed later: inherit — for inheritance; creation — when a special constructor is needed; invariant — for declaring class invariants. The possibility of including two or more feature clauses in a class will also be discussed.
The class POINT demonstrates a number of techniques that will be used in later examples. It is worth spelling out the basic conventions.
The features x and y are declared as being of type REAL with no associated algorithm, so they are attributes. All the other features contain constructs of the form
is do ... Instructions ... end
which describe an algorithm, a sign of a routine. The routines rho, theta and distance return a result of type REAL in all three cases, as reflected in declarations of the form
rho: REAL is ...
This defines them as functions. The two other routines, translate and scale, return no result (the declaration does not end with a construct of the form: T, where T is some type) and are accordingly procedures.
Since x and y are attributes, while rho and theta are functions, this particular class represents the point using a Cartesian coordinate system.
The body of a routine (the do clause) is a sequence of instructions. Successive instructions and declarations can be separated by a semicolon, in the Algol-Pascal tradition, but this is not mandatory. From now on, for simplicity, the semicolon will be omitted between elements on separate lines, but it will always be used as a separator between several instructions or declarations on the same line. (See "The War over Semicolons", in the course "Fundamentals of Object-Oriented Design")
In the routines of the class POINT, all the instructions are value assignments. In this notation, the symbol ":=" is used to denote assignment, again following the conventions adopted in Algol and Pascal. This symbol cannot be confused with the equality symbol "=", used, as in mathematics, in comparison operations.
Another notational convention concerns the use of the routine header comment. It has already been noted that comments begin with two consecutive dashes "--". They can be placed anywhere the author believes additional explanation would be useful. The header comment plays a special role. According to the general style rule, it must be placed at the beginning of every routine, right after the keyword is, indented as in the example of the class POINT. The header comment should briefly state the purpose of the routine.
Attributes are likewise accompanied by comments, placed immediately after their declaration and indented the same way as routine header comments. The declarations of x and y provide an illustration.
At the beginning of our class is placed a clause starting with the keyword indexing. It contains a single entry, labeled description. The indexing clause has no effect on program execution and serves to hold information associated with the class. In general it contains zero or more entries of the form
index_word: index_value, index_value, ...
where index_word is an arbitrary identifier (an index term), and each index_value is an arbitrary language element (an identifier, an integer, a string, etc.) (See "Notes on indexing", ).
This offers two advantages:
The example above contains a single index term — description, whose value is a string describing the purpose of the class. All the class examples in this book will likewise contain a description entry. It is strongly recommended to follow this example and begin the source text of every class with an indexing clause giving a brief characterization of the class, just as every routine begins with a header comment.
The indexing clause and header comments illustrate the correct application of the Self-Documentation principle: wherever possible, the documentation of a module should be placed directly within the module itself. (See "Self-Documentation", )
To understand the text of the functions rho, theta and distance in the class POINT, one more convention is needed.
Any programming language that supports functions (routines returning a result) must provide a notation allowing the value returned by a call to be set within the body of the function. In this book, the value returned by a function will be denoted by the predefined entity Result. (A full definition of this entity will be given at the end of this lecture.)
For example, the body of the function rho contains the following assignment
Result := sqrt (x^2 + y^2)
Result is a reserved word that may appear only within the body of a function. In a function returning a result of type T, Result is treated like any other entity, and a value may be assigned to it using assignment instructions, as shown above.
Whenever a function is called, the last value assigned to Result is returned as the result. It is always defined, thanks to the rules of the language (which will be examined in detail later) requiring that Result be initialized at the start of every routine by assigning it the value predefined for type T. For the data type REAL, the initializing value is zero, and the following function:
non_negative_value (x: REAL): REAL is
-- Returns the value of the argument when x>0; zero when x<=0
do if x > 0.0 then Result := x end end
will always return a well-defined value (as stated in the header comment), even though the conditional instruction contains no else part.
The discussion at the end of this lecture examines the rationale for using the Result convention as opposed to other techniques, such as return instructions. Although this convention concerns all programming languages, it is especially important in the O-O approach.
The source texts of the classes in this book strictly follow a set of basic style rules. They govern indentation, fonts, the choice of names for classes and their features, and the use of upper and lower case.
These rules will be given serious attention throughout what follows, and their detailed discussion is the entire subject of the course "Fundamentals of Object-Oriented Design". Style rules should not be regarded as merely "cosmetic". Developing quality software requires consistency and attention to every detail — to form just as much as to content. The goal of reuse makes adherence to these rules even more important, since the source texts are expected to have a long life, during which many people will need to understand and extend them.
Style rules should be applied correctly from the very first moment of writing a class's source text. Thus, one should never begin a routine without first providing a header comment. This does not take much time, and that time should not be considered wasted. In fact, it produces a substantial saving of time later, when the class's author or other programmers work with it again — whether half an hour, or more likely, five years later. Using consistent indentation, writing comments and choosing identifiers carefully, and applying sound lexical conventions (a space before every opening parenthesis but not after it, and so on) will not make the task much harder, yet will make the result of months of work on a huge body of source text much more polished. Attention to detail is certainly not a sufficient, but it is a necessary, condition for developing quality software.
The elementary style rules are perfectly clear from the class example given. Since the purpose of this section is to study the basic mechanisms of object technology, a detailed treatment of style rules will be the subject of one of the later lectures (in the course "Fundamentals of Object-Oriented Design").
Another aspect of the class POINT that calls for explanation is the presence, in the functions rho and distance, of calls to the function sqrt. It is clear that this function returns the square root of a real number, but where does it come from?
Since it would be impractical to clutter a general-purpose language with specialized arithmetic operations, the best solution is to define such operations as features of a specialized class, called, for example, ARITHMETIC. Any class that needs to use these facilities then simply needs to be declared a descendant of this specialized class. To do this, it suffices to rewrite the class POINT as follows
class POINT inherit ARITHMETIC feature ... The rest of the code unchanged ... end
| This technique of inheriting general-purpose functionality is somewhat controversial. Some might feel that O-O principles imply that functions like sqrt should be features of the class to which the object belongs, for example REAL. However, there are a number of operations on real numbers, not all of which are worth including in that class. In the discussion of design principles we will come back to the usefulness of "helper" classes such as ARITHMETIC. (See "Inheriting Functionality", lecture 6 of the course "Fundamentals of Object-Oriented Design".) |
Let us now turn to the fundamental properties of the class POINT and try to understand how a typical routine body and its constituent instructions are structured. We will then see how a class and its features can be used by other classes — clients of this one.
The Current Instance
Let us look again at the text of one of the routines, the procedure translate:
translate (a, b: REAL) is
-- Move by a horizontally, b vertically
do
x:= x + a
y:= y + b
end
At first sight, this text is perfectly clear — to move the point a distance a horizontally and b vertically, the value a is added to x, and b to y. On closer inspection, things are not quite so obvious. It is not clear from this text which point is being referred to. To which object do the x and y belong, to which a and b are being added? This question touches on one of the most characteristic aspects of the O-O style of development. Before getting an answer, we need to clarify a few intermediate details.
The text of a class describes the properties and behavior of objects of a given type, in this case points. This is achieved by describing the properties and behavior of a typical instance of that type. We could call this instance the "point in the street", following the way newspapers present the opinion of the "man in the street". We will use a more formal name — the current instance of the class.
Sometimes it becomes necessary to refer explicitly to the current instance. The reserved word
Current
provides this facility. In the text of a class, Current denotes the current instance of that class. The need to use Current can arise, for instance, if one tries to rewrite the function distance so that it checks whether the argument p coincides with the current point; in that case the result would be zero without any further computation. This version of distance would look as follows:
distance (p: POINT): REAL is
-- Distance to point p
do
if p /= Current then
Result := sqrt ((x - p.x)^2 + (y- p.y)^2)
end
end
Here /= is the inequality operator. In accordance with the initialization rule stated earlier, the conditional instruction does not need an else part, since the result is zero when p = Current.
Nevertheless, in most cases the current instance is implied, and there is no need to refer to Current by name. Thus a reference to x in the body of translate and other routines denotes "the value of x of the current instance" without any further qualification.
Of course, it still remains a mystery just who this "Current" is. The answer will come later, when we study routine calls; for now, when examining the text, it is enough to assume that every operation can be understood only with respect to some implicitly defined object — the current instance.
Setting aside a few points connected with the mystery of identifying Current, we can consider it settled how to define simple classes. We now need to discuss how these definitions are used — how they are used by other classes. In a consistent O-O approach, every program element is part of some class, so these definitions will be used by other classes.
There are only two ways of using a class, for example POINT. The first way — inheritance — will be examined in detail later. To realize the second possibility, one must create a class that is a client of the class POINT. (Lectures 14-16 are devoted to inheritance.)
The simplest and most general way to become a client of a class S is to declare an entity of type S.
Definition: client, supplier
Let S be some class. A class C is called a client of S if it contains the declaration of an entity a: S. The class S is called a supplier of C.
In this definition, a can be an attribute or a function of class C, or a local entity, or an argument of a routine in class C.
For example, the presence in the class POINT of the declarations x, y, rho, theta and distance makes this class a client of the class REAL. Conversely, other classes can become clients of POINT. For example:
class GRAPHICS feature
p1: POINT
...
some_routine is
-- Perform certain actions on p1.
do
... Create an instance of POINT and attach it to p1 ...
p1.translate (4.0, -1.5) --**
...
end
...
end
Before the instruction marked "--**" is executed, the attribute p1 takes on a value corresponding to a specific instance of the class POINT. Suppose this object represents a point coinciding with the origin, x = 0, y = 0:

Fig. 7.6. The origin
In such cases, one says that the entity p1 is attached to the given object (the object is bound to the entity). At this stage there is no need to worry about how the object was created and initialized (the line "... Create an instance of POINT ..." is not fully spelled out). These questions will be discussed in detail in the next lecture as part of the object model. For now it is enough to know that the object exists and is bound to the entity p1 (it is attached to the object).
The instruction marked with asterisks
p1.translate (4.0, -1.5)
deserves close study, since it is the first example of the use of the basic mechanism of object-oriented computation. This is a reference to a feature, or a feature call. During the execution of an O-O system's code, all computation is carried out by calling the corresponding features of specific objects.
This particular example represents a call to the feature translate of the class POINT applied to the object p1, with arguments 4.0 and -1.5 corresponding to a and b in the declaration of translate in that class. In general, there are two main forms in which a feature call can be written.
x.f
x.f (u, v, ...)
Here x is called the target of the call and can be an entity or expression that, at run time, is attached to a specific object. The target x, like any entity or expression, has a definite type, given by a class C, and therefore f must be one of the components of class C. More precisely, in the first case f must be an attribute or a routine without arguments, and in the second case a routine with arguments. The values u, v, ... are called the actual arguments of the call, and they must be expressions whose number and type exactly match the number and type of the formal arguments declared for f in class C.
In addition, the component f must be accessible (exported) to the client containing the given call. The next section is devoted to restricting access rights (see ); for now, by default all components are accessible to all clients.
The result of the call considered above is defined at run time as follows:
Effect of calling component f for target x
Apply component f to the object attached to x, after initializing all formal arguments of f (if any are provided) with the values of the corresponding actual arguments.
What is so remarkable about a feature call? After all, every programmer knows how to write a procedure translate that moves a point by a given distance. The traditional form of the call, available with minor variations in all programming languages, would look as follows:
translate (p1, 4.0, -1.5)
Unlike the OO style, in this call all arguments are equal. The object-oriented form is not as symmetric: a specific object (in this case the point p1) is chosen as the target, while the other arguments (the real numbers 4.0 and -1.5) are given an auxiliary role. Selecting a single object as the target for every call is central to the OO method of computation.
The Single Target Principle
Every operation in OO computation is associated with a specific object - the current instance at the moment the operation is executed
This aspect of the method often causes the greatest difficulty for beginners. In object-oriented software development, one never says: "Apply this operation to these objects", but rather "Apply this operation to this object right now". If arguments are provided, the following addition is possible: "By the way, I almost forgot, you will need these values here as arguments".
The Single Target principle is a direct consequence of merging the concepts of module and type, considered earlier as the starting point of OO decomposition. Since every module is a type, every operation in a given module is considered relative to a specific instance of that type (the current instance). However, until now the details of this merging have remained somewhat mysterious. As already stated, a class simultaneously represents both a module and a type, but how can we reconcile the syntactic notion of a module (a grouping of related functionality, forming part of a software system) with the semantic notion of a type (a static description of certain possible run-time objects)? The example of the class POINT gives a definite answer:
How the module-type merger functions
The functionality of the class POINT, viewed as a module, corresponds exactly to the operations available for instances of the class POINT, viewed as a type
This identification of operations on instances of a type with services provided by a module lies at the foundation of the structural discipline imposed by the OO method.
Now it is time to use the same example to uncover the mystery of the current instance and find out what it actually represents.
The very form of the call shows why the text of a routine (translate in class POINT) does not need any additional identification of the object Current. Since any routine call is associated with a specific target, explicitly indicated at the call site, when the call is executed the name of every component in the text of the routine (for example, x in the text of translate) will be attached to that same target. Thus, when the call
p1.translate (4.0, -1.5)
is executed, every occurrence of x in the body of translate, as in the following instruction
x := x + a
means: "x of object p1".
These considerations give the precise meaning of the notion of Current, as the target of the current call. So throughout the execution of the call above, Current will denote the object attached to p1. For a different call, Current will denote the target of the new call. We can formulate the following Feature Call principle:
Feature Call Principle
It was noted above that OO computation is based on feature calls. As a consequence of this principle, source texts in fact contain far more calls than may appear at first glance. So far, two forms of calls have been considered:
x.f
x.f (u, v, ...)
Such calls use what is called dot notation and are called qualified, since the target of the call is precisely indicated, its identifier being placed before the dot.
However, other calls may be unqualified, since their target is not indicated. As an example, suppose it is necessary to add to class POINT a procedure transform, which will be a combination of the procedures translate and scale of the point. The text of such a procedure might call the procedures translate and scale:
transform (a, b, factor: REAL) is
-- Move by a horizontally, by b vertically,
-- then change the distance to the origin by a factor of factor.
do
translate (a, b)
scale (factor)
end
The body of the procedure contains calls to translate and scale. Unlike the previous examples, here the exact target is not indicated and dot notation is not used. Such calls are called unqualified.
Unqualified calls do not violate clause F2 of the Feature Call principle, since they too have a target. In this case, the target is the current instance. When the procedure transform is called with respect to a certain target, the calls to translate and scale have the same target. In fact, the code above is equivalent to the following
do
Current.translate (a, b)
Current.scale (factor)
Any call can be rewritten in qualified form by indicating Current as the target (strictly speaking, this is valid only for exported components). The unqualified call form is of course simpler and quite clear.
The unqualified calls shown are procedure calls. Similar considerations can be extended to attributes, although the presence of calls in this case may be less obvious. It was noted earlier that in the body of the procedure translate, the presence of x in the expression x + a means the field x of the current instance. This can be interpreted differently - as a call to component x, and the expression in full form would become Current.x+a.
In general, any instructions or expressions of the form:
f
or:
f (u, v, ...)
are in fact unqualified calls and can be rewritten in the form of qualified calls:
Current.f
Current.f (u, v, ...)
although the unqualified form is more convenient. If such notation is used as an instruction, then f represents a procedure (with no parameters in the first case, or with the corresponding number of parameters of a given type in the second). In expressions, f can be a function or an attribute (in the first form of notation).
Consider the expression:
x + a
This leads us to the important notion of the operator feature. This notion might seem purely cosmetic, having only syntactic significance and adding nothing genuinely new to the OO method. But it is precisely such syntactic properties that can substantially make a developer's life easier when they exist, and miserable when they don't. Operator features are a good example of the successful use of the OO paradigm in long-familiar areas.
To implement this idea we need to realize that the expression x + a contains not one call (of the component x), but two. In computation that does not use the object-oriented approach, + is regarded as the operation of adding two values x and a of type REAL. As already noted, in a pure OO model the only computational mechanism is the feature call. Consequently, we can consider, at least theoretically, that addition too is a call to the corresponding component.
For a better understanding, we need to discuss the definition of the type REAL. The object rule formulated earlier () implies that every type is based on some class. This applies equally to predefined classes such as REAL and to classes defined by the developer, such as POINT. Suppose we need to describe REAL as a class. It is not hard to determine a set of essential components: arithmetic operations (addition, subtraction, sign change...), comparison operations (less than, greater than...). So, a first draft would look like this:
indexing
description: "Real numbers (not the final version!)"
class REAL feature
plus (other: REAL): REAL is
-- Sum of the current value and other
do
...
end
minus (other: REAL) REAL is
-- Difference between the current value and other
do
...
end
negated: REAL is
-- The current value with the opposite sign
do
...
end
less_than (other: REAL): BOOLEAN is
-- Is the current value less than other?
do
...
end
... Other features ...
end
With such a class description, we can no longer write an arithmetic expression as x + a. Instead, we must use the following call:
x.plus (a)
By analogy, instead of the familiar -x, we now have to write x.negated.
One might try to justify this departure from familiar mathematical notation by an appeal to consistent implementation of the OO model, and cite Lisp as an example of the possibility of departing from standard notation within the software development community. But this argument cannot be considered convincing: the use of Lisp has always been rather limited. Departing from a notation that has existed for centuries and has been familiar to everyone since elementary school is extremely risky. All the more so since there is nothing wrong with that notation.
A simple syntactic device lets us preserve the consistency of the approach (the requirement to unify the computational mechanism based on feature calls) while ensuring compatibility with traditional notation. It suffices to treat an expression of the form
x + a
as a call to an additional component of class REAL. To implement this approach, we need to rewrite the component plus so that an operation sign, rather than dot notation, is used to call it. Here is a class description that achieves this goal:
indexing
description: "Real numbers"
class REAL feature
infix "+" (other: REAL): REAL is
-- Sum of the current value and other
do
...
end
infix "-" (other: REAL) REAL is
-- Difference between the current value and other
do
...
end
prefix "-": REAL is
-- The current value with the opposite sign
do
...
end
infix "<" (other: REAL): BOOLEAN is
-- Is the current value less than other?
do
...
end
... Other features ...
end
Two new keywords have been introduced - infix and prefix. The only syntactic novelty is that the names of these components are not identifiers (such as distance or plus), but are written in one of two forms (The next lecture will show how to define an "expanded class". See "The Role of Expanded Types".)
infix "§"
prefix "§"
where § is replaced by a specific operation sign (+, -, *, <, <= and others). A component can have an infix name only if it is a function with a single argument; examples include plus, minus and less_than in the original version of class REAL. The prefix form can be used only for functions with no arguments or for attributes.
Infix and prefix components, hereafter called operator features, are used in the same way as identifier features. There are only two syntactic differences. For the names of operator features, when declared, the forms infix "§" or prefix "§" are used instead of identifiers. A call to an operator feature, for infix components, has the form:
u § v
for prefix components:
§ u
Operator features support only qualified calls. The unqualified call plus (y) in a routine of the first version of class REAL must, in the second version, be written as Current + y. For identifier features, the equivalent notation Current.plus (y) is admissible but not usually used.
Apart from the differences noted, in every other respect operator features are fully syntactically equivalent to identifier features; in particular, they can be inherited in the usual way. Not only base classes similar to REAL, but any other class, can use operator features - for example, for a function that adds two vectors in class VECTOR, it is quite acceptable to use the infix component "+".
Operations used in operator features must obey the following rules. An operation sign is a sequence of one or more printable characters, containing no spaces or line breaks, whose first character can only be one of those listed below:
+ - a / < > = \ ^ @ # | &
The restrictions imposed on the first character make it easier to recognize infix and prefix operations.
In addition, for compatibility with traditional notation for boolean expressions, the following keywords are used to denote operations:
not and or xor and then or else implies
Base classes (INTEGER and others) use what are called the standard operations:
| Here // denotes integer division, \\ denotes the remainder of integer division, ^ denotes exponentiation, xor denotes exclusive "or". In class BOOLEAN, and then and or else are variants of and and or (the differences are discussed later), implies denotes implication: the expression a implies b is equivalent to ( not a ) or else b . |
Operations that are not among the "standard" ones are called free operations. Let us give two examples of free operations.
All operations have a fixed priority; standard operations have their usual priority, while all free operations have a higher priority.
The use of operator features lets us use conventional notation for expressions while at the same time meeting the requirements of full unification of the type system. Implementing arithmetic and boolean operations as components of class INTEGER should not in any way be a cause of reduced performance. Conceptually, a + x is a feature call, but a good compiler can, as a result of processing such a call, produce code no less efficient than compilers for C, Pascal, Ada, or other languages in which "+" is a rigidly fixed language construct.
In most cases we can forget that the use of operations in expressions is in fact a call to routines, since the end effect will be the same as with the traditional approach. At the same time, it is reassuring to know that even in this case there has been no departure from the principles of the OO approach.
Up to now, all components of a class have been accessible to all potential clients. This is of course not always acceptable, since information hiding is an important element of building a consistent and flexible architecture.
Let us consider ways of hiding components from all or some clients. This section provides only an introduction to the notation - a detailed treatment of class interfaces is the subject of one of the subsequent lectures (of the course "Fundamentals of Object-Oriented Design"). In the examples, for simplicity, only identifier features will be considered, but everything said below applies equally to operator features.
By default, all components are accessible to all clients. For a class
class S1 feature
f ...
g ...
...
end
the components f, g, ... are accessible to all clients of S1. This means that if an entity x of class S1 is declared in class C, then the call
x.f ...
is valid, provided all other conditions for the correctness of the call to f are satisfied.
Restricting Client Access
To restrict client access to some component h, we will use the possibility of including two or more feature clauses in the class declaration. The declaration will look as follows
class S2 feature
f ...
g ...
feature {A, B}
h ...
...
end
The components f and g remain accessible to all clients. The component h is accessible only to classes A and B, as well as their descendants (direct or indirect). This means that for some x of type S2, the following call
x.h
is valid only in the source text of classes A, B, or one
продолжение следует...
Часть 1 7. Static Structures: Classes
Часть 2 Style for Declaring Hidden Components - 7. Static Structures: Classes
Comments