Using Patterns for Rapid Development
Revised February 2008
Summary: When defining a software solution, design patterns can enhance productivity. (8 printed pages)
I was hired at Litware, Inc., a Fortune 500 corporation, as a solutions architect. According to the hiring manager, there were a number of candidates for the position. I was offered the position because I had experience designing and developing solutions that solved similar problems in the industry. Through the interview process, I knew that some initial analysis and proof-of-concepts had been done on the product, but that the majority of the effort had yet to begin.
During my first few weeks, I was summoned to the office of the division's general manager. He aired his dissatisfaction at the pace of the progress on the product. He told me that the product had been underway for 18 months, and that he had seen no tangible results. He impressed upon me that he wanted a working solution by the end of the current fiscal year, which was nine months away. A meeting with the division's Chief Technology Officer (CTO) a short time later ended with the CTO telling me that "the success of the division is in your hands."
As I read through the documentation on the product, it became apparent that some of the initial research—which seemed reasonable, at a conceptual level—was not practical, as more details surfaced about the solution's domain. It became clear, at that time, that the product was effectively starting from scratch, and I had nine months to get it released!
I understood the challenge that was presented to me, and I questioned the sensibility of taking the job in the first place. But I also understood that things might not be as dire as they seemed. I have been involved with the development of similar products in the past, and have successfully delivered all of them. As a matter of fact, each time that I had delivered one of the earlier solutions, it became easier. The other products were built on similar frameworks, based on well-known software patterns. For the product to be successful, I knew that I had to leverage my experience and create similar frameworks.
Understanding the relationship between frameworks and patterns is critical to understanding how to design a software solution. Frameworks are traditionally defined as a set of cooperating classes that makes up a reusable design for a specific class of software [Gamma, et al., 1995]. The operative term in the definition is "class of software," which implies that it provides a specific function within a software solution. Frameworks are often based on patterns that are general-purpose solutions to a software problem in a given context. The key difference between frameworks and patterns is that frameworks are an implementation of software constructs, whereas patterns provide a definition on how to solve a particular software problem.
The driver to implementing frameworks that are based on software-design patterns is that they provide consistency through a solution and can increase productivity. In a dynamic environment, they can make a solution more extendible. Frameworks also minimize the amount of code that is required to implement a solution, which makes a solution more maintainable. More importantly, the patterns that are used to create frameworks create a common vocabulary in a development organization, which makes the communicating of a design much easier.
People who have less experience in software patterns might not understand how their implementation can help a product get to market. But those who have seen the productivity gains that are derived from frameworks throughout the life cycle of a solution tend to be true believers. Even some highly regarded software professionals are cautious about creating frameworks early in a product-development cycle, preferring to refactor a solution to frameworks after some initial functionality has been implemented. Other industry experts believe that, when an architect understands a domain, they can feel free to prefactor the application and create frameworks for the initial release. Knowing that the product road map was not going to change for quite some time, I chose to prefactor the solution and build frameworks that were based on the major abstractions of the solution.
Understanding the direction that the architecture was taking solved only part of the problem. The main thing that I had to do was to get the architecture to the point at which it could be approved by management and be developed. This required the ability to define and articulate the architecture. Knowing that development had to commence in the short term, I took the following steps:
1. Defined the system boundaries
2. Defined the structure of the solution
3. Defined the frameworks
4. Mapped the functional requirements to the frameworks
Defining System Boundaries
I took a couple of days to do my initial analysis. Having experience in solving similar problems in the past, I knew that the first thing that I had to do was to define the scope of the initial release. My experience gave me insight into the implicit requirements of the solution. From those requirements, I was able to define boundaries of the solution and restrict the scope of the initial release. The implicit requirements were:
· The solution would require a secure programmatic interface, as well as a user interface.
· To ensure consistency across all persistent stores, the solution would require the ability to manage both long-running transactions maintaining state and short-duration stateless transactions.
· The solution would have to interface with multiple hardware and software systems that communicated through disparate interfaces using various protocols.
Structuring the Solution
Based on the system boundaries and the implicit system requirements, I was able to begin to define the structure of the solution. The following system requirements gave me the direction that I needed to structure the solution:
· Public access to the solution should be based on the HTTPS protocol, requiring the deployment of Web services and Web pages to an HTTP server—possibly, in a perimeter network that sat between an organization's internal network and an external network (sometimes also called a "DMZ").
· Long-running "stateful" transactions create the need to support asynchronous relationships between the invoker of the process and the solution.
· The interfaces to the hardware and software systems should be standardized to the largest extent possible.
· There had to be a reusable component to access systems with command-line interfaces.
Having a good knowledge of the structural patterns in the Pattern-Oriented Software Architecture (POSA) series, I knew that the requirements lent themselves to the Layers pattern [Buschmann, et al., 1996]. The Layers pattern allows a solution to be decomposed into groups of behavior, and allows the groups to be accessed in a consistent fashion. Also, it allows a solution to be conceptualized at distinct levels, which simplifies communicating the solution to the various stakeholders.
Defining the Frameworks
After settling on the scope of the effort, most methodologies would recommend spending some time conducting some formal or informal domain analysis. Having past experience in developing solutions in the same domain, and being under pressure to have development commence, I proceeded at this time to define domain-specific frameworks.
Frameworks define the structure and behavior of software solutions and subsystems. Defined and developed correctly, frameworks can provide a productivity boost by promoting reuse throughout a solution. Frameworks also make the solution and subsystems more consistent by setting the boundaries for development.
The first framework that I defined was for the subsystem that accessed systems with command-line interfaces. Having created similar services in the past, I understood that the following requirements applied to this service:
· Because this service would act as a mediator between different processes at higher layers and different remote systems, this service would have to support a generalized interface for all devices.
· Accessing and programming remote systems was often a long-running process. This behavior can create concurrency issues, causing potential bottlenecks that can affect performance. This framework would have to provide for asynchronous interactions between the consumer of the subsystem and the device that was being accessed.
Understanding that this subsystem required a coarse-grained interface that supported multiple business contexts, I chose to implement the Command Processor pattern [Buschmann, et al., 1996]. This pattern defines a component that takes a Command object and, calling a do() method on the object [Gamma, et al., 1995], defines the method as Execute(). There are a couple of variations of this pattern, with regard to transaction failures and compensation. The variation that I chose was to report back the success or failure of the transaction, which allows the consumers of the service to decide how to compensate for failed transactions.
To allow this service to process commands in the most efficient manner, I specified that the service implement the Active Object pattern [Schmidt, et al., 2000]. This pattern has a service return a future (token) to the invoker, which allows the service to fulfill a request outside of the invoking thread. It becomes the responsibility of the consumer of the service to retrieve the status of the method and to continue processing.
The next framework that I defined was the system-domain framework. The goal of this framework was to provide standardized interfaces to the hardware and software systems with which the solution communicates. Specific requirements that this framework must address are:
· Some systems contain other systems. Systems can also contain multiple systems. For example, an operating system might contain multiple software applications.
· Different versions and revisions of the same system might require access through different interfaces. These disparate interfaces can require access to the system by using different protocols.
· Systems can be accessed through different interfaces, based on the context for which the system is being invoked. Different data might be required to access the system through the different interfaces.
The Composite pattern [Gamma, et al., 1995] allows for the modeling of a tree structure, so I used this pattern to model the containment relationships. The ability to access different versions and revisions of the same system requires that the object expose a consistent interface but behave differently, based on the version of the target system. I chose the Strategy pattern [Gamma, et al., 1995] to facilitate this behavior. This pattern provides a consistent interface to the consumer of the object, while allowing the algorithm of the object to change. By allowing an object to abstract the interface and protocols of the device from the consumer, the system becomes more extendable. There is also an additional benefit of having the ability to encapsulate the different versions of interfaces in different classes.
The ability to communicate with systems through disparate interfaces, based on the context for which the system is being invoked, implies that constructing the object in the correct context might be somewhat complex. There also might be areas of reuse, with regard to object construction. These requirements made me choose the Builder pattern [Gamma, et al., 1995], to construct the objects that represented systems that the solution had to access. This pattern allows for the creation of different representations of complex objects.
Mapping Requirements to Frameworks
As soon as the structure of the solution and the frameworks were defined, five months after my conversations with executive management, we could begin developing the solution. At that point, I began analyzing the functional requirements by creating use cases. After writing the use cases, I was able to map the requirements to the defined architecture and subsystems.
There were also a number of system requirements that still had to be defined. These system requirements included security, event management, and request management. Subsystems had to be defined for each.
Four months later, Release 1 was coded and tested. As Release 1 was under development, we were able to begin specifying additional functional requirements for the subsequent release, and easily map that functionality to the architecture and subsystems.
Although the development effort was a success, I have thought about how I would approach the same situation if I were faced with it again. I continually conclude that I would proceed in the same fashion, even without the same scheduling pressures. The ability to develop a patterns-based solution rapidly has many positive benefits—most importantly, giving the business the ability to realize revenue quicker.
As I would hope with any project, I learned some valuable lessons that made me a better architect. Based on the lessons that I learned, this is the advice that I would give to an architect who might be faced with the same situation:
· Understand that many problems have been solved in the past. When defining a software solution, draw upon your own experience and the experience of others. Look for patterns that solve similar problems, and leverage those patterns into frameworks to solve your particular problem.
· Consider creating frameworks as part of an initial release. Although defining frameworks early in a development effort carries some risks, it can be highly beneficial in terms of productivity gains, if done correctly. If you have significant experience with defining frameworks for a similar domain, consider leveraging that experience. Defining frameworks early in the product life cycle is sometimes referred to as prefactoring.
· Convince management that future refactoring will have to be accounted for. No matter how good are the initial architecture and design, changes will be required as the product matures. Learning occurs during the whole development effort, and lessons from development can be leveraged to refine the solution.
· Make sure that you can map all functionality back to system requirements, either implicit or explicit. In most cases, the job of an architect is to deliver a solution. Most business sponsors will not care how eloquent a solution is, if it does not deliver the functionality that is required by the business.
· If you are new to patterns, start your research by reading Design Patterns [Gamma, et al., 1995]. This book provides the foundation for further research into patterns, and is often referenced in the subsequent literature.
To benefit fully from leveraging software architectural and design patterns, an architect must always try to answer the following questions:
· What is the
problem that I am trying to solve?
Understand conceptually the problem that the solution is intended to solve. Software architecture begins at the conceptual level, and it is imperative that architects have an understanding of the big picture. The ability to articulate the solution succinctly in nontechnical terms is a good indication that the problem space is understood.
· How has this
problem been solved in the past?
In any software solution, there is a good chance that a similar problem has been solved in the past. Research how this problem has been solved, and leverage the design to solve the new problem.
· How might the
solution be extended in the future?
The intent of many patterns is to provide a level of extendibility for a solution. If the solution's requirements for extendibility are understood, it might be beneficial to leverage those patterns early on in the design.
· [Buschmann, et al., 1996] Buschmann, Frank, Regine Meunier, Hans Rohnert, Peter Sommerlad, and Michael Stal. Pattern-Oriented Software Architecture, Volume 1: A System of Patterns. Chichester, England; New York: John Wiley & Sons Ltd., 1996.
· [Gamma, et al., 1995] Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley Professional, 1995.
· [Schmidt, et al., 2000] Schmidt, Douglas, Michael Stal, Hans Rohnert, and Frank Buschmann. Pattern-Oriented Software Architecture, Volume 2: Patterns for Concurrent and Networked Objects. Chichester, England; New York: John Wiley & Sons Ltd., 2000.
· Hofstader, Joseph. "Using Patterns to Define a Software Solution." MSDN Library. November 2006. (Accessed January 5, 2007.)
· Kircher, Michael, and Prashant Jain. Pattern-Oriented Software Architecture, Volume 3: Patterns for Resource Management. Chichester, England; New York: John Wiley & Sons Ltd., 2004.
· Martin, Robert C., Dirk Riehle, and Frank Buschmann. Pattern Languages of Program Design 3. Reading, MA: Addison-Wesley Professional, 1998.
Active Object pattern—A pattern that allows for asynchronous method invocation on an external component [Schmidt, et al., 2000].
Builder pattern—A pattern that allows for the creation of different representations of complex objects [Gamma, et al., 1995].
Command Processor pattern—A pattern that provides a consistent interface to execute operations on processes that are external to a solution [Buschmann, et al., 1996].
Composite pattern—A pattern that allows for the modeling of a tree structure [Gamma, et al., 1995].
Framework—A set of cooperating classes that makes up a reusable design for a specific class of software [Gamma, et al., 1995].
Pattern—A solution to a problem in a context [Gamma, et al., 1995].
Layers pattern—A pattern that allows a solution to be decomposed into groups of behavior and separated into layers [Buschmann, et al., 1996]. This pattern allows a solution to be conceptualized at distinct levels, which simplifies communicating the solution to the various stakeholders.
Prefactor—The process of identifying and creating frameworks for the major abstractions in a software solution, in the early stages of a product life cycle.
Refactor—The process of refining an existing software solution to make it more flexible and extendible, by discovering common abstractions across the solution.
Strategy pattern—A pattern that provides a consistent interface to the consumer of the object, while allowing the algorithm of the object to change [Gamma, et al., 1995].
About the author
Joseph Hofstader is a Systems Architect at Avaya, with the title of Distinguished Member of Technical Staff. He has spent his career architecting, designing, and developing software solutions in the telecommunications industry. Joe holds a master's degree from George Mason University in Fairfax, VA.
This article was published in Skyscrapr, an online resource provided by Microsoft. To learn more about architecture and the architectural perspective, please visit skyscrapr.net.