Home
Index Chapter
6
Chapter 7
A Twisted Look at Object Oriented Programming in C#
By Jeff Louie
10/28/2002
I must admit that my first exposure to object oriented programming (OOP) was
frustrating and difficult. As a hobbyist I have struggled through Z80 assembly
and EPROM burners, BASIC, Turbo Pascal, Java, C++ COM and now C#. The move to
event driven programming and then to object oriented programming presented major
conceptual hurdles to my function driven sequential programming mindset. The “aha”
moment when OOP made sense was most gratifying, but did not come quickly or
easily. It has been a few years since I “got” the OOP mindset and I feel
comfortable enough now to try to help fellow travelers with this journey. If OOP
comes easily to you, feel free to skip this tutorial. If you are having problems
getting your mind around objects and inheritance I hope this tutorial can help
you. This tutorial does not represent a conventional teaching method. It assumes
a passing knowledge of the C# language and familiarity with the Visual Studio
.NET IDE. This is a work in progress and may require correction or
revisions.
Comments are actively requested (email: [email protected]).
Useful Texts
I highly recommend the following books. Much of my understanding of OOP has
been gleamed from these “classic” texts and then reinforced from coding
database projects in Java, C++ and C#. At all times I willfully try to avoid
plagiarizing these authors, but my understanding of OOP is so closely tied to
these texts that I must cite them as sources of knowledge right from the
start!
Object-Oriented Analysis and Design with Applications Grady
Booch, Second Edition, Addison-Wesley, 1994, 589pp.
Design Patterns Elements of Reusable Object-Oriented Software
Gamma Helm, Johnson and Vlissides, Addison-Wesley, 1994, 395pp.
Object-Oriented Software Construction Second Edition Bertrand
Meyer, Prentice Hall, 1997, 1254pp.
Of course, some of this material is a descendent of my writing from our now
out of print book:
Visual Café for Java Explorer Database Development Edition
Brogden Louie and Tittle, Coriolis, 1998, 595pp.
Chapter 7 "To Inherit or Contain? That is the
Question."
In Chapter 2, you visited the design conundrum of using inheritance or
containment. In this chapter you will use both. First you will use inheritance to create a custom, type
safe, null safe collection. You will then use containment to wrap the custom collection and
"adapt" the read/write collection interface to a read only interface.
Here again is the discussion from Chapter 2: One difficult design decision is to decide if a class should inherit from a
parent class or hold a reference to a new object. Inheritance represents an IS_A
relationship from a generalization to a specialization. Containment represents a
HAS_A relationship between the whole and a part. So a car IS_A motorized
vehicle, but HAS_A radio. The two relationships can be expressed in code thusly:
class Radio
{
...
}
class Vehicle
{
...
}
class Car : Vehicle
{
Radio r= new Radio();
}
Let’s Get Your Inheritance
One common real world task is to create a type safe, null safe collection. For instance,
you might want to create a collection that can only store elements that are non
null references of type Drawable. This allows you to iterate over the
collection casting each element and then calling a public method on every
element without fear of a NullReferenceException
or a InvalidCastException.
Here again is the Drawable type implemented as an interface:
// an interface version of Drawable
interface Drawable
{
void DrawYourself();
}
class Circle : Drawable
{
public void DrawYourself()
{
System.Console.WriteLine("Circle");
}
}
class Square : Drawable
{
public void DrawYourself()
{
System.Console.WriteLine("Square");
}
}
You can now create a type safe, null safe collection by extending the abstract base class System.Collections.CollectionBase.
CollectionBase was designed for use as a base class of a custom type safe
collection. Extending CollectionBase automatically
exposes all of the public methods of CollectionBase such as Count
and GetEnumerator(). Here is an example of using inheritance to create a
type safe, null safe collection of Drawable elements. The set indexer calls the
type and null safe Insert method..
Note: You could also create a type safe collection by creating a class that contains
an ArrayList and provides pass through getter and setter methods that take and
return only references of type Drawable. This would require that you provide a
do nothing pass through method for every public type safe method in ArrayList that you
want to expose in the containing class.
/// <summary>
/// DrawableCollection
/// A type safe, null safe collection of Drawable objects
/// Demonstrates the use of Inheritance
/// A DrawableCollection IS_A Collection
/// Extends CollectionBase to create a specialization
/// </summary>
class DrawableCollection : System.Collections.CollectionBase
{
// Custom implementations of the protected members of IList
// returns -1 if parameter is null
public int Add(Drawable value)
{
if (value != null)
{
// throws NotSupportedException
return List.Add(value);
}
else
{
return -1;
}
}
public void Insert(int index, Drawable value)
{
if (value != null)
{
//throws ArgumentOutOfRangeException
List.Insert(index, value);
}
// else do nothing
}
public void CopyTo(Array array, int start)
{
//throws ArgumentOutOfRangeException
List.CopyTo(array, start);
}
// provide an indexer
public Drawable this[int index]
{
get
{
// ArgumentOutOfRangeException
return (Drawable)List[index];
}
set
{
//throws ArgumentOutOfRangeException
Insert(index,value);
}
}
}
The key here is that all of the setter methods (Add, Insert, set) validate
for non null and take a reference of type Drawable. Any attempt to pass a null
reference will be ignored. Any attempt to pass a reference to an object that
does not support the Drawable interface will fail. This guarantees that all
elements in the collection are of the type Drawable and are not null. This
allows you to iterate over the
collection without fear of a NullReferenceException
or a InvalidCastException like this:
foreach(Drawable d in drawableCollection)
{
System.Console.WriteLine(d.ToString());
}
Note: Using foreach hides the call to GetEnumerator().
Here is the explicit call using IEnumerator:
System.Collections.IEnumerator enumerator= dc.GetEnumerator();
while (enumerator.MoveNext())
{
System.Console.WriteLine(((Drawable)(enumerator.Current)).ToString());
}
C# supports the concept of an indexer which supports random access to a
collection using the index operator ([]). A custom indexer does not add support
for a Length property. Here again is the get and set code that creates an
indexer:
// provide an indexer
public Drawable this[int index]
{
get
{
// throws ArgumentOutOfRangeException
return (Drawable)List[index];
}
set
{
//throws ArgumentOutOfRangeException
Insert(index,value);
}
}
You can then use the indexer like this:
// create a DrawableCollection
DrawableCollection dc= new DrawableCollection();
dc.Add(new Circle());
// test indexer
Drawable draw= (Drawable)dc[0];
A Better Class Hierarchy
Although the type safe, null safe collection above works, you could improve the
class design by first creating a null safe collection. The following class
simply insures that null objects cannot be inserted into the collection. Note
that all of the setters are declared protected as this class was designed to be
extended, not instantiated.
// A null safe collection. This class is meant to be
// extended by a type safe class so that the setter
// methods are protected.
class NullSafeCollection : System.Collections.CollectionBase
{
// class is not meant to be instantiated, only inherited
protected NullSafeCollection(){}
// Custom implementations of the protected members of IList
// These methods are for internal use by a type safe subclass
// returns -1 if parameter is null
protected int Add(object value)
{
if (value != null)
{
// throws NotSupportedException
return List.Add(value);
}
else
{
return -1;
}
}
protected void Insert(int index, object value)
{
if (value != null)
{
//throws ArgumentOutOfRangeException
List.Insert(index, value);
}
// else do nothing
}
// provide an indexer
protected object this[int index]
{
get
{
//throws ArgumentOutOfRangeException
return List[index];
}
set
{
//throws ArgumentOutOfRangeException
Insert(index,value);
}
}
// expose single public method, get only CopyTo
public void CopyTo(Array array, int start)
{
//throws ArgumentOutOfRangeException
List.CopyTo(array, start);
}
}
You can now extend from this null safe collection,
creating a type safe and null safe collection of Drawable elements. The advantage of this
design hierarchy is that you can now reuse the NullSafeCollection class to
create a different type safe collection class. Here is the final type safe, null
safe class:
class DrawableCollection : NullSafeCollection
{
// Custom implementations of the protected members of IList
// returns -1 if parameter is null
public int Add(Drawable value)
{
return base.Add(value);
}
public void Insert(int index, Drawable value)
{
base.Insert(index, value);
}
// provide an indexer
public new Drawable this[int index]
{
get
{
//throws ArgumentOutOfRangeException
return (Drawable)base[index];
}
set
{
//throws ArgumentOutOfRangeException
base.Insert(index,value);
}
}
}
Note the key word new which
tells the the compiler that you are explicitly shadowing or hiding the indexer
in the base class. You cannot override the base indexer since the subclass has a
different return type than the base class.
Wrap It Up Please (Using Containment)
Another common task is to pass a "read only" reference to a caller. (This
restriction is available in C++ using the key word const on
a pointer in
the method parameter list declaration. Using const on a pointer prevents
corruption of any data that can be touched with the pointer within the method.) One way to pass a
"read only" reference to
a collection in C# is to
"wrap" a reference in another object. This is an example of
using containment. As I view it, the read/write interface of the
DrawableCollection is "adapted" to a read only interface. When you
adapt an existing interface to a new interface, you are using the Adapter
Design Pattern. Wrapping or adapting a class is a common idiom. (For instance,
you might want to wrap an unmanaged legacy Win32 dll function in a managed C#
class.)
Here is a read only class WrapCollection that contains a
private reference to a DrawableCollection.
/// <summary>
/// WrapCollection
/// Demonstrates wrapping our DrawableCollection to limit access
/// Demonstrates the use of Containment
/// WrapCollection contains, HAS_A, DrawableCollection
/// Adapts the read write interface to a read only interface
/// Demonstrates the Adapter Design Pattern
/// </summary>
class WrapCollection
{
private DrawableCollection collection;
// constructor
public WrapCollection(DrawableCollection collection)
{
if (collection != null)
{
// reference to an existing collection
this.collection= collection;
}
else
{
// new empty collection
this.collection= new DrawableCollection();
}
}
// provide a get only indexer
// throws ArgumentOutOfRange if collection is empty
public Drawable this[int index]
{
get
{
// throws ArgumentOutOfRangeException
return (Drawable)collection[index];
}
}
public System.Collections.IEnumerator GetEnumerator()
{
return collection.GetEnumerator();
}
public int Count
{
get
{
return collection.Count;
}
}
}
The key here is to declare the reference variable collection
as private. Declaring collection private, prevents the public user of the
WrapCollection object from accessing any of the setters in the contained
DrawableCollection.
private DrawableCollection collection;
Note the design decision to create a new empty
DrawableCollection if the caller passes null to the WrapCollection constructor. This design
allows the caller to blissfully iterate over the contained collection using
IEnumerator or foreach
without throwing a runtime exception.
// constructor
public WrapCollection(DrawableCollection collection)
{
if (collection != null)
{
this.collection= collection;
}
else
{
this.collection= new DrawableCollection();
}
}
If the user passes null to the WrapCollection constructor, the calls to
GetEnumerator() and Count will still be valid, returning an empty enumeration
and a count of zero.
Test It!
Go ahead. Compile the DrawableCollection and WrapCollection. Then use the
following code to test the type safe, null safe collection. Passing a
WrapCollection prevents the caller from modifying the contained collection since
the collection is not visible outside of the class.
class Test
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
//
// TODO: Add code to start application here
//
// create a DrawableCollection
DrawableCollection dc= new DrawableCollection();
dc.Add(new Circle());
dc.Add(new Circle());
dc.Add(null);
dc.Add(new Square());
dc.Insert(1,new Square());
// test indexer
Drawable draw= (Drawable)dc[0];
System.Console.WriteLine(draw.ToString());
// dc[0]= null; // no action
// dc[0]= "Hello"; // fails at compile time
// test Count
int num= dc.Count;
System.Console.WriteLine(num.ToString());
// test CopyTo
Drawable[] copy= new Drawable[num];
dc.CopyTo(copy,0);
foreach(Drawable d in copy)
{
System.Console.WriteLine(d.ToString());
}
// test IEnumerator
foreach(Drawable d in dc)
{
System.Console.WriteLine(d.ToString());
}
// Create a WrapCollection
WrapCollection wrap= new WrapCollection(dc);
// try this instead!
// WrapCollection wrap= new WrapCollection(null);
// test Count
int count= wrap.Count;
System.Console.WriteLine(count.ToString());
// test indexer
if (count > 0)
{
System.Console.WriteLine(wrap[0].ToString());
}
// test IEnumerator
foreach(Drawable d in wrap)
{
System.Console.WriteLine(d.ToString());
}
System.Console.ReadLine();
}
Be careful, the code can still throw an ArgumentOutOfRangeException.
Well, I hope you have a better feel for inheritance and containment!
All Rights Reserved Jeff Louie 2002
|