We all know that code should be loosely coupled. That is, objects should avoid knowing too much about unrelated objects. If an object shouldn't need another object to exist and function, then why should that object know about it at all?
There's a lot of nuance in this concept, but part of it goes back to programming in layers.
So what happens when this nuance comes into play?
For a few years now, I've had a solution that worked well enough, but violated loose coupling principles.
I've dealt with this lack of eloquence for a time, partly because I didn't really have a better solution, and partly because it worked well enough.
But I've got a solution now, and it was something that I knew about all along: Event Handlers.
An Event Handler is the perfect solution for loosening up the coupling of your objects, because it puts the flow in the proper perspective. In code, there are vendors, consumers, and states. The Vendor in code is the code that has methods, attributes, etc. The Consumer is the code that uses another object's methods, attributes, etc. A state is sort of an abstract concept that describes when something happens - for example, when an object is out of work to do, or when a button gets clicked, or when an item is added to a collection.
A Vendor should generally have to know as little as possible about the consumer. This is a principle that my code has often failed to follow religiously. And the effect is tightly coupled code.
Consider a System.Windows.Forms.Button object. The Button object knows very little about other controls on a form, and it certainly doesn't know anything about your custom controls that do all sorts of funky stuff. And yet, you can make it so that a click of a button has an impact on all the other controls on the form.
How can this be?
Well, the short answer is by hooking an event handler up to the Clicked event of the button.
This is something a programmer learns within about 10 hours of programming in WinForms or WPF or whatever UI tool he (or she) has. But there's a bigger principle at play that a 10 hour old programmer probably doesn't know, and that is this loose coupling principle.
The Button has a Clicked event that some consumer (in this case, the Form that the Button is on) can subscribe to, and pass a delegate function to. So when the event fires (the button gets clicked), the consumer delegate that is hooked up to the vendor's (Button's) event gets called, and any behavior defined in the consumer's custom delegate gets executed.
This model is a beautiful one, and the underlying principle opens up a whole world of more eloquent solutions.
Consider (some version) of my real life example.
In an application I built about 8 months ago, I have an object called ActivityMonitor. The ActivityMonitor object keeps track of what's going on in the application, and exposes that information when a TCP/IP client requests it.
So, the ActivityMonitor looks something like this:
class ActivityMonitor
{
ActivityCollection _activities;
public void AddActivity(string activity)
{
Activity a = new Activity(activity);
_activities.Add(a);
}
public string GetActivityString()
{
return _activities.ToString();
}
}
So, before considering Event Handlers as a solution for this problem, when non-related objects did something, and I wanted the ActivityMonitor to know about it, I would send the ActivityMonitor to the consumer object as a parameter. This is a tightly coupled pattern, and as I see it now, something I'll avoid in the future.
For example:
public class BigSpecialObject
{
ActivityMonitor _monitor;
public BigSpecialObject(ActivityMonitor monitor)
{
_monitor = monitor;
}
public void DoStuff()
{
_monitor.AddActivity("About to do a bunch of stuff");
///Do a bunch of stuff
_monitor.AddActivity("Done doing a bunch of stuff");
}
}
When I look at BigSpecialObject, I can infer a few things:
1. The consumer of BigSpecialObject knows about ActivityMonitor
2. BigSpecialObject is tightly coupled with ActivityMonitor
3. It didn't have to be this way
The issue of state comes into play. Presumably, at instantiation time, BigSpecialObject is not done doing stuff. After BigSpecialObject.DoStuff() is called, at some point, BigSpecialObject is done doing stuff. This is a great example of where an EventHandler is not only appropriate, but it is the optimal solution.
So, with a couple extra lines of code, I can really "bust some heads" (to use a Ghostbusters reference):
public class BigSpecialObject
{
public event EventHandler WorkRequested;
public event EventHandler WorkCompleted;
public BigSpecialObject()
{
/// Notice that BigSpecialObject
/// knows jack-squat about ActivityMonitor
}
public void DoStuff()
{
this.alertWorkRequest();
///Do a bunch of stuff
this.alertWorkDone();
}
private void alertWorkRequest()
{
if (this.WorkRequested == null)
return;
this.WorkRequested(this, new EventArgs());
}
private void alertWorkDone()
{
if (this.WorkCompleted == null)
return;
this.WorkCompleted(this, new EventArgs());
}
}
So, the consumer of BigSpecialObject would take advantage of this loose coupling by doing the following:
public class Consumer
{
ActivityMonitor _monitor;
BigSpecialObject _bso;
public Consumer()
{
this.initialize();
}
private void initialize()
{
_monitor = new ActivityMonitor();
_bso = new BigSpecialObject();
_bso.WorkRequested += new EventHandler(onBSOWorkRequested);
_bso.WorkCompleted += new EventHandler(onBSOWorkCompleted);
}
private void onBSOWorkRequested(object sender, EventArgs e)
{
_monitor.AddActivity("About to do a bunch of stuff");
}
private void onBSOWorkCompleted(object sender, EventArgs e)
{
_monitor.AddActivity("Done doing a bunch of stuff");
}
public void DoSomeConsumption()
{
_bso.DoStuff();
}
}
So, there you have it. Pattern versus Antipattern. Having the Consumer class alert the ActivityMonitor when the work is done is better, because that means that BigSpecialObject is more reusable. And even though we added more lines of code (believe it or not, I try to find solutions to problems that minimize lines of code), this is a better solution, because of the looser coupling.
In looking at code I've written in the past, it's completely littered with examples of tight coupling. Well, I guess it's time to get to work!
No comments:
Post a Comment