Extending REALbasicand other languages, too!
By Joe Strout
Joe is a senior software engineer at REAL Software. He can be contacted at [email protected]
A mixin class is a small class designed to be blended with other classes through multiple inheritance. Usually, mixin classes are not intended to be instantiated directly, but instead just add a few extra functions or properties to some other class.
I recently needed a mixin class in an editor application. However, this application was being written in REALbasic, which, like Java, supports only single inheritance. REALbasic, from Real Software (the company I work for), is a Basic-based, cross-platform development environment that runs on and targets Windows, Linux, and Macintosh. (For more information, see http://ww.realsoftware.com/.) However, REALbasic also provides some language features that let you provide mixin functionality and syntax in a different way. The technique I present here can be adapted to other languages as well, though in a somewhat more limited fashion.
In my application, objects of a number of different classes needed to be configurable via a Property Editor window. The set of classes that had editable properties did not fit well onto the existing class hierarchy, which was based on more fundamental functionality. In other words, the "has properties" concern cut across the concerns of the inheritance tree. That, and the fact that the property editor needed only a handful of methods, made this an attractive candidate for a mixin.
In languages with multiple inheritance (C++ or Python, for instance), I might have simply defined a small abstract PropertyHolder class, with some pure virtual methods and some additional methods defined in terms of the first set. For example, PropCount, PropName, and PropValue must be implemented by each PropertyHolder, but PropIndex would be implemented by the mixin itself; see Table 1. I could then simply add this as an additional base class wherever needed (watching out for any problems caused by inheriting this base class twice). But since REALbasic does not allow multiple inheritance, this approach was not an option.
One alternative to a class is an interface. An interface is like a class that contains only pure virtual methods; any class that implements an interface must implement all those methods. REALbasic, as well as languages such as Java, lets a class implement any number of interfaces. Because an interface can't itself contain any implementation, the problems of inheriting the same implementation twice are neatly avoided.
An interface isn't quite the same as a mixin class, though. Mixins do more than just define an interface; they provide some functionality, too. In my case, the PropIndex method and the PropValue methods that work by name, would have to be duplicated by every class that uses the interface, with all the maintenance problems that would cause.
REALbasic has an unusual language feature that provides a solution to this problemthe extension method. Extension methods are like global methods, except that they are inserted into the namespace of another type. That type may be atomic (Integer, for instance), a class type, or in this case, a class interface. Combining extension methods with a class interface results in the functional and syntactic equivalent of a mixin class.
I started by defining an interface called PropertyHolder (Figure 1), containing just the low-level methods (ones that would be pure virtual in a C++ mixin class); see Listing One (the complete sample code in REALbasic binary project format is available electronically; see "Resource Center," page 5). The next step is to make a module to contain the extension methods. In REALbasic, method definitions and properties that aren't part of a class live in a module, which is something like a namespace or source file in other languages. Methods in a module may be global to the whole project, or they may be protected or private members of the module's namespace. Or, by using the Extends keyword, they may insert themselves into another namespace.
I created a module called "PropHolderExtensions." When you intend a module to contain only extension (or global) methods, then its name isn't very important, but naming it this way made its purpose more clear when browsing the project. The extension methods could also be put into some other module; for example, I've occasionally made a module called "Globals" to hold all my global methods, and the extension methods would work fine there. But again, that would make their purpose more obscure, and make it harder to transfer the mixin to another project.
Within my PropHolderExtensions module (Listing Two), I added the methods that extend the class interface. A class extension method is like any other global method, except that its first parameter uses the Extends keyword, and identifies the object on which the method will act, like so:
Function PropIndex( Extends ph as
PropHolder, propName as String ) as Integer
Now, wherever I need to make one of my existing classes a property holder, I simply add the PropertyHolder interface, and implement the methods it defines. (REALbasic supplies the method declarations for me, and if I forget to implement one, it generates a compile-time error.) I can then use not only the interface functions just implemented, but also the higher level functions, all with the same syntax. For example, if w is a WindowClass object that implements PropertyHolder, then I could write:
Dim idx as Integer
idx = w.PropIndex( "Title" ) // (1)
w.PropValue( idx ) = "Demo" // (2)
Here, the assignment in line (2) is something implemented directly in WindowClass, but the index look-up in line (1) is not; it's part of the mixin.
This combination of an interface and a set of extension methods has well-defined semantics. First, consider the methods defined by the interface. These are virtual, but an interface method always occupies just a single vtable slot, even if an interface is declared at multiple points in a class's ancestry; see Figure 1(b). So no matter what the static type of an object reference, a call to one of these methods will always invoke the most specific version, preserving polymorphism. This is somewhat different from the equivalent C++ case, where the particular method invoked may depend on the static type of the object reference; see Figure 1(c).
Next, consider methods added to the interface via the extension mechanism. These are really just global methods with a bit of syntactic sugar, and as such, they are nonvirtual. But that means that, again, it doesn't matter what the static type of a reference might be; the same extension method will be invoked regardless. Further, any methods invoked by those methods on the object will be properly polymorphic as previously noted. In a C++ multiple-inheritance mixin, the static type determines which copy of the mixin method is invoked, and that in turn affects which of the low-level functions are called by the mixin code.
For these reasons, the technique described here isn't just a workaround for the lack of multiple inheritanceit's actually better in some ways in that it neatly avoids a class of errors that can occur with the standard mixin approach.
Adapting to Other Languages
The basic principles used here could be applied to other languages, such as Java. You would define an interface (or class containing only pure virtual methods) and an associated set of global methods. Most of the benefits described here would apply.
However, without the extends keyword, the global methods would have to be accessed with a syntax different from the interface methods. That will make it more difficult to change your mind about which methods should be implemented by users of the mixin, and which can be provided by the mixin itself, since such a change will require updating all callers of the code.
Specializing a Mixin
One drawback to this technique is that a class can't easily override the mixin code. For example, suppose I have a property holder class that has a large number of properties and can look one up by name very quickly (perhaps through a hash table). The generic PropIndex method provided by the extension may be too slow in this case, but because it is an extension method, you can't override it as you could if using a traditional mixin.
There is a solution in such cases, but it requires a few extra steps. For this example, I would start by defining an interface called FastPropHolder, with a PropIndex method that accepts a property name and returns an integer. My class with the hash table would implement this interface as well as PropertyHolder. Then, the PropIndex extension method would be modified to check for objects that satisfy the FastPropHolder interface, and make use of it when available (see Listing Three).
Defining another interface and changing the mixin code is certainly not an ideal way to specialize a mixin. But in practice, I have rarely found this to be a problemusually it is clear which methods should be implemented by the mixin itself, and which should be implemented by mixin users. In the occasional case where a specialization is needed, the solution here is quite serviceable.
In most cases, mixins don't need to keep any per-object state. In my example, there is nothing in the mixin methods defined by PropHolderExtensions that require any data from the PropHolder objects. But occasionally, this need does occur; for example, in a game project, a mixin might be used to make some classes of objects react to gravity, and the mixin code that causes an object to fall might need some extra data about the object, such as its terminal velocity.
In this case, the easiest solution by far is to make a getter (and/or setter) for that data part of the mixin interface. Then, any class that uses the mixin will be responsible for storing or calculating the required data in some way, and the mixin code will work by simply getting it via the interface. The only drawback to this approach is that the state data accessors become a public part of those classes, usable not only by the mixin but by other code as well.
There is another approach to the mixin state that is worth mentioning. The module containing the mixin code could have a private hash table (in REALbasic, a Dictionary object) that maps mixin objects to the corresponding state data. But this raises issues of notifying the module when an object is created or destroyed, so that the associated state data can be updated accordingly. I mention this approach for the sake of completeness; in most cases, deferring storage of the state data to the implementor classes is a better solution.
It is commonly thought that mixin classes are a technique particular to languages with multiple inheritance. You now know how to make the functional equivalent of a mixin in single-inheritance languages, and gain some clearer semantics in the process. In REALbasic, in particular, this can be done in a way that preserves the syntactic convenience of standard mixins, and provides flexibility in deciding the mixin interface.
Interface PropertyHolder Function PropCount() As Integer Function PropName(index As Integer) As String Function PropValue(index As Integer) As Variant Sub PropValue(index As Integer, assigns newValue As Variant) End InterfaceBack to article
Function PropIndex(extends obj As PropertyHolder, name As String) As Integer Dim i As Integer for i = 0 to obj.PropCount - 1 if obj.PropName(i) = name then return i next return -1 End Function Function PropValue(extends obj As PropertyHolder, name As String) As Variant Dim idx As Integer = obj.PropIndex( name ) if idx > 0 then return obj.PropValue( idx ) else return nil end if End Function Sub PropValue(extends obj As PropertyHolder, name As String, assigns newValue As Variant) Dim idx As Integer = obj.PropIndex( name ) if idx > 0 then obj.PropValue( idx ) = newValue else raise new KeyNotFoundException end if End SubBack to article
Function PropIndex(extends obj As PropertyHolder, name As String) As Integer if obj IsA FastPropHolder then return FastPropHolder(obj).PropIndex( name ) end if Dim i As Integer for i = 0 to obj.PropCount - 1 if obj.PropName(i) = name then return i next return -1 End FunctionBack to article