It is a common belief in the developer community that since design patterns encapsulate good and proven design solutions, one should use them as often as possible during software development. However, during our design assessments, we have come across many design cases where design patterns have been used but the quality of the resulting design is poor. Does this mean that the belief is wrong? If not, why is there a gap in achieving the desired design quality even when patterns have been applied? Let’s explore.
A design problem can be solved in multiple ways. As designers, we make certain design decisions based on our understanding and expertise to solve the problem in one of the ways. The final solution emerges as a result of the interplay of our chosen design decisions.
But, how do we choose certain design decisions over others? The answer lies in the problem “context”. Each design decision brings along with it a set of benefits and a set of liabilities. We, as designers, are responsible for analyzing the benefits and liabilities of a design decision based on the problem context. We need to understand the technical and business strengths and weaknesses, and constraints that emerge from the particular problem context to reason about how the benefits and liabilities of a design decision will affect the design.
Design patterns are indeed good and proven solutions for a design problem in a particular context. Yes, appropriate “context” is a key ingredient to making a pattern application fruitful for a software system.
Thus, if a pattern brings more benefits than liabilities to a particular problem context, it is generally considered a good design solution for that context. A word of caution here – even if a pattern brings more benefits than liabilities to a problem context, a designer must evaluate whether the liabilities significantly reduce the quality of design. If so, they need to be addressed via further design decisions including application of other patterns; otherwise, the designer may choose to ignore them.
On the other hand, if a pattern brings to a particular problem context a set of liabilities that outweigh the benefits from the pattern, applying the pattern can spoil the design or reduce its quality, and could be termed an anti-pattern. A pattern forced-fit in a design problem is not a good solution.
Let us consider an example where an abstraction A has to inform two other abstractions (say B and C) whenever it observes a change in a data structure. A naïve designer may instantly jump to the conclusion that the Observer pattern should be applied here. However, one has to take a deeper look to understand the “context”. If the context says that the abstractions that require the intimation about a change in the data structure are fixed and not going to change at run-time, then it is not required to implement the Observer pattern. In such a situation, if the Observer pattern is implemented, it results in unnecessary complexity (both in program structure, as well as during the run-time) with no benefits.
There are cases when two or more patterns compete with each other and the problem context decides a suitable pattern. Let’s consider an example of a simple application that needs to read data from different kinds of data stores and present it to the user via a GUI. Whenever a user initiates a data read via the GUI, the application should set up a connection with a particular data store, read the data and show it in the GUI, log the meta-data related to the read operation, and close the data store connection.
In this case, one may think to apply Strategy design pattern where each concrete strategy class realizes data read from one kind of data store. However, there are two problems with this choice of pattern:
- A lot of common code in Strategy hierarchy will be duplicated.
- In case of a change in the order of the steps (for example, if the meta-data has to be logged first before showing data to the GUI), then one has to change in all concrete Strategy implementations.
Here, an alternative to Strategy pattern is Template Method pattern. This pattern not only defines pre-defined steps as a protocol which is followed (and customized, where required) by all the concrete implementations but also pulls up all the common code to the abstract class (thus avoids duplication). In such a context, Template Method pattern is favored over Strategy pattern.
It is also worth realizing that selection of the right pattern is not the end of the story. Patterns come in numerous variations and we need to choose a right variant of the pattern fitting in the problem context. For example, one may consider the use of Observer pattern for a given design problem. However, he has to carefully analyze the design context to decide whether a push or a pull variant of the pattern would be more optimal in that context.
In summary, there are two insights that have emerged from our experience with designing real-world software applications:
- Context plays a big role in software design and architecture.
- Patterns are proven good solutions, but only when we apply them in appropriate design context and the problem.