Often we want to modify existing software by inserting an additional step. Before we do operation X, we want to do operation Y. Consider a simple example of a LabelPrinter class, with a Print method. Suppose a new requirement has come in that before it prints a label for a customer in Sweden, it needs to do some kind of postal code transformation.
Approach 1 – Last Possible Moment
Most developers would immediately gravitate towards going to the Print method, and putting their new code in there. This seems sensible – we run the new code at the last possible moment before performing the original task.
public void Print(LabelDetails labelDetails) { if (labelDetails.Country == "Sweden") { FixPostalCodeForSweden(labelDetails); } // do the actual printing.... }
Despite the fact that it works, this approach has several problems. We have broken the Single Responsibility Principle. Our LabelPrinter class now has an extra responsibility. If we allow ourselves to keep coding this way, before long, the Print method will become a magnet for special case features:
public void Print(LabelDetails labelDetails) { if (labelDetails.Country == "Sweden") { FixPostalCodeForSweden(labelDetails); } if (printerType == "Serial Port Printer") { if (!CanConnectToSerialPrinter()) { MessageBox.Show("Please attach the printer to COM1"); return; } } // do the actual printing.... }
And before we know it, we have a class where the original functionality is swamped with miscellaneous concerns. Worse still, it tends to become untestable, as bits of GUI code or hard dependencies on the file system etc creep in.
Approach 2 – Remember to Call This First
Having seen that the LabelPrinter class was not really the best place for our new code to be added, the fallback approach is typically to put the new code in the calling class before it calls into the original method:
private void DoPrint() { LabelDetails details = GetLabelDetails(); // remember to call this first before calling Print DoImportantStuffBeforePrinting(details); // now we are ready to print labelPrinter.Print(details); }
This approach keeps our LabelPrinter class free from picking up any extra responsibilities, but it comes at a cost. Now we have to remember to always call our DoImportantStuffBeforePrinting method before anyone calls the LabelPrinter.Print method. We have lost the guarantee we had with approach 1 that no one call call Print without the pre-requisite tasks getting called.
Approach 3 – Open – Closed Principle
So where should our new code go? The answer is found in what is known as the “Open Closed Principle”, which states that classes should be closed for modification, but open for extension. In other words, we want to make LabelPrinter extensible, but not for it to change every time we come up with some new task that needs to be done before printing.
There are several ways this can be done including inheritance or the use of the facade pattern. I will just describe one of the simplest – using an event. In the LabelPrinter class, we create a new BeforePrint event. This will fire as soon as the Print function is called. As part of its event arguments, it will have a CancelPrint boolean flag to allow event handlers to request that the print is cancelled:
public void Print(LabelDetails labelDetails) { if (BeforePrint != null) { var args = new BeforePrintEventArgs(); BeforePrint(this, args); if (args.CancelPrint) { return; } } // do the actual printing.... }
This approach means that our LabelPrinter class keeps to its single responsibility of printing labels (and thus remains maintainable and testable). It is now open for any future enhancements that require an action to take place before printing.
There are a couple of things to watch out for with this approach. First, you would want to make sure that whenever a LabelPrinter is created, all the appropriate events were hooked up (otherwise you run into the same problems as with approach 2). One solution would be to put a LabelPrinter into your IoC container ready set up.
Another potential problem is the ordering of the event handlers. For example, checking if you have permission to print would make sense as the first operation. The simplest approach is to add the handlers in the right order.
Conclusion
Always think about where you are putting the new code you write. Does it really belong there? Can you make a small modification to the class you want to change so that it is extensible, and then implement your new feature as an extension to that class? If you do, you will not only keep the original code maintainable, but your new code is protected from accidental breakage, as it is isolated off in a class of its own.
5 comments:
All good. Talking of events... I really like Udi Dahn's take on Domain Events, it's really elegant.
http://www.udidahan.com/2009/06/14/domain-events-salvation/
Could you see a similar approach working for your example?
That's a nice approach, and also very useful for a Command model for a client app. The only down-side is having to have all your objects able to get at the DomainEvents class (and running the risk of people raising events from innapropriate places). So I'm not sure I would want all my events to work in that way, but for the example I gave it would be a powerful approach. It would let you set up your event handlers before the LabelPrinter class was even created.
Taking this kind of approach then actually starts to look - albeit on a smaller scale - like a well built composite application, a la "prism" (http://msdn.microsoft.com/en-us/magazine/dd943055.aspx)
I do like the idea of using events, better known as the observer pattern, to decouple objects. However you shouldn't overdo it as it makes it more difficult to follow the control flow within your code. It may also lead to slightly more housekeeping where you need to take care to de-register the event handler from the event in order to prevent zombie objects eating up your memory.
http://www.martinfowler.com/eaaDev/OrganizingPresentations.html#observer-gotchas
thanks sjobak. Yes, the unsubscribing thing can be a problem if the object that raises the events is long-lived, and the objects that listen are short-lived.
Post a Comment