Type Design Guidelines for Reusable .NET Libraries
Interface Design
Although most APIs are best modeled using classes and structs, there are cases in which interfaces are more appropriate or are the only option.
The CLR does not support multiple inheritance (i.e., CLR classes cannot inherit from more than one base class), but it does allow types to implement one or more interfaces in addition to inheriting from a base class. Therefore interfaces are often used to achieve the effect of multiple inheritance. For example, IDisposable is an interface that allows types to support disposability independent of any other inheritance hierarchy in which they want to participate.
public class Component : MarshalByRefObject, IDisposable, IComponent { }
public struct Boolean : IComparable { } public class String: IComparable { }
DO define an interface if you need some common API to be supported
by a set of types that includes value types.
CONSIDER defining an interface if you need to support its functionality
on types that already inherit from some other type.
AVOID using marker interfaces (interfaces with no members).
If you need to mark a class as having a specific characteristic (marker), in general, use a custom attribute rather than an interface.
// Avoid public interface IImmutable {} // empty interface public class Key: IImmutable { } //Do [Immutable] public class Key { }
Methods can be implemented to reject parameters that are not marked with a specific attribute as follows:
public void Add(Key key, object value){ if(!key.GetType().IsDefined(typeof(ImmutableAttribute), false)){ throw new ArgumentException("The parameter must be immutable","key"); } }
|
The problem with this approach is that the check for the custom attribute can occur only at runtime. Sometimes, it is very important that the check for the marker be done at compile-time. For example, a method that can serialize objects of any type might be more concerned with verifying the presence of the marker than with type verification at compile-time. Using marker interfaces might be acceptable in such situations. The following example illustrates this design approach:
public interface ITextSerializable {} // empty interface public void Serialize(ITextSerializable item){ // use reflection to serialize all public properties }
DO provide at least one type that is an implementation of an interface.
This helps to validate the design of the interface. For example,
System.Collections.ArrayList is an implementation of the System.Collections.IList interface.
DO provide at least one API consuming each interface you define (a
method taking the interface as a parameter or a property typed as the
interface).
This helps to validate the interface design. For example, List Doing so would break implementations of the interface. You should
create a new interface to avoid versioning problems.
Except for the situations described in these guidelines, you should, in
general, choose classes rather than interfaces in designing managed code
reusable libraries.
The general-purpose value type is most often referred to as a struct, its C#
keyword. This section provides some guidelines for general struct design.
Section 4.8 presents guidelines for the design of a special case of value
type, the enum.
This allows arrays of structs to be created without having to run the
constructor on each item of the array. Notice that C# does not allow
structs to have default constructors.
This prevents accidental creation of invalid instances when an array of
the structs is created. For example, the following struct is incorrectly
designed. The parameterized constructor is meant to ensure valid state,
but the constructor is not executed when an array of the struct is created
and so the instance filed value gets initialized to 0, which is not a valid
value for this type.
The problem can be fixed by ensuring that the default state (in this case
the value field equal to 0) is a valid logical state for the type.
The Object.Equals method on value types causes boxing and its
default implementation is not very efficient, as it uses reflection.
IEquatable In general, structs can be very useful, but should only be used for small,
single, immutable values that will not be boxed frequently. Next are guidelines
for enum design, a more complex matter.
Enums are a special kind of value type. There are two kinds of enums: simple
enums and flag enums.
Simple enums represent small, closed sets of choices. A common example
of the simple enum is a set of colors. For example,
Flag enums are designed to support bitwise operations on the enum
values. A common example of the flags enum is a list of options. For
example,
Where it could get worse, I think, is that if less advanced developers don't
realize they are working with a set of flags that can be combined with one
another, they might just look at the list available and think that is all the
functionality they can access. As we've seen in other studies, if an API
makes it look to them as though a specific scenario or requirement isn't
immediately possible, it's likely that they will change the requirement and
do what does appear to be possible, rather than being motivated to spend
time investigating what they need to do to achieve the original goal. Historically, many reusable libraries (e.g., Win32 APIs) represented sets
of values using integer constants. Enums make such sets more strongly
typed, and thus improve compile-time error checking, usability, and readability.
For example, use of enums allows development tools to know the
possible values for a property or a parameter.
You can always simply add values to the existing enum at a later stage.
See section "Adding Values to Enums" for more details on adding values to enums. Reserved
values just pollute the set of real values and tend to lead to user errors.
A common practice for ensuring future extensibility of C APIs is to add
reserved parameters to method signatures. Such reserved parameters
can be expressed as enums with a single default value. This should not
be done in managed APIs. Method overloading allows adding parameters
in future releases.
DO NOT add members to an interface that has previously shipped.
Struct Design
DO NOT provide a default constructor for a struct.
DO ensure that a state where all instance data is set to zero, false, or null
(as appropriate) is valid.
// bad design
public struct PositiveInteger {
int value;
public PositiveInteger(int value) {
if (value <= 0) throw new ArgumentException(
);
this.value = value;
}
public override string ToString() {
return value.ToString();
}
}
// good design
public struct PositiveInteger {
int value; // the logical value is value+1
public PositiveInteger(int value) {
if (value <= 0) throw new ArgumentException(
);
this.value = value-1;
}
public override string ToString() {
return (value+1).ToString();
}
}
DO implement IEquatable
DO NOT explicitly extend System.ValueType. In fact, most languages
prevent this.
Enum Design
public enum Color {
Red,
Green,
Blue,
}
[Flags]
public enum AttributeTargets {
Assembly= 0x0001,
Module = 0x0002,
Cass = 0x0004,
Struct = 0x0008,
}
BRAD ABRAMS We had some debates about what to call enums that
are designed to be bitwise ORed together. We considered bitfields, bitflags,
and even bitmasks, but ultimately decided to use flag enums as it was clear,
simple, and approachable.
STEVEN CLARKE I'm sure that less experienced developers will be able
to understand bitwise operations on flags. The real question, though, is
whether they would expect to have to do this. Most of the APIs that I have
run through the labs don't require them to perform such operations so I
have a feeling that they would have the same experience that we observed
during a recent study-it's just not something that they are used to doing so
they might not even think about it.
DO use an enum to strongly type parameters, properties, and return
values that represent sets of values.
DO favor using an enum over static constants.
// Avoid the following
public static class Color {
public static int Red = 0;
public static int Green = 1;
public static int Blue = 2;
}
// Favor the following
public enum Color {
Red,
Green,
Blue,
}
JEFFREY RICHTER An enum is a structure with a set of static constants.
The reason to follow this guideline is because you will get some additional
compiler and reflection support if you define an enum versus manually
defining a structure with static constants.
DO NOT use an enum for open sets (such as the operating system version,
names of your friends, etc.).
DO NOT provide reserved enum values that are intended for future use.
public enum DeskType {
Circular,
Oblong,
Rectangular,
// the following two values should not be here
ReservedForFutureUse1,
ReservedForFutureUse2,
}
AVOID publicly exposing enums with only one value.
// Bad Design
public enum SomeOption {
DefaultOption
// we will add more options in the future
}
// The option parameter is not needed.
// It can always be added in the future
// to an overload of SomeMethod().
public void SomeMethod(SomeOption option) {
}
DO NOT include sentinel values in enums.
Page 4 of 6