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

Style for Declaring Hidden Components - 7. Static Structures: Classes

Lecture



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

...

of their descendants.

In the special case where it is necessary to hide a component i from all clients, it can be declared as exported to an empty list of clients (Not a recommended style (see S5 below).):

class S3 feature { }

i ...

end

In this case, any call x.i(...) is invalid. The only way to access i is an unqualified call

i (...)

in the text of a routine of class S3 or its descendants. This mechanism provides complete information hiding.

The ability to completely hide a component from clients is available in many OO languages, but the mechanism for selective access restriction, illustrated by the example of h, is unfortunately practically unsupported. This kind of finer access control is needed fairly often. The question of the importance of selective export is discussed in the discussion section at the end of the lecture.

In the examples of the following lectures we will encounter various examples of selective export and consider its methodological role in developing interfaces.

Style for Declaring Hidden Components

The style used above for declaring the hidden component i is not very good. This is easy to see in the following example (Not a recommended style (see S5 below).)

class S4 feature

exported ...

feature {}

secret ...

end

where secret is a hidden component and exported is a public one. The difference in writing feature {} with an empty list in braces and feature with no braces at all is barely noticeable. It is far more sensible to use, instead of an empty list, a list containing the single class NONE (Recommended style.)

class S5 feature

... exported ...

feature {NONE}

... secret ...

end

Class NONE is a base library class and is discussed further in connection with inheritance. By definition it cannot have descendants and no instance of it can be created. Thus a component exported to class NONE is effectively hidden. There is no fundamental difference between the declarations S4 and S5, but in the second case the source text becomes clearer and more readable. It is this style of declaring hidden components that will be used from now on in this book.

"Internal" Export

Consider the class declaration

indexing

note: "Erroneous declaration (explanation below)"

class S6 feature

x: S6

my_routine is do ... print (x.secret) ... end

feature {NONE}

secret: INTEGER

end -- class S6

The presence in the class declaration of an attribute x of type S6 and a call x.secret makes it its own client. But such a call is invalid, since the component secret is hidden from all clients! The fact that the unauthorized client is the class S6 itself changes nothing - the declared status of secret makes any call of the form x.secret invalid. No exceptions violate the simplicity of the rule as formulated.

There is a simple solution: write feature {S6} instead of feature {NONE}, exporting the component to itself and to its descendants.

It should be noted that such a device is necessary only if the text of the class contains a qualified call similar to print (x.secret). Obviously, the unqualified call secret in the instruction print (secret) is valid without any additional tricks. All components declared in a given class can be used in the routines of that class and its descendants. Only in the presence of qualified calls does one have to export a component to itself.

Putting It All Together

Having introduced the basic mechanisms of OO computation, it is now time to answer the question of how an executable system can be built from individual classes.

General Relativity

Surprisingly, all the descriptions given so far of what happens during execution have been relative. The result of executing a routine is always tied to the current instance, which is unknown in the source text of the class. We can attempt to understand the effect of a call only by taking into account the target of that call, for example p1 in the following example:

p1.translate (u, v)

However, the following question arises: what does p1 actually denote? The answer is again relative. Suppose the call given is present in the text of some class GRAPHICS, and p1 is an attribute of GRAPHICS. Then obviously, in this case, p1 actually means Current.p1. But this is not an answer to the question posed, since it is not known what the object Current represents at the moment of the call! In other words, we now need to establish the client that is calling the routine of class GRAPHICS in which our call is used.

The Big Bang

Let us consider an arbitrary call. Understanding the meaning of what happens during the processing of an arbitrary call will let us fully grasp the mechanism of OO computation. Let us use the Feature Call principle formulated earlier:

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

Any call can take one of the following forms:

  • - unqualified: f (a, b, ...);
  • - qualified: x.g (u, v, ...) .

In both cases, arguments may be absent. The call is located in the body of a routine r and can only be executed as part of a call to r. Suppose the target of this call is known - some object OBJ. Then we can easily determine the target of this call - t. There are four possible cases, the first of which relates to an unqualified call, and the rest to a qualified one:

  • - (T1) For an unqualified call, t is simply OBJ.
  • - (T2) If x is an attribute, then x - the field of object OBJ - has a value, which in turn is attached to some object - that object is t.
  • - (T3) If x is a function, then it must first be called (unqualified), and the result of this call gives t.
  • - (T4) If x is a local entity of r, then by the time of the call, the preceding instructions will have computed the value of x, attached to a specific object, which is the object t.

The problem is that all four answers are again relative and can only help if it is known what the current instance OBJ is. Clearly, OBJ is the target of the current call! The situation is like the nursery rhyme about the priest and his dog (in the original: the kitten ate the goat, the kitten was bitten by the puppy, the puppy was hit by a stick ...) - an endless chain.

To turn relative answers into absolute ones, we need to find out what happens when everything is just starting - at the moment of the Big Bang. So, the definition:

Definition: system execution

The execution of an OO software system consists of the following two steps:

  • - Creating a specific object, called the root object of the execution.
  • - Applying a specific procedure, called the creation procedure, to that object.

At the moment of the Big Bang, an object is created and the execution of the creation procedure begins. The root object is an instance of the root class of the system, and the creation procedure is one of the procedures of that class. The execution of the system as a whole reduces to the successful unfolding of the individual parts (directly or indirectly ignited from the initial spark) into a giant, complex fireworks display.

Knowing where it all began, it is not hard to trace the fate of Current through this chain reaction. The first current object, created at the moment of the Big Bang, is the root object. Let us now consider a certain stage of the system's execution. Let r be the last routine called, and let the object current at the moment r was called be OBJ. Then, during the execution of r, the object Current is determined as follows:

  • - (C1) If an instruction is executed in r that is not a routine call (for example, an assignment), then the current object remains unchanged.
  • - (C2) An unqualified call likewise leaves the same object current.
  • - (C3) Launching a qualified call x.f ... makes current the object attached to x. Knowing the object OBJ, we can identify x using the rules T1-T4 formulated earlier. After the call completes, the role of current reverts to object OBJ.

In cases C2 and C3, a call may in turn contain further qualified or unqualified calls, and these rules must be applied recursively.

So, there is nothing mysterious or confusing about determining the target of any call, despite the relativity and recursiveness of the rules. What is truly astonishing is the power of the computers we use, playing the role of sorcerer's apprentices. We create a relatively small text of incantations - software - and then execute it, as a result of which objects are created and computations performed, and the number of these objects and computations is so vast that it seems almost infinite by the standards of human consciousness.

Systems

This lecture focuses on classes - the building blocks of OO software construction. To obtain executable code, classes must be assembled into a system.

The definition of a system follows from the preceding discussion. To build a system, three things are needed:

  • - Create a set of classes CS, called the class set of the system.
  • - Specify a class from CS that is the root (root class).
  • - Specify a procedure in the root class that plays the role of the root creation procedure.

For a system to be obtained, these elements must satisfy the criterion of completeness. Every class directly or indirectly needed by the root must be part of the set CS. This is the condition of system closure.

The notion of necessity should be clarified, as is usually done when constructing a closure:

  • - Class D is directly needed by class C if the text of C refers to D. Two cases can be distinguished here: C can be either a client of D, or a descendant of D.
  • - Class E is needed by class C either if C coincides with E, or if there exists a class D directly needed by class C, and class D needs (possibly recursively) class E. In other words, there is a chain of classes connected by the relation of direct need, the beginning of this chain being class C, and the end being class E.

We can now give the definition of a closed system.

Definition: closed system

A system is closed if and only if its set of classes contains all the classes needed by the root class.

A specialized program, for example a compiler, can process all the classes of a closed system, starting from the root. Recursive reference to needed classes will occur as they are encountered. As a result, executable code corresponding to the system as a whole will be produced.

This process is called the assembly of the system, and it is the final stage of development.

There Is No main Program

It has repeatedly been emphasized that systems developed using the OO approach do not use the notion of a main program. Are we not letting the main program in through the back door by introducing the definition of a root class and a root procedure?

Not quite. In the traditional notion of a main program, two unrelated concepts are combined:

  • - The place from which execution begins.
  • - The apex or fundamental component of the system's architecture.

The first condition is certainly necessary. The execution of any system must begin from a well-defined position. In OO systems this position is determined by the root class and the root procedure. In the case of parallel, rather than sequential, computation, several starting points can be defined - one for each independent thread of computation.

The concept of the apex has already been discussed sufficiently earlier and requires no additional comment.

There are no grounds for combining such different notions. We cannot ascribe a special role to the point at which code execution begins within the architecture of a system. A typical example is the initialization of an operating system, performed by a boot procedure. This small and insignificant component certainly cannot be considered central to the architecture of the operating system. Object technology proceeds from precisely the opposite premise, considering that the most important properties of a system are the ensemble of classes it contains, the functionality of these classes, and their interrelationships. In this context, the choice of root class plays a secondary role and can easily be changed if necessary.

It was noted earlier that we need to abandon, at an early stage of system development, the question - "where is the main program?". If the architecture of a system is built on the answer to this question, extensibility and code reuse cannot be ensured. Another approach is reusable classes, implementations of ADTs. Software systems in this case are reconfigurable ensembles of such components.(For a critique of functional decomposition see "Functional Decomposition", )

Building systems is not always the ultimate goal of development. An important application of the method is developing class libraries for reuse. A library is not a system, and it has no root class. During the development of a library, several systems are often created, but such systems are used only for debugging and are not part of the finished version of the library. The final product is a set of classes that other developers will use to build their own systems or their own libraries.

Assembling the System

How can the process of system assembly be implemented in practice?

Suppose the operating system uses the usual way of storing the source texts of classes in files. The assembly tool (compiler, interpreter) needs the following information:

  • - (A1) The name of the root class.
  • - (A2) The universe of files containing the texts of the classes needed by the root.

This information should not be contained directly in the source texts of the classes. Identifying a class as the root within its own source text (A1) would violate the principle of the absence of a main program. Including in the source texts of classes information about the location of the corresponding files would mean rigid binding to the file system, which is obviously an unacceptable solution. If the location changes, the use of such classes becomes impossible.

From these considerations it follows that assembling a system requires information located outside the source texts of the classes. To provide such information we will use a small control language called Lace. Let us consider the assembly process, but note right away that the details of Lace are entirely irrelevant in the context of the OO approach. The Lace language is simply a specific example of a control language that makes it possible to preserve the autonomy and reusability of classes, using some mechanism for assembling the files of a system.

Let us consider a typical Lace document, the so-called Ace file:

system painting root

GRAPHICS ("painting_application")

cluster

base_library: "\ library\ base";

graphical_library: "\ library\ graphics";

painting_application: "\ user\ application"

end -- system painting

The cluster clause defines the universe of files containing the texts of the classes. It contains a list of clusters. A cluster is a group of related classes representing a subsystem or library. (The cluster model is discussed in the course "Fundamentals of Object-Oriented Design")

Operating systems such as Windows, VMS, or Unix contain a convenient mechanism for supporting clusters - subdirectories. Their file systems have a tree structure. The terminal nodes of the tree (leaves), called "ordinary files", directly contain information, while the intermediate nodes, subdirectories, contain sets of files consisting of ordinary files and subdirectories.

7. Static Structures: Classes

Fig. 7.7. Directory structure

Each cluster can be associated with a subdirectory. Lace uses the following convention: each cluster, for example base_library, has an associated subdirectory whose name is given in double quotation marks - "\ library\ base". This naming convention is used in Windows (\dir1\dir2\ ... ) and is given here purely as an example. The corresponding Unix names are obtained by replacing backslash characters with forward slashes.

The hierarchy of subdirectories can be used to define the hierarchy of clusters. In addition, Lace supports the notion of a subcluster, which makes it possible to define the logical structure of a hierarchy of nested clusters independently of their physical location in the file system.

The directories listed in the cluster clause may contain files of all types. To work with the universe, the system assembly process needs information about which of the files contain the texts of classes. Let us use a simple convention - the text of a class named NAME is placed in a file name.e (lowercase). In this case, the universe is a set of files with names of the form name.e in the directories listed in the cluster clause.

The root clause of Lace serves to specify the root class of the system. In this case, the root is the class GRAPHICS, and it is located in the cluster painting_application. If only one class in the universe is named GRAPHICS, there is no need to specify the cluster.

Suppose the compiler begins building the system described in the Ace file given above. Suppose further that none of the files of the system has yet been compiled. The compiler finds the text of the root class GRAPHICS in the file graphics.e of the cluster painting_application, which is located in the directory \user\application. Analyzing the text of class GRAPHICS, the compiler finds the names of the classes needed by GRAPHICS and searches for files with the corresponding names in the directories of the three clusters. This search is then repeated until all classes, directly or indirectly needed by the root class GRAPHICS, have been found.

The most important feature of this process is that it can be automated. The software developer does not need to compile lists of dependent modules, known as "Makefiles", or specify in each file the names of the files needed for its compilation ("Include directives" in C and C++). Besides being tedious, the process of creating this information by hand is a potential source of errors. The only thing that no utility can determine on its own is the name of the root class and the location of the needed classes in the file system.

To further simplify the programmer's work, a good compiler should be able to generate templates for Ace files whose cluster clause includes the base libraries (the kernel, fundamental data structures and algorithms, graphics, and so on) and a reference to the current directory. In this case, the developer only needs to specify the name of the system and its root class, without needing deep knowledge of Lace syntax.

The final product of the compilation process is an executable file whose name matches the name of the system in the Ace file, in this example - painting.

The language contains a number of other simple constructs that support control over the actions of the linking tools, in particular compiler directives and assertion monitoring levels. Some of these will be used as we continue our study of the OO method. It has already been noted that Lace supports the notion of a logical subcluster and can be used to describe complex structures, including subsystems and multi-level libraries.

Using a system description language independent of the development language, similar to Lace, lets classes remain system-independent. Classes are software components analogous to electronic chips, and a system is assembled from a specific set of classes just as a computer is assembled from a specific set of chips.

The Classic "Hello"

Reuse is a wonderful thing, but sometimes you need to solve a very simple task, such as printing a string. It is interesting to see how to write such a "program". Now that the notion of a system has been introduced, we can answer this burning question as well.

The following small class contains a routine that prints a string:

class SIMPLE creation

make

feature

make is

-- Print a string.

do

print_line ("Hello Sarah!")

end

end

The routine print_line, with a parameter of some type, prints the value of the corresponding object, in this case a string. Another routine named print does the same thing, but without moving to a new line. Both routines are available in every class and are inherited from the universal ancestor GENERAL, discussed later. (On class GENERAL see "Universal Classes", )

To obtain a system that will print this string, you need to do the following:

  • - (E1) Put the class text into the file simple.e.
  • - (E2) Run the compiler.
  • - (E3) If the Ace file has not already been created, you can request automatic generation of a template and, in edit mode, fill in the name of the root class - SIMPLE, of the system - my_first, and specify the cluster directory.
  • - (E4) After exiting the editor, the compiler will compile the system and create the executable file my_first.
  • - (E5) Run my_first. On the command line you simply need to type my_first. On systems with a graphical interface, a new icon named my_first will appear, and the program is launched by double-clicking it.

As a result, the following message will appear on the console:

Hello Sarah!

Structure and Order: The Programmer as Arsonist

We already know the overall picture of the process of building software with the OO method. We also know how to reconstruct the chain of events associated with the execution of some operation. Consider the operation:

[A]

x.g (u, v, ...)

appearing in the text of routine r of class C, and suppose that x is an attribute. How and when will it be executed? Class C must be included in a system, which is then compiled with the help of the corresponding Ace file. Next, execution of the system must be started, which will begin with the creation of an instance of the root class. The root creation procedure must perform one or more operations that will, directly or indirectly, create the object C_OBJ - an instance of class C - and then perform the call:

[B]

a.r (...)

where a is attached to C_OBJ. Next, the call [A] will execute g with the given arguments, using as target the object attached to field x of object C_OBJ.

So now we know how to reconstruct the exact sequence of events occurring during the execution of the system. This assumes that we see the system as a whole. The text of a single class naturally does not allow us to determine the order in which clients will call its routines. In this case, the only sequence of events available for observation is the order in which the instructions in the body of the given routine are executed.

Even at the system level, the structure is so decentralized that the task of precisely determining the order of operations, while certainly solvable, turns out in practice to be very difficult. What matters is that it is also not very interesting. It should be remembered that the root class is a rather superficial property of the system. It is a particular choice made only after the set of classes has already been formed. It is always possible to change the choice of root class fairly easily.

This move away from ordering is part of object technology and encourages the creation of a decentralized system architecture. The focus is not on the "order of program execution", but on the functional capabilities of the set of classes. The "order" in which these capabilities will be realized during the execution of a particular system is a secondary property. (See "Premature Ordering", )

These observations allow us to view the role of the programmer as that of a pyrotechnician, or a person lighting a huge bonfire. He stacks the wood, making sure that all the components are ready for assembly and that the necessary connections are present. Then he lights a match and watches the fire. If the structure has been properly prepared, there is no need to try to predict the sequence of ignitions. It is enough to know that every part that should catch fire will catch fire, and that this will not happen before its proper time.

Discussion

In concluding this lecture, it is worth considering the justifications for, and alternatives to, some of the decisions made in developing the method and notation. All the lectures that introduce new concepts end with a similar section.

The Form of Declarations

Let us sharpen our critical skills first on something less essential. So let us start with syntax. Consider the notation used when declaring components. Unlike many languages, we have not used the keywords procedure or function for routines. The form of a component declaration makes it possible to distinguish whether it will be an attribute, a procedure, or a function. Any component declaration always begins with its name:

f ...

This preserves the possibility of further defining a component of any kind. If a list of parameters is present next

g (a1: A; b1: B; ...) ...

then it is clear that g is a routine, which may be a procedure or a function. Next may follow:

f: T ...

g (a1: A; b1: B; ...): T ...

In the first example there is still a choice - f may be either an attribute or a function without arguments. In the second case the ambiguity is resolved, and g can only be a function. For f, the ambiguity is resolved depending on what follows T. If nothing follows, then f is an attribute, as in the following example:

my_file: FILE

But if the keyword is follows, and after it the body of the routine (do, or the once and external variants, discussed later), as in the example:

f: T is

-- ...

do ... end

then f is a function. One more variant

f: T is some_value

defines f as a constant attribute, whose value equals some_value. (Constant attributes are discussed in )

Such syntax makes it easy to recognize the various kinds of components, while at the same time emphasizing their fundamental commonalities. The very notion of a component, which unifies routines and attributes, follows the principle of uniform access. The commonality in attribute declarations is based on the same principles.

Attributes or Functions?

Let us examine in more detail the consequences of the principle of uniform access and of unifying attributes and routines under the common heading of components. (See "Uniform Access", . See this lecture.)

The principle states that clients of a module access all of its services in an identical way, regardless of how they are implemented. In this case, the class components act as the services, and for clients only the availability of the corresponding components matters, regardless of whether they are implemented as attributes or as functions.

Consider a class PERSON containing a component of type INTEGER with no parameters. If the author of a client class writes the expression

Isabelle.age

then the only thing that matters is that age returns an integer - the value of the age of the PERSON instance that, at run time, is attached to entity Isabelle. The component age may be either an attribute or a function that computes the result using the value of attribute birth_date and the current date. The author of the client class has no need to know which of these solutions the author of PERSON chose.

The notation for accessing an attribute is identical to that for calling a routine, and the notations for declaring these kinds of components are as similar as is conceptually possible. If, later on, the author of the class replaces the implementation of a function with an attribute, or vice versa, this will not affect the clients of that class in any way.

The difference in viewpoint between supplier and client regarding an attribute is shown in and , used to define the notion of component. illustrates the difference between routines and attributes - this is the internal, implementation-oriented view used by the supplier. uses, as its primary criterion, the difference between commands and queries - this is the external view of the client.

The decision to treat attributes and parameterless functions as equivalent for clients has two important consequences, discussed in detail in later lectures:

- The first consequence concerns software documentation. The standard class documentation for clients, known as the short form of the class, is composed in such a way that there is no difference in the descriptions of attributes and parameterless functions. (See "Using Assertions in Documentation: The Short Form of a Class", )

- The second consequence is related to inheritance, as the principal way of adapting software elements to new conditions without breaking existing software. If a class contains a component that is a function with no arguments, it is entirely permissible for descendant classes to redefine it as an attribute. (See "Redeclaring a Function as an Attribute", )

Exporting Attributes

To conclude the preceding discussion, we need to address the question of exporting attributes. The class POINT considered in this lecture has attributes x and y and exports them to clients, just as it does the functions rho and theta. To obtain the value of an attribute of some object, the usual notation for calling components is used, in the form my_point.x or my_point.theta.

This possibility of exporting attributes differs from the conventions adopted in many OO languages. A typical example is Smalltalk, in which only routines (methods) can be exported by a class, and direct access to attributes (properties) is forbidden.

Following the Smalltalk approach, access to an attribute can only be provided through a small exported function that returns the value of the attribute. In the example of class POINT, let us name the attributes internal_x, internal_y and add the functions abscissa and ordinate. The concise Smalltalk syntax allows the same name to be assigned to both an attribute and a function, removing the need to invent special names for attributes.

class POINT feature

-- Publicly available components:

abscissa: REAL is

-- Horizontal coordinate

do Result := internal_x end

ordinate: REAL is

-- Vertical coordinate

do Result := internal_y end

... Other components are similar to the previous version ...

feature {NONE}

-- Components not available to clients:

internal_x, internal_y: REAL

end

This approach has two drawbacks:

- It encourages the authors of classes to write many small functions similar to abscissa and ordinate. Even though such functions are very short, the class author spends additional effort writing them, and their presence makes the source text harder to read.

- A significant loss of performance, since every access to an object field requires a function call. It is no surprise that object technology has earned, in some circles, a reputation for being inefficient. One could, of course, develop an optimizing compiler that substitutes inline code for function calls, but then what would be the role of such functions?

The approach discussed in this lecture appears preferable. It avoids the need to clutter the source text with numerous tiny functions and provides the possibility of export where it is needed. This practice does not interfere with information hiding; in fact, it is a direct implementation of that principle, as well as of the principle of uniform access.

This technique satisfies the requirements of uniform access (an advantage for clients), simplifies the reading of source texts (an advantage for suppliers), and increases efficiency (an advantage for everyone).

Client Access to Attributes

Exporting an attribute using the technique discussed makes it available to clients as read-only, in the form my_point.x. Modifying the attribute by assignment is not permitted. The following syntactic construct is not allowed for attributes (Warning: an invalid construct - for illustration only):

my_point.x := 3.7

A simple rule applies. If attrib is an attribute, then a.attrib is an expression, not an entity. Consequently, no value can be assigned to it, just as no value can be assigned to the expression a + b.

The possibility of modifying attrib is achieved by adding an exported procedure of the form:

set_attrib (v: G) is

-- Sets the value of attrib to v.

do

attrib := v

end

Instead, one could imagine the following syntax for distinguishing users' access rights (Warning: unsupported notation. For discussion only.)

class C feature [AM]

...

feature [A]{D, E}

...

here A denotes read access, and M - modification access. This would eliminate the need to frequently write procedures similar to set_attrib.

Apart from the unwarranted additional language complexity, this approach is also not very flexible. In many cases a specific way of modifying an attribute may be required. For example, a certain class exports a counter whose value cannot be changed arbitrarily, but only in steps of +1 or -1:

class COUNTING feature

counter: INTEGER

increment is

-- Increases the value of the counter

do

counter := counter + 1

end

decrement is

-- Decreases the value of the counter

do

counter := counter - 1

end

end

Similarly, clients of class POINT have no way to directly change the coordinates x and y of a point. The exported procedures translate and scale serve this purpose.

When we study assertions, we will consider one more fundamental reason why direct assignments a.attrib := some_value are not permitted. The reason is that not every value some_value may be valid. One can define a procedure

set_polygon_size (new_size: INTEGER) is

-- Set a new value for the number of vertices of the polygon

require

new_size >= 3

do

size := new_size

end

whose parameter may equal 3 or more. A direct assignment does not allow this condition to be taken into account, and the result is an invalid object.

These considerations show that the author of a class has at their disposal five possible levels for granting clients access rights to attributes ().

7. Static Structures: Classes

Fig. 7.8. Possible variants of client access rights to attributes

Level 0 corresponds to a complete absence of access to the attribute. At level 1, read-only access is open. At level 2, modification is allowed by means of special algorithms. At level 3, a new value may be assigned only if it satisfies certain conditions, as in the polygon example. At level 4, the restrictions are lifted.

The solution described in this lecture follows from the analysis given above. Exporting an attribute gives clients read-only access rights (level 1). Permission to modify is provided by writing and exporting the corresponding procedures. They grant limited rights, as in the counter example (level 2), the right to modify subject to certain conditions (3), and unrestricted access (4).

This solution is a development of ideas found in various OO languages:

  • - In Smalltalk, to provide clients with level-1 access to an attribute, one has to write special functions similar to abscissa and ordinate. This is a source of extra work for the programmer and a cause of reduced performance.
  • - C++ and Java represent the other extreme. If an attribute is exported, it immediately becomes accessible at level 4 for both reading and writing, through direct assignment in the style my_point.x := 3.7. The only way to implement the other levels is to fully hide the attribute and write exported procedures to support levels 2 and 4 and functions for level 1. Everything else then proceeds as in Smalltalk. Support for level 3 is impossible, owing to the absence of an assertion mechanism in these languages.

This discussion illustrates two important principles of language design: do not needlessly create additional problems for the programmer, and do not introduce superfluous language constructs.

Optimizing Calls

At levels 2 and 3, the use of explicit procedure calls, such as my_polygon.set_size (5), to change the value of an attribute is unavoidable. There is a concern that using such a style at level 4 will negatively affect performance. Nevertheless, the compiler can generate code for the call my_point.set_x (3.7) that is just as efficient as it would be for my_point.x := 3.7, if such an assignment were allowed.

The ISE compiler achieves this through a general mechanism of directly inlining the code of routines, substituting the corresponding parameters, so that the need for calls is eliminated.

Inlining routine code is one of the transformations that an optimizing compiler for an OO language should provide. The modular style of development encouraged by object technology involves a large number of small routines. Programmers should not worry that the corresponding calls will lead to reduced performance. They should be concerned with consistently observing the principles of object-oriented architecture, not with the details of execution.

In some programming languages, especially Ada and C++, developers can mark which routines they would like to have inlined. For a number of reasons, it is preferable that this work be done in automatic optimization mode.

  • - Inlining is far from always applicable, and the compiler is in a much better position to make the right decision.
  • - When changes are made to the software, in particular using inheritance, an inlined routine may become non-inlined. The compiler will detect such situations far better than a human can.
  • - In the case of large systems, the compiler is always more efficient. Based on an analysis of routine size and the number of calls, it can more accurately determine which routines are worth inlining. This is, again, especially important in the case of software changes, since a human cannot keep track of the evolution of every fragment.
  • - Programmers can spend their time on more useful work.

The modern concept of software development holds that tedious, automatable, and delicate optimization work should be entrusted to the appropriate tools, not to a human being. This is one of the reasons behind the fundamental criticism of C++ and Ada. We will return to this question when discussing two other key aspects of object technology - memory management and dynamic binding. (See "Requirements for a Garbage Collector", , and "The C++ Approach to Binding", )

The Architectural Role of Selective Export

Selective export is not merely a convenience, but an integral part of OO architecture. It allows a group of conceptually related classes to grant each other access to all of their components while hiding them from the rest of the world, in accordance with the principle of information hiding. Moreover, it is the key to understanding whether modules of a higher level than classes are needed at all.

Without selective export, the only solution would be to introduce a new type of module representing a group of classes. Such super-modules - analogous to Ada and Java packages - would perform information hiding and export according to their own rules. Adding a new and partially incompatible module level to the elegant, class-based structure would lead to increased complexity and a larger language.

The best solution is to use classes themselves as super-modules. This approach is implemented in Simula, which allows the nesting of classes. However, it does not provide any tangible benefits.

The simplicity of object technology rests to a large extent on the use of a simple concept of modules. The support that classes provide for reuse is based on the ability to extract them from their context, retaining only their logical dependencies. There is a risk of losing these benefits if super-modules are introduced. In particular, it would become impossible to directly reuse a class that is part of a package. One would have to either fully import the entire package or make a copy of the class - a clearly unattractive form of reuse.

The need to group classes into structured collections nonetheless remains. In this book it is realized through the notion of a cluster ( of the course "Fundamentals of Object-Oriented Design"). However, the notion of cluster belongs to the domain of management and organization. Making it a language construct would risk the loss of the simplicity of the OO approach and its support for modularity.

If what is needed is a group of classes in which each is granted special privileges, there is no need for super-modules. A simple solution is provided by selective export, which lets classes retain their independent status.

Importing Listings

In the source text of classes, in feature clauses, the components available to other classes are listed. Why not, conversely, include lists of components obtained from other classes? The Modula-2 language, for example, supports the import declaration.

Nevertheless, in the OO approach this brings nothing beyond documentation. To use component f of another class C, the class in question must be a client or a descendant of that class. In the first case, this means that f is used as

a.f

but then a declaration of a must be present:

a: C

unambiguously showing that f is a component of C. In the case of descendant classes, the information will be available from the official documentation of the class, its flat short form.

Consequently, there is no need for an import clause. ("Flat Short Form", )

Nevertheless, a convenient graphical development environment should be able to provide the programmer with information about the suppliers and ancestors of a given class, and about their own suppliers and ancestors, following the chain further.

Assigning a Function's Result

Assigning a function's result is an interesting language issue, discussion of which was begun earlier in this lecture. It is worth examining in more detail given its importance, including for languages that do not use the OO approach.

Consider a function - a routine that returns a result. The purpose of any function call is to compute some result and return it to the calling routine. The question is how to denote this result within the text of the function itself, in particular in the instructions that initialize and modify the result.

The convention introduced in this lecture uses a special entity, Result. It is treated as a local entity, initialized with the appropriate default value, and the returned value equals the final value of Result. According to the initialization rules, this value is always defined, even if the body of the function contains no assignment to Result. Thus the function

f: INTEGER is

do

if some_condition then Result := 10 end

end

will return 10 if the condition some_condition holds at the time of the call, and 0 (the default initialization value for INTEGER) otherwise. As far as the author knows, the technique of using Result was first proposed in this book. Since the first edition appeared, it has been adopted by at least one language - Borland Delphi. It should be noted that it is unsuitable for languages that allow functions to be declared inside other functions, since the name Result would become ambiguous. In various languages, the following techniques are most commonly used:

  • - (A) Terminal return instructions (C, C++/Java, Ada, Modula-2).
  • - (B) Using the function's name as a variable (Fortran, Algol 60, Simula, Algol 68, Pascal).

Convention A is based on an instruction of the form return e, whose execution terminates the function, returning e as the result. The advantage of this method lies in its clarity, since the returned value is clearly identified in the text of the function. However, it also has its drawbacks:

  • - (A1) In practice, the result is often determined in the course of a computation that includes initialization and a series of intermediate changes to the value. This creates a need for a temporary variable to hold intermediate results.
  • - (A2) The technique tends to encourage modules with several exit points. This runs counter to the principles of good program structuring.
  • - (A3) The language must account for the situation where the last instruction executed during a function call is not a return. In Ada programs, a run-time exception is raised in this case.

The last two problems are resolved if return is regarded not as an instruction, but as a syntactic clause that is a mandatory part of the text of every function:

function name (arguments): TYPE is

do

...

return

expression

end

This solution develops the idea of the return instruction and eliminates its most serious drawbacks. Nevertheless, no language uses it, leaving problem A1 unresolved.

Technique B uses the function's name as a variable within the text of the function. The returned value coincides with the final value of that variable. This removes the need to declare a temporary variable, as mentioned in A1.

With this approach, the three problems mentioned above do not arise. But other difficulties appear, since the same name denotes both the function and the variable at once. The presence of the function's name within its own body can be interpreted in two ways: as the name of the variable or as a recursive call. The language must therefore precisely specify in which situations it refers to the variable, and in which to a recursive call of the function. If, in the body of function f, the name f appears as the target of an assignment, it refers to the variable

f := x

whereas if f is part of an expression, a recursive call of the function is implied

x := f

which is permitted only if f has no parameters. However, assignments of the form

f := f + 1

will be rejected by the compiler if f has parameters, and if it has none, will be understood as recursive calls whose result is assigned to variable f. This last interpretation most likely will not match the developer's intent, who simply wanted to increment variable f by one, and will instead produce an infinite loop. To achieve the desired effect, a temporary variable will still have to be introduced.

The convention based on the predefined entity Result eliminates the problems of both techniques A and B. In languages that provide default initialization for all entities, including Result, an additional advantage is obtained. Writing functions is simplified, since a function often needs to return a default value in all cases except those specifically provided for. For example, the function

do

if some_condition then Result := "Some specific value" end

end

does not need an else clause. It is understood that the language must strictly define default values. Such conventions will be introduced in the next lecture.

The final advantage of the Result convention follows from the principle of Design by Contract (see chapter 11). Result can be used to express an abstract property of a function's result, independent of its implementation, in the postcondition of the routine. No other approach allows one to write the following:

prefix "|_": INTEGER is

-- Integer part of a number

do

... Implementation omitted ...

ensure

no_greater: Result <= Current

smallest_possible: Result + 1 > Current

end

The ensure clause contains postconditions asserting two properties of the result: the result must not be greater than the value to which the operation is applied, and that value must be less than the result plus one.

Addendum: A Precise Definition of Entity

It will be useful, in the course of discussing notational issues, to clarify the notion of entity, which we have used throughout. This is, to a large extent, a technical notion generalizing the traditional notion of a variable.

Entities, in the sense in which they are used in this book, denote the names of certain run-time values associated with objects. Three possible cases can be distinguished:

Definition: entity

An entity may be:

  • - (E1) A class attribute
  • - (E2) A local entity of a routine, including the predefined entity Result for a function
  • - (E3) A formal argument of a routine

Case E2 emphasizes that the entity Result is always considered local. Other local entities are introduced in a local declaration. Result and other local entities are reinitialized on every call of the routine.

All entities, with the exception of formal arguments (E3), are available for writing, that is, they may appear as the target x in an assignment x := some_value.

Key Concepts

  • - The fundamental concept of object technology is based on the notion of class. A class is an abstract data type, partially or fully implemented.
  • - A class may have instances, called objects.
  • - Objects (dynamic elements) must not be confused with classes (a static description of properties common to a set of run-time objects).
  • - In a consistent approach to object technology, every object is an instance of a class.
  • - A class serves simultaneously as a module and as a type. The originality and power of the OO model derive in part from the integration of these two notions.
  • - A class is characterized by its components, including attributes, which represent fields in instances of the class, and routines, which represent computations involving the data of instances. A routine may be a function that returns a result, or a procedure, if no result is returned.
  • - The basic mechanism of OO computation is the call of a component (a reference to a component) of a class. A component call applies a component to an instance of a class (possibly with arguments).
  • - Calling named components uses dot notation, while calling operator components uses infix or prefix notation.
  • - Every operation is relative to the "current instance" of a class.
  • - For clients of a class (other classes that use its components), an attribute is indistinguishable from a function with no arguments, in accordance with the principle of uniform access.
  • - An executable ensemble of classes is called a system. A system contains a root class and all the classes that the root class needs, directly or indirectly, through client relationships or inheritance. Executing a system amounts to creating an instance of the root class and calling the creation procedure for that instance.
  • - Systems have a decentralized architecture. The order of actions is not essential for development.
  • - The refinement of the assembly process is achieved through the simple system description language Lace. In a Lace specification, called an Ace file, the root class is specified along with the set of directories in which the system's clusters are located.
  • - The compilation process can be automated without using Make files or Include directives.
  • - The information hiding mechanism must be flexible. Alongside unrestricted access and complete hiding, export to only some clients may be required. Attributes may be made available read-only, for reading plus restricted modification, or in full-access mode.
  • - Exporting an attribute means read-only access to it. Modification requires calling the corresponding exported procedure.
  • - Selective export makes it possible for groups of related classes to grant a special access mode for each component.
  • - There is no need for add-ons on top of classes - super-modules. Classes should remain independent software components.
  • - The modular style of OO development requires a large number of small routines. The potential risk of reduced performance can be addressed by having these routines inlined by an optimizing compiler. Responsibility for finding such fragments should rest with the compiler, not with the developers.

Bibliographical Notes

The notion of class came from the language Simula 67 (see the bibliographical references for the course "Fundamentals of Object-Oriented Design"). In Simula, a class is simultaneously a module and a type, but this feature was not specifically emphasized and was lost among Simula's successors.

The principle of a single purpose can be viewed as an analogue of a technique well known in mathematical logic and theoretical computer science: currying. Currying a function of two variables f means replacing it with a function g of one variable, which returns as its result a function of one variable. As a result of currying, for any valid values of x and y:

(g (x)) (y)= f (x, y)

To curry a function is, in other words, to specialize it with respect to its first argument. This technique is analogous to the one used in this lecture, replacing the traditional procedure rotate, which has two parameters:

rotate (some_point, some_angle)

with a function taking a single parameter, applied to a target:

some_point.rotate (some_angle)

[M 1990] describes currying and some of its applications in computer science, in particular in the formal study of the syntax and semantics of programming languages. Currying will be discussed further when we consider graphical user interfaces ( of the course "Fundamentals of Object-Oriented Design").

Unlike the position taken in this lecture, in some languages an object is treated as a language construct rather than as a run-time notion. Such an approach is intended for research purposes and does not need the notion of class. The best-known representative of this school is the language Self [Chambers 1991], in which "prototypes" are used instead of classes.

Details of the convention for infix and prefix operations, in particular the precedence table, are given in [M 1992].

James McKim drew my attention to the last argument in favor of the Result convention (its use for postconditions).

Exercises

E7.1 POINT as an Abstract Data Type

Write the specification of an abstract data type describing a point on a plane.

E7.2 Completing the Implementation of POINT

Complete the source text of class POINT. Fill in the missing fragments, add a procedure rotate (rotating the point about the origin), as well as any other components you consider necessary.

E7.3 Polar Coordinates

Rewrite class POINT so that the representation of a point in polar, rather than Cartesian, coordinates is used as the base representation.

See Also

  • OOP
  • Class
  • Object
  • Uniform Access
  • Design by Contract
  • Requirements for a Garbage Collector
  • The C++ Approach to Binding
  • Using Assertions in Documentation: The Short Form of a Class
  • Redeclaring a Function as an Attribute
  • Premature Ordering
  • Universal Classes
  • Functional Decomposition
  • The Role of Expanded Types
  • Inheritance of Functionality
  • Self-Documentation
  • Notes on indexing
  • The War Over Semicolons
  • Categories of Functions

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


Часть 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