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. This chapter covers the various types you will use in the .NET framework.

This chapter is from the book

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 4-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. Extensibility and base classes are covered in Chapter 6.

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

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 MARIANA

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!

4.1 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 PEOIN

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.
See section 3.3.1 for details.

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.

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.

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

4.1.1.1

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.

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

4.1.1.2

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.

4.1.1.3

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