Type Design Guidelines for Reusable .NET Libraries

By Krzysztof Cwalina and Brad Adams

From the CLR perspective, there are only two categories of types- reference types and value types-but for the purpose of framework design discussion we divide types into more logical groups, each with its own specific design rules. Figure 1 shows these logical groups.

Classes are the general case of reference types. They make up the bulk of types in the majority of frameworks. Classes owe their popularity to the rich set of object-oriented features they support and to their general applicability. Base classes and abstract classes are special logical groups related to extensibility.

Interfaces are types that can be implemented both by reference types and value types. This allows them to serve as roots of polymorphic hierarchies of reference types and value types. In addition, interfaces can be used to simulate multiple inheritance, which is not natively supported by the CLR.

Structs are the general case of value types and should be reserved for small, simple types, similar to language primitives.

Enums are a special case of value types used to define short sets of values, such as days of the week, console colors, and so on.

Static classes are types intended as containers for static members. They are commonly used to provide shortcuts to other operations.

Delegates, exceptions, attributes, arrays, and collections are all special cases of reference types intended for specific uses, and guidelines for their design and usage are discussed elsewhere in our book, Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries.

Figure 1: The logical grouping of types

DO ensure that each type is a well-defined set of related members, not just a random collection of unrelated functionality.

It is important that a type can be described in one simple sentence. A good definition should also rule out functionality that is only tangentially related.

BRAD ABRAMS If you have ever managed a team of people you know that they don't do well without a crisp set of responsibilities. Well, types work the same way. I have noticed that types without a firm and focused scope tend to be magnets for more random functionality, which, over time, make a small problem a lot worse. It becomes more difficult to justify why the next member with even more random functionality does not belong in the type. As the focus of the members in a type blurs, the developer's ability to predict where to find a given functionality is impaired, and therefore so is productivity.

RICO MARIANI Good types are like good diagrams: What has been omitted is as important to clarity and usability as what has been included. Every additional member you add to a type starts at a net negative value and only by proven usefulness does it go from there to positive. If you add too much in an attempt to make the type more useful to some, you are just as likely to make the type useless to everyone.

JEFFREY RICHTER When I was learning OOP back in the early 1980s, I was taught a mantra that I still honor today: If things get too complicated, make more types. Sometimes, I find that I am thinking really hard trying to define a good set of methods for a type. When I start to feel that I'm spending too much time on this or when things just don't seem to fit together well, I remember my mantra and I define more, smaller types where each type has well-defined functionality. This has worked extremely well for me over the years. On the flip side, sometimes types do end up being dumping grounds for various loosely related functions. The .NET Framework offers several types like this, such as Marshal, GC, Console, Math, and Application. You will note that all members of these types are static and so it is not possible to create any instances of these types. Programmers seem to be OK with this. Fortunately, these types' methods are separated a bit by types. It would be awful if all these methods were defined in just one type!

Types and Namespaces

Before designing a large framework you should decide how to factor your functionality into a set of functional areas represented by namespaces. This kind of top-down architectural design is important to ensure a coherent set of namespaces containing types that are well integrated, don't collide, and are not repetitive. Of course the namespace design process is iterative and it should be expected that the design will have to be tweaked as types are added to the namespaces over the course of several releases. This leads to the following guidelines.

DO use namespaces to organize types into a hierarchy of related feature areas.

The hierarchy should be optimized for developers browsing the framework for desired APIs.

KRZYSZTOF CWALINA This is an important guideline. Contrary to popular belief, the main purpose of namespaces is not to help in resolving naming conflicts between types with the same name. As the guideline states, the main purpose of namespaces is to organize types in a hierarchy that is coherent, easy to navigate, and easy to understand.

I consider type name conflicts in a single framework to indicate sloppy design. Types with identical names should either be merged to allow for better integration between parts of the library or should be renamed to improve code readability and searchability.

AVOID very deep namespace hierarchies. Such hierarchies are difficult to browse because the user has to backtrack often.

AVOID having too many namespaces.

Users of a framework should not have to import many namespaces in most common scenarios. Types that are used together in common scenarios should reside in a single namespace if at all possible.

JEFFREY RICHTER As an example of a problem, the runtime serializer types are defined under the System.Runtime.Serialization namespace and its subnamespaces. However, the Serializable and NonSerialized attributes are incorrectly defined in the System namespace. Because these types are not in the same namespace, developers don't realize that they are closely related. In fact, I have run into many developers who apply the Serializable attribute to a class that they are serializing with the System.Xml.Serialization's XmlSerializer type. However, the XmlSerializer completely ignores the Serializable attribute; applying the attribute gives no value and just bloats your assembly's metadata.

AVOID having types designed for advanced scenarios in the same namespace as types intended for common programming tasks.

This makes it easier to understand the basics of the framework and to use the framework in the common scenarios.

BRAD ABRAMS One of the best features of Visual Studio is Intellisense, which provides a drop-down for your likely next type or member usage. The benefit of this feature is inversely proportional to the number of options. That is, if there are too many items in the list it takes longer to find the one you are looking for. Following this guideline to split out advanced functionality into a separate namespace enables developers to see the smallest number of types possible in the common case.

BRIAN PEPIN One thing we've learned is that most programmers live or die by Intellisense. If something isn't listed in the drop-down, most programmers won't believe it exists. But, as Brad says above, too much of a good thing can be bad and having too much stuff in the drop-down list dilutes its value. If you have functionality that should be in the same namespace, but you don't think it needs to be shown all the time to users, you can use the EditorBrowsable attribute. Put this attribute on a class or member and you can instruct Intellisense to only show the class or member for advanced scenarios.

RICO MARIANI Don't go crazy adding members for every exotic thing someone might want to do with your type. You'll make fatter, uglier assemblies that are hard to grasp. Provide good primitives with understandable limitations. A great example of this is the urge people get to duplicate functionality that is already easy to use via Interop to native. Interop is there for a reason-it's not an unwanted stepchild. When wrapping anything, be sure you are adding plenty of value. Otherwise, the value added by being smaller would have made your assembly more helpful to more people.

JEFFREY RICHTER I agree with this guideline but I'd like to further add that the more advanced classes should be in a namespace that is under the namespace that contains the simple types. For example, the simple types might be in System.Mail and the more advanced types should be in System.Mail.Advanced.

DO NOT define types without specifying their namespaces. This organizes related types in a hierarchy, and can help to resolve potential type name collisions. Please note that the fact that namespaces can help to resolve name collisions does not mean that such collisions should be introduced.

DO NOT define types without specifying their namespaces.

This organizes related types in a hierarchy, and can help to resolve potential type name collisions. Please note that the fact that namespaces

BRAD ABRAMS It is important to realize that namespaces cannot actually prevent naming collisions but they can significantly reduce them. I could define a class called MyNamespace.MyType in an assembly called MyAssembly, and define a class with precisely the same name in another assembly. I could then build an application that uses both of these assemblies and types. The CLR would not get confused because the type identity in the CLR is based on strong name (which includes fully qualified assembly name) rather than just the namespace and type name. This can be seen by looking at the C# and ILASM of code creating an instance of MyType.

C#:
new MyType();

IL:
IL_0000: newobj instance void
[MyAssembly]MyNamespace.MyType::.ctor()

Notice that the C# compiler adds a reference to the assembly that defines the type, of the form [MyAssembly], so the runtime always has a disambiguated, fully qualified name to work with.

JEFFREY RICHTER Although what Brad says is true, the C# compiler doesn't let you specify in source code which assembly to pull a type out of, so if you have code that wants to use a type called MyNamespace.MyType that exists in two or more assemblies, there is no easy way to do this in C# source code. Prior to C# 2.0, distinguishing between the two types was impossible. However, with C# 2.0, it is now possible using the new extern aliases and namespace qualifier features.

RICO MARIANI Namespaces are a language thing. The CLR doesn't know anything about them really. As far as the CLR is concerned the name of the class really is something like MyNameSpace.MyOtherNameSpace. MyAmazingType. The compilers give you syntax (e.g., "using") so that you don't have to type those long class names all the time. So the CLR is never confused about class names because everything is always fully qualified.

Standard Subnamespace Names

Types that are rarely used should be placed in subnamespaces to avoid cluttering the main namespaces. We have identified several groups of types that should be separated from their main namespaces.

The .Design Subnamespace

Design-time-only types should reside in a subnamespace named .Design. For example, System.Windows.Forms.Design contains Designers and related classes used to do design of applications based on System. Windows.Forms.

System.Windows.Forms.Design
System.Messaging.Design
System.Diagnostics.Design

DO use a namespace with the .Design suffix to contain types that provide design-time functionality for a base namespace.

The .Permissions Subnamespace
Permission types should reside in a subnamespace named .Permissions.

DO use a namespace with the .Permissions suffix to contain types that provide custom permissions for a base namespace.

KRZYSZTOF CWALINA In the initial design of the .NET Framework namespaces, all types related to a given feature area were in the same namespace. Prior to the first release, we moved design-related types to subnamespaces with the .Design suffix. Unfortunately, we did not have time to do it for the Permission types. This is a problem in several parts of the Framework. For example, a large portion of the types in the System.Diagnostics namespace are types needed for the security infrastructure and very rarely used by the end users of the API.

Type Design Guidelines for Reusable .NET Libraries

The .Interop Subnamespace

Many frameworks need to support interoperability with legacy components. Due diligence should be used in designing interoperability from the ground up. However, the nature the problem often requires that the shape and style of such interoperability APIs is often quite different from good managed framework design. Thus, it makes sense to put functionality related to interoperation with legacy components in a subnamespace.

You should not put types that completely abstract unmanaged concepts and expose them as managed into the Interop subnamespace. It is often the case that managed APIs are implemented by calling out to unmanaged code. For example the System.IO.FileStream class calls out to Win32 CreateFile. This is perfectly acceptable and does not imply that the FileStream class needs to be in System.IO.Interop namespace as FileStream completely abstracts the Win32 concepts and publicly exposes a nice managed abstraction.

DO use a namespace with the .Interop suffix to contain types that provide interop functionality for a base namespace.

DO use a namespace with the .Interop suffix for all code in a Primary Interop Assembly (PIA).

Choosing Between Class and Struct

One of the basic design decisions every framework designer faces is whether to design a type as a class (a reference type) or as a struct (a value type). Good understanding of the differences in the behavior of reference types and value types is crucial in making this choice.

Reference types are allocated on the heap, and garbage-collected, whereas value types are allocated either on the stack or inline in containing types and deallocated when the stack unwinds or when their containing type gets deallocated. Therefore, allocations and deallocations of value types are in general cheaper than allocations and deallocations of reference types.

Arrays of reference types are allocated out-of-line, meaning the array elements are just references to instances of the reference type residing on the heap. Value type arrays are allocated in-line, meaning that the array elements are the actual instances of the value type. Therefore, allocations and deallocations of value type arrays are much cheaper than allocations and deallocations of reference type arrays. In addition, in a majority of cases value type arrays exhibit much better locality of reference.

RICO MARIANI The preceding is often true but it's a very broad generalization that I would be very careful about. Whether or not you get better locality of reference when value types get boxed when cast to an array of value types will depend on how much of the value type you use, how much searching you have to do, how much data reuse there could have been with equivalent array members (sharing a pointer), the typical array access patterns, and probably other factors I can't think of at the moment. Your mileage might vary but value type arrays are a great tool for your toolbox.

Value types get boxed when cast to object or one of the interfaces they implement. They get unboxed when cast back to the value type. Because boxes are objects that are allocated on the heap and are garbage collected, too much boxing and unboxing can have a negative impact on the heap, the garbage collector, and ultimately the performance of the application.

Reference type assignments copy the reference, whereas value type assignments copy the entire value. Therefore assignments of large reference types are cheaper than assignments of large value types.

Finally, reference types are passed by reference, whereas value types are passed by value. Changes to an instance of a reference type affect all references pointing to the instance. Value type instances are copied when they are passed by value. When an instance of a value type is changed, it of course does not affect any of its copies. Because the copies are not created explicitly by the user, but rather implicitly when arguments are passed or return values are returned, value types that can be changed can be confusing to many users. Therefore value types should be immutable. (1)

RICO MARIANI If you make your value type mutable you will find that you end up having to pass it by reference a lot to get the semantics you want (using, e.g., "out" syntax in C#). This might be important in cases in which the value type is expected to be embedded in a variety of other objects that are themselves reference types or embedded in arrays. The biggest trouble from having mutable value types is where they look like independent entities like, for example, a complex number. Value types that have a mission in life of being an accumulator of sorts or a piece of a reference type have fewer pitfalls for mutability.

As a rule of thumb, majority of types in a framework should be classes. There are, however, some situations in which the characteristics of a value type make it more appropriate to use structs.

CONSIDER defining a struct instead of a class if instances of the type are small and commonly short-lived or are commonly embedded in other objects.

DO NOT define a struct unless the type has all of the following characteristics:

  • It logically represents a single value, similar to primitive types (int, double, etc.).
  • It has an instance size under 16 bytes.
  • It is immutable.
  • It will not have to be boxed frequently.

In all other cases, you should define your types as classes.

JEFFREY RICHTER In my opinion, a value type should be defined for types that have approximately 16 bytes or less. Value types can be more than 16 bytes if you don't intend to pass them to other methods or copy them to and from a collection class (like an array). I would also define a value type if you expect instances of the type to be used for short periods of time (usually they are created in a method and no longer needed after a method returns). I used to discourage defining value types if you thought that instances of them would be placed in a collection due to all the boxing that would have to be done. But, fortunately, newer versions of the CLR, C#, and other languages support generics so that boxing is no longer necessary when putting value type instances in a collection.

Choosing Between Class and Interface

In general, classes are the preferred construct for exposing abstractions.

The main drawback of interfaces is that they are much less flexible than classes when it comes to allowing for evolution of APIs. Once you ship an interface, the set of its members is fixed forever. Any additions to the interface would break existing types implementing the interface.

A class offers much more flexibility. You can add members to classes that have already shipped. As long as the method is not abstract (i.e., as long as you provide a default implementation of the method), any existing derived classes continue to function unchanged.

Let's illustrate the concept with a real example from the .NET Framework. The System.IO.Stream abstract class shipped in version 1.0 of the framework without any support for timing out pending I/O operations. In version 2.0, several members were added to Stream to allow subclasses to support timeout-related operations, even when accessed through their base class APIs.

     public abstract class Stream {
          public virtual bool CanTimeout {
             get { return false; }
          }
          public virtual int ReadTimeout{
              get{
                 throw new NotSupportedException(…);
              {
              set {
                   throw new NotSupportedException(…);
              }
          }
     }

     public class FileStream : Stream {
          public override bool CanTimeout {
              get { return true; }
          }
          public override int ReadTimeout{
              get{
                  …
              {
              set {
                  …
              }
          }
     }

The only way to evolve interface-based APIs is to add a new interface with the additional members. This might seem like a good option, but it suffers from several problems. Let's illustrate this on a hypothetical IStream interface. Let's assume we had shipped the following APIs in version 1.0 of the Framework.

     public interface IStream {
          …
     }

     public class FileStream : IStream {
          …
     }

If we wanted to add support for timeouts to streams in version 2.0, we would have to do something like the following:

     public interface ITimeoutEnabledStream : IStream {
          int ReadTimeout{ get; set; }
     }

     public class FileStream : ITimeoutEnabledStream {
          public int ReadTimeout{
              get{
                 …
              {
              set {
                  …
              }
          }
     }

But now we would have a problem with all the existing APIs that consume and return IStream. For example StreamReader has several constructor overloads and a property typed as Stream.

     public calss StreamReader {
     public StreamReader(IStream stream){ … }
     public IStream BaseStream { get { … } }
     }

How would we add support for ITimeoutEnabledStream to Stream- Reader? We would have several options, each with substantial development cost and usability issues:

Leave the StreamReader as is, and ask users who want to access the timeout-related APIs on the instance returned from BaseStream property to use a dynamic cast and query for the ITimeoutEnabledStream interface.

     StreamReader reader = GetSomeReader();
     ITimeoutEnabledStream stream = reader.BaseStream as
     ITimeoutEnabledStream;
     if(stream != null){
     stream.ReadTimeout = 100;
     }

This option unfortunately does not perform well in usability studies. The fact that some streams can now support the new operations is not immediately visible to the users of StreamReader APIs. Also, some developers have difficulties understanding and using dynamic casts.

Add a new property to StreamReader that would return ITimeout- EnabledStream if one was passed to the constructor or null if IStream was passed.

     StreamReader reader = GetSomeReader();
     ITimeoutEnabledStream stream = reader.TimeoutEnabledBaseStream;
     if(stream!= null){
     stream.ReadTimeout = 100;
     }

Such APIs are only marginally better in terms of usability. It's really not obvious to the user that the TimeoutEnabledBaseStream property getter might return null, which results in confusing and often unexpected Null- ReferenceExceptions.

1. Immutable types are types that don't have any public members that can modify this instance. For example, System.String is immutable. Its members, such as ToUpper, do not modify the sting on which they are called, but rather return a new modified string and leave the original string unchanged.

Type Design Guidelines for Reusable .NET Libraries

Add a new type called TimeoutEnabledStreamReader that would take ITimeoutEnabledStream parameters to the constructor overloads and return ITimeoutEnabledStream from the BaseStream property. The problem with this approach is that every additional type in the framework adds complexity for the users. What's worse, the solution usually creates more problems like the one it is trying to solve. StreamReader itself is used in other APIs. These other APIs will now need new versions that can operate on the new TimeoutEnabledStreamReader.

The Framework streaming APIs are based on an abstract class. This allowed for an addition of timeout functionality in version 2.0 of the Framework. The addition is straightforward, discoverable, and had little impact on other parts of the framework.

     StreamReader reader = GetSomeReader();
     if(reader.BaseStream.CanTimeout){
         reader.BaseStream.ReadTimeout = 100;
     }

One of the most common arguments in favor of interfaces is that they allow separating contract from the implementation. However, the argument incorrectly assumes that you cannot separate contracts from implementation using classes. Abstract classes residing in a separate assembly from their concrete implementations are a great way to achieve such separation. For example, the contract of IList says that when an item is added to a collection, the Count property is incremented by one. Such a simple contract can be expressed and, what's more important, locked for all subtypes, using the following abstract class:

     public abstract class CollectionContract : IList {

         public void Add(T item){
         AddCore(item);
         this.count++;
         }
         public int Count {
             get { return this.count; }
         }
         protected abstract void AddCore(T item);
         private int count;
     }

KRZYSZTOF CWALINA I often hear people saying that interfaces specify contracts. I believe this is a dangerous myth. Interfaces, by themselves, do not specify much beyond the syntax required to use an object. The interface- as-contract myth causes people to do the wrong thing when trying to separate contracts from implementation, which is a great engineering practice. Interfaces separate syntax from implementation, which is not that useful, and the myth provides a false sense of doing the right engineering. In reality, the contract is semantics, and these can actually be nicely expressed with some implementation.

COM exposed APIs exclusively through interfaces, but you should not assume that COM did this because interfaces were superior. COM did it because COM is an interface standard that was intended to be supported on many execution environments. CLR is an execution standard and it provides a great benefit for libraries that rely on portable implementation.

DO favor defining classes over interfaces.

Class-based APIs can be evolved with much greater ease than interfacebased APIs because it is possible to add members to a class without breaking existing code.

KRZYSZTOF CWALINA Over the course of the three versions of the .NET Framework, I have talked about this guideline with quite a few developers on our team. Many of them, including those who initially disagreed with the guideline, have said that they regret having shipped some API as an interface. I have not heard of even one case in which somebody regretted that they shipped a class.

JEFFREY RICHTER I agree with Krzysztof in general. However, you do need to think about some other things. There are some special base classes, such as MarshalByRefObject. If your library type provides an abstract base class that isn't itself derived from MarshalByRefObject, then types that derive from your abstract base class cannot live in a different AppDomain. My recommendation to people is this: Define an interface first and then define an abstract base class that implements the interface. Use the interface to communicate to the object and let end-user developers decide for themselves whether they can just define their own type based on your abstract base class (for convenience) or define their own type based on whatever base class they desire and implement the interface (for flexibility). A good example of this is the IComponent interface and the Component base class that implements IComponent.

DO use abstract classes instead of interfaces to decouple the contract from implementations.

Abstract classes, if designed correctly, allow for the same degree of decoupling between contract and implementation.

CHRIS ANDERSON Here is one instance in which the design guideline, if followed too strictly, can paint you into a corner. Abstract types do version much better, and allow for future extensibility, but they also burn your one and only one base type. Interfaces are appropriate when you are really defining a contract between two objects that is invariant over time. Abstract base types are better for defining a common base for a family of types. When we did .NET there was somewhat of a backlash against the complexity and strictness of COM-interfaces, Guids, variants, and IDL, were all seen as bad things. I believe today that we have a more balanced view of this. All of these COMisms have their place, and in fact you can see interfaces coming back as a core concept in Indigo.

BRIAN PEPIN One thing I've started doing is to actually bake as much contract into my abstract class as possible. For example, I might want to have four overloads to a method where each overload offers an increasingly complex set of parameters. The best way to do this is to provide a nonvirtual implementation of these methods on the abstract class, and have the implementations all route to a protected abstract method that provides the actual implementation. By doing this, you can write all the boring argument- checking logic once. Developers who want to implement your class will thank you.

DO define an interface if you need to provide a polymorphic hierarchy of value types.

Value types cannot inherit from other types, but they can implement interfaces. For example, IComparable, IFormattable, and IConvertible are all interfaces so value types such as Int32, Int64, and other primitives can all be comparable, formattable, and convertible.

     public struct Int32 : IComparable, IFormattable, IConvertible {
         …
     }
     public struct Int64 : IComparable, IFormattable, IConvertible {
         …
     }
CONSIDER defining interfaces to achieve a similar effect to that of multiple inheritance.

RICO MARIANI Good interface candidates often have this "mix in" feel to them. All sorts of objects can be IFormattable-it isn't restricted to a certain subtype. It's more like a type attribute. Other times we have interfaces that look more like they should be classes-IFormatProvider springs to mind. The fact that the best interface name ended in "er" speaks volumes.

BRIAN PEPIN Another sign that you've got a well-defined interface is that the interface does exactly one thing. If you have an interface that has a grab bag of functionality, that's a warning sign. You'll end up regretting it because in the next version of your product you'll want to add new functionality to this rich interface, but you can't.

For example, System.IDisposable and System.ICloneable are both interfaces so types, like System.Drawing.Image, can be both disposable, cloneable, and still inherit from System.MarshalByRef- Object class.

     public class Image : MarshalByRefObject, IDisposable, ICloneable {
     …
     }

JEFFREY RICHTER When a class is derived from a base class, I say that the derived class has an IS-A relationship with the base. For example, a FileStream IS-A Stream. However, when a class implements an interface, I say that the implementing class has a CAN-DO relationship with the interface. For example, a FileStream CAN-DO disposing.

Abstract Class Design

DO NOT define public or protected-internal constructors in abstract types.

Constructors should be public only if users will need to create instances of the type. Because you cannot create instances of an abstract type, an abstract type with a public constructor is incorrectly designed and misleading to the users.(2)

     // bad design
     public abstract class Claim {
         public Claim (int number) {
         }
     }
     // good design
     public abstract class Claim {
         // incorrect Design
         protected Claim (int number) {
         }
     }

DO define a protected or an internal constructor on abstract classes.

A protected constructor is more common and simply allows the base class to do its own initialization when subtypes are created.

     public abstract class Claim {
         protected Claim() {
             …
         }
     }

An internal constructor can be used to limit concrete implementations of the abstract class to the assembly defining the class.

     public abstract class Claim {
         internal Claim() {
             …
         }
     }

BRAD ABRAMS Many languages (such as C#) will insert a protected constructor if you do not. It is a good practice to define the constructor explicitly in the source so that it can be more easily documented and maintained over time.

DO provide at least one concrete type that inherits from each abstract class that you ship.

This helps to validate the design of the abstract class. For example, System. IO.FileStream is an implementation of the System.IO.Stream abstract class.

BRAD ABRAMS I have seen countless examples of a "well-designed" base class or interface where the designers spent hundreds of hours debating and tweaking the design only to have it blown out of the water when the first real-world client came to use the design. Far too often these realworld clients come too late in the product cycle to allow time for the correct fix. Forcing yourself to provide at least one concrete implementation reduces the chances of finding a new problem late in the product cycle.

Static Class Design

A static class is defined as a class that contains only static members (of course besides the instance members inherited from System.Object and possibly a private constructor). Some languages provide built-in support for static classes. In C# 2.0, when a class is declared to be static, it is sealed, abstract, and no instance members can be overridden or declared.

     public static class File {
         …
     }

If your language does not have built-in support for static classes, you can declare such classes manually as in the following C++ example:

     public class File abstract sealed {
         …
     }

Static classes are a compromise between pure object-oriented design and simplicity. They are commonly used to provide shortcuts to other operations (such as System.IO.File), or functionality for which a full objectoriented wrapper is unwarranted (such as System.Environment).

DO use static classes sparingly.

Static classes should be used only as supporting classes for the objectoriented core of the framework.

DO NOT treat static classes as a miscellaneous bucket.

There should be a clear charter for the class.

DO NOT declare or override instance members in static classes.

DO declare static classes as sealed, abstract, and add a private instance constructor, if your programming language does not have built-in support for static classes.

BRIAN GRUNKEMEYER In the .NET Framework 1.0, I wrote the code for the System.Environment class, which is an excellent example of a static class. I messed up and accidentally added a property to this class that wasn't static (HasShutdownStarted). Because it was an instance method on a class that could never be instantiated, no one could call it. We didn't discover the problem early enough in the product cycle to fix it before releasing version 1.0. If I were inventing a new language, I would explicitly add the concept of a static class into the language to help people avoid falling into this trap. And in fact, in C# 2.0 did add support for static classes!

JEFFREY RICHTER Make sure that you do not attempt to define a static structure, because structures (value types) can always be instantiated no matter what. Only classes can be static.



2. This also applies to protected-internal constructors.

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

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.

Type Design Guidelines for Reusable .NET Libraries

Although they are sometimes helpful to framework developers, they are confusing to users of the framework. Sentinel values are values used to track the state of the enum, rather than being one of the values from the set represented by the enum. The following example shows an enum with an additional sentinel value used to identify the last value of the enum, and intended for use in range checks. This is bad practice in framework design.

public enum DeskType {
Circular = 1,
Oblong = 2,
Rectangular = 3,
LastValue = 3 // this sentinel value should not be here
}
public void OrderDesk(DeskType desk){
if((desk > DeskType.LastValue){
throw new ArgumentOutOfRangeException(…);
}
…
}

Rather than relying on sentinel values, framework developers should perform the check using one of the real enum values.

public void OrderDesk(DeskType desk){
if(desk > DeskType.Rectangular || desk < DeskType.Circular){
throw new ArgumentOutOfRangeException(…);
}
…
}

RICO MARIANI You can get yourself into a lot of trouble by trying to be too clever with enums. Sentinel values are a great example of this: People write code like the above but using the sentinel value LastValue instead of Rectangular as recommended. When a new value comes along and LastValue is updated, their program "automatically" does the right thing and accepts the new input value without giving an ArgumentOutOf- RangeException. That sounds grand except for all that we didn't show, the part that's doing the actual work, and might not yet expect or even handle the new value. The less clever tests will force you to revisit all the right places to ensure that the new value really is going to work. The few minutes you spend visiting those call sites will be more than repaid in time you save avoiding bugs.

DO provide a value of zero on simple enums.

Consider calling the value something like None. If such value is not appropriate for this particular enum, the most common default value for the enum should be assigned the underlying value of zero.

public enum Compression {
None = 0,
GZip,
Deflate,
}
public enum EventType {
Error = 0,
Warning,
Information,
…
}

CONSIDER using Int32 (the default in most programming languages) as the underlying type of an enum unless any of the following is true:

  • The enum is a flags enum and you have more than 32 flags, or expect to have more in the future.
  • The underlying type needs to be different than Int32 for easier interoperability with unmanaged code expecting different size enums.

BRAD ABRAMS This might not be as uncommon a concern as you first expect. We are only in version 2.0 of the .NET Framework and we are already running out of values in the CodeDom GeneratorSupport enum. In retrospect, we should have used a different mechanism for communicating the generator support options than an enum.

RICO MARIANI Did you know that the CLR supports enums with an underlying type of float or double even though most languages don't choose to expose it? This is very handy for strongly typed constants that happen to be floating point (e.g., a set of canonical conversion factors for different measuring systems). It's in the ECMA standard.

  • A smaller underlying type would result in substantial savings in space. If you expect for enum to be used mainly as an argument for flow of control, the size makes little difference. The size savings might be significant if:
  • You expect to the enum to be used as a field in a very frequently instantiated structure or class.
  • You expect users to create large arrays or collections of the enum instances.
  • You expect a large number of instances of the enum to be serialized.

For in-memory usage, be aware that managed objects are always DWORD aligned so you effectively need multiple enums or other small structures in an instance to pack a smaller enum with to make a difference, as the total instance size is always going to be rounded up to a DWORD.

DO name flag enums with plural nouns or noun phrases and simple enums with singular nouns or noun phrases.

DO NOT extend System.Enum directly. System.Enum is a special type used by the CLR to create user-defined enumerations. Most programming languages provide a programming element that gives you access to this functionality. For example, in C# the enum keyword is used to define an enumeration.

BRAD ABRAMS Keep in mind that it is a binary breaking change to change the size of the enum type once you have shipped, so choose wisely with an eye on the future. Our experience is that Int32 is usually the right choice and thus we made it the default.

Designing Flag Enums

DO apply the System.FlagsAttribute to flag enums. Do not apply this attribute to simple enums.

[Flags]
public enum AttributeTargets {
…
}

DO use powers of two for the flags enum values so they can be freely combined using the bitwise OR operation.

[Flags]
public enum WatcherChangeTypes {
Created = 0x0002,
Deleted = 0x0004,
Changed = 0x0008,
Renamed = 0x0010,
}

CONSIDER providing special enum values for commonly used combinations of flags.

Bitwise operations are an advanced concept and should not be required for simple tasks. FileAccess.ReadWrite is an example of such a special value.

[Flags]
public enum FileAccess {
Read = 1,
Write = 2,
ReadWrite = Read | Write
}

JEFFREY RICHTER I use flag enums quite frequently in my own programming. They store very efficiently in memory and manipulation is very fast. In addition, they can be used with interlocked operations, making them ideal for solving thread synchronization problems. I'd love to see the System.Enum type offer a bunch of additional methods that could be easily inlined by the JIT compiler that would make source code easier to read and maintain. Here are some of the methods I'd like to see added to Enum: IsExactlyOneBitSet, CountOnBits, AreAllBitsOn, AreAnyBitsOn, and TurnBitsOnOff.

DO apply the System.FlagsAttribute to flag enums. Do not apply this attribute to simple enums. [Flags] public enum AttributeTargets { … }

DO use powers of two for the flags enum values so they can be freely combined using the bitwise OR operation. [Flags] public enum WatcherChangeTypes { Created = 0x0002, Deleted = 0x0004, Changed = 0x0008, Renamed = 0x0010, }

CONSIDER providing special enum values for commonly used combinations of flags. Bitwise operations are an advanced concept and should not be required for simple tasks. FileAccess.ReadWrite is an example of such a special value. [Flags] public enum FileAccess { Read = 1, Write = 2, ReadWrite = Read | Write }

AVOID creating flag enums where certain combinations of values are invalid.

The System.Reflection.BindingFlags enum is an example of an incorrect design of this kind. The enum tries to represent many different concepts, such as visibility, staticness, member kind, and so on.

[Flags]
public enum BindingFlags {
Instance,
Static,
NonPublic,
Public,
CreateInstance,
GetField,
SetField,
GetProperty,
SetProperty,
InvokeMethod,
…
}

Certain combinations of the values are not valid. For example, the Type.GetMembers method accepts this enum as a parameter but the documentation for the method warns users, "You must specify either BindingFlags.Instance or BindingFlags.Static in order to get a return." Similar warnings apply to several other values of the enum.

If you have an enum with this problem, you should separate the values of the enum into two or more enums or other types. For example, the Reflection APIs could have been designed as follows:

[Flags]
public enum Visibilities {
Public,
NonPublic
}
[Flags]
public enum MemberScopes {
Instance,
Static
}
[Flags]
public enum MemberKinds {

Constructor,
Field,
PropertyGetter,
PropertySetter,
Method,
}
public class Type {
public MemberInfo[] GetMembers(MemberKinds members,
Visibilities visibility,
MemberScopes scope);
}

AVOID using flag enum values of zero, unless the value represents "all flags are cleared" and is named appropriately as prescribed by the following guideline.

The following example shows a common implementation of a check that programmers use to determine if a flag is set (see the if-statement below). The check works as expected for all flag enum values except the value of zero, where the Boolean expression always evaluates to true.

[Flags]
public enum SomeFlag {
ValueA = 0, // this might be confusing to users
ValueB = 1,
ValueC = 2,
ValueBAndC = ValueB | ValueC,
}
SomeFlag flags = GetValue();
if ((flags & SomeFlag.ValueA) === SomeFlag.ValueA) {
…
}

ANDERS HEJLSBERG Note that in C# the literal constant 0 implicitly converts to any enum type, so you could just write:

if (Foo.SomeFlag == 0)…

We support this special conversion to provide programmers with a consistent way of writing the default value of an enum type, which by CLR decree is "all bits zero" for any value type.

DO name the zero-value of flag enums None. For a flag enum, the value must always mean "all flags are cleared."

[Flags]
public enum BorderStyle {
Fixed3D = 0x1,
FixedSingle = 0x2,
None = 0x0
}
if (foo.BorderStyle == BorderStyle.None)....

VANCE MORRISON The rational for avoiding zero in a flag enumeration for an actual flag (only the special enum member None should have the value zero) is that you can't OR it in with other flags as expected.

However, notice that this rule only applies to flag enumerations; in the case where enumeration is not a flag enumeration, there is a real disadvantage to avoiding zero that we have discovered. All enumerations begin their life with this value (memory is zeroed by default). Thus if you avoid zero, every enumeration has an illegal value in it when it first starts its life in the run time (we can't even pretty print it properly). This seems bad.

In my own coding, I do one of two things for nonflag enumerations.

If there is an obvious default that is highly unlikely to cause grief if programmers forget to set it (program invariants do not depend on it), I make this the zero case. Usually this is the most common value, which makes the code a bit more efficient (it is easier to set memory to 0 than to any other value).

If no such default exists, I make zero my "error" (none-of-the-above) enumeration value. That way when people forget to set it, some assert will fire later and we will find the problem.

In either case, however, from the compiler (and runtime), point of view, every enumeration has a value for 0 (which means we can pretty print it).

Type Design Guidelines for Reusable .NET Libraries

Adding Values to Enums

It is very common to discover that you need to add values to an enum after you have already shipped it. There is a potential application compatibility problem when the newly added value is returned from an existing API, because poorly written applications might not handle the new value correctly. Documentation, samples, and FxCop rules encourage application developers to write robust code that can help applications deal with unexpected values. Therefore, it is generally acceptable to add values to enums, but as with most guidelines there might be exceptions to the rule based on the specifics of the framework.

CONSIDER adding values to enums, despite a small compatibility risk.

If you have real data about application incompatibilities caused by additions to an enum, consider adding a new API that returns the new and old values, and deprecate the old API, which should continue returning just the old values. This will ensure that your existing applications remain compatible.

CLEMENS SZYPERSKI Adding a value to an enum presents a very real possibility of breaking a client. Before the addition of the new enum value, a client who was throwing unconditionally in the default case presumably never actually threw the exception, and the corresponding catch path is likely untested. Now that the new enum value can pop up, the client will throw and likely fold.

The biggest concern with adding values to enums is that you don't know whether clients perform an exhaustive switch over an enum or a progressive case analysis across wider-spread code. Even with the FxCop rules above in place and even when it is assumed that client apps pass FxCop without warnings, we still would not know about code that performs things like if (myEnum == someValue) ... in various places.

Clients might instead perform point-wise case analyses across their code, resulting in fragility under enum versioning. It is important to provide specific guidelines to developers of enum client code detailing what they need to do to survive the addition of new elements to enums they use. Developing with the suspected future versioning of an enum in mind is the required attitude.

Nested Types

A nested type is a type defined within the scope of another type, which is called the enclosing type. A nested type has access to all members of its enclosing type. For example, it has access to private fields defined in the enclosing type and to protected fields defined in all ascendants of the enclosing type.

// enclosing type
public class OuterType {
private string name;
// nested type
public class InnerType {
public InnerType(OuterType outer){
// the name field is private, but it works just fine
Console.WriteLine(outer.name);
}
}
}

In general, nested types should be used sparingly. There are several reasons for this. Some developers are not fully familiar with the concept. These developers might, for example, have problems with the syntax of declaring variables of nested types. Nested types are also very tightly coupled with their enclosing types, and as such are not suited to be generalpurpose types.

Nested types are best suited for modeling implementation details of their enclosing types. The end user should rarely have to declare variables of a nested type and almost never explicitly instantiate nested types. For example, the enumerator of a collection can be a nested type of that collection. Enumerators are usually instantiated by their enclosing type and because many languages support the foreach statement, enumerator variables rarely have to be declared by the end user.

DO use nested types when the relationship between the nested type and its outer type is such that member-accessibility semantics are desirable.

For example, the nested type needs to have access to private members of the outer-type.

public OrderCollection : IEnumerable {
Order[] data = …;
public IEnumerator GetEnumerator(){
return new OrderEnumerator(this);
}
// This nested type will have access to the data array
// of its outer type.
class OrderEnumerator : IEnumerator {
}
}

DO NOT use public nested types as a logical grouping construct; use namespaces for this.

AVOID publicly exposed nested types. The only exception to this is if variables of the nested type need to be declared only in rare scenarios such as subclassing or other advanced customization scenarios.

KRZYSZTOF CWALINA The main motivation for this guideline is that many less skilled developers don't understand why some type names have dots in them and some don't. As long as they don't have to type in the type name, they don't care. But the moment you ask them to declare a variable of a nested type, they get lost. Therefore, we, in general, avoid nested types and use them only in places where developers almost never have to declare variables of that type (e.g., collection enumerators).

DO NOT use nested types if the type is likely to be referenced outside of the containing type.

For example, an enum passed to a method defined on a class should not be defined as a nested type in the class.

DO NOT use nested types if they need to be instantiated by client code. If a type has a public constructor, it should probably not be nested.

If a type can be instantiated, it seems to indicate that the type has a place in the framework on its own (you can create it, work with it, and destroy it, without ever using the outer type), and thus should not be nested. Inner types should not be widely reused outside of the outer type without any relationship whatsoever to the outer type.

DO NOT define a nested type as a member of an interface. Many languages do not support such a construct.

In general, nested types should be used sparingly, and exposure as public types should be avoided.

Summary

This article presented guidelines that describe when and how to design classes, structs, and interfaces.

About the Authors

Krzysztof Cwalina is a Program Manager on the Common Language Runtime team at Microsoft Corporation. He began his career at Microsoft designing APIs for the first release of the .NET Framework. He has been responsible for several namespaces in the Framework, including System.Collections, System.Diagnostics, System.Messaging, and others. He was also one of the original members of the FxCop team. Currently, he is leading a companywide effort to develop, promote, and apply the design guidelines to the .NET Framework and WinFX. Krzysztof graduated with a B.S. and an M.S. in computer science from the University of Iowa.

Brad Abrams is a Program Manager on the .NET Framework team at Microsoft, where he has been designing the Framework Class Libraries for the past five years. He is the primary author of the .NET Framework Design Guidelines, the Common Language Specification, and the class libraries for the ECMA CLI specification.

About the Book

Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries
By Krzysztof Cwalina and Brad Adams



Published: September 19, 2005, Paperback: 384 pages
Published by Addison-Wesley
ISBN: 0321246756
Retail price: $39.99
This material is from Chapter 4 of the book.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • This paper introduces IBM Java on the IBM PowerLinux 7R2 server and describes IBM's implementation of the Java platform, which includes IBM's Java Virtual Machine and development toolkit.

  • Not all enterprise applications are created equal. Sophisticated applications need developer support but other more basic apps do not. With the right tools, everyone is a potential app developer with ideas and a perspective to share. Trends such as low-code development and model driven development are fundamentally changing how and who creates applications. Is your organization ready? Read this report and learn: The seven personas of enterprise app delivery How application ownership is spreading to the …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds