JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

Chapter 27 "Simulated Multiple Inheritance Using Plug Ins"

In this chapter you will learn two new methods that mimic MI (Multiple Inheritance) of implementation that allow you to "plug in" a new concrete implementation at compile time. In the first solution, you will use what I am going to call containment by newed reference. In the second solution, you will learn to use a type based constructor. Or if you are using .NET 2.0, you can use type based generics to simulate MI.

Delegation

In the classic technique of delegation, you use containment by ownership to encapsulate one or more concrete classes in a wrapper class, mimicking multiple inheritance. You then forward (delegate) method calls in the wrapper class to the appropriate concrete classes, mimicking multiple inheritance of implementation. This solution is not very extensible however, since the user of the wrapper class cannot "plug in" new concrete classes with a different behavior at compile time. The obvious solution is to use containment by reference so that the caller can pass a new concrete class to the constructor at compile time. Unfortunately this breaks the encapsulation since it does not guarantee that an object outside of the wrapper class does not hold a reference to the plug in class.

PIMPL

The PIMPL or private implementation idiom is designed to limit compilation times in C++ when a broadly used class is revised. The PIMPL idiom is a form of the Bridge pattern that that separates the abstraction from the implementation.

In a nutshell, in the PIMPL idiom, you wrap a pointer to the concrete class so that the actual implementation details are hidden from the wrapper class. The wrapper class has no knowledge of the implementation details. You can use this idiom to simulate multiple inheritance by passing one or more pointers to one or more concrete classes "newed" in the initialization list of the constructor of the wrapper class. You can then forward calls in the wrapper class to the appropriate contained objects. 

String::String(): pimpl (new String::StringImpl) {}
String::~String() {delete pimpl; }

Using Containment By Newed Reference

The first solution is to use an idiom that I am going to call containment by newed reference. In this idiom, you first declare a constructor that takes references to the concrete classes. 

public Program(I1 pimplI1, I2 pimplI2)
{    
     if (pimplI1 == null || pimplI2 == null)
     {
        throw new NullReferenceException();
     }
     this.pimplI1 = pimplI1;
     this.pimplI2 = pimplI2;
}

You then pass implicit references to the concrete classes to the constructor using the new keyword as in:

Program p = new Program(new Implementation1(), new Implementation2());

This is a weak guarantee when compared to the use of containment by ownership since is dependent upon the caller using the newed reference idiom.  

If you change the implementation details of the concrete class, there is no need to change the wrapper class. This idiom allows the user of the class to add new behavior at compile time by passing a different concrete class to the wrapper class constructor. As long as the concrete class implements the proper interface, the pass through methods will remain valid.

A Simple Example

The first step in using PIMPL is to create the abstractions using an interface:


    interface I1
    {
        void SayHello();
    }
    interface I2
    {
        int GetValue();
    }
    interface IProgram : I1, I2 { }

The second step is to write concrete classes that implement the interfaces:


    class Implementation1 : I1
    {
        public void SayHello()
        {
            Console.WriteLine("Hello.");
        }
    }
    class Implementation2 : I2
    {
        private int i = 1;
        public int GetValue()
        {
            return i;
        }
    }

You can then write a wrapper class that contains "pointers" to the concrete classes. Inside the wrapper class, you can forward calls in pass through methods:


    class Program :IProgram
        private I1 pimplI1 = null;
        private I2 pimplI2 = null;
        // forward calls to the contained object
        public void SayHello()
        {
            pimplI1.SayHello();
        }
        public int GetValue()
        {
            return pimplI2.GetValue();
        }
        // we pass "pointers" to the concrete classes
        // in the wrapper class constructor
        // ASSERT pimpleI1 and pimplI2 not null
	// ASSERT no other object holds a reference to
	// pimpleI1 or pimpleI2
	// USAGE Program p= new Program(new Impl1(),new Impl2())
        public Program(I1 pimplI1, I2 pimplI2)
        {
            if (pimplI1 == null || pimplI2 == null)
            {
                throw new NullReferenceException();
            }
            this.pimplI1 = pimplI1;
            this.pimplI2 = pimplI2;
        }
        // or we can provide a no arg constructor that for all intents and 
        // purposes acts as containment by ownership
        public Program() : this(new Implementation1(), new Implementation2()) { ;}
    }
}

You can then create an instance of the wrapper class and call methods that are forwarded to the contained objects. You can do this by simply calling the default no arg constructor or by passing concrete classes at compile time to the arg constructor.

        static void Main(string[] args)
        {
            Program p = new Program(new Implementation1(), new Implementation2());
            p.SayHello();
            Console.WriteLine(p.GetValue());
            Console.ReadLine();
        }

If you create an explicit reference and then pass the reference to the constructor there is a chance that the caller might hold a reference to the object, breaking the encapsulation. The key point is to use new in the constructor so that an implicit reference is passed to the constructor. Remember, you cannot pass an object as a parameter, only a reference to an object. Using new in the constructor allows you to create an object using parameters.

Pretty cool! Unlike true inheritance, even if the writer of the concrete classes add new methods to their classes, these new methods, which could break the bridge class, will _not_ be visible in the bridge class. Only the methods with pass through calls will be visible in the bridge class. This is an important point that is discussed in 17, "Designing Extendable Classes."

Using Type Based Construction

As I said earlier, the downside of simulating multiple inheritance with the containment by reference is:

  1. The extra level of indirection and coding needed to write the pass through methods.
  2. The loss of encapsulation since this solution uses containment by implicit reference as opposed to containment by ownership (composition).

The second problem can, I think, be addressed by providing a type based constructor or a type based class factory that provides the proper encapsulation.

Here is my twisted code that uses a type based constructor and type based factory method to return a valid instance of the bridge class. It guarantees that no object outside of the class holds a reference to the private implementation at construction. This twisted approach appears to combine the dynamic advantages of containment by reference with the encapsulation provided by containment by ownership. This is the strong guarantee.

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
namespace PIMPL
{
    // first we create the abstractions
    interface I1
    {
        void SayHello();
    }
    interface I2
    {
        int GetValue();
    }
    interface IProgram : I1, I2 { }
    // second we write the concrete classes that implement the abstractions
    class Implementation1 : I1
    {
        public void SayHello()
        {
            Console.WriteLine("Hello.");
        }
    }
    class Implementation2 : I2
    {
        private int i = 1;
        public int GetValue()
        {
            return i;
        }
    }
    // finally we write the wrapper class that contains "pointers"
    // to the concrete classes
    class Program :IProgram
    {
        private I1 pimplI1 = null;
        private I2 pimplI2 = null;
        // we forward calls to the contained object
        public void SayHello()
        {
            pimplI1.SayHello();
        }
        public int GetValue()
        {
            return pimplI2.GetValue();
        }
        // we pass "pointers" to the concrete classes
        // in the wrapper class constructor
        // ASSERT i1 and i1 not null
        // we make this private so that it cannot be instantiate
        // except from the static factory method GetInstance and
        // the no arg constructor
        private Program(I1 pimplI1, I2 pimplI2)
        {
            if (pimplI1 == null || pimplI2 == null)
            {
                throw new NullReferenceException();
            }
            this.pimplI1 = pimplI1;
            this.pimplI2 = pimplI2;
        }
        // we can provide a public no arg constructor that for all intents and 
        // purposes acts as containment by ownership
        public Program() : this(new Implementation1(), new Implementation2()) { ;}
        // we can provide a public type based constructor
        // ASSERT t1 implements I1 and t2 impelements I2
        public Program(Type t1, Type t2)
        {
            if ((typeof(I1).IsAssignableFrom(t1) && !t1.IsAbstract)
                && (typeof(I2).IsAssignableFrom(t2) && !t2.IsAbstract))
            {
                I1 pimplI1 = (I1)Activator.CreateInstance(t1);
                I2 pimplI2 = (I2)Activator.CreateInstance(t2);
                if (pimplI1 == null || pimplI2 == null)
                {
                    throw new NullReferenceException();
                }
                this.pimplI1 = pimplI1;
                this.pimplI2 = pimplI2;
            }
            else { throw new ArgumentException(); }
        }
        // we can provide a static factory method to guarantee
        // that no object outside of the instance has a reference
        // to the private implementations at construction
        // return null if t1 does not implement I1 or t2 does not implement I2
        public static Program GetInstance(Type t1, Type t2) {
            if ((typeof(I1).IsAssignableFrom(t1) && !t1.IsAbstract) 
                && (typeof(I2).IsAssignableFrom(t2) && !t2.IsAbstract) ){
                    I1 pimplI1= (I1)Activator.CreateInstance(t1);
                    I2 pimplI2= (I2)Activator.CreateInstance(t2);
                    return new Program(pimplI1,pimplI2);
            }
            else {return null;}
        }
        // we create the object here
        static void Main(string[] args)
        {
            Program p = Program.GetInstance(typeof(Implementation1), 
			typeof(Implementation2));
            if (p != null)
            {
                p.SayHello();
                Console.WriteLine(p.GetValue());
            }

            Console.ReadLine();
        }
    }
}

You can obtain an instance of the class using three idioms: 

You can just call the no arg constructor:

Program p1 = new Program();
p1.SayHello();
Console.WriteLine(p1.GetValue());

You can call the type based constructor:

Program p2 = new Program(typeof(Implementation1), 
	typeof(Implementation2));
p2.SayHello();
Console.WriteLine(p2.GetValue());

Finally you can call the type based class factory:

Program p = Program.GetInstance(typeof(Implementation1), typeof(Implementation2));
if (p != null)
{
      p.SayHello();
      Console.WriteLine(p.GetValue());
 }

Unlike the newed reference idiom, you cannot easily create an object with parameters using type based construction.

A Generic Solution

Finally, in .NET 2.0 it is possible to use generics to simulate type based MI. Here is the sample code:

namespace GenericMI
{
    // first we create the abstractions
    public interface I1
    {
        void SayHello();
    }
    public interface I2
    {
        int GetValue();
    }
    public interface IProgram : I1, I2 { }
    // second we write the concrete classes that implement the abstractions
    public class Implementation1 : I1
    {
        public void SayHello()
        {
            Console.WriteLine("Hello.");
        }
    }
    public class Implementation2 : I2
    {
        private int i = 1;
        public int GetValue()
        {
            return i;
        }
    }
    // finally we write the generic class that contains
    // the concrete classes
    public class GenericMI<T1, T2> : IProgram where T1 : class, I1, new()
        where T2: class,I2, new()
    {
        private T1 pimplI1= new T1();
        private T2 pimplI2 = new T2();
        // we forward calls to the contained object
        public void SayHello()
        {
            pimplI1.SayHello();
        }
        public int GetValue()
        {
            return pimplI2.GetValue();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            GenericMI<Implementation1,Implementation2> mi=
             new GenericMI<Implementation1,Implementation2>();
            mi.SayHello();
            Console.WriteLine(mi.GetValue());
            Console.ReadLine();
        }
    }
}

Conclusion

In this chapter I propose an approach to simulating multiple inheritance of implementation that provides the extensibility of containment by reference with the encapsulation of containment by ownership. Unlike the traditional solution using containment by ownership, I coded a solution that uses containment by a newed reference (weak encapsulation guarantee) or type based construction (strong encapsulation guarantee). This allows the user of the class to provide a new behavior at compile time by "plugging in" a new concrete class to the class constructor.

Unlike true MI (Multiple Inheritance) of implementation (or Eiffel's compile time MI), this solution shields the wrapper class from exposing any new methods added to a base class at a latter date. In other words, it seems to meet the requirements of Joshua Bloch's "favor composition over  inheritance" while providing the extensibility of multiple inheritance of implementation.

If this discussion has blurred the distinction between containment by reference and containment by ownership, my apologies. In a garbage collected environment the classic concepts of containment by ownership and containment by reference are less distinct.

Under the strong guarantee, when the bridge object is eligible for garbage collection the concrete implementation objects should also be eligible for garbage collection. In this scenario, the lifetime of the concrete implementations are dependent on the lifetime of the bridge object, which is the effective definition of containment by ownership in a garbage collected environment. Combining containment by reference with type based construction provides the extensibility of inheritance with the encapsulation usually associated with containment by ownership. 

Finally, for user of .NET 2.0 generics offers an elegant type based solution that lets you create a wrapper class as:

GenericMI<Implementation1,Implementation2> mi=
             new GenericMI<Implementation1,Implementation2>();

Wow. Now _I_ have a headache. I think I am going to take a nap :)

Note: Of course, there is nothing stopping you from inheriting from one concrete class, using the newed reference idiom for any additional concrete classes.

class Program2 : Implementation1, IProgram
{
        private I2 pimplI2 = null;
        public int GetValue()
        {
            return pimplI2.GetValue();
        }
        // ASSERT pimplI2 is not null
        public Program2(I2 pimplI2)
        {
            if (pimplI2 == null)
            {
                throw new NullReferenceException();
            }
            this.pimplI2 = pimplI2;
        }
	static void Main(string[] args)
        {
            Program2 p = new Program2(new Implementation2());
            p.SayHello();
            Console.WriteLine(p.GetValue());
            Console.ReadLine();
        }
}
 
Send mail to [email protected] with questions or comments about this web site. Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 © 
Last modified: 08/04/09
Hosted by www.Geocities.ws

1