This chapter provides an overview of Telescript's particular flavor of object-oriented programming. If you're already familiar with object-oriented programming, you can skim through, taking note of special terms used in the Telescript world, and special conventions not found in other object-oriented languages such as C++ or Smalltalk. If this is your first exposure to object-oriented programming, you'll want to pay close attention to the concepts in this chapter; they lie at the heart of all Telescript programming. And if you feel you need a deeper introduction to object-oriented programming than a single chapter can provide, you may want to check the Bibliography at the end of this book for other books on object-oriented programming. <<<The Bibliography is not yet written.>>>
You can compare an object with non-object-oriented programming mechanisms used in other languages, mechanisms such as the data structure and the subroutine. A data structure can, like an object, store data in meaningful forms. And a subroutine, like an object, can work on data and then return (we hope) with meaningful results. Subroutines and data structures are, in fact, often used together in the same way that an object's procedures work with the object's data. So what's the difference? In a word, encapsulation.
An object's data (called its properties in the Telescript language) and its procedures (called its methods in Telescript) are encapsulated; that is, the object's properties can only be written or read by the object's methods, and the object's methods can work directly only with the object's own properties. The operations of one object can't directly read or write the properties of another object, which ensures that an object's data is always worked on in a meaningful way (at least if the methods are written in a meaningful way). For example, consider the tide and moon object: The dates and times of tides in the object can't be overwritten by a method that calculates daily prime interest rates in Argentina. And the tide object's methods can't be turned loose on outside data say, for example, a table containing the daily average rainfall in Kalamazoo.
Figure 2-1 : Encapsulation ensures that the methods of an object will only work with properties of the same object, not with properties of another object.
An object's properties and methods give the object unique characteristics. Its properties set its state (if you change the value of a property, you've changed the state of the object), and its methods control the object's operating characteristics. Properties and methods working together give the object a sense of entity, and make it easy to create objects that model real-world objects. That's why you can expect a "directory-assistance object" to accept names and return phone numbers, and a "tax-return" object to ask embarrassing financial questions and suggest the amount of money you owe to the government.
Encapsulation, at first glance, seems to prevent this kind of communication. It insulates objects from each other by ensuring that the methods of one object can't work directly with the properties of a second object. It does not, however, prohibit the method of one object from executing the method of a second object. The second object's method may affect the second object's own properties, a fact that allows the first object to work indirectly with the properties of the second object. Say, for example (referring back to Figure 2-1), that a method in Object A includes an instruction to execute a method in Object B that changes one of Object B's properties. In this way, Object A has indirectly worked on a property of Object B. This form of inter-object communication--where the method of one object executes the method of a second object--is accomplished by calling an operation.
When the method of one object calls an operation on a second object, it calls the operation by name, and it must satisfy the operation's argument requirements: it passes along the specified arguments. The operation may return a result to the method that called it, or it may return no result at all. These arguments and results allow objects to exchange information.
In the relationship just described, where one object calls an operation on a second object, the first object is called the requester because it requests an operation. The second object is called the responder because it responds to that request. Because an operation hides its method, a requester sees no farther than the operation--what arguments it takes, what result it returns. The operation is a black box into which a requester may pass arguments and out of which it may receive a result, a box that hides its inner workings.
Figure 2-2 : A method of the requester object calls an operation on the responder object.
Consider, for example, a Calendar object that keeps an appointment calendar. The object offers the operation findFreeHour
. The operation requires a starting date and time and an ending date and time; it returns the date and time of a free hour within the specified interval. An object calling findFreeHour
on the Calendar object need only know the name of the operation, that it must supply starting and ending times as arguments, and that the operation returns a time value specifying a free hour in the calendar. The requester doesn't need to know how the responder stores its appointment data or how the method associated with findFreeHour
searches for a free hour.
Because this is such a common occurrence, the Telescript language offers a simple way to do this: a feature called an attribute. An attribute of one object acts like a property that may be directly manipulated by another object. As you'll read in later chapters, a method can work with an attribute almost as if the attribute were a simple variable. The requester can assign values to an attribute or read an attribute's current value. For example, an attribute named wheels
can be assigned a value with the simple expression wheels = 2
. It doesn't require a special operation, and it can work across objects; one object can read or change an attribute of a second object.
The fact that an attribute acts as if it were a directly-manipulated property is an illusion of the Telescript language. An attribute is, in reality, either one operation or two operations paired together. These operations are typically linked to a property. If the attribute is a read/write attribute (that is, a method can read its value or write a new value to it), the attribute has two operations: one called the getter, the other called the setter. The getter accepts no arguments and typically returns the current value of its property. The setter accepts a single argument and typically writes that argument into its property. If the attribute is a read-only attribute (that is, a method can only read the attribute's value), it has no setter--only a getter.
Figure 2-3 : A read/write attribute has a getter and a setter; a read-only attribute has only a getter. These operations are typically associated with a single property.
An attribute's getter and setter operations are invisible to other objects. They can't be called directly; Telescript calls them automatically whenever a method assigns a value to the attribute or reads an attribute's value. This creates the illusion that an attribute is a property that may be directly manipulated by other objects.
Attributes are, in almost all cases, a property accompanied by simple read or write operations that act as the getter and setter. This is how Telescript creates a default attribute. There are rare occasions, however, when an attribute is created using a custom getter or setter that does more than simply read or write a property. A getter may, for example, perform a calculation to return a value instead of reading a property. Consider a read-only attribute named daysElapsed
that, when read, checks a hardware clock and calculates how many days have gone by since a fixed date. This advanced aspect of attributes allows you to create your own custom attributes that behave in ways other than Telescript's default attributes.
Figure 2-4 : The components of an object are divided into its interface (the outer ring) and its implementation (the inner circle).
An object's implementation is its properties and methods. Each property stores a value; the values of all of the properties together set the object's state. Each method is a sequence of Telescript instructions that, when executed, perform a specific task for the object. An object's methods determine the behavior of the object. An object's method may read or write directly to the object's properties because encapsulation allows an object to work directly with its own properties. Other objects have no direct access to properties or methods.
An object's interface is its operations and attributes that are available to other objects. These operations and attributes are called the object's features. An operation is always associated with a method and, when called, executes that method. The operation provides a way for a requester to pass arguments to the method being executed; it also provides a way back to the requester for a result generated by the method's execution.
An attribute may be a read/write attribute, in which case it is a pair of special operations: a getter and a setter. Or it may be a read-only attribute, in which case it is a single operation: a getter. An attribute is almost always associated with one of the object's properties, in which case the getter and setter read and write that property. In rare cases, an attribute has no associated property, but is calculated instead.
Other objects may use an object's features by calling an operation or by using an attribute; the object may, in fact, use its own interface by calling its own operations or using its own attributes. The Telescript language allows an object to control use of its features through public and private features. If an object declares one of its features to be a public feature, other objects can use that feature, as can the object itself. If an object declares one of its features to be a private feature, only the object itself can use that feature--it's not available to other objects. Private operations are a useful mechanism for creating modules of code within an object. A private operation can be called at any time within any of the object's methods--a useful convenience.
(A note for C++ programmers: The C++ language uses the terms private and public in different ways than Telescript, so take care that you don't get confused. What C++ calls private is, in the Telescript world, a property. What C++ calls protected is, in Telescript, a private feature. You can take some relief in the fact that what C++ calls public is also public in Telescript.)
The largest object in this chain contains all the objects it uses as its properties. It also contains all the objects that are properties of its property objects, all the objects that are properties of the properties of its property objects, and--you get the picture--so on down to the simplest objects that have no properties of their own. The mail-order store object, for example, contains the widget, gizmo, and doodad objects, as well as the integers used as properties in each of those objects. Objects contained by another object are said to be nested in that object; the object itself, along with all objects contained within the object, are said to be in the object's closure.
Figure 2-5 : Objects nested within another object are said to be within that object's closure. All the objects in this figure are in the closure of the multi-national-corporation object--including the multi-national-corporation object itself.
It's important to note that although we talk of objects nesting one within another, and of a complex object containing other objects, every object exists as an individual entity within a Telescript engine. If one object is nested within a second object, the Telescript engine maintains the two objects separately, but keeps a reference from the containing object to the nested object so that it knows one object is nested in the other. References allow a single object to be contained by two different outside objects: each outside object has a separate reference to the contained object.
One other important facet of object nesting: nesting is transparent to other objects. An object's properties are part of its implementation and are therefore not directly available to other objects. When a requester works with a responder, the requester simply uses the object's features and lets the Telescript engine take care of connections between the responder and the other objects in its closure.
The class is a mechanism that defines objects that are identical except for state. A class defines and stores a set of methods, operations, and attributes for a group of objects; it also defines the types of properties the objects use. This definition makes it easy to create objects that share the same characteristics. Each object created from a class is called an instance of the class; the process of creating an instance of a class is called instantiation. All objects in the Telescript language are instances of a class, so all objects are defined by a class--their parent class. The parent class of an object is the class that defines the object's interface and implementation.
Classes promote efficient programming; once you define object characteristics in a class, you can simply instantiate the class to get as many instances of that type of object as you want. Each object behaves in the same way but has its own unique state, so each object can be put to a different use.
To see how a class works effectively, consider the objects used to store inventory information in the mail-order store example. These objects could all be instances of a class; let's call it InventoryItem
. This class defines three attributes: inStock
, onOrder
, and sold
. Each of these attributes either gets or sets an integer representing the number of items in stock, the number of items on order, and the number of items sold, respectively.
When InventoryItem
is first instantiated, it creates an inventory-item object that we use to keep track of widgets. The next instance of InventoryItem
is an object we use to keep track of gizmos, and the third instance is an object we use to keep track of doodads. Although these three objects are all instantiated from the same class, they are completely independent objects. If you increase the inStock
attribute of the widget object, the inStock
attributes of the gizmo and doodad objects aren't affected. And if you need a new inventory-item object (to keep track of thingamabobs, for example), you can simply instantiate InventoryItem again without working to define a new object.
Classes also promote Telescript engine efficiency. A class contains all of the interface and methods necessary for an object. Whenever an object is instantiated from the class, the engine need only set up and maintain the properties for that object. It doesn't have to create a new interface and set of methods for the object.
The engine maintains a reference from each object to the object's parent class. Whenever something uses an attribute or calls an operation on an object, the engine runs the method stored with the class; that method affects the properties of the object. For example, if an object sets a new onOrder
value for the doodad object, the engine executes the setter method that is a part of the parent class--InventoryItem
. That setter sets the appropriate property in the doodad object, so the object behaves appropriately as an instance of its class, but affects only its own state.
A superclass can have one or more subclasses; each of its subclasses inherits the superclass's interface (its features) and its implementation (its methods and property types). Each subclass typically elaborates on those inherited characteristics, making a distinct class of itself by adding new operations or attributes. It can also alter one (and only one) part of the characteristics it inherits: it can completely redefine one or more of the inherited methods that underlie the inherited operations. This means that a subclass is guaranteed to have the interface--the operations and attributes--of its superclass, but its internal behavior may be completely different when those features are used because it has changed the method that's executed when a feature is used.
Subclasses can have their own subclasses, those subclasses can have their own subclasses, and so on, creating many levels of inherited characteristics. Inheritance ensures that a superclass passes all of its characteristics on to all of its subclasses. Those subclasses can in turn pass their own characteristics (which include the original superclass's characteristics) on to their subclasses, and so on down the line. Inheritance ensures that a superclass's characteristics are present in all of its subclasses, no matter how many levels of inheritance a subclass is removed from the superclass.
To see how inheritance works, consider an example class called DataRecorder
. It defines an object with operations that--among other things--accept daily information and store and retrieve the information by date. DataRecorder
is used as a superclass for a set of subclasses that all work with daily information: SeismicRecords
, which records seismic activity each day; CensusRecords
, which records changes in population of an area throughout each day; WeatherRecords
, which records temperatures, wind, and rainfall throughout each day; and FinancialRecords
, which records daily price data.
Each of these subclasses has all of the features of DataRecorder
--operations that accept, store, and retrieve information by date--and all of the methods and property types of DataRecorder
. Each subclass goes on to add new features appropriate to the kind of data it records--an operation in WeatherRecords
, for example, that calculates and stores the current day's rainfall total, or an operation in CensusRecords
that records the intended destination of a departing individual. Each subclass may also override the method for an inherited operation or attribute if it wants; remember that inherited methods may be overridden with new methods.
The subclasses can have subclasses themselves. Consider CensusRecords
, which may be subclassed to create classes more specifically adapted to different kinds of populations: BirdRecords
to record the number of a species of birds in an acre of forest, for example, or AntRecords
to record the number of a caste of ants in an anthill. These sub-subclasses inherit all the characteristics of DataRecorder; they also inherit all the characteristics of their direct superclass, CensusRecords
. They go on to embellish those characteristics with characteristics of their own. They may even supply new methods for inherited operations. For example, the BirdRecords
class may supply a new method for the intended-destination operation of CensusRecords
that can recognize directions (north, south, east, west) in addition to those normally recognized (Chicago, Alabama, Brazil, and so on).
A class tree is a useful convention for showing inheritance relationships among classes. Figure 2-6 shows a class tree that defines the relationships of the example classes we just discussed. Each box is a class; a line between two classes shows that the class above is the superclass of the class below.
Figure 2-6 : A class tree shows inheritance relationships among classes.
At the top of the class tree is the root class. This is the superclass at the top of the inheritance hierarchy; all of its characteristics are inherited by every other class in the class tree. If you look at any other class in the tree, its characteristics are all inherited by any and all classes that are connected below it. For example, the characteristics of CensusRecords
are inherited by its subclasses BirdRecords
and AntRecords
; they're also inherited by the sub-subclasses GroundbirdRecords
and FlightbirdRecords
.
Throughout this book, you'll see class trees represented typographically--a representation used for convenience and efficient use of space. These typographic class trees show the same relationships as a graphical class tree, but use different conventions. A typographic class tree shows superclasses to the left and subclasses to the right, unlike a graphical tree that typically shows superclasses above and subclasses below. Each level of indentation (shown with a single bullet for each level) is a deeper level of subclass. And any classes indented below another class are all subclasses of that class. Consider, for example, the typographical class tree shown in Figure 2-7. DataRecorder
is the root class; SeismicRecords
, CensusRecords
, WeatherRecords
, and FinancialRecords
are its direct subclasses. CensusRecords
and FinancialRecords
have their own subclasses, which are sub-subclasses of DataRecorder
.
DataRecorder
* SeismicRecords
* CensusRecords
* * BirdRecords
* * * GroundbirdRecords
* * * FlightbirdRecords
* * AntRecords
* WeatherRecords
* FinancialRecords
* * StocksRecords
* * MFundsRecords
* * BondsRecords
Figure 2-7 : A typographical class tree shows subclasses indented beneath superclasses.
AntRecords
class in Figure 2-7, that object is an instance of AntRecords
. It's not an instance of any of AntRecord
's superclasses such as CensusRecords
or DataRecorder
. The object is, however, a member of both its own class and its class's superclasses. So the AntRecords
object in this example is a member of DataRecorder
, a member of CensusRecords
, and a member of AntRecords
. It is an instance only of AntRecords
.The concept of instance and member is important whenever you consider the features of an object. If you know that an object is an instance of a class, you know that the object has exactly the class's features and no more. If you know that an object is a member of a class, you know that the object has the class's features but that it may have extra features or different methods for the operations because the object may be an instance of a subclass, not an instance of the class itself.
A mix-in class (mix-in for short) is a special class that can't be instantiated; it's simply a collection of characteristics meant to be mixed into the inheritance of a subclass. Classes that aren't mix-in classes (in other words, classes as they've been described up to this point in this chapter) are called flavor classes (flavors for short). Only flavors can be instantiated to create objects. Every flavor has one--and only one--flavor superclass. A flavor class can have as many mix-in superclasses as desired, however, which provide secondary sources of inherited characteristics. Mix-ins can also have superclasses, which must be mix-ins--a mix-in can't have a flavor superclass.
When you define new classes, mix-ins allow you to create a set of common characteristics that you can add to classes that have different superclasses. They also allow you to add characteristics to some of a class's subclasses without adding those characteristics to all of the class's subclasses. To see how this works, let's go back to the class tree of the last example and add some mix-ins.
We have two example mix-in classes to use. The first, Measurement
, is a mix-in that includes operations that accept measurements in English or metric units, and convert from one unit to another as necessary. These features are handy for data recorders that accept information measured in feet, centimeters, hectares, and so on. The second, Currency
, is a mix-in that includes operations that accept amounts measured in monetary units--such as dollars, yen, pounds, francs, and marks--and converts amounts among them as necessary.
When we look at DataRecorder
's four subclasses, two of them use measurements that might be entered in English or metric units: SeismicRecords
and WeatherRecords
. They are prime candidates for the Measurement
mix-in. A third subclass, FinancialRecords
, would benefit from the Currency
mix-in, which allows it to tally and convert amounts of money.
Measurement
has been added to the inheritance of the classes SeismicRecords
and WeatherRecords
; the mix-in Currency
has been added to the inheritance of the class FinancialRecords
.
Figure 2-8 : Mix-ins appear in a graphical class tree as dashed boxes.
Now consider how a mix-in works by looking at the FinancialRecords
class. It inherits all the characteristics of its primary superclass, DataRecorder
--the operations that accept information and then store and retrieve it by date. It also inherits all the characteristics of the mix-in Currency
--the operations that can accept and convert values in monetary units. All of these inherited features, from both the primary superclass and the mix-in, are passed on to FinancialRecords
' subclasses: StocksRecords
, MFundsRecords
, and BondsRecords
.
Notice in the class tree that the mix-in Measurement
has been added to SeismicRecords
and WeatherRecords
, but not to CensusRecords
and FinancialRecords
, which don't need the features offered by Measurement
. If these features had been included as part of the DataRecorder
definition, all of the subclasses would have had measurement features whether they needed them or not. By putting the features in a mix-in, those features can be applied as necessary within a class tree without affecting all subclasses.
In typographical class trees, a mix-in is represented by a class name in parentheses. It follows the name of the class to which it has been added. The class tree shown in Figure 2-9 is the typographical form of the class tree shown in Figure 2-8. Notice that the mix-ins Measurement
and Currency
appear in parentheses just after the classes into which they've been mixed. If you want to determine a class's inheritance, you look first at the mix-ins that follow it, and then look above it for its primary superclass. For example, WeatherRecords
inherits from Measurement
(the mix-in to its right) and from DataRecorder
(the primary superclass above it). The class BondsRecords
inherits from FinancialRecords
, its flavor superclass; from Currency
, the mix-in of its flavor superclass; and from DataRecorder
, the flavor superclass of its flavor superclass.
DataRecorder
* SeismicRecords (Measurement)
* CensusRecords
* * BirdRecords
* * * GroundbirdRecords
* * * FlightbirdRecords
* * AntRecords
* WeatherRecords (Measurement)
* FinancialRecords (Currency)
* * StocksRecords
* * MFundsRecords
* * BondsRecords
Figure 2-9 : Mix-ins appear in parentheses in a typographical class tree.
Measurement
mix-in might inherit some of its characteristics from two other mix-ins: Metric
, which supplies conversions between metric units of measurement; and English
, which supplies conversions between English units of measurments. You'll learn much more about the way mix-ins are created in Chapter <<<yet to be written>>>. Just keep in mind that the features of any class include those of its primary superclass and its superclasses, and of its mix-in classes and their superclasses. In the Telescript society, breeding is everything.
Sealed classes provide security; you know that there can be no members of a sealed class except instances of the class itself. An object that depends on other objects that are members of a sealed class can be assured that no subclasses exist whose objects may behave differently than expected. For example, if the StockRecords
class of the last example were sealed, there could be no subclasses that add new operations or that redefine the way the inherited operations work. If an accounting object accepted StockRecords
objects as arguments, it would know that it would not get objects instantiated from subclasses, objects that might override the behavior it expects.
It's important to note that there are many Telescript predefined classes that are sealed but also have subclasses. Predefined classes are the only case where a sealed class can be subclassed--by the designers of Telescript. This technique is used to create a node of a class tree that can't take on additional branches created by subclassing with custom classes.
For example, consider the class DataRecorder
used in the last class tree. It's a root class whose features accept and store data, but aren't adapted to accepting the specific kinds of data that might come in from the real world. If you instantiate DataRecorder
, the DataRecorder
object you get won't be useful. This class is a perfect candidate for an abstract class. By making it abstract, you ensure that no one will try to create an object from it, but you make its characteristics available to subclasses that can in turn be instantiated. The instances of the subclasses then have the characteristics of DataRecorder
along with the other characteristics necessary to operate practically on real-world data.
On closer examination, however, inheritance won't work to define a group of parallel classes. That's because a subclass can only differentiate itself from its superclass by adding new features or by supplying new methods for inherited operations. A subclass must not (and cannot) change its inherited operations, attributes, and property types: inherited operations must accept the same types of arguments and return the same type of result as their superclass equivalents, inherited attributes must get and set the same type of object as their superclass equivalents, and inherited properties must be the same type of object as their superclass equivalent.
To see how this works, consider two example classes: IntegerArray
and RealArray
. Both of these classes keep a two-dimensional array of values and provide operations that store and retrieve values from the array. The only difference is that IntegerArray
keeps an array of integer values and defines its operations to accept and return only integer values, while RealArray
keeps an array of real-number values and defines its operations to accept and return only real numbers.
The interface and implementation of both of these classes must be defined using specific types of values--integers in one case, real numbers in the other case. It's impossible to define a superclass with features common to both IntegerArray
and RealArray
because the operations and attributes must specify either integer arguments and results or real-number arguments and results. If the superclass defines its features using integers, then RealArray
can't use those features; if it defines its features using real-number values, then IntegerArray
can't use those features. IntegerArray
and RealArray
must define their features individually without depending on inheritance.
The Telescript language provides a mechanism to accommodate parallel classes like the two just described: the class family. Simply put, a class family is a class-producing function--something like a class template into which you plug classes to create a new class. It is not a class itself, although it seems like a class in many ways.
A class family is defined very much like a class. It has operations, attributes, methods, and properties. The main difference between a class family and a class is that a class family uses formals to hold the place of classes in its definition. Think of a formal as a class variable, a named placeholder that says "This can be any class" or "This can be any of a defined set of classes." In the array class examples we just discussed, we could create a class family named Array
. The class family uses a formal named "Number" wherever an operation or attribute would specify Integer
or Real
. The formal could be constrained to be either an integer or a real number.
The class family can't be used as a class itself--it can only create a class. That class can then be instantiated or subclassed as you see fit. To create a class using a class family, you satisfy the family's formals with actuals. An actual is a class that replaces a formal throughout the class family definition. The result is a class that is defined using the supplied actuals. For example, the class family Array
has a formal named Number
. If you supply Integer
as an actual to satisfy the Number
formal, the class family creates an array class that handles integers. If you supply Real
to the class family, the family creates an array class that handles real numbers. The new class would be named Array[Integer]
or Array[Real]
depending on the actual you supply. You can then instantiate the new class to create array objects.
Generated with Harlequin WebMaker