Sunday, November 21, 2010

Designing For Change - Part 3 of 3 (Applying the GoF Advices)

In part2, we discussed about the approaches to design suggested by GoF and what they actually mean when applied to the problem and how they help in achieving code qualities. If you have not gone through the last blog, then please read that first as in this blog we will work through a design problem and see how we can use advice given by GoF to solve it.

Let us consider a file transfer program which sends stream of data over network. It has a 'Sender' which works in following steps:
  1. Compress the data using LZW (Lempel-Ziv-Welch) algorithm
  2. Encrypt the data using  AES (Advanced Encryption Standard)
  3. Transmit the data using TCP (Transmission Control Protocol)


Now, say we have a requirement that we need to switch the transmit method between TCP and UDP depending on some user input. So as an obvious solution we go ahead and create a new class, say, 'SenderUDP' which inherits from 'Sender' class and overrides the 'Transmit()' method, and this solves the problem.


Now, say, we have got a new requirement that the 'Sender' should also be able to work with multiple compression algorithms (for e.g. RLE, SFC, Huffman Coding etc) and encryption algoritms (for e.g. DES, Tripple DES, Serpent etc). Proceeding in previous manner, for compressing with RLE and transmitting with  UDP, we create a new class, say, 'SenderUDP_RLE' which inherits from 'SenderUDP' and overrides 'Compress()' method. For compressing with RLE and transmitting with TCP, we create a new class, say, 'SenderRLE' which inherits from class 'Sender' and overrides 'Compress()' method, and so on and so forth. Continuing this way, we will have a class hierarchy which would look like as shown in picture below.


So "Is there any problem with this approach?". Looking carefully at the hierarchy, we can see:
  • There is lot of code duplication (Redundancy). For, example classes 'SenderUDP_RLE' and 'SenderRLE' both uses RLE for compression but they have their own local copy of code for this.
  • Class explosion. For each new variation, the number of class is increasing multiplicative. If we have 'p' types of compression, 'q' types of encryption and  'r' ways of transmitting data, we will end up having p*q*r classes.
  • Weak cohesion and tight coupling. Each of the classes are responsible for doing three unrelated tasks - compression, encryption and transmission. Code for all these tasks have easy access to state of 'Sender' but at the cost of tight coupling.
Now let us try to apply the approaches to design as suggested by GoF. 
  • Consider what should be variable in your design and 'encapsulate the concept that varies'
  • Design to interfaces, not to implementations
  • Favor object composition over class inheritance

There is no specific order in which these approaches should be applied. They kind of work in parallel. Let us start with first advice: Consider what should be variable in your design and 'encapsulate the concept that varies'. Let us analyze the 'Sender' class which is responsible for compressing, encrypting and transmitting the data. we can see that we have different ways(algorithms) in which we can compress, encrypt and transmit data and in future we may need to use new algorithms to perform these tasks. Hence, we would like our design to be able to accommodate new ways of compressing/encrypting/transmitting data with least possible TCO(Total Cost of Operation). In order to achieve this, we identify that, Compression, Encryption and Transmission, are the three concepts that should be variable in our design.


To encapsulate the variation in these three concepts, we need to define three conceptual entities. Remember, GoF said Design to Interfaces, not to Implementations. So let us define three interfaces to represent these concepts:
  • ITransmit containing Transmit() method for transmitting data
  • ICompress containing Compress() method for compressing data
  • IEncrypt containing Encrypt() method for encrypting data

Since the 'Sender' class is the one that will be marshaling these operations, we need to define relationships between the 'Sender' class and the above interfaces(concepts) we defined. Remember, GoF said Favor object composition over class inheritance. So let us make the 'Sender' class have a reference to an instance of each of these concepts (or interfaces).


Finally, we implement these interfaces in concrete classes with each concrete class responsible for one particular algorithm. The final design will look like as shown in the picture below:



Let us analyze some of the main benefits that this design brings us:
  • The 'Sender' class is now decoupled from the fact which particular algorithm(or concrete class) is used for compression/encryption/transmission. It only knows that the object whose reference it has will do the work it. Hence, we have loose coupling. 
  • For each of the concepts(interfaces) we defined, we can now have as many variation as needed, with each variation implemented in a separate concrete class, without the 'Sender' class being affected even slightly. Hence, the design can now easily accommodate any new way of compressing, encrypting or transmitting data.
  • Also, for 'p' types of compression, 'q' types of encryption and  'r' ways of transmitting data, we will end up having only (p + q + r) classes as opposed to p*q*r class previously. Hence, problem of class explosion is solved.
  • No redundancy. Each piece of code doing a particular thing occurs only once in design.
  • Testability is highly improved. The 'Sender' class and each of the algorithm for compression, encryption and transmission can now be tested in isolation.
  • The 'Sender' can switch between different algorithms for compression, encryption and transmission at run-time, without re-instantiating the full 'Sender' class.

If you have studied Design Patterns before, you will see that we have just came up with a design pattern called "The Strategy Pattern". Now you see how does the advices given by GoF helps the design to evolve step by step. Following are few basic points we should keep in mind about design patterns:
  • Design Patterns are examples. Each pattern is an example of a design that follows this general advice by GoF well, in a given context. Each pattern is manifestation of these advices and code qualities playing in particular context. 
  • Design patterns are discovered, not invented. They are often what you would do, if you thought of it, to solve the same problem. 
  • Studying design patterns is a good way to study good design, and how it plays out under various circumstances. 
  • Even if you do not see any cataloged design pattern in your design, you should always follow the advice by GoF as these advices are key forces that eventually lead to a good design.

1 comment: