More Requirements for the International E-Commerce Case Study
New requirement: Access multiple SQL database systems |
In the process of writing the international e-commerce application, suppose I get a new requirement to support both Oracle and SQL Server databases. Both of these systems are based on SQL (Structured Query Language), the common standard that makes it easier to use databases. Even though this is a common standard at the general level, however, there are still differences in the details.
This steps are the same at the general level… |
For example, I know that in general, when executing queries on these databases, I will use the following steps:
1.
|
Format the
CONNECT command.
|
2.
|
Send the database the
CONNECT command.
|
3.
|
Format the
SELECT command.
|
4.
|
Send the database the
SELECT command.
|
5.
|
Return the selected dataset.
|
…but the details differ |
The specific implementations of the databases differ, however, requiring slightly different formatting procedures.
The Template Method Pattern
Standardizing on the steps |
The Template Method is a pattern intended to help us generalize a common process, at an abstract level, from a set of different procedures. According to the Gang of Four, the intent of the Template Method pattern is as follows:
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Redefine the steps in an algorithm without changing the algorithm's structure.[1]
[1] Gamma, E., Helm, R., Johnson, R., Vlissides, J., Design Patterns: Elements of Reusable Object-Oriented Software, Boston: Addison-Wesley, 1995, p. 325.
In other words, although there are different methods for connecting and querying Oracle databases and SQL Server databases, they share the same conceptual process. The Template Method pattern gives us a way to capture this common ground in an abstract class while encapsulating the differences in derived classes. The Template Method pattern is about controlling a sequence common to different processes.
Applying the Template Method to the International E-Commerce Case Study
The details are varying |
In the international e-commerce case study, the variations in database access occur in the particular implementations of the steps involved. Figure 19-1 illustrates this.
Figure 19-1. Using the Template Method pattern to perform a query.
How this works: Virtual methods for the steps that vary |
I have created a method called doQuery that handles the query I need to perform. I pass it the name of the database and the query specification. The doQuery method follows the five general steps above, providing virtual methods for the steps (such as formatConnect and formatSelect) that must be implemented differently.
The doQuery method is implemented as follows. As shown in Figure 19-1, it first needs to format the CONNECT command required to connect to the database. Although the abstract class (QueryTemplate) knows this format needs to take place, it doesn't know how to do it. The exact formatting code is supplied by the derived classes. This is true for formatting the SELECT command as well.
The Template Method pattern manages to do this because the method call is made via a reference pointing to one of the derived classes. That is, although QueryControl has a reference of type QueryTemplate, it is actually referring to an OracleQT or an SQLSvrQT object. Thus, when the doQuery method is called on either of these objects, the methods resolved will first look for methods of the appropriate derived class. Suppose our QueryControl is referring to an OracleQT object. Because OracleQT does not override QueryTemplate, the QueryTemplate's doQuery method is invoked. This starts executing until it calls the formatConnect method. Because the OracleQT object was requested to perform doQuery, the OracleQT's formatConnect method is called. After this, control is returned to the QueryTemplate's doQuery method. The code common to all queries is now executed until the next variation is needed—the formatSelect method. Again, this method is located in the object that QueryControl is referring to (OracleQT in this example).
When a new database is encountered, the Template Method provides us with a boilerplate (or template) to fill out. We create a new derived class and implement the specific steps required for the new database in it.
Using the Template Method Pattern to Reduce Redundancy
Eliminating redundancy can lead to abstractions |
Many times in my consulting practice, I work with teams of people who are very sharp but who do not yet have strong backgrounds in object orientation. They are transitioning to agile methods, and they appreciate the need for concepts such as eliminating redundancy, having strong cohesion, and having loose coupling; however, they do not know how to go about getting there.
On one occasion, I was asking the team lead to describe a typical problem they had. He related to me how they had to support systems for different partner companies. The rules were pretty much the same, but there were always subtle differences. The code was becoming increasingly hard to maintain because it had so many "if-then-else" statements scattered throughout to check which situation was current and how to handle it. He could only see two alternatives, neither of them very good:
Continue putting in more "if-then-else" statements, further degrading the code
Copy and paste the code for each case, resulting in duplication
Using "if-then-else" statements or switches is the common approach taken here. The difficulty with this is switch creep. At first, a few "if-then-else" statements or switches is not bad. But at some point (around 57 of them?) the code is really difficult to understand. The advantage of the copy-and-past approach is that although it doesn't seem particularly elegant, at least each section is clear because it only relates to one situation.
Because I knew the Template Method pattern, I could offer a third alternative. I will illustrate the alternative in two steps. First I will show what happens when people copy and paste and then update the code. It turns out that even if all of the code is changed, there is still a process common to both; if not there wouldn't be a reason to do the copy and paste. Second, I'll show how after this duplication of process is recognized, one can refactor the code to eliminate it.
Copy and paste and changing code leaves redundancy |
Figure 19-2 shows an example of some "code" that I will use to show the shortcomings of copy and paste. Note that I am just showing the "code" as a sequence of letters, because the details of the code do not matter. I am trying to help you see the problems of copy and paste more easily.
Figure 19-2. The original code.
After I use copy and paste to change the code, I might end up with a new sequence of code, as shown in Figure 19-3. Note that in this copy-and-paste operation, some of the a's have been converted to A's, b's to B's, etc. Where a letter has changed to a capital, this indicates code that was changed after the copy and paste. Where it remains lowercase, this is code that was simply duplicated. The new row of X's is code that was added to the new case, but did not exist at all in the original.
Figure 19-3. The original code and the new code.
Different types of duplication |
There are at least two types of duplication here. The first is the more obvious: the lines with c's, f's, and i's, are duplicated code. I copied and pasted the original code, and then changed most, but not all of it. The part I didn't change is obviously redundant. However, there is another duplication. There is a sequence of operations that is common to both code fragments. In other words, this type of copy and paste is mostly employed when there is a well-defined sequence of steps but the implementation of some of the steps has changed. I illustrate this in Figure 19-4.
Figure 19-4. Comparing the code to identify redundancies.
The steps point out those things that are conceptually the same, but that are implemented in different ways (the difference illustrated here by the use of capital and lowercase letters, and by the addition of new code in some cases, shown as X's). So, for example, in the original class Step 1 consisted of lowercase a's and b's, but in the new (second) class, it consisted of capital A's, X's, and B's. The step exists in both classes, and so that fact is redundant, even though the implementation is not.
Eliminating the duplication |
The Template Method pattern could be used to eliminate duplication here. It prescribes a base class that implements the step sequence. Each case then has its own derivative class to implement the specific steps, as shown in Figure 19-5.
Figure 19-5. Leads to the Template Method pattern.
Some details missing |
To actually implement this takes filling in several details. First of all, the steps in the derived classes need to be declared as abstract and/or protected methods in the base class. Second, there may have been local method variables used between the steps. These either need to be passed in and returned or made as data members of the class. Those common to both steps should be put in the base class.
An approach to avoid the duplication in the first place |
Of course, refactoring to fix a bad situation after it is identified is preferable to just leaving it alone. However, refactoring to avoid the bad situation in the first place is preferable even more. In this situation, if we recognized the Template Method pattern approach's viability when the second system became a requirement, we could take the following approach:
1.
|
First, refactor the first solution (using the
Extract method) to pull out the code that will change. This is shown in
Figure 19-6. By the way, "pull out the code that will change" should sound a little similar to the Gang of Four's advice, "find what varies and encapsulate it." Instead of specializing the method, I have the
someMethod method contain other methods that will change.
Figure 19-6. Refactoring out the steps that will change. |
2.
|
Second, create a base class that contains the method (
someMethod) that will not change. The code now looks like the code in
Figure 19-5 for
BaseClass and
MyClass. (I have not yet written
MySecondClass.)
|
3.
|
Write
MySecondClass. Note that adding the new function now will not affect any other code except for the factory (and possibly not that).
|
Of course, wherever I indicate that I should "extract method," you might notice that this would not have been necessary if I'd been programming by intention in the first place. Good practices like this one tend to show up again and again, which is why they're good practices in the first place.
Knowing patterns helps guide refactoring |
This is a generally good approach for refactoring. When a new requirement needs to be handled, take this two-step approach:
1.
|
Refactor your code without adding any function so the new function can go in following the open-closed principle.
|
2.
|
Add your new code, touching only the factory and the new code.
|
Field Notes: Using the Template Method Pattern
The Template Method is not coupled Strategies |
Sometimes a class will use several different Strategy patterns. When I first looked at the class diagram for the Template Method pattern, I thought, "Oh, the Template Method pattern is simply a collection of Strategies that work together." This is dangerous (and usually incorrect) thinking. Although it is not uncommon for several Strategies to appear to be connected to each other, designing for this can lead to inflexibility.
The Template Method pattern is applicable when there are different, but conceptually similar processes. The variations for each process are coupled together because they are associated with a particular process. In the example I presented, when I need a format a Connect command for an Oracle database, if I need a format a Query command, it'll be for an Oracle database as well.