Previous Table of Contents Next


5.4.3. Multiple Inheritance: Using Mix-ins to Get Modularity

This section shows how multiple inheritance using mix-in classes permits you to write clean, modular code. In general, multiple inheritance has a bad reputation, probably arising from its implementation in C++. In C++, if different classes inherit from the same superclass and a subclass inherits from both of the derived classes, then instances of the subclass contain two copies of the data members of the original superclass. This is not the case in CLOS, which supports a cleaner and more intuitive form of multiple inheritance.

Multiple inheritance is useful because it allows the new classes to be constructed by putting together orthogonal components. As an example, suppose you are modeling ice cream varieties in an ice cream store. You break up the types of ice cream into plain flavors, mix-ins that are added into any of the flavors, and sprinkles that get added on top of the ice cream.

The possible combinations have been broken down:

   (defclass edible ()

   (defclass ice-cream (edible))

   (defclass vanilla (ice-cream))
   (defclass chocolate (ice-cream))
   (defclass strawberry (ice-cream))

   (defclass basic-mix-in (edible))

   (defclass marshmallows (basic-mix-in))
   (defclass chocolate-chip (basic-mix-in))
   (defclass oreos (basic-mix-in))
   (defclass almonds (basic-mix-in))

   (defclass sprinkles-mix-in (edible))

   (defclass sugar-sprinkles (sprinkles-mix-in))
   (defclass chocolate-sprinkles (sprinkles-mix-in))

Ice cream is modeled as a series of variant flavors with two different types of mix-ins. Complex flavors can be easily defined by using multiple inheritance to combine the components:

   (defclass cookies-n-cream (vanilla oreos))

   (defclass rocky-road (chocolate marshmallows almonds))

To make cookies-n-cream, basic-mix-in might have a standard interface: do-mix-in. On the mix-in oreos, do-mix-in is done with a machine at the store:

   (defmethod do-mix-in ((m cookies-n-cream))
      (format nil “take vanilla ice-cream and mix-in in oreos”))

Clearly, it is possible to easily extend this structure, which has three orthogonal behavioral components, to include many more flavors, sprinkles, and basic mix-ins. Note that flavor is conveniently modeled as subclasses rather than as another mix-in because the ice cream must have a flavor but need not have any other mix-in types. However, it is easy to see that flavor could have been easily modeled as just another mix-in type.

Using single inheritance only, the model is different. A single inheritance implementation of cookies-n-cream might look like this:

       (defclass ice-cream ()
          ((basic-mix-in)
            (sprinkles-mix-in)))

       (defclass vanilla (ice-cream))

This is a delegation model, but it hides the basic structure of the problem. cookies-n-cream now no longer appears as a distinct entity. do-mix-in must be implemented as a function that takes an ice cream flavor as an argument and calls a second method based on the value in the basic-mix-in slot.

An alternative would be to subclass vanilla to get cookies-n-cream, but this won’t scale. This is even worse than the delegation model. Using multiple inheritance, the system represents objects as an orthogonal breakdown of several different types of mix-in classes. The code need only consider the sum total of all the basic mix-in types, and this alleviates the fact that the number of possibilities is actually the product of the number of distinct mix-in subclasses per mix-in type. Using single inheritance to achieve the same thing means that the organization into mix-in types is lost and the programmer is faced with a large number of classes without any obvious organizing principles.

5.4.4. Inheritance Rules: Precedence

In complex inheritance structures, there may be multiple possibilities from which to inherit either methods or slots.

Suppose you have a class structure as in the previous example:

       (defclass edible ()
          ((fat-content)
          (type :accessor type :initarg :type)))

       (defclass ice-cream (edible))

       (defclass basic-mix-in (edible))

       (defclass vanilla (ice-cream)
          ((fat-content ‘high)))

       (defclass oreos (basic-mix-in)
          ((fat-content ‘low)))

      (defclass cookies-n-cream (vanilla oreos))

In this case, you can see that cookies-n-cream inherits the fat-content slot from two different possibilities: vanilla and oreos. Both of these have fat-content slots that override the fat-content slot of edible. CLOS has a default rule that decides which slot cookies-n-cream uses. CLOS builds a precedence list: an ordering of the class and its superclasses by a convention known as most specific to least specific. This is a linearization of the inheritance tree class and its superclasses. CLOS makes a list of all superclasses to control inheritance: Slots appearing early in the list always shadow similarly named slots later in the list. For each slot in the class, CLOS chooses the first slot it finds in the precedence list to inherit from. In CLOS terminology, it uses the most specific class definition to inherit the slot from.

I demonstrate the rule that CLOS uses by using a diagram: The class structure for cookies-n-cream is a class hierarchy as follows:


Previous Table of Contents Next