On Designing Good Libraries

Helping to grow the .NET Framework
in a consistent way is one of my daily passions. On
the one hand I am extremely pumped that we are signed up to grow the .NET Framework
in all kinds of exciting ways. On the other hand I am nervous about the number of
new developers and other API designers that have to get the sprit of the .NET Framework. It
is no longer a team of a few tight nit folks, it is now a HUGE undertaking. />

One of the ways I make myself feel
better about this undertaking is to spread the spirit of .NET in a two-day MSTE class
that covers the basics of API design. Mostly
this is a living version of the .NET
Framework Design Guidelines
. I spent sometime yesterday preparing for the next
session (this time we will have >200 folks). So,
naturally I thought of you folks and wanted to share much of that information with
you. Here is a sampling. Love
your feedback and comments on what else you’d like to make sure these new API designers
“get”. Examples of what you consider
good and bad APIs out of the shipping Framework are very helpful.

First Principles

• What
does your library look like to program against?

• Design
in reverse

• Write
the 3 lines of code the developer will have to write then model the APIs around making
that possible

• Example: Console.WriteLine()

• Understand
what it is like from other languages as well

Naming Patterns

• All
types and publicly exposed members are PascalCased

• Parameters
are camelCased

• These
show up in IntelliSense and docs

• Abbreviations
of more than 2 letters are cased as words, otherwise ALLUPPER

• IO
vs. Html

Naming Issues

• Good
naming is extremely hard

• Meaningful
but brief

• Use
US-English

• Ex:
Coluor vs. Color

• Avoid
abbreviations

• Principle
of least surprise

• Look
for prior-art

• Ex:
NumberOfElements vs. Count

Reference Types And Value Types

• When
to use what?

• Value
Types

• Value
semantics (such as an Int32)

• Small
instance size (< 16 bytes)

• For
efficient copying

• Typically
immutable

            For
example int is immutable; you can’t change the 5ness of 5

• Use
Reference Types everywhere else

Using Structs

• What
happens?

MyStruct[] arr = new MyStruct[10];

• The
constructor for MyStruct is not run here

• Do
not provide a default constructor

• Do
design for a meaningful “zero” state

• Do
not depend on a constructor always being run

Using Enums

• Very
similar to C++ enums

• All
integral values are legal; not just the defined ones

• But
always scoped by type name

• Performance
the same as underlying type (such as int)

• Use
enums to strongly type return types, parameters, properties,
etc.

• Use
an enum instead of static constants

• Validate
enum values

  File.Open
("foo.txt", (FileMode) 42);

• Do
not suffix enums with “Enum”

• Avoid
enums for user extensible values

Using
Enums

• Use
FlagsAttribute for enums that are combinable

• Use
powers of 2 for enum values

• Do
not use zero for any value

• Use
plural names

• Provide
named constants for common flags

[Flags] public enum FileAccess
{

Read = 1,

Write = 2,

Audit = 4,

ReadWrite = Read | Write

}

Using Arrays

• Do
not return internal instances of arrays

public sealed class Path
{

  private
Path () {}

  private
static char[] badChars = { '\"', '<'};

  public
static char[] GetInvalidPathChars() {

     return
badChars;

  }

}

• Callers
can simply set the value and change your internal data structure.

• Security
Issue!

Path.GetInvalidPathChars()[0]
= 'A';

• Clone
arrays before returning them

return (char[]) badChars.Clone();

• Consider
using a strongly typed collections rather than arrays for ease of use

• Do
not use readonly fields of arrays or other mutable types

• The
array reference itself is readonly and cannot be changed, but elements in the array
can be changed

public sealed class Path
{

private Path () {}

public static readonly char[]

InvalidPathChars
= {'\"','<','>'};

}

//Calling code:

Path.InvalidPathChars[0]
= 'A';

• Callers
can change the values in the array

• Do
return an empty array instead of a null reference

• Nulls
are generally unexpected to developers

• A
common usage pattern for arrays:

foreach (char c in Path.InvalidPathChars)
{

}

• Throws
an exception if InvalidPathChars returns null

Exceptions

• Exceptions
are “thrown”

• Suffix
with “Exception”

• Exceptions
rather than error codes

• Robust:
failures get noticed

• Your
method is defined to do something…

• If
it succeeds in performing its purpose, return

• If
it fails to do what it was written to do, throw an exception

• Only
create separate classes if you think developers will handle the exception differently
at runtime

• Do
not just map error codes on to a single exception with an error code property (e.g. ,
the WMIException)

• Use
separate exception types if this is really necessary

• Do
not catch and eat exceptions

• Exceptions
should be handled only where there is enough context to do the right thing

• That
is rarely true in a library

• An
example of what not to do is File.Exists()

• It
catches all exceptions and returns false, so you have no idea what the underlying
issue is

• You
should use try..finally 10 times as often as try..catch

• Catches
eat exceptions making it hard to debug

• Finally
allows you to clean up, but let the exception continue

• Every
exception should have at least the top three constructors

public class XxxException
: YyyException {

public XxxException () {}

public XxxException (string message)
{}

public XxxException (string message,

Exception
inner) {}

protected XxxException (

SerializationInfo
info,

StreamingContext
context) {}

}

• Note:
making an exception fully serializable implies a little more work…

• Create
new exceptions when needed

• Favor
shallow and wide exception hierarchies

• Error
Messages

• Localize

• Use
a complete sentence (end in a period)

• Don’t
expose privacy related information (such as file paths)

• Minimize
the number of exceptions you throw in your API’s success code-paths

• You
don’t pay for exceptions until you throw in managed code

• Throwing
exceptions degrades performance

• Perf
counters tell you exactly how many exceptions your application is throwing

• Consider
providing a way to avoid an exception being thrown

• Tester
methods

I’ll post more later if folks think
this is interesting…