This chapter presents Smalltalk's class hierarchy and the concept of inheritance. You'll see inheritance through an example of animal classification and see how to generalize the pattern matching method you used in Chapter 5. You'll also add more new classes to Smalltalk/V using the Class Hierarchy Browser. And finally, you'll be introduced to the Smalltalk concept of polymorphism and how to process nested data structures.
As always, the examples for this section are stored on a disk file, Chapter.6. You can use the Open... item in the File menu to access this file if you do not want to type the examples.
You will add important new classes and methods to your environment during this lesson, so be sure to save the image when you exit the environment so you can build on them in later tutorials.
Much of Smalltalk's power comes from arranging its classes in a hierarchy. Each class has an immediate superclass and possibly one or more subclasses, with class Object at the top of the hierarchy. You're already familiar with this same system in biology, which arranges living organisms in classes, based on characteristics common to each class. Classes higher in the hierarchy represent more general characteristics, while classes lower in the hierarchy represent more specific characteristics. For example, fish and tree are more abstract than halibut and maple.
In Chapter 5, you saw how Smalltalk organizes its code (methods) by class. In this and later chapters, you will see how you can develop generic problem solutions using abstract classes, and then develop more application-specific solutions which "specialize" the general solution by adding a small amount of code in subclasses.
If you haven't already done so, close the Class Hierarchy Browser window opened in Chapter 5. Under the File menu in the Transcript window's menu bar, select Browse Classes. Select class Boolean in the class list pane. Choose Show Subclasses from the Classes menu. Now select class True which shows you the following window:
Figure 6.1
Class True
Notice that the classes in the class list pane are indented. The indentations show the class hierarchy. Each class is a superclass of the classes indented below it. As you can see, Object is the superclass of all classes, and Boolean is the superclass of True and False.
A class with "..." following its name has subclasses that are not displayed. When you first open the Class Hierarchy Browser, it displays only the first level subclasses of class Object. This keeps the pane from becoming too cluttered. To display or hide the subclasses of a class, use Show Subclasses or Hide Subclasses, respectively in the Classes menu or double click on the class name. Try hiding the subclasses of class Boolean using the double click shortcut.
Inheritance is the Smalltalk capability that allows you to re-use software by specializing already existing general solutions. To see this, define a new class hierarchy of animals.
Figure 6.2
Animal Hierarchy
You will be using these same classes again in the following chapters to illustrate collections, graphics and window applications. The class Animal is a subclass of class Object. In turn, classes Bird and Mammal are subclasses of class Animal. Finally, classes Parrot and Penguin are subclasses of class Bird, and classes Dog and Whale are subclasses of class Mammal.
Whenever you define a new class, you also declare its instance variables. The following shows in parentheses the instance variables defined for each class in the animal hierarchy:
Animal (name, knowledge, habitat, topSpeed, color, picture) Bird (flying) Parrot (vocabulary) Penguin ( ) Mammal ( ) Dog (barksAlot) Whale ( )
An object inherits all the instance variables: defined in its superclasses in addition to containing the ones defined in its own class. For example, parrots, penguins, dogs and whales each contain the following instance variables:
Parrot (name, knowledge, habitat, topSpeed, color, picture, flying, vocabulary) Penguin (name, knowledge, habitat, topSpeed, color, picture, flying) Dog (name, knowledge, habitat, topSpeed, color, picture, barksAlot) Whale (name, knowledge, habitat, topSpeed, color, picture)
In this chapter, we'll use the instance variables name, vocabulary and barksAlot. (You'll see the others used in subsequent chapters.) The instance variable name contains a string representing the animal's name, vocabulary contains a string of all words known by a parrot, and barksAlot contains either true or false, depending upon how much a dog barks.
Normally, you create new classes using the Class Hierarchy Browser. (You'll do that again later in this chapter.) Since we have several classes and methods to define for the animal class hierarchy, however, we've simplified the procedure for you by putting these initial class definitions in a file. To add the animal classes to your Smalltalk/V environment, install the file by evaluating the following expression:
(File pathName: 'tutorial\animal6.st') fileIn; close
Now, in order to see these new classes with the Class Hierarchy Browser, activate the Class Hierarchy Browser window, pull down the Classes menu and select the Update function. Now all the animal classes are visible. If they are not, use the Hide/Show selection from the Classes menu. By selecting its classes and methods, you can now browse the animal hierarchy.
As you can see from the Class Hierarchy Browser, the methods you have just included for class Animal are answer:, name:, and talk. The code for these methods follows:
answer: aString "Display a message for the receiver animal on the Transcript window, consisting of the animal's class name and name preceding aString." Transcript nextPutAll: self class name, ' ', name, ': ', aString; cr name: aString "Change the receiver animal's name to aString." name := aString talk "Display a message that the receiver can't talk." self answer: 'I can"t talk'
Similarly, the methods for class Parrot are:
talk "Display a message containing the receiver parrot's vocabulary." self answer: vocabulary vocabulary: aString "Change the receiver parrot's vocabulary to aString." vocabulary := aString
And finally, the methods for class Dog are:
bark "Have the receiver dog bark by ringing the bell and displaying a bark message." Terminal bell. barksAlot ifTrue: [self answer: 'Bow Wow, Bow Wow, Bow Wow!'] ifFalse: [self answer: 'Woof'] beNoisy "Change the status of the receiver dog to noisy." barksAlot : true. self answer. 'I''ll bark a lot' beQuiet "Change the status of the receiver dog to quiet." barksAlot := false. self answer:'I won"t bark much' talk "Have the receiver dog talk by barking unless barksAlot is nil, in which case the superclass can decide how to talk." barksAlot isNil ifTrue: [super talk] ifFalse: [self bark]
We didn't define any methods for classes Penguin and Whale. However, these classes inherit the methods of class Animal, so we can create Whale and Penguin objects and send messages to them, as we do later in this chapter.
Like instance variables, methods are also inherited. When a message is sent to an object, Smalltalk looks for the corresponding method defined in the object's class. If it finds the method, Smalltalk performs it. If it doesn't find the method, however, Smalltalk repeats the procedure in the object's superclass. This process continues all the way to class Object. If no method is found in any superclass, a Walkback window pops up to display the error.
For example, look at the name: method defined in class Animal. Since this method is not defined in any of Animal's subclasses, the name: method in class Animal is evaluated whenever a name: message is sent to instances of classes Dog, Parrot, Penguin, or Whale.
As another example, look at the talk method in the animal classes. Classes Penguin and Whale inherit talk from class Animal, whereas classes Dog and Parrot re-implement their own versions of talk.
The method inheritance of the animal hierarchy is shown in Figure 6.3.
Figure 6.3
Animal Hierarchy Method Inheritance
Occasionally, you may want to override a method and use a method higher in the superclass chain. Generally, you'd do this whenever the specialized processing done by a method doesn't apply in a particular case. You would instead use the more general processing of a method with the same name which appears higher in the superclass chain.
For example, look at the method talk for class Dog. A dog doesn't know how to talk if its instance variable barksAlot is undefined (has the value nil). In this case, it uses the following message to request the superclass's talk method:
super talk
The special variable super represents the same object as the special variable self--the receiver in the method in which it appears. The difference is that when a message is sent to super, Smalltalk starts to look for the method not in the receiver object's class, but instead in the superclass of the class containing the method in which super appears. In the example talk method, the search begins in Mammal, the superclass of class Dog. There is no talk method in class Mammal, but there is one in class Animal, so that one is used.
Evaluate the following expressions to create and assign to global variables five animal objects: two dogs, a penguin, a parrot and a whale (the animals "talk" to the Transcript window, so first reframe your windows to not overlap the Transcript):
"Creating animals" Snoopy := Dog new. Snoopy name: 'Snoopy'. Snoopy beQuiet. Lassie := Dog new. Lassie name: 'Lassie'. Lassie beNoisy. Wally := Penguin new. Wally name: 'Wally'. Polly := Parrot new. Polly name: 'Polly'. Polly vocabulary: 'Polly want a Cracker'. Moby := Whale new. Moby name: 'Moby'
Polymorphism is a unique characteristic of object-oriented programming whereby different objects respond to the same message with their own unique behavior. For example, evaluate the following messages to see how the various animals respond to the talk message:
"Let's hear them talk" Lassie talk. Snoopy talk. Wally talk. Polly talk; talk; talk. Polly vocabulary: 'Screeech@#!? Don"t bother me!'. Polly talk. Moby talk. Snoopy beNoisy; talk. Lassie beQuiet; talk
Polymorphism lets you use entirely new classes of objects in existing applications, as long as they implement the messages required by the application. This greatly facilitates the reusing of generic code. A simple example of generic code is the method max: defined in class Magnitude, which returns the "maximum" of two objects:
max: aMagnitude self > aMagnitude ifTrue: [^self] ifFalse: [^aMagnitude]
The existing max: will work in any new subclass of Magnitude, as long as the new class implements the greater than (>) method.
In Chapter 5, you created a method indexOfString: to do pattern matching on strings. By relocating this method to a superclass of class String, we can use it to do pattern matching for several more classes. We'll change the name of the method to indexOfCollection: to suggest its more general capability, but we won't change the processing. Use the Class Hierarchy Browser to add the method indexOfCollection: to class IndexedCollection, a subclass of Collection:
indexofCollection: aCollection "Answer the index position of the first occurrence of aCollection in the receiver. If no such element is found, answer zero." | index1 index2 limit1 limit2 | limit2 := aCollection size. limit1 := self size - limit2 + 1. index1 := 1. [index1 <= limit1] whileTrue: [ (self at: index1) = (aCollection at: 1) ifTrue: [ index2 := 2. [index2 <= limit2 and: [ (self at: index1 + index2 - 1) = (aCollection at: index2)] ] whileTrue: [index2:= index2 + 1]. index2 > limit2 ifTrue: [^index1] ]. index1 := index1 + 1]. ^0
Try the more general pattern matcher by evaluating each of the following examples using Strings and Arrays.
'the time has come' indexOfCollection: 'tim' #($c $a $n $ $y $o $u $ ) indexOfCollection: 'you' #(l 2 3 (4 5) 'abc' 6) indexOfCollection: #(2 3) #(l 2 3 (4 5) 'abc' 6) indexOfCollection: 'abc'
As an example of polymorphism and the processing of nested data structures, consider the following method for equality (=), which appears in class IndexedCollection. This method compares an instance of IndexedCollection or one of its subclasses (e.g., an Array) to a similar object by sending the = message to corresponding elements of both objects. If the element is a kind of IndexedCollection, then the method performs a recursive send, invoking the = method. If the element is an object such as a Number, the method performs a non-recursive send, invoking a different = method.
aCollection "Answer true if the elements contained by the receiver are equal to the elements contained by the argument aCollection." | index | self == aCollection ifTrue: [^true]. (self class == aCollection class) ifFalse: [^false]. index := self size. index ~= aCollection size ifTrue: [^false]. [index <= 0] whileFalse: [ (self at: index) = (aCollection at: index) ifFalse: [^false]. index := index - 1.] ^true
This method also demonstrates the difference between equality (=) and equivalence (==). Equality tests whether two objects contain the same elements. Equivalence, on the other hand, tests whether two objects are, in fact, the same object. For example, the expression
self == aCollection
tests whether the receiver object is the same actual object as the argument. If so, then they are obviously equal, and the method returns true.
To help see this difference, evaluate the following statements:
| a b | a := #( l 2 ), #( 3 4 ). b := a copy. ^a = b
This expression returns true, because the two objects contain the same elements. Now substitute = with ==, and re-evaluate the statement. This returns false, because, although the two objects contain the same elements, they are still two different objects. As an example of a true equivalence, evaluate the following expression:
| a b c | a := #( 1 2 3 4 ). b := a. c := b. ^c == a
To use the inherited = method on nested data structures, evaluate these expressions:
#(1(2 (3))) = #(1 (2 (3))) #(john smith) = #(john smith) #(l 'two' 3) = #(l 'two' 3)
Since the indexOfCollection: method defined above compares elements with the message, it can be applied to nested collections. For example, show the results of each of the following expressions:
#((l 2) (3 4) (5 6)) indexOfCollection: #((3 4) (5 6)) #(l 2 3 (4 5) 'abc' 6) indexOfCollection: #(3 (4 5) 'abc') #(l 2 3 (4 5) 'abc' 6) indexOfCollection: #('abc')
The final example of this chapter creates a new class to monitor the frequency of access to the data in an Array. For instance, suppose you have an Array of sales tax rates for California, indexed by zip code minus 90000 (California zip codes begin with 9). If you know how frequently each sales tax rate is looked up by zip code, you can compute the average sales tax paid, shipments to each region, and several other statistics.
To do this, we create class MonitoredArray as a subclass of class Array. A MonitoredArray is like a normal Array, except that it also maintains a parallel Array containing the number of times the at: message was used for each index value. A MonitoredArray can be substituted for an Array in any application. Like any subclass, it inherits all the behaviors of its superclass, Array, and implements some special behaviors of its own.
To add the new class, click on Array in the class list pane of the Class Hierarchy Browser. You'll find it as a subclass of FixedSizeCollection, which is a subclass of IndexedCollection, which is a subclass of Collection. Pull down the Classes menu and select Add Subclass. A prompter dialog box asks for the New Subclass name; enter MonitoredArray. Make sure that the No and Pointers radio buttons are selected and press the Enter key or click on the OK button. The class is then updated, with class MonitoredArray selected.
Now you must specify the new class' instance variables. Proceed to the text pane in the bottom half of the window and edit the class definition to appear as follows:
Array variableSubclass: #MonitoredArray instanceVariableNames: 'atCounts' classVariableNames: ' ' poolDictionaries: ' '
Pull down the File menu and select Save or press the Alt + S keys. The class definition is updated. Next you'll give this new class its own unique behavior.
Class methods respond to messages sent to class objects, rather than to instances of the class. Class methods are often used for creating initialized instances of a class. As an example, we'll create a class method for class MonitoredArray.
First, take a closer look at the Class Hierarchy Browser window. You'll find a set of radio buttons, one labeled instance and one class, just above the middle list pane. These buttons are mutually exclusive; that is, when one is selected, the other is deselected. Click the instance button and the list pane to the right will show all the instance methods of the selected class and the variables pane below the buttons shows the names of its instance variables. Click on the class button and all the class methods of the selected class, if any, will be listed in the methods pane and names of its class variables will be seen in the variables pane.
Now select class MonitoredArray in the left class list pane. Click the class button. Any methods added now will be class methods. Pull down the Methods menu and select New Method. The bottom text pane displays a template reminding you of a new method's format. Type the following into the contents pane, replacing the template:
new: anInteger "Answer a new MonitoredArray." | answer | answer := super new: anInteger. answer initialize. ^answer
Pull down the File menu and select Save or press the Alt + S keys to add this class method to class MonitoredArray. You will get a prompter asking you to confirm that you want to redefine a superclass method; click the Yes button. We re-implement new: for class MonitoredArray because the inherited new: method for Array does not initialize the atCounts instance variable.
The remaining three MonitoredArray methods are instance methods. Click the instance button and add these three instance methods one at a time (note that you will get a prompter for redefinin at:):
accessCounts "Answer the array of 'at:' counts." ^atCounts at: anInteger "Answer the element in the receiver at index position anInteger. Increment the count for accesses to the receiver using anInteger." atCounts at: anInteger put: (atCounts at: anInteger) + 1. ^super at: anInteger initialize "Private - Initialize the MonitoredArray by allocating and initializing the parallel atCounts array." | size | size := self size. atCounts := Array new: size. 1 to: size do: [:index | atCounts at: index put: 0]
As an example of using a MonitoredArray, evaluate and show the results of the following expressions:
| array | array:= MonitoredArray new: 20. 1 to: 10 do: [ :i ] 1 to: 10 do: [ :j | array at: i + j ] ]. ^array accessCounts
After completing this tutorial, you should now be familiar with:
As always, you can review any of these topics by repeating the sections of the tutorial, or by referring to the detailed description in Part 3 of this manual.
If you exit the environment before beginning the next tutorial, be sure to save the image.