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