- 6.1 Extensibility Mechanisms
- 6.2 Base Classes
- 6.3 Sealing
- Summary
6.2 Base Classes
Strictly speaking, a class becomes a base class when another class is derived from it. For the purpose of this section, however, a base class is defined as a class designed mainly to provide a common abstraction or for other classes to reuse some default implementation though inheritance. Base classes usually sit in the middle of inheritance hierarchies, between an abstraction at the root of a hierarchy and several custom implementations at the bottom.
Base classes serve as implementation helpers for implementing abstractions. For example, one of the abstractions for ordered collections of items in .NET is the IList<T> interface. Implementing IList<T> is not trivial, so the framework provides several base classes, such as Collection<T> and KeyedCollection<TKey,TItem>, that serve as helpers for implementing custom collections.
public class OrderCollection : Collection<Order> { protected override void SetItem(int index, Order item) { if(item==null) throw new ArgumentNullException(...); base.SetItem(index,item); } }
Base classes are usually not suited to serve as abstractions by themselves because they tend to contain too much implementation. For example, the Collection<T> base class contains lots of implementation related to the fact that it implements the non-generic IList interface (to integrate better with non-generic collections) and to the fact that it is a collection of items stored in memory in one of its fields.
As previously discussed, base classes can provide invaluable help for users who need to implement abstractions, but at the same time they can be a significant liability. They add surface area and increase the depth of inheritance hierarchies, thereby conceptually complicating the framework. For this reason, base classes should be used only if they provide significant value to the users of the framework. They should be avoided if they provide value only to the implementers of the framework, in which case delegation to an internal implementation instead of inheritance from a base class should be strongly considered.
CONSIDER making base classes abstract even if they don’t contain any abstract members. This clearly communicates to the users that the class is designed solely to be inherited from.
CONSIDER placing base classes in a separate namespace from the mainline scenario types. By definition, base classes are intended for advanced extensibility scenarios and are not interesting to the majority of users. See section 2.2.4 for details.
AVOID naming base classes with a “Base” suffix if the class is intended for use in public APIs.
For example, despite the fact that Collection<T> is designed to be inherited from, many frameworks expose APIs typed as the base class, not as its subclasses, mainly because of the cost associated with a new public type.
public Directory { public Collection<string> GetFilenames(){ return new FilenameCollection(this); } private class FilenameCollection : Collection<string> { ... } }
The fact that Collection<T> is a base class is irrelevant for the user of the GetFilename method, so the “Base” suffix would simply create an unnecessary distraction for the user of the method.