Inheritance, Modification and Overriding

We have already discussed the basic principles of inheritance above, when we observed that any object or class you define in your game must inherit from one or more classes (the so-called superclasses of the class or object being defined). In this section we shall discuss a few of the practical consequences of this, and how to make use of them when writing Interactive Fiction with adv3Lite.

Overriding

You may recall towards the end of the previous chapter we tested for the nest being placed on the branch with the following code:

+ branch: Thing 'wide firm bough; flat; branch'
    "It's flat enough to support a small object. "
    
    iFixed = true
    isListed = true
    contType = On
    
    afterAction()
    {
        if(nest.isIn(self))
            finishGameMsg(ftVictory, [finishOptionUndo]);
    }
;

But you may be wondering, where exactly did this afterAction() method come from, and how does the Heidi game know how to use it? The answer is that afterAction() is in fact defined on Thing, and that the action-handling routines in the adv3Lite library know that they have to call the afterAction() method on every Thing in scope after the performance of each successful action. On the Thing class, however, the afterAction() routine is defined like this:

class Thing:  Mentionable
  ...
  
  afterAction() { }
 
  ...
;

Thus the afterAction() method on Thing is an empry method that does nothing at all (since in general we don't want most objects to react to actions, and in any case the library can't know how you would want particular objects to react in your particular game). What the afterAction() method on the branch object does is to override the afterAction() method define on the Thing class, so that when branch.afterAction() is called, it's the branch's version of afterAction that's used.

This is about as simple an example of overriding as one can get. To understand the full implications we need a slightly more complex one. The Thing class defines an isOn property top determine whether a Thing is switched on or off. It also defines an isSwitchable property to determine whether that Thing can be switched on and off (obviously most things can't). On the Thing class this property and this method are defined as follows:

isSwitchable = nil

makeOn(stat) { isOn = stat; }

If we want to create a Thing that can be switched on and off, we'd therefore need to override its isSwitchable property to true, for example:

lightSwitch: Thing 'light switch'
   "It's just an ordinary light switch that can be turned on and off. "
   isSwitchable = true
   isFixed = true
;

That's all we have to do in adv3Lite to create a light switch (by the way, I hope by now it's obvious why we also defined isFixed = true; light switches aren't generally the kinds of things you can pick up and carry around with you unless the electrician hasn't fitted them yet). In particular the lightSwitch object inherits from Thing the handling of the SWITCH ON and SWITCH OFF commands, the rule that these commands can be carried out if isSwitchable is true, the isOn property that determines whether or not the Thing is currently switched on or off, and the the makeOn(stat) method that's used by the SWITCH ON and SWITCH OFF commands; the former calls makeOn(true) and the latter makeOn(nil).

That's all fine as far as it goes, and thanks to inheritance we're getting a lot of the right kind of behaviour on our light switch simply by defining a couple of properties, but in practice it probably isn't enough. Normally when you turn a light switch on and off you want something else to happen, like a light going on and off, and you'd probably want the same in your game. The obvious way to handle this would be to override the makeOn(stat) method on the lightSwitch to light up or turn off a light bulb somewhere. Here's a first (but erroneous) attempt:

lightSwitch: Thing 'light switch'
   "It's just an ordinary light switch that can be turned on and off. "
   isSwitchable = true
   isFixed = true
   
   makeOn(stat) { lightBulb.makeLit(stat); }
;

Can you see what's wrong with this? We'll assume you've defined the lightBulb object elsewhere, and I can assure you makeLit(stat) is another method defined on Thing, so the code will make the bulb light up and go out again. The problem is that by overriding makeOn(stat) in this way we've removed what it originally did on Thing so we're no longer keeping track of whether the light switch is turned on and off. One way to fix it would be to copy the original code from Thing.makeOn(stat) into lightSwitch.makeOn(stat), giving us:

lightSwitch: Thing 'light switch'
   "It's just an ordinary light switch that can be turned on and off. "
   isSwitchable = true
   isFixed = true
   
   makeOn(stat) 
   { 
     isOn = stat; 
     lightBulb.makeLit(stat); 
   }
;

This would certainly work, and after a fashion it does solve the problem, but it really isn't a terribly good solution. Although in this case it doesn't seem much effort to copy a single line of code, if we were overriding a rather more elaborate method it would both tedious and wasteful to have to copy lines and lines of code into our overridden method, especially if all we wanted to do is to add a single line of code or two to what the method does on the base class. Also, if there's subsequently an improvement or correction to the method on the base class (perhaps in a new version of the library), our overridden method won't benefit from it. In fact, if we override methods in the manner of the code sample immediately above, we're missing out on most of the benefits of the inheritance model. The proper solution is to use the inherited keyword, thus:

lightSwitch: Thing 'light switch'
   "It's just an ordinary light switch that can be turned on and off. "
   isSwitchable = true
   isFixed = true
   
   makeOn(stat) 
   { 
     inherited(stat); 
     lightBulb.makeLit(stat); 
   }
;

Here, inherited(stat) means "at this point in the code, do whatever the method I'm overriding would have done". In this case, then, inherited(stat) effectively calls Thing.makeOn(stat) to carry out the base handling that would have been carried out on Thing. Note that the call to the inherited method doesn't have to be the first statement in the overriding method; you can put it wherever it's convenient to do so within the overriding method. However, you do have to call interited() with the same parameter list as the method you're overriding; since we're overriding the makeOn(stat) method of Thing, we thus need to call inherited(stat).

This is a very important technique to bear in mind when you come to override object and class methods in your own code, as you'll find yourself increasingly doing as the more adventurous you become with what you want your games to do. While in certain cases, like the afterAction() method on the branch object in the first example above, we may know from experience that the base method on Thing doesn't do anything so there's no point in calling the inherited method when overriding it, if in any doubt at all, the best rule of thumb is to always include a call to inherited() when overriding a method.

Modifying

In addition to inheriting behaviour from classes (and objects), we can also modify existing classes and objects, by making use of the modify keyword. This is best explained by means of an example. Suppose we were writing a game in which there were going to be a lot of light switches in different places, each one controlling a different light bulb. It might become a bit tedious to have to define the same code over and over again on each and every light switch. An alternative would be to add the desired behaviour to the Thing class by modifying it, perhaps like this:

modify Thing
   bulbObj = nil
   isSwitchable = (bulbObj != nil)
   
   makeOn(stat)
   {
      inherited(stat);
      if(bulbObj != nil)
         bulbObj.makeLit(stat);
   }
;

There are several things to note here. The first is that we've added a new property, bulbObj, to the Thing class to hold a reference to the light bulb that should be lit when this Thing is switched on. We've then overridden the isSwitchable property so that it becomes true if bulbObj is not nil, in other words if we have defined a bulb object associated with this Thing. This means that when we come to define our individual light switch objects, we don't have to remember to define isSwitchable = true along with setting the bulbObj property to indicate the bulb controlled by this switch. Next note the use of the inherited() keyword here to call the version of the makeOn() method on the original Thing class. Finally, note the check in the if statement to make sure we don't accidentally try to call the makeLit() method of a nil value, which would cause a run-time error that could easily happen if we made a Thing switchable without defining an associated bulbObj. If we modified Thing as above we could then define an individual light switch object thus:

lightSwitch: Thing 'light switch'
   "It's just an ordinary light switch that can be turned on and off. "
   bulbObj = lightBulb
   isFixed = true   
;

This is a big improvement if we have a lot of light switches to define in our game. It has to be said, though, that unless virtually every switchable object in our game was going to be a light switch, and perhaps even then, this probably isn't the best way to go about addressing this particular issue. We'd almost certainly be better off defining a custom LightSwitch class along the following lines:

class LightSwitch: Thing
   bulbObj = nil
   isSwitchable = true
   isFixed = true
   
   makeOn(stat)
   {
      inherited(stat);
      if(bulbObj != nil)
         bulbObj.makeLit(stat);
   }
;

...

lightSwitch: LightSwitch 'light switch'
   "It's just an ordinary light switch that can be turned on and off. "
   bulbObj = lightBulb      
;

This is neater primarily because this way we can now give the light switch behaviour only to those objects that actually need it, and also because we can simply define isSwitchable = true and isFixed = true on the LightSwitch class knowing that these will almost certainly be the appropriate values on every light switch we create. The example nevertheless serves to illustrate the principle of modifying classes, and in future chapters we shall come across cases when this is indeed the best approach. In the meantime you may have noticed that the code on the modified Thing class and the code on the new LightSwitch class look very similar. This is no accident: what we are in fact doing when we use the modify keyword on a class (or object) is to create a new version of that class (or object) that inherits from the old one.