On Designing Good Libraries -- Part IV
As
always, comments welcome!
Object Oriented
Design"urn:schemas-microsoft-com:office:office" />
•
This is not an OO Design
class
•
A type represents a set of
responsibilities
•
Modeling the real
world
•
Group related responsibilities
together in a class
•
Only 5% of developers will ever
explicitly extend base classes
•
But 100% will use
classes
Danger of over
design
•
The most common library design
mistake
•
A new dev/pm is designing the widget
feature
•
Wants it to be extensible and feature
rich
•
The reality of shipping
hits
•
Design only partly thought
out
•
Long bug tail
•
Forced hard
cuts
•
We ship it cumbersome and
broken
•
In V.next
•
We now we know the extensibility needed
•
But blocked by the broken design the
shipped
Abstract and Base
classes
•
Base classes serve as the root of an
inheritance hierarchy
•
Abstract classes are a special kind
of base class that are non-instantiable and may contain members that aren’t
implemented
•
Prefer broad, shallow
hierarchies
•
Less than or equal to 2 levels –
Rough rule!
•
Contracts and responsibilities are
difficult to maintain and explain in deep complex
hierarchies
•
Consider making base classes not
constructible
•
Make it clear what the class is
for
•
Provide a protected constructor for
subclasses to call
•
System.Exception should not have had
a public constructor
Virtual, Abstract
and Non-virtual
•
Virtual members are
points of specialization or callbacks in your code
public
virtual int Length { get {..} }
•
Abstract
members are required points of
specialization in your code
public
abstract int Read()
•
Non-virtual members
cannot be overridden in derived types
public
string Remove (int index, int count)
public class
Foo {
public override string ToString()
{
return
"foo";
}
}
public class
Bar : Foo {
public override string ToString()
{
return
"bar";
}
}
What is printed
out?
Bar b
= new Bar ();
Console.WriteLine (b.ToString());
Foo f =
b;
Console.WriteLine (f.ToString());
Object
o = b;
Console.WriteLine (o.ToString());
•
They all print “bar”.
Why?
•
Method call virtualizes at
runtime
•
The static type doesn’t
matter
•
This is the danger and power of
virtual methods
•
Danger: Owner of base classes cannot
control what subclasses do
•
Power: Base class doesn’t have to
change as new subclasses are created
Overriding
•
Don’t change the semantics of
member
•
Follow the contract defined on the
base class
•
Don’t require clients to have
knowledge of your overriding
•
Consider whether you should call the
base implementation
•
Favor calling it unless you have
good reason not to
Virtual and
non-virtual
•
Use non-virtual members unless you
have specifically designed for specialization
•
Have a concrete scenario in
mind
•
Write the
code!
•
Think before you virtualize
members
•
Modules that use references to base
types must be able to use references to derived types without knowing the
difference
•
Must continue to call in the same
order and frequency
•
Cannot increase or decrease range of
inputs or output
•
See the Liskov Substitution
Principle (https://www.brent.worden.org/tips/2000/liskovSubstitutionPrinciple.html)
Abstract
Members
•
Methods, Properties and Events can
be abstract
•
Use abstract members only where it
is absolutely required that subclasses provide a custom
implementation
•
Only use when the base class cannot
have any meaningful default implementation
•
Default to making members
non-virtual
•
Make it virtual if it is designed to
be specialized by subclasses
•
Make it abstract if no meaningful
default implementation is possible
•
Unless versioning issues prohibit
it, in which case throw a NotImplementedException
Interfaces versus
Base Classes
•
Favor using base classes over
interfaces
•
Base classes version better in
general
•
Allows adding
members
•
Members can be added with a
default implementation
•
Avoids incompatibilities common in
ActiveX
•
Interfaces are good for versioning
behavior (changing semantics)
•
Avoid having both base class and
interfaces
•
Adds confusion about which to
use
•
Component vs.
IComponent
•
Little
advantage
•
Consider using Aggregation
•
Don’t use attributes where a
contract is needed
Explicit method
implementations
•
Implementing members of an interface
“privately”
•
Not a security
boundary!
•
Only accessible when cast to the
interface type
•
Hides implementation
details
•
Clean public interface
•
IConvertible on Int32,
etc
•
Differentiates
implementations
•
Simpler strong
typing
•
The 19 ToXxx methods on IConvertible
don’t “pollute” the Int32 public view
•
But they are there when cast to
IConvertible
•
Solution: Implement them
privately
public
struct Int32 : IConvertible, IComparable {
public override string ToString ()
{..}
int
ICovertible.ToInt32 () {..}
...
}
int i =
42;
i.ToString(); //
works
i.ToInt32(); // does
not compile
((IConvertible) i).ToInt32(); // works
•
Interfaces developed by different
groups can have the same signature for different
meanings
•
Draw() a picture and Draw() a
gun
•
Frequently you want to differentiate
the implementation
•
Explicit method implementations
enables this
•
Avoids us recommending “unique”
names in interfaces
interface
IGraphics {
void
Draw();
Brush Brush { get; set;
}
Pen Pen { get; set;
}
}
interface
IBandit {
void
Draw();
void Duel(IBandit
opponent);
void = st2 ns = "urn:schemas:contacts" />Rob(Bank bank, IBandit[]
sidekicks);
}
class
Bandit: IBandit, IGraphics {
void IBandit.Draw()
{...}
void IGraphics.Draw()
{...}
}
•
There is a natural tension between
generic typing and strong typing
•
List of Object vs. List of Employees
•
Generic
typing
•
Polymorphism
•
Strong
typing
•
More “ns = "urn:schemas-microsoft-com:office:smarttags" />RAD” experience
•
Avoids boxing of value
types
•
Avoids ugly
casts
•
Often mutually
exclusive
•
You can’t have both of these in the
same class
•
They differ only in return
type
This gives you a
compile time error
public
object this [int index] {
get{..} set{..}
}
public string this [int
index] {
get{..}
set{..}
}
This
works:
public class
StringList : IList
public
string this [int index] {
get{..}
set{..}
}
object IList.this [int index]
{
get{..} set{..}
}
}
StringList
slist = new StringList();
string s1 = slist[0];
IList genList =
slist;
string s2 = (string) genList[0];
Of course Generics are a better
solution