On Designing Good Libraries -- Part II

You asked for it, you got it..
feedback always welcome.

Attribute
Usage


Suffix with
“Attribute”


Perf tip: seal attribute
classes for faster runtime lookup


Specify the AttributeUsage attribute
completely


Don’t rely on the
defaults!


Be as restrictive as
possible


You can always open it up
later

[AttributeUsage(

AttributeTargets.All,
Inherited = true,

AllowMultiple = false)]


AttributeTargets – where is
the attribute allowed to be applied?


Inherited – should
derived members/types be considered to have this
attribute?


AllowMultiple – Is it
legal to put more than one instance of the attribute on a particular
member?


Use constructor arguments for
required parameters (positional arguments)


Provide a read-only property with
the same name


Use read-write properties for
optional parameters (named arguments)


Never use overloaded
constructors

[AttributeUsage(AttributeTargets.All,
AllowMultiple=true,

Inherited=false)]
public
class NameAttribute : Attribute {
public NameAttribute (string
userName) {..}
public
string UserName {get {..}}

public int Age {get {..} set{..}}
} //end
class

Usage:

[NameAttribute("Bob",
Age=87)]

UserName – positional argument

Age – named
argument

Static
Classes


Static classes contain just static
members


Compromise between pure OO design
with usability


Commonly used
for


Shortcuts for other operations
(System.IO.File)


Functionality for which a full OO
wrapper is unwarranted (System.Environment)

public
sealed class Environment
{

  private Environment(){} //prevents
creation

  public static void Exit(int exitCode)
{..}

  public static int ExitCode
{

    get
{..}

    set
{..}

  }

  public static string CommandLine
{

    get
{..}

  }

}


Best used
when:


Clear charter for the
class


Not a “miscellaneous”
bucket


Not the center point of a
design


Use
sparingly


Watch out for disconnected
design


Static
classes


Are
sealed


Have private default
constructor


No instance
members


Static Classes: Bad
Design


Late in the final milestone we added
a method to tell if the runtime is being shut down


However we added the method as an
instance method making it completely uncallable


public sealed class Environment {

   private Environment()
{} //Prevent creation

// ---snip---
public bool HasShutdownStarted {

get {
return nativeHasShutdown(); }
}
public
static string UserName { get {...} }
private static extern bool
nativeHasShutdown();
//
---snip---
}

Constructors


Do minimal work in the
constructor


Only capture the
parameters


Cost is delayed


You can throw exceptions from
constructors


Be consistent in the ordering and
naming of constructor parameters

public class
Foo {
private const string defaultA =
"..";
private const
string defaultB = "..";

public Foo():

this(defaultA,defaultB) {}
public Foo(string a):
this(a, defaultB)
{}
public Foo(string a,
string b) {

/* do work here */
}
}


Many languages automatically add a
public default constructor if you don’t specify any


Abstract classes get a protected
constructor


These two code snippets are
equivalent:

public class
Foo {

}

public class
Foo {

   public Foo ()
{}

}


Always explicitly add a default
constructor to avoid versioning issues


Adding a new constructor removes the
default one, breaking clients

//
V1

public class
Foo {

}

Calling Code works:
Foo f = new Foo()

//
V2

public class
Foo {
public Foo (int
value)

}

Calling code
breaks: Foo f = new Foo()

Method
Usage


Use overloading only when the
overloads do semantically the same thing


Incorrect
overload:

String.IndexOf(string
value) {}

String.IndexOf(char[]
anyOf) {}


Correct
overload:

Convert.ToString(int
value) {}

Convert.ToString(double
value) {}


Used to avoid
boxing


Write(object) works for any
type


But specialization avoids
boxing


Do only when completely special
casing

public
static void Write (bool value);
public static void Write (int
value);
public static void Write (double value);
public static void Write
(object value);


Use appropriate default
values


Simple method assumes default
state


More complex methods indicate
changes from the default state

MethodInfo
Type.GetMethod (string name);
//ignoreCase =
false

MethodInfo
Type.GetMethod (string name,
boolean
ignoreCase);


Use a zeroed state for the default
value (such as: 0, 0.0, false, “”, etc. )


Be consistent in the ordering and
naming of method parameters


Only the method with the most
parameters should be virtual if needed

public class
Foo {
private const
string defaultForA = "a default";
private const int defaultForB =
42;
public void
Bar(){

Bar(defaultForA, defaultForB);
}
public void Bar (string
a){

Bar(a, defaultForB);

}
public
/*virtual*/ void Bar (string a, int b){
// core
implementation here

}
}


Variable number of arguments
(e.g.
printf)


Use params

public
static string Format(string format,

     params object[]
args);


Not used for in/out params (e.g. scanf)


Only provide overloads for
performance reasons IF you special case each code
path

void Format
(string formatString, object arg1)

void Format
(string formatString, object arg1, object arg2)

void Format
(string formatString, params object [] args)


Allowing method inlining by the
JIT


Minimize the use of virtual
methods


Don’t write really large
methods


Don’t have large numbers of
locals

More to come...