3.8. A Hybrid Abstract Base Class

The solution-set data member is the same for each derived-class query type: an array of integers indicating the matching lines of text. In our initial design, our Query class defines an abstract property encapsulating the solution set:

abstract public class Query
{
    abstract public int [] Solution { get; }
    // ...
}

Each derived class, in turn, defines the actual solution-set instance data member and provides the actual implementation of get for the Solution property—for example,

public class NotQuery : Query
{
   private int [] solution_set;
   virtual public int [] Solution
   {
          get{ return solution_set; }
   }
   // ...
}

Consider the following code fragment, in which parseQuery() represents a utility that transforms a string representation of the user's query into a corresponding class representation. It returns an abstract Query node that addresses the actual derived-class query type:

Query theQuery = parseQuery( userQueryString );
theQuery.eval();
int [] solution = theQuery.Solution;

In this sequence, both eval() and Solution are invoked at runtime through the virtual function mechanism. eval() is clearly dependent on the actual query type; the virtual mechanism is the only viable solution. Is the declaration of Solution as abstract equally as necessary?

The implementation of Solution is the same for each of the derived query types. The type of the instance member for solution_set is also the same. An alternative class design, in this case, is to refactor the definitions of the shared instance member and property into the abstract Query base class:

abstract public class Query
{
    abstract public void eval();

    protected int [] solution_set;
    public    int [] Solution
              { get{ return solution_set; }}
    // ...
}

This refactoring does not break source compatibility. The invocation

int [] solution = theQuery.Solution;

correctly returns the solution_set associated with the derived query object returned from parseQuery(). The difference is that it is now resolved statically at compile time.

There are various design implications when we introduce an instance member into the base class. For example, we now need a constructor for the Query class, which is odd if you think about it: The class is still an abstract base class, and independent instances of the class cannot be created. We look at these issues and others in the following subsections.

3.8.1. The Single-Inheritance Object Model

A derived class inherits all the members of its base class. (And all the members of the base class of its base class, and so on. Remember that all classes implicitly derive from Object.) Physically, each derived-class object contains a base-class subobject that consists of all the instance members of the base class. For example, a NotQuery object contains a Query subobject, and a Query subobject contains an Object subobject. You can think of the different subobjects as Lego blocks stacked atop one another. At the base of each is the Object subobject.

Although it may seem confusing to think of each base class as a separate subobject within the derived-class object, doing so provides for an efficient model of single inheritance:

  • In terms of initialization, an associated base-class constructor is automatically applied to its subobject before the derived-class constructor is executed. When we create a NotQuery object, for example, first an associated Object constructor is invoked, then an associated Query constructor, and then the body of the NotQuery constructor. (In Section 3.9 we look at how to pass arguments to the immediate base-class constructor.)

  • In terms of conversion, because a derived-class object contains a full subobject instance of its base class, no runtime work is required to accomplish the conversion. What is lost in the conversion is knowledge of the nonvirtual derived-class interface—for example,

NotQuery nq = new NotQuery( ... );
Query oper = nq.Operand; // OK

// still a NotQuery object,
// but now only the Query interface can be invoked
Query q = nq;

// OK: eval() is part of the Query interface,
//     and it is virtual: it invokes NotQuery.eval()
q.eval();

// OK: Solution is part of the Query interface,
//     but it is nonvirtual: it invokes Query.Solution
int [] solution = q.Solution;

// error: Operand is part of the NotQuery interface;
//        it cannot be invoked through Query
Query op = q.Operand;

// still a NotQuery object,
// but now only the Object interface can be invoked
Object o = q;

// error: eval() is part of the Query interface;
//        it cannot be invoked through Object
o.eval();

Now I'm going to contradict myself—but with an explanation. I've claimed that a derived class can directly access the inherited members of its base class. That's only partially true: Whereas the derived class inherits all the members of its base class, the derived class cannot access the inherited private members of its base class. (In fact, C# does not allow a function to be declared both virtual and private.)

If a private base-class member cannot be accessed by the derived class, why does the language bother to have it be inherited, especially given that this inheritance takes up space in each derived-class object? The reason is to maintain the integrity of the base-class subobject. The problem is that although we cannot directly access a private member, we often indirectly access it through a base-class property, indexer, constructor, or method. It has to be there.

3.8.2. How Is a Hybrid Abstract Class Different?

Once we introduce one or more instance members into an abstract base class, we must also introduce support for user-directed initialization of those members. This means introducing one or more constructors—for example,

abstract public class Query
{
    protected Query( int [] aSolution ){ ...}

    protected int [] solution_set;
    public    int [] Solution
              { get{ return solution_set; }}
    // ...
}

Notice that the constructor is protected, not public. A Query object is intended only to serve as a subobject of one of the derived query types. If the constructor is declared protected, only the derived query types can create an instance of Query.

By refactoring the shared instance data within the base class, we eliminate the abstract nature of the supporting property. There are two primary benefits:

  1. The Solution property executes significantly faster. Rather than delaying until runtime the resolution of which Solution instance to invoke, the instance is resolved during compilation. A simple get accessor is almost certain to be expanded inline at each call point, eliminating the overhead of a function call altogether.

  2. The implementation of the derived-class instances is simplified. In the pure abstract design, each derived class must provide not only the unique algorithms of the type, but the shared infrastructure for storing and returning the type. In the hybrid design, the designer of the derived class inherits the infrastructure support. The design of each derived type has therefore been simplified.

Is the performance improvement significant? That depends on how often the method or property is invoked. Were Solution heavily invoked in a critical portion of the application, the design change would be significant. Otherwise it would not.

Similarly, the simplification of the derived-class implementation may or may not prove significant. If the designer of the abstract base class is also providing the derived-class query types, and if the set of derived classes is not expected to grow very often, this hybrid design change is not likely to be significant. If, however, a primary activity of the hierarchy is the delivery of new query class types, and if that activity has been delegated to individuals more comfortable with library science than with programming, the refactoring makes for a better design.

So what's the rule? There is no rule—just a design choice to be aware of. An abstract base class can legitimately define both instance data members and nonvirtual methods. This is referred to as implementation inheritance. For example, if you come from the Component Object Model (COM) programming model, this sort of implementation refactoring is not an option. Under C# and .NET, however, it is. Even Object provides a nonvirtual GetType() method.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.151.44