Delving Deeper into Interface-Based Programming - Interfaces and Class Hierarchies
(Page 4 of 4 )
In component-oriented programming, you focus on defining and implementing interfaces. In object-oriented programming, you model your solution by using class hierarchies. How do the two concepts interact? The answer depends on the way you override or redefine the interface methods at the different levels of the class hierarchy. Consider the code in Example 3-9, which illustrates that when defining an interface only at the root of a class hierarchy, each level must override its base-class declarations to preserve semantics.
Example 3-9. Overriding an interface in a class hierarchy
using System.Diagnostics;//For the Trace class
public interface ITrace
{
void TraceSelf();
}
public class A : ITrace
{
public virtual void TraceSelf()
{
Trace.WriteLine("A");
}
}
public class B : A
{
public override void TraceSelf()
{
Trace.WriteLine("B");
}
}
public class C : B
{
public override void TraceSelf()
{
Trace.WriteLine("C");
}
}
In a typical class hierarchy, the topmost base class should derive from the interface, providing polymorphism with the interface to all subclasses. The topmost base class must also define all the interface members asvirtual, so that subclasses can override them. Each level of the class hierarchy can override its preceding level (using theoverride inheritance qualifier), as shown in Example 3-9. When the client uses the interface, it then gets the desired interpretation of the interface. For example, if the client code is:
C# Inheritance Directives
In C#, you are required to explicitly indicate the semantics of inheritance when you supply a method with a name and signature identical to those of a base-class method. If you wish to override the base-class method when instantiating a base-class type with a subclass reference, you need to use the override directive:
public class BaseClass
{
public virtual void TraceSelf()
{
Trace.WriteLine("BaseClass");
}
}
public class SubClass : BaseClass
{
public override void TraceSelf()
{
Trace.WriteLine("SubClass");
}
}
BaseClass obj = new SubClass();
obj.TraceSelf(); //Outputs "SubClass"
If you want to provide the base-class behavior instead, use thenewdirective, with or withoutvirtual, at the base class:
public class BaseClass
{
public virtual void TraceSelf()
{
Trace.WriteLine("BaseClass");
}
}
public class SubClass : BaseClass
{
public new void TraceSelf()
{
Trace.WriteLine("SubClass");
}
}
BaseClass obj = new SubClass();
obj.TraceSelf(); //Outputs "BaseClass "
ITrace obj = new B();
obj.TraceSelf();
the object traces “B” to the output window, as expected.
Things are less obvious if the subclasses use thenewinheritance qualifier. Thenewmodifier gives subclass behavior only when dealing with an explicit reference to a subclass, such as:
B obj = new B();
In all other cases, the base class implementation is used. If the code in Example 3-9 was written as:
public class A : ITrace
{
public virtual void TraceSelf()//virtual is optional
{
Trace.WriteLine("A");
}
}
public class B : A
{
public new void TraceSelf()
{
Trace.WriteLine("B");
}
}
public class C : B
{
public new void TraceSelf()
{
Trace.WriteLine("C");
}
}
then this client code:
ITrace obj = new B();
obj.TraceSelf();
would now trace “A” to the output window instead of “B.” Note that this is exactly why thenew inheritance modifier is available. Imagine a client that somehow depends on the base class’s particular implementation. If a new subclass is used instead of the base class, thenewmodifier ensures that the client will get the implementation it expects. However, this nuance makes sense only when you’re dealing with clients that don’t use interface-based programming but rather program directly against the objects:
A obj = new B();
obj.TraceSelf();//Traces "A"
You can support such clients and still provide interface-based services to the rest of the clients. To achieve that, each class in the hierarchy can reiterate its polymorphism with the interface by explicitly deriving from the interface (in addition to having the base class derive from the interface). Doing so (as shown in Example 3-10) makes thenewmodifier yield the same results as theoverridemodifier for the interface-based clients:
ITrace obj = new B();
obj.TraceSelf();//Traces "B"
Note that usingvirtualat the base-class level is optional.
In general, you should use theoverridemodifier, as in Example 3-9, with virtual interface members at the topmost base class. Such code is readable and straightforward. Code such as that in Example 3-10 makes for an interesting exercise but is rarely of practical use.
Example 3-10. Deriving from the interface explicitly at each level of the class hierarchy
using System.Diagnostics;//For the Trace class
public interface ITrace
{
void TraceSelf();
}
public class A : ITrace
{
public virtual void TraceSelf()//virtual is optional
{
Trace.WriteLine("A");
}
}
public class B : A,ITrace
{
public new void TraceSelf()
{
Trace.WriteLine("B");
}
}
public class C : B,ITrace
{
public new void TraceSelf()
{
Trace.WriteLine("C");
}
}
If you want to combine explicit interface implementation and class hierarchy, you should do so in a way that allows a subclass to call its base-class implementation. Because with explicit interface implementation, the implementation is private, you will need to add at the topmost base class a protected virtual method for each interface method. Only the topmost base class should explicitly implement the interface, and its implementation should call the protected virtual methods. All the subclasses should override the protected virtual methods:
public class A : ITrace
{
protected virtual void TraceSelf()
{
Trace.WriteLine("A");
}
void ITrace.TraceSelf()
{
TraceSelf();
}
}
public class B : A
{
protected override void TraceSelf()
{
Trace.WriteLine("B");
base.TraceSelf();
}
}
Please check back next week for the continuation of this article.
| DISCLAIMER: The content provided in this article is not warranted or guaranteed by Developer Shed, Inc. The content provided is intended for entertainment and/or educational purposes in order to introduce to the reader key ideas, concepts, and/or product reviews. As such it is incumbent upon the reader to employ real-world tactics for security and implementation of best practices. We are not liable for any negative consequences that may result from implementing any information covered in our articles or tutorials. If this is a hardware review, it is not recommended to open and/or modify your hardware. |
|
This article is excerpted from chapter three of Programming .NET Components, Second Edition, written by Juval Lowy (O'Reilly, 2006; ISBN: 0596007620). Check it out today at your favorite bookstore. Buy this book now.
|
|