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.
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.
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.
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.
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:
Any call can take one of the following forms:
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:
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:
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:
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:
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:
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.
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 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.
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:
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.

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.
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:
As a result, the following message will appear on the console:
Hello Sarah!
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.
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.
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.
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", )
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).
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 ().

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:
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.
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.
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", )
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.
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 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:
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:
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.
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:
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.
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).
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.
Часть 1 7. Static Structures: Classes
Часть 2 Style for Declaring Hidden Components - 7. Static Structures: Classes
Comments