Let decorated Commands take over

An exercise in software maintenance

The completely green pastures of software development are rare these days, with many development assignments having some history or legacy associated with them. The reality is that these historical elements often translate themselves into constraints that must be dealt with. These constraints can manifest themselves in a multitude of areas, from hardware and network configurations, to programming language choice and software design. This article will focus on the latter and illustrate how two classical Gang of Four design patterns, Decorator and Command, can be used together within existing applications to assist in their maintenance.

For any exercise in maintenance there first must be some code to maintain. I will describe a billing application that contains a similar design and, hence, is constrained in a similar manner to one I encountered recently.

Commands, here, there, everywhere

Pretend you have been brought in to maintain a billing application for an Internet service provider (ISP). This application handles the ISP's billing requirements. It calculates the following set of charges:

  • Joining fee
  • Monthly usage
  • Excess consumption (i.e., penalty rates applied if allocated download limit is exceeded)

While sifting through the code, you notice that these calculations are implemented as command objects. A command's intent, according to the Gang of Four, is to:

"Encapsulate a request as an object, thereby letting you parameterize clients with different request, queue, or log requests, and support undoable operations."

Simply speaking, a command can appear as an object-oriented function pointer. It encapsulates a method invocation into one object. So in our system, there are different command classes for the different types of calculations in the system. Separate classes calculate joining fees, monthly usage, and excess consumption. Figure 1 shows these classes.

Figure 1. All calculators in the system

All the calculation command classes implement a generic command interface. This allows the rest of the application to treat different calculations generically:

public interface Calculator
{
   public BigDecimal calculate(BillingContext context);
}

The interface accepts a BillingContext that holds the necessary data for the calculation to occur and returns a BigDecimal, representing the amount to be charged.

Let's look at a simple calculation class that implements this interface. This class calculates a user's monthly usage fee using this formula: rate (in dollars per MB) multiplied by consumption (in MB):

public class UsageCalculator implements Calculator
{
   public BigDecimal calculate(BillingContext context)
   {
      return context.getRatePerMB().multiply(context.getConsumptionInMB());
   }
}

A unit test ensures that this class functions as expected:

public class TestUsageCalculator extends TestCase
{
   private static final BigDecimal CONSUMPTION = new BigDecimal(100);
   private static final BigDecimal RATE = new BigDecimal(0.50);
   public void testUsage()
   {
      UsageCalculator usageCalculator = new UsageCalculator();
      BillingContext context = new BillingContext(CONSUMPTION, RATE);
      // 100 * 0.5 = 50
      assertEquals("Charge not as expected", 50, usageCalculator.calculate(context).intValue());
   }
}

I have omitted other calculator implementations, but you should be able to see how classes similar to UsageCalculator can exist to cater to the system's different calculation types.

The problem

Now that we have looked over our billing application, imagine the government decides to enforce a special ISP tax requiring all ISPs to add a flat 10 percent tax across the board to all their customers' charges. A number of options are available for implementing this change, but all must realistically fit within the aforementioned design.

Often when presented with a scenario similar to this, drastically changing the system—like rewriting everything or replacing the command framework with an external rules engine, such as ILog or Jess—is not feasible. Therein lies one of the maintenance challenges: dealing with existing software designs and constraints. Whatever solution is adopted, it should exist harmoniously within the current infrastructure. In this article, I examine several possible options and weigh their prospective benefits and costs.

The brute force approach

The most immediately obvious approach is to adopt a brute force strategy and go through each calculator class and add 10 percent to the finished charge. This solution is not isolated and not encapsulated, as it requires changes to all the system's calculator classes. This approach also requires the original source code to be available, which, in our circumstance, is true, but may not be the case all the time. The code below illustrates our brute force strategy:

public class UsageCalculator implements Calculator
{
   private BigDecimal tax;
   public UsageCalculator(BigDecimal tax)
   {
      this.tax = tax;
   }
   public BigDecimal calculate(BillingContext context)
   {
      BigDecimal charge = context.getRatePerMB().multiply(context.getConsumptionInMB());
      // Changing the UsageCalculator to cater for the tax
      return charge.add(charge.multiply(tax));
   }
}

Because we changed our original UsageCalculator, we must update its unit test. This unit test not only tests usage calculation logic, but also taxation calculation logic:

public class TestUsageCalculator extends TestCase
{
   private static final BigDecimal CONSUMPTION = new BigDecimal(100);
   private static final BigDecimal RATE = new BigDecimal(0.50);
   private static final BigDecimal TAX = new BigDecimal(0.10);
   public void testUsage()
   {
      UsageCalculator usageCalculator = new UsageCalculator(TAX);
      BillingContext context = new BillingContext(CONSUMPTION, RATE);
      // 100 * 0.5 * 1.1 = 55, must change the expected result to cater for the tax
      assertEquals("Charge not as expected", 55, usageCalculator.calculate(context).intValue());
   }
}

Subclassing anyone?

What if we subclassed UsageCalculator and added the taxation logic in the child class? That means we do not have to alter each class directly. This approach does not require the source code and resolves our need to alter the existing unit tests. However, it still does not solve the isolation problem. The taxation logic will disperse into many similar subclasses across the system. Even if we extracted the method that calculates the tax to a common utility class that the subclasses could call, a proliferation of subclasses would remain, as shown in Figure 2 and the code that follows.

Figure 2. Proliferation of subclasses. Click on thumbnail to view full-size image.
public class TaxUsageCalculator extends UsageCalculator
{
   private BigDecimal tax;
   public TaxUsageCalculator(BigDecimal tax)
   {
      this.tax = tax;
   }
   public BigDecimal calculate(BillingContext context)
   {
      BigDecimal originalCharge = super.calculate(context);
      return originalCharge.add(originalCharge.multiply(tax));
   }
}

The subclass's unit test depends on its parent class. If the parent calculator changes its logic, this unit test will have to be updated as well:

public class TestTaxUsageCalculator extends TestCase
{
   private static final BigDecimal CONSUMPTION = new BigDecimal(100);
   private static final BigDecimal RATE = new BigDecimal(0.50);
   private static final BigDecimal TAX = new BigDecimal(0.10);
   public void testTaxUsage()
   {
      TaxUsageCalculator taxUsageCalculator = new TaxUsageCalculator(TAX);
      BillingContext context = new BillingContext(CONSUMPTION, RATE);
      // 100 * 0.5 * 1.1 = 55
      // This test is still dependent on the UsageCalculator so changes in that class will
      // result in changes to this unit test
      assertEquals("Charge not as expected", 55, taxUsageCalculator.calculate(context).intValue());
   }
}

Enter the Decorator

So what are our requirements? We want to add functionality to our classes transparently, without subclassing, and maintain a solid encapsulation of logic while minimizing the changes to our existing codebase. This sounds like a job for the decorator.

A decorator's intent in the Gang of Four lingo is to:

"Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality."

A decorator is merely a wrapper that surrounds another object while extending a common class or implementing a common interface as the surrounded object. This allows behavior to be added to the original object while maintaining a constant interface.

Often people think of decorators as a means to enhance a class visually—for example, as the example in Design Patterns depicts, adding a border to a component. Even though a decorator proves suitable for achieving this objective, as the Java I/O packages demonstrate, it is also quite adept at "decorating" non-UI classes:

  FileInputStream fis = new FileInputStream("c:/test.txt");
  BufferedInputStream bis = new BufferedInputStream(fis);

To add buffering capabilities to FileInputStream, we wrap it with a buffered version of the input stream. Clients cannot tell the difference between the input streams unless they perform instanceof or getClass() checks. We will do that with our calculator; we will wrap it with a decorator that applies a tax to the final charge, as illustrated in Figure 3 and the code that follows:

Figure 3. Use a decorator to add the taxation logic
public class TaxCalculator implements Calculator
{
   private Calculator baseCalculator;
   private BigDecimal tax;
   public TaxCalculator(Calculator baseCalculator, BigDecimal tax)
   {
      this.baseCalculator = baseCalculator;
      this.tax = tax;
   }
   public BigDecimal calculate(BillingContext context)
   {
      BigDecimal originalCharge = baseCalculator.calculate(context);
      return originalCharge.add(originalCharge.multiply(tax));
   }
}

It is important to note that the TaxCalculator is a Calculator and also wraps a Calculator (baseCalculator). It does not reference the UsageCalculator directly but through the Calculator interface. This way, any number of calculators can be decorated with this class.

The unit test defines its own MockCalculator to protect it from changes to any real calculators. The Decorator pattern enables that:

public class TestTaxCalculator extends TestCase
{
   public static final BigDecimal ORIGINAL_CHARGE = new BigDecimal("100");
   public static final BigDecimal TAX = new BigDecimal("0.10");
   public void testTaxCalculator()
   {
      MockCalculator originalCalculator = new MockCalculator(ORIGINAL_CHARGE);
      TaxCalculator taxCalculator = new TaxCalculator(originalCalculator, TAX);
      // Use dummy values, these are not used in the mock calculator
      BillingContext context = new BillingContext(null, null);
      // Check that the charge is 100 * 1.1 = 110
      assertEquals("charge not as expected", 110, taxCalculator.calculate(context).intValue());
   }
   private static class MockCalculator implements Calculator
   {
      private BigDecimal charge;
      public MockCalculator(BigDecimal charge)
      {
         this.charge = charge;
      }
      public BigDecimal calculate(BillingContext context)
      {
         return charge;
      }
   }
}

Construct calculators

The last issue to deal with is object construction. To obtain true transparency, we need to put a level of indirection between the calculator construction and the classes that use them. To achieve this goal, a couple of approaches, such as the Spring Framework, spring to mind. The first approach would be a component/service locator or factory model, where construction responsibility is delegated and encapsulated within a class. Another approach would be the "Dependency Injection" pattern that appears to be all the rage these days. This technique would see the calculators configured externally and passed into the application.

To get into a full-blown discussion regarding these two approaches is well beyond this article's intended scope. The most pertinent point is that these approaches abstract away object construction and allow for decorators to be added or removed without affecting the surrounding application.

Let decorated commands take over

It should be apparent now how other similar pieces of functionality can be added in this fashion. If a special Christmas discount were to be applied to charges during the festive season, another decorator could be written and added. The pattern's flexibility comes to the fore with the example illustrated in Figure 4. As you want to tax only the final charge, you can sandwich the new decorator between the UsageCalculator and the TaxCalculator.

Figure 4. Reordering decorators

A slightly more advanced business application would use the Decorator pattern to calculate reversals. A reversal is a change to a charge that has been previously invoiced, which occurs when a client has been billed incorrectly. If we conceptually think of a reversal as a generic calculation that calculates a new charge and subtracts the value of the original charge, this logic can be encapsulated in a reversal decorator.

1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more