xperience suggests the working life of applications is enhanced if developers acknowledge the inherent requirements of standards-based development. For instance, if an application is based on emerging or evolving technology, then it must be able to easily adapt to changes in the standards related to that technology. If it is based on technology for which there are competing standards, then it should be configurable and capable of supporting any of the available standards.
The trouble is, developers too often find that these requirements result in complicated, hard-coded, conditional logic strewn throughout their application code. This code can get downright ugly when some of the conditions require common processing. As support for standards changes, the conditional logic can become increasingly difficult to read and extremely fragile, and even the most conscientious developer could inadvertently introduce a subtle error based on a misunderstanding of the code.
|What You Need|
|Some familiarity with object-oriented programming, design patterns, and Java.|
However, support for myriad standards, or conditional behaviors in general, can be achieved without sacrificing flexibility, robustness, or maintainability. Ideally, conditional behaviors should be isolated, and should not require changes to the main application code. By applying two well-known software design patterns, you can encapsulate the conditional behavior in one or more classes to make your code more readable, easier to maintain, and less susceptible to errors. Using this particular combination of design patterns will also allow your applications’ behaviors to be configurable.
The Crucial Role of Design Patterns
The two design patterns that are key to writing highly-maintainable applications that are also malleable to standards evolution are Strategy (a.k.a. Policy) and Abstract Factory.
The intent of the Strategy pattern, as defined by Erich Gamma et al in their book Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley), is to “Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.” If you replace the word “algorithms” in this definition with “conditional behaviors,” the result is a description of our goal. Therefore, the Strategy pattern is obviously applicable to the problem.
The Abstract Factory pattern is applied to achieve a loose coupling between the application and the configurable conditional behaviors so they can vary independently.
Suppose you’re developing an application that routes SOAP messages. Criteria for making routing decisions are innumerable. It follows that your application design must not, as much as possible, preclude the use of these criteria. Some standards that specify a set of criteria illustrate this assertion. For instance, the application ideally supports current Web services routing standards such as WS-Routing, WS-Referral, and WS-Addressing. Moreover, your application should also support emerging standards, or modifications to the existing standards. Additionally, it should be easy to develop proprietary or custom routing algorithms independently from the router application itself.
A typical solution developers might use for this routing example is shown in Listing 1. Perhaps this class was initially developed with a single algorithm in mind, before any of the Web services standards were defined. Since then, support for some of the standards was added, and additional algorithms were requested by customers. The current implementation, see Listing 1, only supports two algorithms, with a place-holder for a third; the route method is already quite long.
The Real Challenges of Application Development in Changing Times
Some developers might ask, “So what if the route method is long?” As Martin Fowler suggests in Refactoring: Improving the Design of Existing Code (Addison-Wesley), longer procedures are difficult to understand. There are other techniques for reducing method length, but the length of the route method is not the only problem. Consider some others:
- Depending on complexity, conditional logic can detract from readability.
- There is a high likelihood of duplicate code among similar algorithms. Be aware: the typical solution to this problem, unifying code, can add even more complex conditional logic while trying to isolate the similarities.
- The addition, removal, or modification of an algorithm requires that the router application, or at least the SOAPRouter class, be modified and recompiled.
- Removing an algorithm can easily result in lingering unused data structures or methods because its implementation is not encapsulated.
Adding a New Algorithm
Let’s consider the steps required to add a round robin algorithm to the application. First, there has to be a unique identifier for this algorithm. Begin by checking all existing identifiers to choose one that is not already used. Second, add a data member to keep track of the round robin state. Next, add the algorithm implementation to the route method. Finally, recompile the SOAPRouter class. Listing 2 shows the updated class.
While adding an algorithm complicates the SOAPRouter class, removing or modifying an algorithm can cause more confusion because it’s easy to remove or change the “else if” block from the route method, and inadvertently leave lingering data structures and algorithm identifiers. These lingerers make the class confusing and difficult to read. If left unresolved, over time, the responsibilities and capabilities of the SOAPRouter class become unclear.
Leveraging Design Patterns
With a clear understanding of these development challenges, let’s apply appropriate design patterns to address them. The result is illustrated by the class diagram in Figure 1.
|Figure 1. Refactored SOAP Router Architecture: The new architecture is the result of applying the Strategy and Abstract Factory design patterns.|
The algorithm implementation code is encapsulated in individual classes, each of which implements the RoutingStrategy interface. A RoutingStrategy implementation in this architecture is an algorithm definition. SOAPRouter now uses a RoutingStrategy instance to route request SOAP messages. It neither knows nor cares about how the SOAP messages are routed. The refactored SOAPRouter code is shown in Listing 3.
Listing 4 shows the WS-Routing RoutingStrategy implementation, and Listing 5 shows the round robin implementation.
Now, the process of adding the round robin algorithm is reduced to a single step. Simply set the value of the RoutingStrategy system property (the name of the property is determined by the RoutingStrategyFactory implementation) to routing.strategy.RoundRobinRoutingStrategy. There is no need to recompile SOAPRouter because no changes were required. Removing the algorithm is just as easy; just set the system property to another value.
Clearly Measurable Benefits for Developers
Conditional logic and associated problems are eliminated using the above technique, making the code more readable. Duplication of code can be limited through inheritance; Families of algorithms can be developed as a hierarchy with common code in one or more base classes. The refactored router code is concise, and the responsibility of the SOAPRouter class is clear.
Because the SOAPRouter class now uses the RoutingStrategyFactory to create RoutingStrategy instances, rather than constructing concrete implementations, it can vary independently from the algorithms. As such, algorithms can be implemented and modified without requiring the router source code to be recompiled. Adding or removing an algorithm from the router is a configuration step, and enables customers to implement their own routing algorithms without requiring access to the application source.
Also significant, there is no need to define unique identifiers for each algorithm; an algorithm’s implementation class name essentially serves as its unique identifier. Nor is there a possibility of clutter or lingering data structures because each algorithm is encapsulated in its own class. Removing an algorithm has become a one-step process.
The configurability is provided by virtue of the RoutingStrategyFactory implementation. The example implementation relies on a system property to determine which RoutingStrategy implementation to use and reflection to create instances of the configured type. To configure an algorithm, a developer needs only to specify the desired implementation class name as a system property. One benefit of this implementation is that the algorithm can be changed at runtime (assuming there is a means for setting this system property while the application is running).
This article focused on one application of a technique for solving a general problem. Hopefully, you can see its universal value in building dynamic applications with configurable behavior. Maybe, next time you encounter a class with a lot of confusing conditional logic, you’ll refactor it using this technique. Anyone who has to maintain that code after you will surely thank you for you effort.