http://www.developer.com/

Back to article

Working with Design Patterns: Strategy


June 13, 2007

Some developers hold design patterns up as a lofty standard, an ideal. I've even seen some developers worship patterns so much that they were lax to use them on anything but a high-visibility implementation. On the contrary, many design patterns should really be used fairly frequently, almost to the point where you just think of them as standard programming constructs. Don't put patterns on a pedestal.

In other words, you shouldn't wait for the perfect example. You also shouldn't fear design patterns. Although the original Design Patterns book is daunting, most of the patterns it contains are simple to understand and apply. Many design patterns are so simple that you might find yourself using them every few minutes. Certainly, the iterator pattern falls into this category, but so do patterns such as template method and strategy.

The strategy pattern, according to Design Patterns, says to "define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it [Gamma]." Doesn't sound too tough!

The first thing to remember with patterns is that you must have a need. The first time I code a solution, or part of a solution, I look to keep the implementation as simple as possible. Usually, that means I'm not even thinking about patterns. As I add new code to the current, existing solution, I look for ways to eliminate redundancies, to simplify complex code areas, or to just make the code easier to test. As I look to make these corrections, I'll often recognize that the best approaches to doing so are recognized design patterns. From there, I'll use my knowledge of the pattern to help guide me toward an end goal.

For an example that demonstrates derivation of the strategy pattern, I'll build a portion of an SQL generator. The basic idea is that, given metadata about relational tables and columns, the generator returns an appropriate SQL string. In the spirit of keeping things as simple as they need to be, I'll concentrate on just the piece that generates the values clause for a date column that needs to be inserted.

The application that uses the SQL generator needs to support MySQL, according to my customer. As part of the SQL generator, I need to be able to produce an insert value for MySQL. MySQL expects dates to be formatted in year-month-day order, with each of month and day being two digits.

'2007-07-15'

Here's a test I wrote that specifies the basic functionality needed to support MySQL:

import static org.junit.Assert.*;
import java.util.*;
import org.junit.*;

public class DateColumnTest {
   private static final String NAME = "name";
   private DateColumn column;

   @Before
   public void initialize() {
      column = new DateColumn(NAME);
   }

   @Test
   public void create() {
      assertEquals(NAME, column.getName());
   }

   @Test
   public void insert() {
      assertEquals("'2007-07-15'",
         column.insertString(DateUtil.createDate(2007,
            Calendar.JULY, 15)));
   }
}

The code in DateColumnTest uses a simple Date utility method. Here's its code:

import java.util.*;

public class DateUtil {
   private static final Calendar calendar = Calendar.getInstance();

   public static Date createDate(int year, int month,
      int dayOfMonth) {
      calendar.set(year, month, dayOfMonth);
      return calendar.getTime();
   }
}

The implementation for the MySQL solution is, as you might expect, very simple:

import java.util.*;

public class DateColumn {
   private String name;

   public DateColumn(String name) {
      this.name = name;
   }

   public String insertString(Date date) {
      return String.format("'%1$tY-%1$tm-%1$td'", date);
   }

   public String getName() {
      return name;
   }
}

I get the MySQL-specific code in place and shipped as part of a complete iteration's work.

In the subsequent iteration, the customer indicates that the application now needs to support Oracle. Unfortunately, the two different database systems each require dates to be formatted differently. So much for compatibility! I've actually had to do something similar in the distant past on a "real" project, which had to be deployed into two separate environments.

No worries; I'll use my unit tests to help put a solution in place and keep me out of trouble. First, however, I'll research and briefly think about how to best solve the problem.

Oracle provides at least two ways to format a date, both differing from the MySQL approach. A quick web search reveals that one way to supply dates in Oracle SQL is to assume the default date format of DD-MON-YY. An example insert string is '30-may-2007'. Another way is to provide a TO_DATE function that specifies the format of the date string:

TO_DATE('2007-07-15', 'yyyy/mm/dd')

I like that better—it keeps me from being confused about whether '07-may-06' is the 6th day of May in the year 2007 or the 7th day of May in 2006.

I can imagine a solution that involves an if statement buried within insertString. That would probably mean I'd need "if" statements in many other places in code—for use in generating select strings, perhaps. That doesn't sound good. Code with promiscuous if statements quickly gets out of hand.

What I'd much rather have is one place where the DBMS type gets specified. That one place could also hand out DBMS-specific objects that contain appropriate behavior for generating formatted date strings.

At this juncture, I'll just worry about altering DateColumn to deal with the variant behavior. The whole idea of the strategy design pattern, with respect to this example, is that I pass a DBMS-specific formatter object to a DateColumn object. The class of this formatter object contains the appropriate behavior.

From an implementation standpoint, I want to build a DateFormatter interface that declares a single format method. This interface will have MySQLDateFormatter and OracleDateFormatter implementations. I'll then alter the DateColumn class to retain a DateFormatter reference.

Now that I have a rough roadmap, I take small steps toward my destination. My initial effort involves changing DateColumn to delegate to a MySQLDateFormatter. I make the changes ...

// DateColumn.java
...
public class DateColumn {
   ...
   public String insertString(Date date) {
      return new MySQLDateFormatter().format(date);
   }
}

// MySQLDateFormatter.java
import java.util.*;
public class MySQLDateFormatter {
   public String format(Date date) {
      return String.format("'%1$tY-%1$tm-%1$td'", date);
   }
}

... and run my tests. I also create a new test for the new class by copying and altering the test code for DateColumn. It's always a good idea to add tests as you create new classes. These new test classes act as gentle reminders to other developers that every class needs to be verified and documented with unit tests.

import static org.junit.Assert.assertEquals;
import java.util.*;
import org.junit.*;

public class MySQLDateFormatterTest {
   @Test
   public void insert() {
      MySQLDateFormatter formatter = new MySQLDateFormatter();
      assertEquals("'2007-07-15'",
            formatter.format(DateUtil.createDate(2007,
            Calendar.JULY, 15)));
   }
}

I then create the Oracle counterpart:

// OracleDateFormatterTest.java:
import static org.junit.Assert.assertEquals;
import java.util.*;
import org.junit.*;

public class OracleDateFormatterTest {
   @Test
   public void insert() {
      OracleDateFormatter formatter = new OracleDateFormatter();
      assertEquals("TO_DATE('2007/07/15', 'yyyy/mm/dd')",
            formatter.format(DateUtil.createDate(2007,
            Calendar.JULY, 15)));
   }
}

// OracleDateFormatter.java:
import java.util.*;

public class OracleDateFormatter {
   public String format(Date date) {
      return String.format("TO_DATE('%1$tY/%1$tm/%1$td',
         'yyyy/mm/dd')", date);
   }
}

With both variants in place, I alter the DateColumn test to specify use of the formatter.

import static org.junit.Assert.*;
import java.util.*;
import org.junit.*;

public class DateColumnTest {
   private static final String NAME = "name";
   private DateColumn column;
   private DateFormatter formatter;

   @Before
   public void initialize() {
      formatter = new MySQLDateFormatter();
      column = new DateColumn(NAME, formatter);
   }

   @Test
   public void create() {
      assertEquals(NAME, column.getName());
   }

   @Test
   public void insert() {
      Date date = DateUtil.createDate(2007, Calendar.JULY, 15);
      assertEquals(formatter.format(date),
            column.insertString(date));
   }
}

To get the test to compile, I need a DateFormatter interface:

import java.util.*;

public interface DateFormatter {
   String format(Date date);
}

The MySQLFormatter class now must implement this interface:

public class MySQLDateFormatter implements DateFormatter {
...

Getting the test to pass requires a few quick modifications to the DateColumn class:

import java.util.*;

public class DateColumn {
   private String name;
   private DateFormatter formatter;

   public DateColumn(String name, DateFormatter formatter) {
      this.name = name;
      this.formatter = formatter;
   }

   public String insertString(Date date) {
      return formatter.format(date);
   }

   public String getName() {
      return name;
   }
}

Am I done? Not quite.

I need to ensure that the OracleDateFormatter is usable. I could write an additional test in DateColumn. Or, I could take a shortcut and alter the test:

@Before
public void initialize() {
   formatter = new OracleDateFormatter();
   column = new DateColumn(NAME, formatter);
}

That change forces OracleDateFormatter to implement the interface:

public class OracleDateFormatter implements DateFormatter {
...

The unit tests verify everything that I've implemented to this point. Missing, someplace, is the client code that uses DateColumn. This client code is responsible for passing the correct type of formatter into each DateColumn instance. Perhaps this logic is part of a factory, for which I can easily write a unit test.

For purposes of the strategy pattern, my work is done. When a client asks the DateColumn class to provide a value string, it delegates its work to an appropriate DateFormatter instance. The code in DateColumn is oblivious of the DBMS type for which it's doing the work. There's no need for an "if" statement in DateColumn. The DateColumn class is "closed" for modification, at least with respect to changes in date formatting logic.

In Conclusion

I was able to arrive at a cool pattern implementation by virtue of very small, incremental steps. It's "cool" to me because each of the classes that comprise the pattern are small and easy to understand. Better, everything flowed naturally as a result of my interest in driving the implementation through tests, and everything ended up very easy to test. Once again, unit testing via test-driven development (TDD) has helped me produce a solution that is in alignment with notions of good design.

Referencess

[Gamma] Gamma, E., et. al. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional, 1995, p315.

About the Author

Jeff Langr is a veteran software developer celebrating his 25th year of professional software development. He's authored two books and dozens of published articles on software development, including Agile Java: Crafting Code With Test-Driven Development (Prentice Hall) in 2005. You can find out more about Jeff at his site, http://langrsoft.com, or you can contact him via email at jeff at langrsoft.com.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date