dcsimg
October 17, 2017
Hot Topics:

Type Design Guidelines for Reusable .NET Libraries

  • December 23, 2005
  • By Krzysztof Cwalina and Brad Abrams
  • Send Email »
  • More Articles »

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 {
     …
     }

The other situation in which defining an interface is appropriate is in creating a common interface that can be supported by several types including some value types. Value types cannot inherit from types other than System.ValueType, but they can implement interfaces, so using an interface is the only option to provide a common base type.
     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");
         }
         …
     }

RICO MARIANI Of course any kind of marking like this has a cost. Attribute testing is a lot more costly than type checking. You might find that it's necessary to use the marker interface approach for performance reasons- measure and see. My own experience is that true markers (with no members) don't come up very often. Most of the time, you need a nokidding- around interface with actual functionality to do the job, in which case there is no choice to make.

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.Sort consumes IComparer interface.

DO NOT add members to an interface that has previously shipped.

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.

Struct Design

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.

DO NOT provide a default constructor for a struct.

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.

DO ensure that a state where all instance data is set to zero, false, or null (as appropriate) is valid.

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.

     // 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();
         }
     }

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.

     // 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 on value types.

The Object.Equals method on value types causes boxing and its default implementation is not very efficient, as it uses reflection. IEquatable.Equals can have much better performance and can be implemented such that it will not cause boxing.

DO NOT explicitly extend System.ValueType. In fact, most languages prevent this.

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.

Enum Design

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,

     public enum Color {
         Red,
         Green,
         Blue,
         …
    }

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,

     [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.

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.

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.

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.

     public enum DeskType {
         Circular,
         Oblong,
         Rectangular,
         // the following two values should not be here
         ReservedForFutureUse1,
         ReservedForFutureUse2,
     }

AVOID publicly exposing enums with only one value.

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.

     // 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



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date
Rocket Fuel