Here are some notes from a presentation on Designing Inheritance Hierarchies by Brad Adams
The Danger of Over Design
Here is the most common mistake story. A new program manager is appointed who is given the task of producing a new widget feature. He then starts producing the most phenomenal feature. Make it super feature rich by doing a lot of competitor analysis making sure that every feature from every competitor is covered. Then they have this really complex extensibility story so that any partner can plug in and do anything. Then the development starts and we are half way through, then the reality of shipping sets in and you get a mail from one of the release managers saying if its not done it's cut out. So we have to decide on which features are actually going to survive and which are going to be cut. At this point we don't usually make a deep reflection over how can we redesign this thing to make it simpler so it's scoped to fit. Instead we will look at what has a long bug trail. So what happens is you end up shipping this thing with the wire still hanging outside of the box, with a design that is not quite complete because we have lopped of some things. So not only did it not fully meet the requirements but even worse in VNext when we actually know how we need to make this thing extensible ie concrete partner requirements well thought out design and a well thought out extensibility scenario we are actually stopped from doing it becuse the wires are still hanging out of the box. In other word we do not still have freedom of design.
Moral: Do the absolute minimum know so that you have the freedom in the future to add the more extensibility that's needed
The point is that we absolutely know that certain products will have a next version. So you can do the minimum now and add more to it in the future.
Abstract and Base Classes
- Base Classes serve as the roof of an inheritance hierarchy
- Abstract classes are a special kind of base class that are non-instantiable and may NOT contain members that are not implemented
- Prefer broad, shallow hierarchies
- Less than or equal to two additional levels = rough rule
- Contracts and responsibilities are difficult to maintain and explain in deep complex hierarchs.
Consider making base classes not constructible (eg use abstract classes)
- Make it clear what the class is for (is it a utility class OR is it a base in a hierarchy)
- Provide a protected constructor for subclasses to call
- A bad example is System.Exception should not have a public constructor
Virtual, abstract and Non-Virtual Methods
- Virtual members are points of specialization or callbacks in your code
public virtual int Length [ get {...}}
This is like saying this is code, I am not in love with it, you are free to override it with your own provided you meet the contract
- Abstract members are required points of specialization in your code
public abstract int Root()
This is like saying There is no seed code for this method, you must implement it yourself to the contract specified
- Non-virtual members cannot be overridden in derived types
public string Remove (int index, int count)
This is like saying, I AM in love with this code, when ever this is called I want to be sure that my implementation of the code is being used because I have some dependency on this.
There is not enough time to make something infinitely extensible so it is important to make sure that the extensibility hooks are in place. So by default you make it non virtual and then as you understand the extensibility requirements you can make those things virtual. It is not a breaking chnage to go from non-virtual to virtual. But it is a breaking change to go from virtual to non-virtual.
Virtual Methods An example of where it goes wrong
public class TheBase : Object
{
public override string ToString()
{
return "Hallo from base";
}
}
public class Derived : TheBase
{
public override string ToString()
{
return "Hallo from derived";
}
}
One problem above is that Object.ToString should give the type name. So what gets printed out in the following examples
Dervived d = new Derived()
Console.WriteLine(d.ToString());
TheBase tb = d;
Console.WriteLine(tb.ToString());
TheBase o = d;
Console.WriteLine(o.ToString());
The answer is...
Virtual methods all output "Hallo from Derived"... Why?
- Method call virtualizes at run time
- The static type doesn't matter
- "The most derived guy wins"
The danger am the power of virtual methods
- Danger: Owner of the base classes cannot control what subclasses do
- Power: Base class doesn't have to change as new subclasses are created
Overriding
- Don't change the semantics of member
- Follow the contract defined on the base class
- Don't require clients to have knowledge of overriding (ie no checking for types)
- Consider whether you should call the base implementation
- Choose to call it unless you have good reason not to
Virtual and Non-Virtual
- Use non-virtual members unless you have specifically designed for specialization
- Have a concrete scenario in mind
- Write the code!
So you should be able to answer the question "why is that member virtual?" The answer should be a useful example of how this should work on a concrete scenaro and that no more is required
- Think before you virtualize members
- Modules that use references to base types must be able to use references to derived types without knowing the difference
- Must continue to call in the same order and frequency
- Cannot increase or decrease range of inputs or outputs
- See the Liskov Substitution Principle
Abstract Members
- Methods, properties and events can be abstract
- Use abstract members only where it is absolutely required that subclasses provide a custom implementation
- Only use when the base class cannot have any meaningful default implementation
The thing that really bothers Brad is that there is an increased opportunity for bugs to be introduced
Abstract, Virtual and non-virtual members
- Default to making non-virtual
- Make virtual if it is designed to be specialized by subclasses
- Make abstract if no meaningful default implementation is possible
- Unless versioning issues prohibit it, in which case throw NotImplementatedException
Interfaces vs Base Classes
Choose to use Base classes over interfaces
- Bases classes version better in general
- Allows for adding members
- Members can be added with a default implementation
- Avoid incompatibilities common in MS ActiveX (Interface based)
- Interfaces are good for versioning behavior (changing semantics)
- Avoid having both base class and interfaces
- Adds confusion about which to use (so do decide)
- Component vs. IComponent
- Little advantage
- Consider using aggregation
- Don't use attributes where a contract is needed
Aggregation
- Some of the needs for multiple inheritance can be solved with aggregation
- Instead of having C derive from A and B, have C derive from A and have member that returns an instance of B
// WRONG
public class C : A, B {}
// RIGHT
public class C:A
{
public B MyB { get{...}}
}
Versioning of interfaces
Version1
interface IStream // version1
{
void Read (byte[] value);
}
class FileStream: IStream
{
public void Read (byte[] value){...}
}
Version2
interface IStream // version1
{
void Read (byte[] value);
void Write (byte [] value); // WRONG TypeLoad Exception in FileStream
}
In the java world this became such a problem that they took this check out of the compiler
Versioning of Base Classes
Version1
public class Stream
{
public virtual void Read(Byte[] value {...}
}
public class FileStream : Stream{
public override virtual void Read(Byte[] value {...}
}
Version2
public class Stream
{
public virtual void Read(Byte[] value {...}
public virtual void Write(Byte[] value {...}
}
FileStream continues to work with default implantation for Write()
Interface Usage
public interface IComparable
{
int CompareTo(object obj):
}
- Interfaces are useful
- The smaller and more focused the interface the better
- 1-2 members are best
- But interfaces can be defined in terms of other simpler interfaces
- Eg IComparable, IFormattable
Explicit Method Implementation
- Implementing members of an interface "privately"
- Not a security boundary (because you can cast that type into it's base type)
- Only accessible when cast to the interface type
- Hides implementation details
- Clean public interface
- IConvertable on Int32 etc
- Differentiates implementations
- Simpler strong typing
Explicit Method Implementation: Clean Public Interface
- The 19 ToXxxx methods on IConvertable don't "pollute" the Int32 public view
- But they are there when the case to IConvertable
Solution is to implement them privately
public struct Int32 : IConvertable, IComparable
{
public override string ToString () {...}
int IConvertable.ToInt () {...}
...
}
int i = 42;
i.Tostring(); // Works
i.ToInt32(); // Does not compile
((Iconvertable), i).ToInt32(); // Works
Explicit Method Implementation: Differentiates Implementations
- Interface developed by different groups can have the same signature for different meanings
- Draw() a picture and Draw() a gun (wild different implementations)
- Frequently you want to differentiate the implementation
- Explicit method implementations enables this
- Avoids us recommending "unique" names in interfaces
interface IGraphics
{
void Draw();
Brush Brush {get; set;}
Pen Pen {get; set;}
}
interface IBandit
{
void Draw();
void Duel(IBandit oponnent);
void Rob(Bank bank, IBandit[] sidekicks);
}
class Bandits: IBandit, IGraphics
{
void IBandit.Draw() {...}
void IGraphics.Draw() {...}
}