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.