Library tutorials & articles
A Twisted Look at Object Oriented Programming in C#
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>
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:
/// 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);
}
}
}
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!
Related articles
Related discussion
-
Help me how to dynamic create row column of TableLayoutpanel at run time ??????
by anatha1 (0 replies)
-
Very Urgent regarding deleting the images from a folder
by rameshbandi (2 replies)
-
How to Export Datagridview contents to Excel
by BarbaMariolino (8 replies)
-
Help accessing sound card
by daz4904 (0 replies)
-
How to Write a GPS Application
by stoyac (19 replies)
Related podcasts
-
Object-Oriented Programming in Ruby
In this episode, I talk with Scott Bellware about object-oriented programming in Ruby, and Ruby's object model. This is taken from a private conversation, and the audio quality suffers at times. Much thanks to Scott for allowing this to be released.This episode of the Alt.NET Podcast is bro...
thanks for the useful info
regards
If you have tried developing large extensible business applications where data needs are in the gigabytes if not terabytes in any static OO based programming language, then you are in for a lot of headaches. OOP does not lend itself well to this type of development as so called real world objects do not translate well into OO objects, most so called real world objects in the business world translate to data. This fact is lost on many managers, developers and analysts. Most applications in the real world are business related and not some academic project. I have been privy to many failed projects because of the selection of high level development language invariably they have always been statically typed. The one overlooked programming paradigm that has been neglected because of the OO hype is table or data driven programming. This form lends itself well to business related applications and can be used in conjunction with any programming paradigm but work wonders with a dynamically type languages. It is easily extensible, scalable and can be easily modified without throwing hundreds of users of the network.
As for Interfaces, function overloading, generics, templates, reflection, RTTI, default parameters, adding scripting etc, these are kludges to make a static language do what a dynamic language already does, and done badly at that.
When I first learned OO programming I was primarily excited by code re-use on the object side of the equation. Objects considered as providers of services to other code that acts as a client.
After many years of developing OO application I eventually grew to see the vast web of code dependencies and to realize that every piece of client code is itself serving some other code i.e. tier 1 calls tier 2 calls tier 3 (tier 2 code is being served by tier 3 but is serving tier 1). I realized that my objects provided a great deal of code re-use but what about all the code that used those objects? That is where Interfaces come in. Thanks to an interface all of your code that uses object A can be swapped out to use object B, C, D, or E regardless of the object type and therefore ancestry. Regardless of object implementation.
In another sense, Interfaces are more "fair". If an object can perform the services declared in the interface then it can be used by code that uses the interface. This means no strict binding to particular object hierarchies and that can save a lot of headaches.
Read the article even if you work with OO, give it to people that are just starting OO.
I'd really like to see this article as the core of explaining more OO topics, in the same format.
Workaround:
Save printable page as complete web page.
Edit default.css file in _files subfolder of saved page.
Change the CODE.Pre line to:
CODE.pre {
PADDING-RIGHT: 7pt; PADDING-LEFT: 7pt; WIDTH: 100%; COLOR: #000000; PADDING-TOP: 7pt; BACKGROUND-COLOR: #fbedbb
}
(removes the display:block)
RRIOS:
Thanks for the excellent write up on this concept. It has actually helped quite a bit to give me a better perspective on the subject of Interfaces. I particularly liked your analogy of Royal family vs democracy.
Thanks!
Inheritance is rigid. It´s like being born into the royal family. Sure, you get alot of privileges without doing anything, but you carry all the baggage and stigmas that come with your family name. You can´t JOIN the royal family. You have to be born into it.
Interfaces are democratic. A system that declares an interface is saying "I dont care who you are or how big or small you are: if you agree to these rules you may join the club". Think of your drivers license. The DVM declares an interface and ANYONE can use the road system as long as they stick to the rules (implement the interface).
In programming terms, suppose you are using two systems, in one you have created a Vehicle class and have several child classes like Car, Bicycle, and Bus. In the other you have cooking items, say for a recipe system. You may have classes like Flour, Eggs, Carrot. These two groups of classes have nothing to do with each other.
You create a third point of sale system for a retail store, and you plan to sell among other things, bicycles and flour. Your point of sale system could declare an IRetail interface that defines to other classes what they need to implement in order to participate in the Point of Sales system like Price(), TaxKey() and UPC().
This means you can reuse your Bicycle and Flour classes, just by adding the implementation of the IRetail interface in order to use them in the Point of Sale system. This new system will be capable of handling any kind of object as long as it implements IRetail.
So Interfaces are an advantage to both the system that declares the interface (they dont care what class of object they are handling) and to the classes that implement them (they can participate in any system as long as they agree to the rules).
http://www.jroller.com/page/haruki_zaemon/20031201#adapter_classes
An interface allows a developer, but more specifically a designer, to say "These features must be implemented." It is then up to the developer to work out the details of how to deliver on those features/abilities. It creates a boundary of expectation from other users of the object.
They can say "Oh I i see it implements IInterface, therefore it will definitely have X, Y and Z". It is quite useful when you wnat to implement concepts in multiple objects instead of concrete functionality. For instance, say you had two types of collections one was a tree and one was a jagged array of parent/child IDs (which in many ways is a tree represented another way but they are syntactically different). If you wanted to implement something like "GetParent", you couldn't (well maybe you could, but go with me for the sake of example) implement one generic function for "GetParent" that would work for both the Jagged Array and the Tree. YOu could however have both objects implement an interface, and then have the developer do the syntactic details of returning an items parent.
Anyway, more half baked analogies.
One of the questions that still persists for me, (and something I have never seen a discussion on) is what real benefit Interfaces offer; it seems they offer some, but that it is somewhat limited. Let me give an analogy, in the form of an old joke. An engineer, a phycicist and an economist were on a desert island with a can of pineapple that had survived the boat wreck. Both the engineer and the physicist made multiple attempts using their scientific knowledge to open the can of pineapple, but to no avail. After some time the economist remarked: "This shouldn't be a difficult problem to solve." "Well, tell us", replied the engineer and physicist in unison. Said the economist, " Assume a can opener."
This is what economists do. They make assumptions and think they have contributed something. Is this not like Interfaces, whose job, it appears, is to make policy statements without ever getting around to implementing anything? All the same amount of coding has to be done whether the interface is used or not; in fact there may be considerable duplication of code if minor changes to an implementation are needed. So what has been achieved? Not nearly as much, it seems, as inheritance would offer, were full inheritance of pure methods possible. This is perhaps why an Abstract class seems to be more useful.
I would appreciate anyone who can resolve my perplexity here. I could learn something.
Pretty good article. Its the first time i acutally laughed and smiled while reading your article. Very good read..
There are a few inaccuracies though just to point out...
1.
MyClass c;
c.someMethod()
does not throw a null reference exception. It actually throws a compiletime error "Use of unassigned local variable". In c# variables much be assigned before use or something like that..
2.
array[] = new array[10]
only returns an array of null references if it you are declaring an array of ref type. If it is a value type, the values will be assigned their default values..
and 3.
comparing type compatibility in most cases should use "as" instead of "is" because if you use is before casting the type, you in effect check the type twice, once before the cast, and once during the cast..
Its just an optimisation really..
Other than that, like i said before, best read i have ever had. Learned a few new words too like "gumption". Who would have thought..
This intro to C-Sharp is a very good tutorial, but its only an intro. The naming of variables and methods need to be standardized for easier readability. It also does not cover data driven dynamic web apps.
...it is a good article and Microsoft does advocate clearer naming of Object variables.
It's a shame you never LEARNED proper grammer.
This thread is for discussions of A Twisted Look at Object Oriented Programming in C#.