Design patterns exploded onto the scene when the seminal work, Design Patterns: Elements of Reusable Object-Oriented Software, was published in 1994. Since that time, numerous books on patterns have been written, conferences devoted solely to the patterns movement have emerged, and entire Web sites are dedicated to discussions on patterns. Compound patterns that represent a combination of patterns have even been discovered.
While the benefits we've realized through this movement should not be questioned, the sheer number of patterns has created a situation in which it has become increasingly difficult to identify the most useful patterns for a particular context. In addition, it's common to find that many patterns have strikingly similar characteristics. When designing an application, I've found that simply determining which pattern to use can be as much of a struggle as actually customizing it to my specific context and implementing it.
As a Java developer and architect, I always try to ensure that my designs are resilient, robust, and maintainable. But equally important as designing flexible systems is designing architecturally simple systems. Because all patterns have consequences, it's important to consider if the advantages of using a pattern to solve a design challenge outweigh the disadvantages associated with the additional complexity. This decision, combined with the overwhelming number of patterns from which we have to choose, can paralyze a design effort.
In reality, the usage of a pattern is typically not an up-front decision. Instead, the incorporation of a pattern into our system is an evolution of the system's design because of the need for additional flexibility. We may start with a simple, straightforward solution, only to find that as the system evolves, additional flexibility is required. Based on the situation and the flexibility required, some patterns are better candidates than others.
If this is the case, we must be able to bridge the gap between our initial designs and the resulting patterns we incorporate into our designs. Some underlying principles must exist that we can judiciously apply throughout the entire design process, helping to ensure we create the simplest design possible that offers the maximum amount of flexibility. Were we to examine the structure of a set of patterns, we would find some fundamental principles that reside at their core. If we can understand these principles, it's much more likely that we can not only take better advantage of patterns, but also ensure a more solid design when we're not able to find the most appropriate pattern. Even more so, we may discover our own patterns.
The Challenge of Objects
Object-orientation has been promising reuse for a long time. As we've come to realize, reusable components are not designed and developed early in the project, but typically grow as the project evolves. For our systems to grow, however, their structure must be resilient and flexible enough so they can undergo iterations of change without compromising their structural integrity.
For a system to exhibit the resiliency we require to allow constant evolution, the relationships existing between our system's classes must be flexible. Flexible dependency management, therefore, is key in establishing our software architecture and should be considered each time we create a new relationship between classes. In fact, were we to examine any arbitrary collection of patterns, we would find that many focus on managing the dependency relationships between classes.
Dependency management facilitates reuse in that it allows us to use the existing behaviors exhibited by a class, while configuring the class with the desired new behaviors. In an extreme case, if we could configure all behaviors of a class with the behaviors we desired, the class would be highly reusable, though it would be of little use because of its high-configuration demands. An example of an extremely flexible and configurable design is the Visitor pattern.
The Open-Closed Principle
The Open-Closed Principle lies at the heart of the object-oriented paradigm. In a sense, we can say that its the goal of our object-oriented designs. Examining a myriad of patterns reveals its application often. It states the following:
Software entities should be open for extension, but closed for modification.
At first glance, this statement seems quite contradictory. How can we possibly create a collection of Java classes so that we can extend the system, adding new functionality, without modifying the existing application classes? Consider a financial institution where we have to accommodate different types of accounts in which individuals can make deposits. In addition, we've found that the algorithm used to deposit information to an account varies significantly depending on the type of account. Figure 1 shows a UML class diagram with accompanying code snippets that illustrates how we might structure a portion of our system. Let's discuss the significant aspects of this design.
Our Account class has a relationship with our AccountType abstract
class. In other words, our Account class is coupled at the abstract level to the AccountType inheritance hierarchy. Because our Savings and Checking classes each inherit from the AccountType class, we know that through dynamic binding we can substitute instances of either of these classes wherever the AccountType class is referenced. Subsequently, Savings and Checking can be freely substituted for AccountType within the Account class. This is the intent of an abstract class, and it's what allows us to effectively comply with OCP. Because Account is not directly coupled to either of the concrete Savings or Checking classes, we can extend the AccountType class, creating a new class such as MoneyMarket without having to modify our Account class. We have achieved OCP compliance and can now extend our system without modifying its existing code base.
Experienced developers typically have an uncanny ability to create flexible, resilient, and robust software designs. Realizing that many new challenges are simply different flavors of previous challenges, these developers have built an arsenal of best design practices, or patterns, that they apply continuously, often in a slightly different form. Sharing these patterns with others allows the entire development community to realize the benefits of using these proven designs to help create more flexible software.
Formally, a design pattern is a documented solution to a recurring design challenge that's customized given the context of the problem. In addition to describing the solution to a common design challenge, a pattern also discusses the various consequences of using that solution, as well as common implementation techniques. Because patterns can be referenced by name, developers who are knowledgeable about patterns find them to be resilient design solutions as well as effective communication devices when discussing design alternatives.
What is possibly more interesting is how we arrived at the solution in the first place. We know from the requirements that we may need to accommodate new types of accounts in the future. If we didn't know this, our design might be different because we wouldn't need this flexibility and accompanying complexity. Let's first discuss how we arrived at the resulting design seen in Figure 1.
Whenever we consider the use of inheritance, we have a few options, such as:
Simple attribute: This option doesn't utilize inheritance at all. Instead, we forgo the use of inheritance to encapsulate the logic directly in the class. If the value of this attribute, at any point in the life of an object, impacts behavior, we'll find conditional logic present thats based on the value of this attribute. While simple conditional logic may not be cause for concern, as the system grows it's likely that this simple logic becomes more complex and that new behaviors are realized as different attribute values are introduced. This would certainly violate OCP as we would need to modify our existing code base for each new type of account. This is shown in Figure 2, with accompanying Java code snippets.
Subclass directly: In this scenario, we subclass directly based on the variant cases. Subsequently, what was previously a conditional statement when using a simple attribute is now deferred to the individual subclasses. While this approach can help rid the class hierarchy of complex conditional logic, it introduces a different problem. If we find that other attributes impact behavior in a way that's not consistent with our subclasses, we're forced to introduce potentially complex conditional logic based on this attribute, or subclass again. This is shown in Figure 3, with accompanying Java code snippets.
Composition: Using composition, the design challenge is solved using a flexible composite relation, which is the solution we illustrated in Figure 1. While this may be the most complex of the three solutions, it's also likely the most flexible. This scenario also results in the most classes being produced, adding to the complexity.
Let's examine each of the above solutions in the context of the deposit calculation using our Account class. From the requirements we know that deposit is based on the type of account we're dealing with. We'll assume that three different account types - savings, checking, and a newly added money market - each have an algorithmically different calculation for deposit.
The discriminator is a term used to describe the single variant that's the cause for our creation of subclasses. The relationship between any descendant and ancestor should involve only a single discriminator. If we find that any ancestor-descendant relationship has multiple discriminators, we should carefully consider an alternative solution, such as composition. Rarely does subclassing the descendant based on another discriminator result in a flexible enough solution.
We'll begin by examining the solution using a simple attribute. Figure 2 shows an Account class with a single private instance attribute named _accountType. The value for this attribute is set within the constructor when the Account instance is created. When the deposit method is invoked, the value of this attribute will be checked and the deposit will be made appropriately, based on the account type.
There are a few disadvantages with this approach. First, new types of accounts or new deposit calculations require a change to our existing Account class. This is an obvious violation of OCP, as the Account class is not closed to modifications. Second, the _accountType variable is not type-safe. For instance, if a value of four was passed to the constructor, the Account instance would blindly set the value of _accountType equal to the invalid value of four. Of course, we can always put some code in the constructor to ensure that the value passed in is within the appropriate range, but this only adds to the OCP violation.
Now, for new types of accounts or deposit calculations we need to update the deposit method with an additional conditional and make sure we remember to modify the constructor to allow the _accountType instance attribute to accommodate this new value within its range of values. As the functionality grows and evolves, the Account class will become unwieldy to work with.
Figure 3 illustrates an equivalent solution using subclasses. Here, we subclass an abstract Account class with three subclasses representing each account type. In this situation we rely on some client to create the appropriate instance of the descendant class directly. This creation might be done in an object factory. In this situation, we no longer have any conditional logic based on the type of account because all this logic is now deferred to the appropriate Account descendant.
Common attributes and behaviors can easily be implemented on the ancestor Account class. In addition, any class referencing any of the descendants of the Account class in reality holds a reference to the Account class data type directly. Due to the nature of inheritance and dynamic binding, we should be able to substitute descendants anywhere the ancestor is referenced. It appears we have achieved OCP compliance.
In addition, our type safety problem discussed earlier should no longer be cause for concern when subclassing. In this situation, we have only three classes that can be instantiated. We don't have the ability to create an instance of any other type of class that could cause a similar problem.
While a bit more complex, this approach is certainly more flexible than that used in the previous example; however, it has some disadvantages. First, inheritance is a very static solution. If we were to identify additional behavioral variants that didn't fit into the realm of our subclasses, we would be left with a structure that doesn't flex in the direction of our application. For instance, consider a new or changing requirement that impacts the behavior of our Account class based on an account status. Unfortunately, the account status may not fall along the same behavioral lines as the account type. In fact, contriving an example, our account status may fall into one of three categories: active, inactive, or overdrawn. If this new requirement impacts how a deposit is made, or any behavior on the Account class, we might find we're relegated to using a simple attribute with conditionals. Considering this new requirement, let's examine some of our options.
Object factories allow us to decouple the instantiation of an object from the object that will actually be using the instance. Using a factory allows us to reference only an ancestor class or interface in our client classes. Subsequently, modifications made to the descendant of an abstract class or interface are encapsulated within the inheritance hierarchy.
First, we can try subclassing each of our account descendants directly and create separate status classes for each AccountType subclass. This would result in a proliferation of classes and a high probability of duplicate code. In essence, we would have an active, inactive, and overdrawn set of subclasses for each account descendant, resulting in nine total classes. This is extremely inflexible.
Second, we could define some default status behavior in our ancestor Account class that would override this behavior in descendants with variant behavior. This, however, is not without its problems either. Overriding methods is an indication of a violation of the generalization over specialization relationship inheritance is used to represent. While some methods are designed to be overridden, it typically creates a situation where code must be duplicated, especially as new descendants are added.
While this second approach works well for simple situations, it doesn't scale well as complexity increases, or if we use the Account class to store common attributes and behaviors. Any time we decide to use inheritance, we should fully understand the discriminator that's the cause for the creation of the descendants.
Referring back to our original design in Figure 1, we see the solution resolved using composition. Here, we're back to a single Account class that's composed of an AccountType abstract class. We again see that upon instantiation of the Account class, the AccountType is passed to the constructor. While a bit more complex, this solution is much more flexible. We certainly have the ability to extend AccountType and create new types of accounts without modifying the Account class directly. Our AccountType abstract class serves as a contract between the Account and its descendants.
In addition, if a new requirement is introduced that presents us with the need to incorporate account status into the structure, we can easily create a new AccountStatus abstract class with the appropriate descendants to satisfy status-specific behavior. In other words, our inheritance hierarchies are very small and involve only a single discriminator. This is a much more flexible solution.
To recap, we've discussed three alternatives to deal with the design challenge. Using a simple attribute is the easiest, but it also offers the least flexibility. This approach works well when the attribute does not impact behavior. The remaining two comply with OCP, but each has different advantages. Subclassing works well if we're sure we won't need to introduce a new discriminator. It's been my experience that this approach works well for nondomain-oriented approaches. Typical business domains are usually too dynamic to ever be sure of anything. The compostion approach, while the most complex, also offers the greatest flexibility.
Should we decide to go with either of the remaining two approaches, there are a few implementation-specific issues we'll need to address. If the intent of OCP is to ensure we can extend a system without modifying the existing elements, we must make sure the system does not refer to any of our concrete classes directly. This works fine, except for object creation. To create an object, we must refer directly to the class that the object is an instance of. We have two options: to dynamically load the class or use an object factory. While discussion of these topics is beyond the scope of this article, you're encouraged to experiment with each of these approaches.
The fact that we must reference the concrete class explicitly in order to create an instance indicates that an entire system can never be closed against all types of modifications. Therefore, effort should be spent applying OCP in a tactical manner, focusing on the areas of the system that are most dynamic and demand the flexibility of OCP.
While patterns contribute positively to the resiliency of many object-oriented designs, the proliferation of patterns often makes it an overwhelming challenge to find the one most suitable given the current context. However, underlying many patterns is a set of fundamental principles that are pervasive in object-oriented systems. The first of these, the Open-Closed Principle, enables us to create systems that are flexible and more easily maintained, allowing us to evolve these systems gracefully.
In reality, achieving OCP-compliance system-wide is not possible. There will always exist some change that the system is not closed against. Therefore, closure must be applied judiciously to the areas of the system that are most complex and dynamic. Even partial OCP compliance results in more resilient systems. Isolating violations of OCP to specific classes, such as object factories, certainly serves to reduce the overall maintenance efforts.
Quite frequently, striving to apply these principles throughout our systems creates a more natural way to discover the solutions to challenging design decisions that are appropriate candidates for flexible patterns. In this article we've discussed just one of the principles underlying many of the patterns we should be using in everyday development.
Meyer. B. (1988). Object-Oriented Software Construction. Prentice-Hall.
Martin, R.C. (2000). "Design Principles and Design Patterns."
Gamma, E., et al. (1995). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
Knoernschild, K. (2002). Java Design: Objects, UML, and Process. Addison-Wesley.
Kirk Knoernschild is a senior consultant and
instructor with TeamSoft, Inc. (
a firm that offers development, training, and mentoring
services to major corporations. He shares his experiences
through courseware development and teaching, writing,
and speaking at seminars and conferences. Kirk is the
author of Java Design: Objects, UML, and Process (Addison-Wesley).