Friday, November 12, 2010

Designing For Change - Part 2 of 3 (Approaches to Design)

In part1, we discussed about the problem of user requirements and code qualities which are first step towards a good design. In this blog, we will be discussing about approaches to design. If you have not gone through the last blog, then please read that first as this blog builds on top the discussions in part1.

Approaches to Design
You might have heard about the famous book on software design patterns "Design Patterns: Elements of Reusable Object-Oriented Software" by Erich Gamma, Richard Helm. Ralph Johnson and John Vlissides often referred to as GoF (Gang of Four). This was the first book on design patterns in software engineering, published in 1994. The book was essentially a catalog of best practice design patterns. The first two chapters basically treat us on object oriented programming(OOP) design and paradigm as they understood it, which is actually different from the OOP design that we learnt first time during our college days when we got our hands on with OOP languages like C++ and Java. However since this was primarily an academic book (it was in fact Eric Gamma’s phd thesis), a lot of people have hard time in extracting this advice from those first two chapters. These advices are difficult sometimes for people who had grown up with previous OOP concept, to understand what exactly they mean and how exactly it is supposed to help us. Basically, GoF said three things:
  • Design to interfaces, not to implementations
  • Favor object composition over class inheritance
  • Consider what should be variable in your design and 'encapsulate the concept that varies'

Design to interfaces
As we read 'Design to interfaces', we might think all we can design to is interfaces and public method of the entities in my design. But this is not what GoF actually meant. They were actually trying to talk about what is revealed and what is hidden and how decisions are made in design. What they meant is an entity should be used with complete disregard to its implementation. We should design classes and method signatures purely from the perspective of consuming entities.

To understand this let us consider an example, say you are sitting with your friends. You go to your friend 'A' and ask him "Tell me your driving license number?". 'A' takes out his wallet of his pocket, removes the driving license from it and read the license number. Then you go to friend 'B' and ask the same question and he calls back his home and gets the driving license number. Then you go to friend 'C' and he tells you the license number from memory and so on. The point to be noted here is that, you asked each person same question in same way to get the same result, but with different people getting it in different ways. However, if you would have asked "Take out your wallet, remove driving license and tell me your driving license number". Then this would have worked fine with friend 'A' but not with 'C'. The reason being, in this case you have been 'implementation specific' in using the person and hence tightly coupled with the 'implementation' of getting driving license number. So the lesson is, design to outward behavior(interfaces) and not to specifics of inner implementation.

Also GoF said, to hide the implementation of your entities and for a good reason, so that you have the freedom to change the implementation later on. This improves coupling as we cannot couple to what is hidden(implementation) and relation will exist only between public interfaces.

As we saw in part1, Programming by Intention is a systematized way of designing to interfaces, at the method level. Now, let us analyze the relationships between classes in a design. Let us consider two abstractions, AbstractionA and AbstractionB as shown in figure below. Impl1 to Impl3 are implementations/derived classes of AbstractionA and Impl4 & Imple5 are implementations/derived classes of AbstractionB.


Now if these two issues(or whatever they are in the design) need to have relationship in between, then we might be tempted to create a relationship between the implementations (or concrete classes). But the GoF is saying if you can get away with it, try not to do that. Try to keep relation up high between the abstractions. There are multiple benefits of doing this. The obvious one is, keeping relationship high between the abstractions, results in fewer relationships(one to one or one to many) as opposed to n*m (3*2 in this case) relationships between implementations.

So summarizing the things, two points should be kept in mind.
  • First, when considering the signature of a method (its name, parameter list, and return type), make this decision based upon the need of the entity(ies) that will consume it, not from its implementation details. 
  • Second, when creating relationships between entities (typically classes), try to do this between abstract types, not concrete types, whenever possible.
 
Favor object composition over class inheritance
The next thing that GoF said is favor composition over inheritance. Before we delve deeper into it, lets first see what these things are. Let us take an example here of a network socket which communicates stream of data over the network but uses two different ways of compressing data before sending. So using inheritance what we have is, a base class 'socket' and two derived classes 'cmp1' and 'cmp2' with different ways of compressing data.


The class 'socket' will have all the base behavior of socket with probably a default behavior for compressing data or it may be an abstract class. The derived class will have the compression behavior overridden one way in 'cmp1' and another way in 'cmp2'. This is called class inheritance, which is done at design time. Another way of doing the same thing would be to use contained polymorphism. That is to pull out the compressing idea out into a base class and create two versions of it and use it through delegation at run time.

What GoF were really saying was that this kind of inheritance where the socket is inherited into different version is actually inheritance for specialization. This distinction is critical to understand what GoF are actually talking about because if you will read the book of GoF, you will find that inheritance has been used all over the book. Seems like they are not following their own advice :).

Remember that they said “Favor” not that don’t use inheritance at all. In this case here we are using inheritance to specialize something real (socket). What GoF are saying is don't use inheritance for that, at least most of the time.  Its better to use composition. However, you may think we are using inheritance in case of 'composition' also. But point to be noted here is that we are not specializing something real here. The 'compression' idea is just a concept, not a real thing. Specific compression are real but generic idea of compression is not anything that you can create an instance of, because it is just a concept. This is inheritance for categorization. Like dogs and cats are animals. Animals off-course are not real things but dogs and cats are. So GoF are saying that use inheritance for categorization and use composition to handle variation at run time rather than through inheritance at design time.

Looking at the two cases
Specialization:
  • As we can see, in case of specialization, we have one less class than we have in case of composition, as we need one extra class to represent the conceptual entity (compression) for delegation to work. We often think having fewer number of classes in our design is good. But literally that’s not the case. Otherwise best designs in the world would have had single class only :). Adding classes is not necessarily a bad things as long as it gets you something.
  • Also, since the compression code is part of socket so it has easy access to state of socket. But it also means that it is completely coupled with the socket code. The problem with that is one class is having two different responsibilities(weakly cohesive), compressing data and transmitting. Hence, the compression code with a bug can harm the transmitting code and vice-versa or it can create side effects which is dangerous.
  • Also inheritance for specialization works well only if nothing else in socket varies and as more things start varying it will create redundancy and tight coupling which we do not want. Also, choosing specialization here would mean that we are predicting that nothing else in future will vary and we know we are not good at 'predicting' business, hence by choosing inheritance for specialization we will not be setting ourselves for a design that is accommodating to changes in future.
  • Another point to note is, this works well only if socket doesn’t have to switch between compression methods at run-time. In case we need to change the compression method at run time, say based on some user input, then we will have to re-instantiate the socket class and dispose the old one and along with that we will also need to restore the state in which the previous socket object was.
Composition:
  • Composition does adds an extra class, but it improves cohesion. Now each class has only one responsibility (strong cohesion). Socket class is about transmitting only and each compression class is about one particular compression technique only.
  • Also, we can vary socket (different version of it) without changing the compression object. This considerably increases the testability of design. We can test the socket and the compression in completely isolated environment. Unit test that passes for compression today will pass tomorrow also irrespective of changes in transmitting code and vice-versa.
  • Also with delegation we can handle different compression types in one run-time session without affecting the state of socket instance (by using setters on socket class). It allows us to defer the decisions until run-time.
So what GoF is saying is, that, we should lean towards delegation rather than inheritance. If we have one way or the other, don’t choose inheritance (for the reasons we just covered). Specializing functions with inheritance is a short path to problems. Inheritance for specialization does work but does not scale up. Till one thing vary and there is no requirement for dynamism, its fine, but as more things start to vary, the design starts to fall apart. But with composition approach, you can keep doing it again and again and again. There might be other behaviors with socket that might vary in future, so we can pull out those variations. And actually this improves the things by making socket simpler and simpler. The point that should be kept in mind is our focus is not just on what the design will do for us today but also where it is leading us to and how easily it can accommodate changes that we cannot foresee now.

Encapsulate the variation
Whenever we talk of encapsulation, we essentially think of hiding the data. But actually encapsulation is any kind of hiding at all and whenever we do it, it always help us, as hiding anythings gives us the freedom to change it later. So what GoF said:
  • Identify the varying behavior or consider what should be variable in your design.
  • Define an entity that encapsulates this variation conceptually.
This is often interpreted as a "Design Up-Front" point of view, because of the notion that certain things "should be variable".  In fact, given that the book was published in 90's, this may be the case.  However, in lean-agile software development, we can still follow this advice in a new context: something should vary when we have a requirement, based on business value, for it to do so.  The critical aspect here is, that such variations should be encapsulated.  Also, we must acknowledge that this refers to any variation, not simply varying behavior.  For e.g. it could be varying relationships, cardinality, sequence, construction, dependencies, structure etc. All these things can vary, and when they do, all these variations should be encapsulated.  In a sense, every design pattern encapsulates a different varying thing, or set of things, conceptually.

Basically, taken all together what is being said is we want to treat the things which are conceptually similar as if they are exactly the same thing. This allows us to deal with things purely at abstract level.


In the figure above, client here is not even coupled to fact that 'AbstractionA' here is an abstraction, from its point of view it might be a simple single concrete class. We want to have that kind of relationship even when we know we have variation here. Because we want to be able to add another variation by adding a new implementation rather than modifying the existing implementation. This is called Open closed principle – open for extension closed for modification.

How GoF Advice Promotes Quality
Design to Interfaces:
  • Helps eliminate redundant relationships
  • Avoid subclass coupling
Favor aggregation over inheritance:
  • Promotes strong cohesion
  • Helps eliminate redundancies
  • Makes designs more dynamic
Encapsulate Variation:
  • Promotes encapsulation
  • Decouples client objects from the services they use
  • Leads to component-based architecture

Coming Up Next
In part3 i will
take up a design problem and demonstrate how to apply these approaches to design while trying to work out the final design. Till then stay tuned :).

No comments:

Post a Comment