Effective Class Design
1. Type Design
1.1. Nested Types
– When logical grouping and name-collision avoidance are your only goals, namespace should be used.
– Nested types should be used, the notion of member accessibility semantics is a desirable part of you design.
1.2. Class Design
– Do favor using classes over interfaces; Unlike other type systems (e.g., COM) uses interfaces heavily, in the managed world, classes are the most commonly used constructs.
– Best Practice: Always define at least one constructor (and make it private if you don’t want the class creatable).
1.3. Base Class Design
– Recommended to use base class:
– Base class is more versionable; With a class, you can ship V1, and then in V2 decide to add another method.
As long as the method is not abstract (as long as a default implementation is provided), any existing derived classes continue to function unchanged.
Adding a method to an interface is like adding an abstract method to a base class, any class that implements the interface will break.
– Interface is more appropriate in the following scenarios:
– Several unrelated classes want to support the protocol.
– These class already have a base class (no multiple implementation inheritance, but multiple interface inheritance).
– Aggregation is not appropriate or practical.
– For instance, make IByteStream an interface, so a class implement multiple stream types (multiple interface inheritance).
Make ValueEditor as abstract class because dervied class from ValueEditor have no other purpose than to edit value (single implementation inheritance).
– Recommended to provide customization through protected virtual methods:
Customizers of a class often want to implement the fewest methods possible to provide a rich set of functionality to consumers.
To meet this goal, provide a set of non-virtual or final public methods that call through a very small set of protected virtual "Core" methods.
public Control{
public void SetBounds(int x, int y, int width, int height) {
…
SetBoundsCore(…);
}
public void SetBounds(int x, int y, int width, int height, BoundsSpecified specified) {
…
SetBoundsCore(…);
}
protected virtual void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) {
// Do the real work here.
}
}
– Do define a protected constructor on all abstract classes; Many compilers will insert a protected constructor if you do not.
– Avoid prefixing or postfixing "Base", or "Root" as a type is likely to show up in public APIs. Exceptional e.g. ButtonBase class.
– Do prefer using base types in public signatures rather than concrete types to provide better opportunity of substitution and more flexibility in changing the implementation. e.g., public Stream GetDataStream() is prefered to public FileStream GetDataStream().
– Do not change inherited public members to private. This does not prevent callers from accessing the base class implemenation.
public class ABaseType {
public void BasePublicMethod() {
Console.WriteLine("Called base implementation.");
}
}
public void ADerivedType {
private new void BasePublicMethod() {
Console.WriteLine("Called derived implementation.");
}
}
1.4. Designing for Subclassing
– Do minimize the number and complexity of virtual methods in your APIs. Virtual members defeat some performance optimizations the CLR provides and could "box-in" your design.
– Do think twice before you virtualize members:
– As you version your component (like template methods), you must be sure that the virtual members are called in the same order and frequency.
– GoF: A template method defines an algorithm in terms of abstract operations that subclasses override to provide concrete behavior.
– As you version your virtual members, you must not widen or narrow the inputs and outputs to the members.
– e.g., ArrayList.Item property calls several virtual methods per each MoveNext and Current. Fixing these performance problems could break template methods that are dependent upon virtual method call order and frequency.
– 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 introducing subtle bugs in subclasses.
– Consider sealing virtual members you override from your base class. There are potential bugs with virtual members you introduce in your class as well as virtual members you override.
– Do fully document the contract for any virtual members. It is especially important to specify what guarantees are NOT given about the order in which virtuals are called relative to each other, relative to raising of events, and relative to changes in observable states.
1.5. Interface Design
– Recommended to provide a default implementation of an interface where it is appropriate. E.g., System.Collections.DictionaryBase
class is the default implementation of System.Collections.IDictionary interface.
– Use a marker interface to mark a class and verify the presence of the marker at compile time.
public interface ITextSerializable { } // empty interface
public void Serialize(ITextSerializable item) {
// use reflection to serialize all public properties
…
}
– Use a custom attribute to mark a class (as having a specific characteristic) and verify the presence of the marker at runtime.
[immutable]
public class Key {
…
}
// methods can reject paramters that are not marked with a specific marker at runtime:
public void Add(Key key object value) {
if (!key.GetType().IsDefined(typeof(ImmutableAttribute))) {
throw new ArgumentException("The paramter must be immutable", "key");
}
}
– Consider implementing interface members privately (or explicitly), if the members are intended to be called only through the interface.
– Consider implementing interface members privately (or explicitly) to simulate covariance (to change the type of parameters or return values in "overriden" members).
// Covariance creates strong typed collections.
public class StringCollection : IList {
public String this[int index] { … }
string IList.this[int index] { …}
}
– Do provide protected method that offers the same functionality as the privately implemented mehtod if the functionality is meant to be specialized by dervied classes. Otherwise, it is impossible for inheritors of the type to call the base method(???).
public class List<T> : ISerializable {
void ISerializable.GetObjectData(
SerializationInfo info, StreamingContext context) {
GetObjectData();
}
protected virtual void GetObjectData(
SerializationInfo info, StreamingContext context) {
GetObjectData();
}
1.6. Value Type Design
– Do not define methods on structs that mutate the struct’s state.
NOTE: The pattern for implementing IEnumerable which often involves using a mutable struct (MoveNext() method modifies an internal counter). Although technically this pattern violates this guideline, it is acceptable because the struct is soley for foreach statement, not for users.
1.7. Enum Design
– Do use an Enum to strongly type parameters, properties, and return values.
pulbic enum TypeCode {
Boolean,
Byte,
Char,
DateTiem
…
}
– Do use the System.FlagsAttribute custom attribute for an enum only if a bitwise OR operation is to be performed on the numeric values. Do use powers of two for the enum values so they can easily be combined.
[Flags()]
public enum WatcherChangeTypes {
Created = 1,
Deleted = 2,
Changed = 4,
Renamed = 8,
All = Created | Deleted | Changed | Renamed
}
– Consider providing named constants for commonly used combinations of flags.
[Flags()]
public enum FileAccess {
Read = 1,
Write = 2,
ReadWrite = Read | Write
}
– Do not use flag enum values that are zero or negative.
[Flags]
public enum SomeFlag
{
ValueA = 0, // bad design, don’t use 0 in flags
ValueB = 1,
ValueC = 2,
ValueBAndC = ValueB | ValueC;
}
SomeFlag flags = GetFlags();
if (flags & SomeFlag.ValueA) // this will never evaluate to true
{
}
– Do argument validation; Do not assume enum arguments will be in the defined range. It is legal to cast any integer value into an enum even if the value is not defined in the enum.
public void PickColor(Color color) {
switch (color) {
case Red: …
case Blue: …
case Green: …
default:
throw new ArgumentOutOfRangeException();
}
// do work
}
NOTE: Do not use Enum.IsDefined() for these checks as it is based on the runtime type of the enum, so thus deceptively expensive and changible out of sync with the code.
public void PickColor(Color color) {
if (!Enum.IsDefined(typeof(Color), color) { // this check is expensive and breakable by versioning
throw new InvalidEnumArgumentException("color", (int)color, typeof(Color));
}
// Security issue: never pass a negative color value
NativeMethods.SetImageColor(color, byte[] image);
}
2. Member Design
2.1. Property Design
– Do allow properties to be set in any order; The semantics are the same regardless of the order in which the developer sets the property values or how the developer gets the object into the active state; Properties should be stateless with respect to other properties.
– Recommended: Components raise <Property>Changed events and there is a protected helper routine On<Property>Changed, which is used to raise the event. Data binding uses this pattern to allow two-way binding of the property.
– Each property that raises <Property>Changed event should provide metadata to indicate that the property supports two-way binding.
– Recommended: Components raise <Property>Changing events when the value of a property change via external forces and allow developers to cancel the change by throwing an exception.
class TextBox{
public event TextChangedEventHandler TextChanged;
public event TextChangingEventHandler TextChanging;
public string Text {
get { return text; }
set {
if (text != value) {
OnTextChanging(Event.Empty);
text = value;
OnTextChanged(…);
}
}
}
protected virtual void OnTextChanged(…) { TextChanged(this, …); }
protected virtual void OnTextChanging(…) { TextChanging(this, …); }
}
– Avoid throwing exceptions from property getters; If a getter may throw an exception, it should be redesigned to be a method. TIP: One expects exceptions from indexers as a result of validating the index (index bound, type mismatch, etc).
2.2. Properties vs. Methods
– Prefer methods to properties in the following situations:
– The operation is a conversion.
– The operation is expensive, e.g. planning to provide an async version of this operation not to be blocked.
– The operation is not local, e.g. having an observable side effect; different results in succession of calling.
– The member is static but it returns a mutable value.
– The member returns an array, IOW, avoid using array valued properties.
– Instead return a readonly collection (ArrayList.ReadOnly() method to get a readonly IList wrapper).
Type type = // get a type window
for (int i = 0; i < type.Methods.Length; i++)
{
if (type.Methods[i].Name.Equals("foo"))
{
…
}
}
* Common Design Considerations
– Have fewer properties and "complex" methods with a large number of parameters to maintain "atomicy" of the actions—Less Customizable.
public class PersonFinder {
public string FindPersonsName (int height, int weight, string hairColor, string eyeColor, int sheoSize, int whichDatabase)
{
// perform operation using the parameters
…
}
}
– Have more properties to set "configuration" (a set of states individually set and hold) and "simple" methods with a small number of parameters.
public class PersonFinder {
public int Height { get {…} set {…} }
public int Weight { get {…} set {…} }
public string HairColor { get {…} set {…} }
public string EyeColor { get {…} set {…} }
public int ShoeSize { get {…} set {…} }
public string FindPersonsName (int whichDatabase)
{
// use the perperties already set to perform operation
}
}
2.3. Read-Only and Write-Only Properties
– Do use read-only properties when a user cannot change the logical backing data field.
– Do not use write-only properties; Implement the functionality as a method instead if the property getter cannot be provided.
2.4. Indexed Property (Indexer) Design
– Do use only Int32, Int64, String, and Object for the indexer; If the design requires others for the indexer, strongly re-evaluate if it really represents a logical backing store. If not, use a method.
– Consider using only one index; If the design requires multiple indices reconsider if it really represents a logical backing store. If not, use a method.
– Do use the name Item for indexers unless there is an obiviously better name (e.g. Chars property on String).
2.5. Event Design
– Do use the "raise" terminology for events rather than "fire" or "trigger" terminology. When referring to events in documentation, use the phrase "an event was raised" instead of "an event was fired" or "an event was triggered".
– Do not create a special event handler when not using a strongly typed EventArgs subclass; Simply use the EventHandler class.
– Do use a protected virtual On<Event> method to raise each event in order to provide a way for a derived class to handle the event using an override.
– Do assume that anything can go wrong in an event handler and do not assume that anything about the object state after control returns to the point where the event was raised.
public class Button {
ClickedHandler onClickedHandler;
protected void DoClick() {
PaintDown(); // paint button in depressed state
try {
OnClicked(args); // call event handler
}
finally {
// window may be deleted in event handler
if (windowHandle != null) {
PaintUp(); // paint button in normal state
}
}
}
protected virtual void OnClicked(ClickedEvent e) {
if (onClickedHandler != null)
onClickedHandler(this, e);
}
}
– Do raise cancelable events to allow the caller to cancel events w/o generating an error:
public class MainForm : Form {
TreeView tree = new TreeView();
void LabelEditing(Object source, NodeLabelEditEventArgs e) {
e.Cancel = true;
}
}
– Do raise an exception to cancel an operation and return an exception.
public class MainForm : Form {
TreeView tree = new TreeView();
void TextChanging(Object source, EventArgs e) {
throw new InvalidOperationException("Edit not allowed in this mode");
}
}
2.6. Method Design
– Avoid methods that cannot be called on an object instantiated with a simple constructor because the method requires further initialization of the object. If this is not unavoidable, throw an InvalidOperationException exception clearly stating in the message. e.g., throw new InvalidOperationException("Connection must be valid and open");
– Do use default values correctly in a family of overloaded methods; The complex method should use parameter names that indicate changes from the default states assumed in the simple method.
MethodInfo Type.GetMethod(String name); // case sensitive is assumed.
MethodInfo Type.GetMethod(String name, Boolean ignoreCase); // a change from case sensitive to case insensitive.
– Avoid method families with the simplest overload having more than 3 parameters; Do not have constructor families with the simplest overload having more than 5 parameters.
– If extensibility is needed, do make only the most complete overload virtual and define the other overloads in terms of it.
2.8. Variable Number of Arguments
– Consider using Params if a simple overload could use Params but a complex overload could not and ask yourself "would users value the utility of having params on this overload even if it wouldn’t be on all overloads".
– Do order parameters so that it’s possible to apply "params" to the methods:
e.g., Find (String[], IComparer) would have been written as Find (IComparer, String[]).
– Do not use Params when the array can be modified by the method since the array is temporary and any modifications will be lost.
– Do provide special code paths for a small number of elements in order for performance sensitive code to special case the entire code path. The following pattern is recommended:
void Format (string formatString, object arg1)
void Format (string formatString, object arg1, object arg2)
…
void Format (string formatString, params object[] args)
– CLS requirement: All types that overload operators include alternative methods with appropriate domain-specific names that provide equivalent functionality.
public struct DateTime {
public static TimeSpan operator- (DateTime t1, DateTime t2) { }
public static TimeSpan Substract (DateTime t1, DateTIme t2) { }
}
2.10. Implementing Equals and Operator==
2.11. Conversion Operators
– Do not throw exceptions from an implicit cast because it is very difficult for the users to understand what is happening.
– Do case values that are in the same domain; Do not cast values from differnt domains:
e.g., it’s appropriate to convert a Time or TimeSpan into an Int32 where the Int32 still represents the time or duration.
It does not make sense to convert a file name string such as, "c:\mybitmap.gif", into a Bitmap object.
2.12. Constructor Design
– Do have only a default private constructor if there are only static methods and properties on a class.
– Do make static constructors private (called a class constructor or a type initializer).
– Do minimal work in constructors; Constructors should not do much work other than to capture the constructor parameters.
The cost of the other work is delayed until the user uses a particular feature of the instance.
– Do not call a virtual method in a constructor if the virtual method is in the constructor’s type.
When a constructor calls a virtual method, there is a chance that the constructor for the instance of the virtual method hasn’t executed.
Annotation: In C++, the vtable is updated during the construction, so that a call to a virtual function during construction only calls to the level of the object hierarchy that has been constructed. For the CLR, the decision is ultimately based on the desire to support extremely fast object creation.
– Do throw exceptions from constructors if appropriate. When an exception propagates out of a constructor, the object is created but the operator new does not return the reference. The finalizer of the object will run when the object becomes eligible for collection.
– Do use a private constructor, if the type is not meant to be creatable.
– The best practice is to always explicitly specify the constructor even if it is a public default constructor.
– Do provide a protected constructor that can be used in derived types.
– Recommended: You do not provide a default (parameterless) constructor for a struct (a value type).
Note: Many compilers don’t allow structs to have default parameterless constructors for this reason.
With no constructors, the CLR initializes all the fields of the struct to zero; This is for faster array and static field creation.
* Constructor vs. Static Factory Methods
– Do use static factory if:
– If the parameters to a constructor do not completely describe the object being created, a method name is required to provide more context or more of the semantics of the member.
– If you need to control the instantiation (e.g., in a singleton class), a static factory may return a cached instance to limit the expensive instance creation.
– If you need to return a subclass of the return type, it is often handy to return a subclass to hide implementation details.
public static ArrayList ReadOnly(ArrayList list) {
if (list==null) throw new ArgumentNullException("list");
return new ReadOnlyArryList(list);
}
– Do use constructor if:
– If the member needs to be available for subclasses to specialize; Subclasses are not able to customize the behavior of static factory methods.
– if none of the above applies, use constructors as they are more convient than static factories.
2.13. Field Design
– Do not use instance fields that are public or protected; Use a protected property to expose a field to a derived class.
– Do not expose fields to be more versonable:
– Able to change a field to a property while maintaining binary compatibility.
– Able to allow demand-creation or change-notifcation of an object upon usage of the property.
– Do use const (static literal) fields for constants that will NEVER change; compilers burn the values of const fields directly into calling code.
– Do use public static readonly fields for predefined object instance:
public struct Color{
public static readonly Color red = new Color(0x0000FF);
public static readonly Color green = new Color(0x00FF00);
public static readonly Color blue = new Color(0xFF0000);
…
};
– Do not use readonly fileds of mutable types (or arrays).
2.14. Explicit Member Implementation
– Explicit member implementation allows an interface member to be implemented such that it is only available when cast to the interface type.
– Do use explicit members to hide implementation details from public view (or to approximate private interface implementations).
– Do expose an alternative way to access any explicitly implemented members that subclasses are allowed to override.
public class ViolatingBase : ITest
{
void ITest.SomeMethod() { … }
}
public class FixedBase : ITest
{
void ITest.SomeMethod() { SomeMethod(); }
virtual protected void SomeMethod() { … }
}
public class Derived : FixedBased, ITest
{
override protected void SomeMethod()
{
// …
base.SomeMethod();
// This would cause recursion and a stack overflow.
((ITest)this).SomeMethod();
}
}
3.1. Argument Checking
– Perform argument validation for every public or protected method and property set accessor and throw meaningful exceptions to caller. The actual checking could happen at a lower level in some private routines.
class Foo{
public int Count{
get{
return count;
}
set{
if (value < 0 || value >= MaxValue)
throw new ArgumentOutOfRangeException(Sys.GetString("InvalidArgument", "value", value.ToString()));
count = value;
}
}
public void Select(int start, int end){
if (start < 0)
throw new ArgumentException(Sys.GetString("InvalidArgument", "start", start.ToString()));
if (end < start)
throw new ArgumentException(Sys.GetString("InvalidArgument", "end", end.ToString()));
}
}
3.2. Parameter Passing
– In general, favor using enums where it makes the source code more readable; In case where enums would add unneeded complexity and actually hurt readability of the source code, Boolean should be prefered.
– Do use enums if there are two or more Boolean arguments:
FileStream fs = File.Open("foo.txt", true, fale); // No context to understand the meanings of true and false.
FileStream fs = File.Open("foo.txt", CasingOptions.CaseSensitive, FileMode.Open); // Much easier to work with.
– Annotation: Most of the time, numerics are passed around as constants and variables (not as "magic numbers"). 80% of the time, a Boolean argument is passed in as a constant to turn a piece of behavior on or off.
– Annotation: Developers inadvertently switches two Boolean arguments; It’s somewhat easier to make a mistake even with just one Boolean argument, does true mean "case insensitive" or "case sensitive"?
– Annotation: If there is even a slight possibility of needing more options in the future, use an enum now (not to be forced to add another Boolean option).
– Avoid using enums if the method does not take more than one Boolean arguement and there are now and forever only two states.
– If a value is typically set in the constructor, an enum is better; For constructor parameters that map onto properties, a Boolean is better.
– Annotation: Today, there is a very small overhead (on the order of 300 bytes for each enum type referenced) but this is at the noise range and should not be considered a factor in choosing to use a Boolean over an enum.