Subclassing Usage Guidelines

Hot off the presses.. I
just added this to the working draft fo the Design Guidelines document
internally. As always, comments welcome.

 

 

4.1.2 Subclassing
Usage Guidelines

One of the advantages of an object oriented
environment is that developers can extend and customize the system in ways we
did not expect. This is both the power and danger of extensible design.

           
Do minimize number and complexity of virtual
members in your APIs.  Virtual members defeat some performance
optimizations the runtime provides and could “box-in” your design.

           
Do not use virtual members unless you have
specifically designed for specialization:

           
You have a concrete extensibility scenario in
mind.

           
You have written a quality sample demonstrating the
extensibility scenario.  Preferably the sample should be done by a
“3rd party” to independently validate the design.

           
You have clearly documented contracts of all virtual
members.

           
Do think twice before you virtualize
members:

           
Modules that use references to base types must be
able to use references to derived types without knowing the difference. 
See Liskov’s
Substitution Principle

           
As you version your component, you must be sure that
the virtual members are called in the same order and frequency.

           
As you version virtual members you must not change
the range of the outputs or the inputs to the member.  For non-virtual
members widening inputs is allowed.

Rationale: Clients of the base class may
start calling the virtual member with increased input or can assume the output
is always true. But then all the subclasses need to be modified to support the
new contract.

 

Annotation  -
The peril. If you ship types with virtual members you are promising to forever
abide by subtle and complex observable behaviors and subclass interactions.

I think API designers underestimate their
peril. For example, we found that ArrayList item enumeration calls several
virtual methods per each MoveNext and Current. Fixing those performance problems
could (but probably doesn't) break user-defined implementations of virtual
members on the ArrayList class that are dependent upon virtual method call
order/frequency. 

 

Annotation  -
To understand the level of complexity that can be introduced by general purpose
virtual methods you need only remember the sort of pain that developers (both
creators and users) of the Win32 API feel with regard to generation and handling
of windows messages.  WM_WHATEVER is nothing more than custom virtual
dispatch mechanism with all of the usual pitfalls.  There are messages of
both imperative (commands) and informative (events) variety and there are many
levels of interaction which enrich the message stream.  The net of all this
is that even when considering relatively harmless looking messages there are
tough decisions to make such as "will I break the universe if I send (another)
WM_HITTEST before the WM_RESIZE when resizing a normal frame?"   I
assure it doesn't say anywhere that you get any WM_HITTEST messages at all, much
less exactly 1 and it certainly doesn't say where they come relative to
WM_RESIZE but chances are you'll break something if you were to change
it.   Or worse yet, "Is anyone originating their own WM_HITTEST to do
some exotic mouse tests in response to WM_RESIZE and if I change WM_HITTEST will
I now cause re-entrancy problems that didn't use to be there?"

 

FxCop Rule (draft): Flag non-sealed public
types that introduce virtual members with the message: “Virtual members
represent points of specialization in your type. Great care should be
taken in exposing virtual members, please see the design guidelines document on
this topic and fully consider the ramifications.”

 

           
Do make only the most complex overload
virtual to minimize the number of virtual calls needed to interact with your
class and reduce the risk of subclasses introducing subtle bugs.
public class Foo
{
private const string defaultForA = "a
default";
private const int defaultForB = 42;

public void Bar(){
Bar(defaultForA,
defaultForB);
}
public void Bar (string
a){
Bar(a, defaultForB);

}
public virtual void Bar (string a, int
b){
// core implementation
here
}
}

           
Consider designing classes with inheritance
scenarios in mind and appropriate virtual members. 
Doing this
correctly requires a careful specification outlining the exact contract of all
the virtual members.  It is strongly suggested that you create at least one
subclass ideally in the shipping product.   Although this is the
ideal, it is somewhat costly.

           
Do default to implementing unsealed classes
with no virtuals if you do not have the resources for extensibility testing and
design of the APIs as described above or if you do not see a need for
extensibility through overriding.  Even this plan requires minimal testing
to be sure inadvertent virtual members were not added.

           
Do default to implementing unsealed class
with no protected members if you have not designed the class for
extensibility. 
Rationale: Although the risks are lower than with
virtual members the lack of extensibility is clearer if there are no protected
members.

           
Do seal a class if:

           
It is a static class (TODO, add section
reference)

           
It is an attribute that requires very fast runtime
look up. (TODO, add section reference)

           
You don’t have the resources to test that
inadvertent virtual members were added.  Notice this cost is very small as
FXCop can flag virtual members. 

As an example consider
System.Drawing.Graphics class. This class is sealed and internally implemented
in unmanaged code such that developers can not extend its functionality in any
meaningful way. Every extensibility scenario we were aware of was to add
new functionality something that actually would not work because of their
implementation details. So if we had it unsealed we would have just
delayed the developer’s frustration. 

 

As another example consider the
System.WeakReference class. We have no defined scenarios for subclassing
this type but there are also no implementation details that cause subclassing to
be impossible. We therefore made sure there were no virtual members and
made the class unsealed. 

 

           
Do not declare protected or virtual members
on sealed types

By definition, sealed types cannot be inherited from.
This means that protected members on sealed types cannot be called, and virtual
methods on sealed types cannot be overridden.

           
Do fully document the contact for any virtual
members.  It is especially important to specify what the guarantees that are NOT given about
the order in which virtuals are called relative to each other, relative to the
raising of events, and relative to changes in observable state (including
properties).

For platform types you are required to
guarantee all observable behavior, while we allow libraries to specifically list
orderings in which some observable behavior is NOT guaranteed.
Rationale:
With Platforms types there is no testing when they are upgraded. So
customers who extend them without thoroughly understanding the contract (and you
can bet that's most of the extenders) will find their code broken when the
relative order changes even though it wasn't guaranteed.

It may
be useful to follow a standard template for all virtual member reference pages
as it is useful to structure your thinking about these contracts. As an
example consider this example from the Object.Equals() method from the ECMA\ISO
CLI standard.

The
following statements must be true for all implementations of the
System.Object.Equals method. In the list, x, y, and z represent object
references that are not null.


x.Equals(x) returns true, except in cases that involve floating-point types. See
IEC 60559:1989, Binary Floating-point Arithmetic for Microprocessor
Systems.


x.Equals(y) returns the same value as y.Equals(x).


x.Equals(y) returns true if both x and y are NaN.


(x.Equals(y) && y.Equals(z)) returns true if and only if x.Equals(z)
returns true.


Successive calls to x.Equals(y) return the same value as long as the objects
referenced by x and y are not modified.


x.Equals(null) returns false.

See
System.Object.GetHashCode for additional required behaviors pertaining to the
System.Object.Equals method.

Implementations
of System.Object.Equals must not throw exceptions.

For
some kinds of objects, it is desirable to have System.Object.Equals test for
value equality instead of referential equality. Such implementations of
System.Object.Equals return true if the two objects have the same "value", even
if they are not the same instance. The type's implementer decides what
constitutes an object's "value", but it is typically some or all the data stored
in the instance variables of the object. For example, the value of a
System.String is based on the characters of the string; the System.Object.Equals
method of the System.String class returns true for any two string instances that
contain exactly the same characters in the same
order.