Frag Logo
  Frag Home | Frag SF Project   index | contents | previous | next

Class Hierarchy and Class Path Linearizing

Dispatch Order

The class hierarchy is determined by the class, superclass, and mixin relationships of an object. The result is a directed graph. This graph is interpreted in the specific context of each object. The class, superclass, and mixin relationship graph might contain more than one way to reach one and the same class from one object. To avoid ambiguities, Frag uses a simple set of resolution rules to determine the order of dispatch: Using these resolution rules it is ensured that Object is always the last class in the list, as it is the superclass of all other classes.

Let us walk through a number of examples to illustrate class path linearization. In general, if you are unsure about how a class hierarchy gets linearized, you can use getNextPath to introspect the class path of an object.

Consider we define a simple inheritance hierarchy:
Object create Person
Object create Student -superclasses Person
Object create Lecturer -superclasses Person
Object create Assistant -superclasses Person

If we define a student object:
Student create jim
the linearized list of classes of that object would be: Student Person Object. This can be introspected using getNextPath:
puts [jim getNextPath]
In this example, it might happen rather often that a lecturer is also an assistant. Hence it makes sense to combine these two classes in one class using multiple inheritance. If we add such a class TeachingAssistant to the example, create an TeachingAssistant object, and print the next path of that object, we get the result: TeachingAssistant Assistant Lecturer Person Object.
Object create TeachingAssistant -superclasses Assistant Lecturer
TeachingAssistant create joe
puts [joe getNextPath]  
In seldom cases, a student might be a teaching assistant as well. But if this is rather seldom, and no new methods or state need to be introduced for this case, it makes no sense to introduce a new subclass. Instead, we can use Frag's multiple classes feature, to deal with this situation in an object-specific way. Consider joe starts to study again. We can dynamically reclass the object joe to have both classes:
joe classes TeachingAssistant Student
puts [joe getNextPath]
The resulting class path includes Student: TeachingAssistant Assistant Lecturer Student Person Object.

Consider we want to introduce a cross-cutting concern, such as logging, for all persons in our system. This can be done using a mixin that intercepts the methods to be logged. (For such tasks it often makes sense to use the "*" method, see Section Method "*".)
Object create PersonLogger
Person mixins PersonLogger
puts [joe getNextPath]
puts [jim getNextPath]         
The resulting next path includes the mixin before all other classes, so that it can intercept all invocations. The next path for joe is: PersonLogger TeachingAssistant Assistant Lecturer Student Person Object. The next path for jim is: PersonLogger Student Person Object.

Mixins inherit from superclasses and their own mixins. These superclasses and mixins of mixins are also included in the class path before the object's classes and superclasses. For instance, we could add a Logger superclass for PersonLogger, which would for instance be included in jim's class path like this: PersonLogger Logger Student Person Object.
Object create Logger
PersonLogger superclasses Logger
puts [joe getNextPath]
puts [jim getNextPath]         

The "next" Command

In the description so far, the most specific class in the hierarchy overrides the more general classes that come later in the linearized class path. That is, if two classes define a method with the same name, the method of the more specific class is executed.

Just consider again the ConnectionLogger class for socket connections as used in examples before. Here, we want to observe specific methods of the socket connection (like open, close, etc.), but the logger should not override the methods of SocketConnection.

To resolve this problem, Frag offers a primitive next to proceed in the linearized order of classes from within a method. Consider the method close should be logged before and after a connection is actually closed:
ConnectionLogger method close {} {
      self writeMsg stdout "Before close [self]"
      next
      self writeMsg stdout "After close [self]"
}

The next primitive in between the two output statements forwards the invocation to the next class(es) on the linearized class list (also called "next path"). That is, for the rest of the next path close is searched and invoked, if found.

Without arguments, next invokes the next method on the class path with exactly the same arguments as the ones that were used to invoke the current method. That is, it reads the local variables of the current method signature and passes them along to the next method. Sometimes the next method, however, has a different method signature than the current method. Then you can add methods to next to pass any other arguments along with the next call. The special argument -noArgs can be used instead of other arguments in ?args ...?. Then next invokes the next method on the class path without arguments. -noArgs is used when the current method has arguments, and the next method on the class path has no arguments. The details for next are:

Syntax

next ?args ...?
Special argument:
-noArgs

Description

next forwards invocations along the class path to the next method in the class hierarchy. It mixes this next method into the execution of the current method. See Section Class Hierarchy and Class Path Linearizing for details about using next.

Without arguments, next invokes the next method on the class path with exactly the same arguments as the ones that were used to invoke the current method. That is, it reads the local variables of the current method signature and passes them along to the next method. Sometimes the next method, however, has a different method signature than the current method. Then you can use the optional ?args ...? arguments to pass any other arguments along with the next call.

The special argument -noArgs can be used instead of other arguments in ?args ...?. Then next invokes the next method on the class path without arguments. -noArgs is used when the current method has arguments, and the next method on the class path has no arguments.

In many cases, when you consider to invoke next with arguments because same-named methods on subclasses and superclasses have different interfaces, it might be a good idea to refactor the methods to use the args argument for variable argument lengths (see Section The "args" Parameter). This is especially advisable for "generic" methods, such as the constructor (see Section Constructor).

Examples

The following code uses next in a subclass CheckedAccount to add some checking to an Account class: the parameter amount should not be negative and the limit should not be exceeded. If the checks succeed, next lets the Account class do the actual work of changing the balance of the account, otherwise an error is raised.
Object create Account -defaults {
    balance 0
}
Account method add {amount} {
    self set balance ($balance + $amount)
}
Account method withdraw {amount} {
    self set balance ($balance - $amount)
}
Object create CheckedAccount -superclasses Account -defaults {
    limit 1000
}
CheckedAccount method add {amount} {
    if {$amount < 0} {
        throw [Exception create -msg "'$amount' must be a positive value"]
    }
    next
}
CheckedAccount method withdraw {amount} {
    if {$amount < 0} {
        throw [Exception create -msg "'$amount' must be a positive value"]
    }
    if {$balance - $amount < -$limit} {
        throw [Exception create -msg "'$amount' cannot be withdrawn: limit exceeded"]
    }
    next
}    
Consider, limit would not be given as an instance variable, but must be passed to CheckedAccount as a parameter. Then the interface of the method in superclass and subclass would differ. Hence, an adaptation in the next call of withdraw would be necessary:
Object create CheckedAccount -superclasses Account 
CheckedAccount method add {amount} {
    if {$amount < 0} {
        throw [Exception create -msg "'$amount' must be a positive value"]
    }
    next
}
CheckedAccount method withdraw {limit amount} {
    if {$amount < 0} {
        throw [Exception create -msg "'$amount' must be a positive value"]
    }
    if {$balance - $amount < -$limit} {
        throw [Exception create -msg "'$amount' cannot be withdrawn: limit exceeded"]
    }
    next $amount
}

  index | contents | previous | next