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

7. Static Structures: Classes

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.)

Definition of a Class

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.

Removing a Traditional Confusion

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:

  • - Metaclasses can be used to specify properties available to many or all classes. The same result can be achieved by creating a family of classes, all descendants of a common ancestor — the class ANY, which contains the declarations of universal properties.
  • - Some operations are characteristic of the class as a whole rather than of its individual instances, so they can be regarded as metaclass methods. But there are usually few such operations, and they are well known. Again, they can be introduced in the definition of the class ANY, or implemented through special language constructs. The most obvious example is the class constructor, which performs the operation of creating objects.
  • - A metaclass can be used to obtain additional information about a class — its name, the list of its properties, the list of its parents, and so on. But here too there is no need for a metaclass. It is enough to develop a special library class E_CLASS, whose instances represent classes and their properties. When creating such an instance, the corresponding class C must be passed as a parameter, and this instance can then be used to obtain information about class C by accessing the corresponding components of E_CLASS.

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.

The Role of Classes

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.

Modules and Types

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.

The Class as Module and as Type

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.

A Unified Type System

An important aspect of the O-O approach is the simplicity and universality of the type system, which is built on a fundamental principle.

The Object 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.

  • - It is always preferable to have a simple and universal scheme rather than a multitude of special cases. The proposed type system relies entirely on the notion of class.
  • - Describing basic types as abstract data structures, and then as classes, is simple and natural. It is not hard to imagine, for example, a definition of the class INTEGER with functionality including arithmetic operations such as "+", comparison operations such as "=", and associated properties following from the corresponding mathematical axioms.
  • - Defining basic types as classes makes it possible to use all the capabilities of O-O, chiefly inheritance and genericity. If basic types were not classes, it would be necessary to introduce a number of restrictions and treat them as special cases.
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.

A Simple Class

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.

Components

The example uses the representation of a point in a two-dimensional graphics system:

7. Static Structures: Classes

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.

Attributes and Routines

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:

7. Static Structures: Classes

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).

7. Static Structures: Classes

Fig. 7.3. Representation of a point in polar coordinates

The example given points to the need to consider two kinds of features:

  • - Some features are represented in space and, one might say, are associated with some piece of information belonging to each instance of the class. They are called attributes. For a point represented in Cartesian coordinates, the attributes are x and y, while in polar coordinates the attributes are rho and theta.
  • - Other features are represented in time, and accessing them requires describing certain computations (algorithms) that are then applicable to all instances of the given class. These are called routines or methods of the class. In the Cartesian representation of points, rho and theta are routines, while x and y act as routines when polar coordinates are used.

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:

7. Static Structures: Classes

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:

7. Static Structures: Classes

Fig. 7.5. Classification of class features by implementation method

Uniform Access

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.

The Class POINT

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.

Basic Conventions

The class POINT demonstrates a number of techniques that will be used in later examples. It is worth spelling out the basic conventions.

Recognizing the Kind of a Feature

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 Routine Body and Header Comments

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.

The indexing Clause

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:

  • - Readers of the source text get a summary of the class's properties without having to examine the details.
  • - In development environments that support code reuse, the corresponding tools (often called browsers, code navigators, code inspectors, and so on) can use the information from this clause to help prospective users find the classes they need. These tools usually allow searching by a given pattern among index terms and their index_value values. (The course "Fundamentals of Object-Oriented Design" discusses the basic mechanism of O-O browsers.)

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", )

Denoting the Result of a Function

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.

Style Rules

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").

Inheriting General-Purpose Functionality

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".)

The Object-Oriented Style of Computation

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.

Clients and Suppliers

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:

7. Static Structures: Classes

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).

Feature Call

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.

The Single Target Principle

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".

Merging the Concepts of Module and Type

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.

The Role of the Object Current

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

  • - (F1) Any element of a program can only be executed as part of a routine call.
  • - (F2) Every call has a target.

Qualified and Unqualified Calls

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).

Operator Features

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:

  • - prefix: + - not
  • - infix: + - a / < > <= >= = // \\ ^ and or xor and then or else implies .
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.

  • - Later, in class ARRAY, an infix operator feature "@" will be used for a function that returns the specified element of the array. Accessing the i-th element of the array will look like a @ i.
  • - In class POINT, instead of the function distance, we could introduce the operator feature "|-|", and the distance between points p1 and p2 would be written as p1 |-| p2, rather than as p1.distance(p2).

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.

Selective Export and Information Hiding

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.

Unrestricted Access

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

created: 2020-07-23
updated: 2026-03-08
411



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 programming"

Terms: Object oriented programming