Introduction
A good class design should be such that there should be a loose coupling between the different classes involved and the design must be extensible. A loose coupling between classes means that the interdependency between them should be minimal. Extensibility is the ease with which the design can take changes in functionality.
In this article, I have taken a simple real life problem and tried to solve it using three different approaches:
- A simple approach
- Interface based approach
- Delegate based approach
The Problem Statement
An employee is assigned a certain time consuming task. He is supposed to finish this task and notify his managers about the result.
A Simple Approach
This approach is a usual developer approach which aims to solve the problem at hand and not think about future enhancements. So as per the problem, we can identify two entities namely Manager
and Employee
. So let us have two classes Employee
and Manager
. Employee
will have a method which we shall name as DoWork
and Manager
will have a method called Notify
. The implementation is as given below:
Employee:: DoWork
public void DoWork(string workDesc,
params Manager[] managers)
{
Console.WriteLine("Doing work... - " + workDesc);
Thread.Sleep(3000);
foreach(Manager manager in managers)
manager.Notify("Done with the work!!!");
}
Manager:: Notify
public void Notify(string status)
{
Console.WriteLine("Manager {0} notified.
Status of work: {1}",_name, status);
}
There is a tight coupling between the two classes in this approach. What would happen if the functionality changes. Let us say that along with managers, the employee also needs to notify the customers about the status of the work. We would then have a new class called Customer
. The DoWork
method would then need to take an array of customers and also notify them. This would require some rework of the Employee
class.
Interface Based Approach
Taking extensibility into consideration, we change the design slightly by adding an interface called IBoss
. IBoss
declaration is as given below:
public interface IBoss
{
void Notify(string status);
}
The Manager
class and the newly added class called Customer
would implement IBoss
.
Let us change the DoWork
method of the Employee
and get rid of the parameter managers
. So how do we notify all the bosses? For this, let us add an interface called IWorker
with a declaration as given below:
public interface IWorker
{
void Register(IBoss boss);
void UnRegister(IBoss boss);
void DoWork(string workDesc);
}
Employee
will now implement IWorker
as follows:
private ArrayList _bosses = new ArrayList();
public void Register(IBoss boss)
{
_bosses.Add(boss);
}
public void UnRegister(IBoss boss)
{
_bosses.Remove(boss);
}
public void DoWork(string workDesc)
{
Console.WriteLine("Doing work... - " + workDesc);
Thread.Sleep(3000);
for(int index = 0; index < _bosses.Count; index++)
{
IBoss boss = (IBoss)_bosses[index];
boss.Notify("Done with the work !!!");
}
}
This is an implementation of the Observer design pattern. Here the worker is an observable object and the bosses are the observers. This certainly seems to be a better approach as anyone who needs to get notified needs to implement the IBoss
interface and register with a worker. Hence once implemented, there is absolutely no change required in the Employee
class!!! Moreover the bosses are not tied to the Employee. They are free to register with any Worker
.
Delegate Based Approach
Though the interface based approach seems to be the right solution, let us also try out implementing the same using delegates. Delegates and Interfaces are conceptually similar because they are both implementation contracts.
First of all, we need to declare a delegate
.
public delegate void NotifierDelegate(string status);
The Register
method of the Employee
has to be changed to take this delegate
as a parameter.
private NotifierDelegate _notifyDelegate;
public void Register
(NotifierDelegate notifyDelegate)
{
_notifyDelegate = notifyDelegate;
}
Employee::DoWork
would now look like this:
public void DoWork(string workDesc)
{
Console.WriteLine("Doing work... : " + workDesc);
Thread.Sleep(3000);
if(_notifyDelegate != null)
_notifyDelegate("Done with the work !!!");
}
The Manager
and Customer
classes now need not implement from any interface. They just need to have methods that can be encapsulated by the NotifierDelegate
.
NotifierDelegate manDelegate
= new NotifierDelegate(manager.Notify);
NotifierDelegate custDelegate
= new NotifierDelegate(customer.SetStatus);
Let us now combine the delegates to form a multicast delegate. Note that since the NotifierDelegate
has been declared with a void
return type, this is possible.
NotifierDelegate combinedDelegate
= manDelegate + custDelegate;
All we then have to do is to register the multicast delegate
with the employee and call DoWork
.
Employee emp = new Employee();
emp.Register(combinedDelegate);
emp.DoWork("Get the vegetables.");
This approach is very similar to the interface approach. But here the classes are not bound to implement any interface. They only need to have methods which match the delegate
signature. The employee also need not know whom to notify and which methods to call. It just has a set of delegate
s which it needs to invoke. Hence this removes the tight coupling between the two classes.
Conclusion
I hope this article has given the readers an insight into how to make the class design more manageable and extensible. I also believe that it has demonstrated the application of delegates in a design and the similarities between interfaces and delegates.
History
- 1st June, 2007: Initial post
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.