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.

More by Author

Must Read