| Matthew's profileMatthew WrenPhotosBlogLists | Help |
|
February 25 Separation of ConcernsSince I started writing this blog it has been through the mill a bit. I started looking into explaining some standard principles to use when writing extensible code, in particular I was trying to formulate a process whereby at each step you could apply a particular principle. It seemed logical to put separation first, but as I progressed through new principles the more I had to return to the same topic of separation. At times established principles just fell apart through not properly separating out concerns before hand, at others principles revealed new lines of separation in the modules. In short, separation proved to be a fundamental principle underlying good OO design. So, I have chosen to walk though a very simple calculator application to illustrate this point. The objective is to write a calculator that you can allow the developer to extend with new function buttons, and in order to keep the samples as simple as possible I have focused on this particular area of the problem. The first place any modern application will start is with the god-like class. Because it is how most dabblers cut their first code, and is an example of how not to do it. public class Calculator : Form { public void AddButton_Click() { //implementation here } }
A lot of developers do try to separate out their code, but unfortunately its not that easy to get right. The first simple separation we might make is separating the calculator out into its own class, and hence untangle it from the form (whose responsibility it is to draw controls and capture events on the screen). This seems sensible, but returning to our aim of achieving extensibility we still have a problem: how do I add a new function buttons to the class? Requiring future developers to modify the original class is plain bad - we are meant to be providing encapsulation, e.g. hide the complex inner workings of our calculator, it also assumes that the code base will not be compiled. But if it is compiled can we just derive from the base class and implement the new function there? There is a problem with this. Say, one person makes a derived calculator that adds a multiplication function and another derives one that adds a division button. If we wanted to combine these two we would end up with messy code, and as we continue the problem becomes exacerbated and more complex to solve (remember OO is there to break down complexity). Rather than a flaw in inheritance (which of course has its place) the flaw is rooted in a failure to fully separate out our concerns, or better still, correctly apply the single responsibility principle. In our example, then, let's separate the functions out into their own classes. Another crucial part of separating responsibilities is identifying who creates what. Some may have heard of GRASP, and in particular the creator principle, which states that B should create A if one of the following conditions are met:
Great, so I can create my buttons in the calculator class. Or can I? This is where there is a missing proviso in the creator principle. It assumes you have got your design right, how else will you know what aggregates or contains what? In reality deciding who creates what needs to come after you have hammered out your design. So the creator principle falls apart if you haven't correctly separated out your concerns. OK, so let's get back to our scenario. We want to be able to add and remove function buttons as we please, so we need a way of talking to a button without knowing exactly which button we are talking to. The answer to this particular problem is dependency inversion. Great! So, now we are starting to decouple our buttons from the calculator so that it can potentially talk to any button, but it's at this point a lot of people get confused as to how to keep the calculator talking to the abstraction. You might discover code like the following in the calculator class (switch statements are also symptomatic of not understanding dependency inversion): foreach (IFunctionButton functionButton in functionButtons) { if (functionButton is SquareRootFunctionButton) { //do some special rendering for product } else { //do normal rendering } }
Casting down into derivations breaks the basic principle of dependency inversion (you must be talking to an abstraction), and is avoidable. The fact you are breaking the principle means that you have not grasped all the reasons why we use dependency inversion and how it improves your general application design. To explain this let us take the example in hand. In effect the way in which a button is rendered is button specific. Therefore the calculator needs to ask the button how it wishes to be rendered, not visa versa. Once you understand this simple principle, you will see the simplest method would be just to allow the button abstraction to describe its rendering preferences, but for cases where the effect upon the calling class is indeterminate/complex, it might prove a lot easier just to yield the calculator's interface to the button which the button can manipulate itself (inversion of control). Again though if we ran ahead and implemented all of this we would be missing the point that dependency inversion (as with the creator principle) is reliant on us having our concerns separated out properly. We will see how these dependencies disappear once we start considering the views. But for now I want to show how principles can organically reveal the separations of concern. So let's continue with applying the dependency inversion principle. We need to yield to the button an interface to the calculator. But what interface do we yield? Part of SOLID is the interface segregation principle. This means giving the button only access to that interface which is needed. (Just as a footnote to this I would also advise that it is probably best to stick to passing interfaces rather than delegates. Delegates are like method interfaces so it is tempting to pass a delegate where just one method is required, but if you need to expand your interface for a good reason, I think it is preferable not to start swapping over from delegate calls to interface calls - I like the idea of using one paradigm for the same problem domain.) So in answer to which interface we yield to the function button, the answer would be an interface that allows us simply to set the next function the calculator will use once it has collected the two values on which to perform the operation, let's call that interface INextFunctionRegister. Now this is where the interface segregation principle is great at smashing out our separations. According to this principle we should not pass an interface to the entire functionality of the function button to the function register, but only the interface it requires, which we will call IFunction. Should we in fact untangle the function button from the function and just aggregate an IFunction in a function button? The answer is most certainly "yes", as this is separating our modules out into nicely defined single responsibilities. Now, the function button still has the problem of rendering itself on the calculator. If we add a method on the calculator to add the button, all the calculator is going to do is pass that straight on to the view. The calculator is really a redundant and passive agent in this interaction, as well as binding it unnecessarily to an interface it personally does not need. So, the solution is to add the function button directly to the view. Again getting the components talking to only the interfaces required reveals more of the lines of separation - we can start to see potential for breaking the view down into a function button panel, a display panel, etc. So here is the class diagram so far: As you can see the components are broken out now quite well now and offer the potential for nice and granular unit tests. In the next blog I will go on to discuss responsibility of component creation to complete the calculator example, and I might have a think about how this all fits into the world of MVC/P. I hope that this article might have served as food for thought, particularly regarding the importance of SoC. If you have any ideas or comments about what I have written I'd be very interested to hear them. TrackbacksThe trackback URL for this entry is: http://mdwren.spaces.live.com/blog/cns!9C2D8683D2D11400!303.trak Weblogs that reference this entry
|
|
|