In this article you learn about the patterns that were applied during the design of a service-oriented component middleware for telephony devices with limited resources. It covers the problem context of the project, stated as a set of forces and gives a brief overview of the associated architecture vision. You'll also get a description of the patterns that were applied to solve the problem and create the software architecture, and their method of application and relationships to one another.
This article focuses on the concrete experiences of applying patterns on a particular project, rather than describing an idealized approach to applying patterns to solve a problem. It is assumed you are familiar with the patterns that are discussed.
The following definition of Service-Oriented Architecture, taken from Wikipedia, is used in this article:
"The term Service-Oriented Architecture (SOA) expresses a business-driven approach to software architecture that supports integrating the business as a set of linked, repeatable business tasks, or "services." Services are self-contained, reusable software modules with well-defined interfaces and are independent of applications and the computing platforms on which they run."
This definition captures the concept of SOA as it was understood by the project team. The key characteristics of SOA that were important for the project were: self-contained reusable software modules, well-defined interfaces, and platform independence; hence the term "service-oriented component middleware" to describe the system. The platform independence in this case was required to enable reuse of code on alternative platforms.
The patterns were applied for a software development project for a range of commercial telephony devices with limited resources. One of the main goals was the creation of a new software architecture and service-oriented software components that could be reused on future software development projects within the organization. That goal is the focus of this article; it was the main driver behind the application of patterns on the project, and led to an emphasis on architecture development during the early phases. The other goals of the project are not described here for confidentiality reasons.
Several reusable, telephony-domain specific services had been identified during the inception of the project, and the software architecture under development was specifically required to support those services. Typical examples of the required services:
- Allow telephone directory lookups both locally within the user's personal directory and remotely in enterprise directory applications.
- Provide "buddy" presence information to local telephony applications, and propagate user presence information to remote enterprise applications.
- Record and manage telephony-related user actions and associated events for subsequent user and administrative reference.
Several reasons were behind these goals, described in Table 1 as forces that define the problem context.
Table 1. Forces that define the problem context
| Force | Description/requirement |
|---|---|
| Future reuse of software components in a product-line development strategy | Development of future software systems based on a set of reusable components was a key goal. |
| Seamless deployment of services at run time | A dynamic approach to service deployment and redeployment to allow flexible management and updates of telephony device software. |
| Service invocation transparency independent of deployment model | Ability to invoke services both locally and remotely in a transparent way, and a decoupling between service interfaces and providers. This enables flexible composition of telephony services into new configurations for new development scenarios. |
| A common approach to management, testing, handling of services | A simple and consistent way of managing, testing, and other general purpose handling of services within the target system. |
| Reuse and protection of existing software components | Make optimum use of a large base of existing telephony software available when the project commenced. |
| Acceptable performance within resource-limited environments | Software architecture was being targeted at telephony devices with limited resources such as memory, processor power, and non-volatile storage. The expectation was that the resources would be not be upgradable. |
| Independence from specific computing platforms | Software architecture and reusable software components could not have dependencies on particular computing platforms, given the product line development strategy. |
The development process used during the initial phases of the project was an agile process based mainly on Scrum and Extreme Programming. The process was user-story driven, architecture-centric, and focused on application of architectural patterns, concurrency and optimization patterns, and resource management patterns as described in the Pattern Oriented Software Architecture books. (See Resources.)
Generally, candidate patterns were investigated and selected at the start of each iteration based on the requirements for that iteration. A small proportion of the patterns presented here were actually recognized retrospectively as having been applied. (They are presented as patterns that were applied to provide a complete picture of the architecture through patterns.)
Along with the requirements list, a high-level architecture vision was established, which drove the pattern selection. An architect guided the team through pattern selection and rough, up front design, and the patterns were applied as the iteration progressed using test-driven development and continuous integration. The resulting implementations were proven at the end of the iteration according to the acceptance tests, which also served as input to the iteration.
The initial team structure was based around the architectural building blocks described in the following section. The patterns described in this article were applied predominantly by one team, which was responsible for creating the service-oriented component middleware and initial services.
The programming language selected for the majority of development was C++.
The software architecture vision required the creation of a service-oriented component middleware for telephony devices with limited resources. The following list shows the key concepts, or terms, introduced at the start of the project.
- Service
- Reusable software asset encapsulating business logic, typically composed of smaller granularity components such as classes.
- Service container
- Manages service lifecycle, communication, and invocation; enables services to focus on business logic.
- Service interfaces
- The interfaces exposed by a particular service, representing the capabilities that service clients can invoke. Expected to support both local and remote invocation.
- Service bus
- General purpose service communication mechanism providing location transparency and encapsulation of underlying communication.
- Platform abstraction
- A set of interfaces providing platform independence.
Figure 1. Architecture vision overview

Figure 1 gives a broad overview of how several of the concepts relate to one another in the architecture vision.
This section describes:
- Patterns applied on the project.
- Reasons for their selection.
- Discussion of the pattern applications, and the interactions or relationships that were observed between them.
The layers pattern gave the overall, fundamental structure to the software architecture to support the project's product-line development strategy. This pattern was part of the architecture vision shown above, so it framed the team's thought process from the beginning.
The layers were selected so that the high-value, reusable, service, and service-related code was localized in the services layer and could only make use of core platform functions using the strict interface to the platform layer. The application layer localized code was, along with the platform layer, expected to be specific to each computing platform.
The impact of this pattern on the rest of the project was profound. All subsequent implementation decisions were framed by the established layers. Unlike most of the following patterns, there was no actual code associated with the pattern; it formed the basic conceptual framework for all following implementation.
There were, however, two concrete artifacts associated with the layers pattern: the initial team structure, and the initial configuration management data-store structures. Both were based around the layers that were established, which is believed to have given the layering greater substance and meaning on the project. This was a logical approach, given the product-line development strategy where different teams form and disband based around different architectural blocks over time, and where different blocks are likely to be accessed, modified, and used in different ways throughout the life of the software. "Conway's Law" and the related organizational patterns, such as Organization Follows Architecture, provide deeper insight into how team structure is related to architecture.
Interestingly, the layers pattern was actually applied a second time, later on in the project. Certain other architectural abstractions were observed within the previously established layers, and the configuration management data-stores (based around the original layering) were too large, which caused technical problems with the infrastructure. The services layer was decomposed into (from top to bottom) business services, infrastructure services, and framework. None of the layering, except for the original platform layer, was strict; it was based around logical, observable groupings of software elements. Similar decomposition took place within the application layer, which resulted in application, app framework, and presentation layers.
The second application of layers acted as a stepping-stone to a final, UML package-based approach. The layers introduced by the second application of the pattern were subsequently transformed into packages with explicitly defined dependencies. This was to enable more accurate representation and analysis of the dependencies between the layers.
The introduction of layers was not perceived to have caused performance problems on the project, mainly because the targeted device hardware had the capacity to deal with the associated overhead. This may not be the case in all scenarios; you should take care when applying this pattern where the resulting software has to run in environments with limited resources.
In the following discussion, the patterns were applied predominantly in the context of the framework, infrastructure services, and business services layers with the exception of the wrapper-facade pattern, which was applied in the context of the platform layer.
The next step was to introduce platform independence. Without platform independence, it would be difficult to do anything without violating a core principle of the architecture vision. A wrapper-facade approach was taken to encapsulate low-level, host-specific functions, data structures, and calls to legacy and third-party code.
By applying the wrapper-facade pattern, it was possible to define pure OS abstractions that would be realized for particular operating systems as needed by telephony product developments. And, several legacy software components with known maintainability issues were integrated into the project. The creation of wrapper-facades for these components hid the details of complex interactions between the legacy components, and also provided an abstraction that protected new code from legacy code. A similar approach was taken for certain third-party software components.
A typical example of legacy code contained by a wrapper-facade was a group of related modules responsible for interacting with a remote telephony-device management platform. Operating system examples include file access and network access.
By abstracting operating system, legacy, and third-party code, the fundamental bedrock of platform independence was laid. One consequence of this approach was a requirement for support and development of the abstractions. Given the iterative and incremental nature of the project, abstractions were only added as needed, but this meant an ongoing investment of time and effort to create the abstractions, and on some occasions reworking of code written before abstractions were available.
By introducing the concept of platform abstraction at the very start, it was much easier to establish momentum in the creation and maintenance of the abstractions throughout the project.
With platform independence established, it was time to introduce services into the system. Creation and deletion of services seemed a reasonable place to start. Component configurator, chosen early in the project to manage service life cycle, was applied largely as described in the pattern.
The initial implementation of the component configurator pattern provided a service creation and initialization mechanism, and laid the groundwork for the re-initialization and shutdown of services in future iterations. Typical examples of initialization performed by services when prompted by the component configurator implementation include:
- Service discovery
- Subscription to other services for event notification
- Creation and initialization of platform and third-party resources, such as network sockets or telecommunication protocol stacks
The component configurator role was later expanded to provide for the discovery and management of the connections between services. An architectural goal of the project was the decoupling of services from underlying communication details such as transport, message protocols, location and so on. The implementation was extended to provide a dynamic lookup mechanism that could be called by services to discover other services.
Implementation of the component configurator pattern partially delegated this "discovery" responsibility to another element within the system, but on reflection, it was not appropriate to assign the responsibility in this area at all. The management of connections between services should have been handled separately, but because of its central role in the system, it was easy to modify the responsibility of the component configurator implementation. This made the associated classes more difficult to understand; it would have been better to create a separate class with the responsibility of managing service connections, which could consult the component configurator implementation if necessary.
Fortunately we could limit the effect of the inappropriately assigned responsibilities to a small number of classes by applying encapsulated context object and decoupled context interface to the component configurator implementation to selectively expose the discovery methods. The component configurator implementation was modified to implement a service context interface that was passed to services using their initialization method. Similarly, a framework context interface was implemented and passed to several different framework layer elements to expose related methods.
On reflection, both the naming and definition of the framework context interface was too broad, and it was passed to too many framework layer elements. It would have been better to break this context interface down, rather than having a catch-all interface for framework layer elements.
Introduce service communication and location transparency
The next problem was that services needed to communicate with one another, and one service should not be aware of the location of another service.
Both of these problems were dealt with at the same time. A service bus subsystem was introduced to encapsulate communication between named endpoints. This was a key step in achieving communication between services, which were initially able to create instances of named service bus endpoints for communication with other services. The approach was inspired by the Qt "CopChannel" mechanism (© TrollTech) and the broker pattern. The implementation was an instance of the message passing broker system variant of the pattern, as in the POSA pattern documentation.
The initial implementation of the broker pattern provided intra- and interprocess communication. One consequence was that services could be easily deployed into different processes with the component configurator implementation, which later proved useful in separating services according to how critical their operation was. For example, a service providing essential telephony event processing would be in a different process than a service collecting nonessential performance statistics.
The broker implementation was later extended by adding a bridge that enabled remote communication. The initial design for this element of the architecture was not directly inspired by the broker pattern, but was conceived independently. It was only later in the project that the correlation between the bridge role and the element in question was identified. The cohesion of the bridge role into the rest of the system was adversely affected by this sequence of events; a more detailed understanding of the broker pattern across the project team probably could have alleviated this.
Examples of remote applications that invoked services with the bridge include a test tool and a telephony device management application. Outgoing service invocation using the bridge was not elaborated on this project.
Establish service interactions
In the system thus far, services were able to talk to one another but the nature of the interactions between services was not understood.
Client-server interactions were established as central to the overall behavior of the system -- services were expected to be both clients and servers. Other elements of the system, such as application presentation logic, were only expected to be clients.
Two types of client-server interaction were introduced: synchronous and asynchronous. The service bus subsystem automatically provided for both types of interaction, given that it was purely message based and supported a read method that could wait for an incoming message.
A typical example of a synchronous interaction would be a subscription request from one service to another, where the requesting service required a successful response message before proceeding. Subsequent event notification is a typical example of asynchronous interaction.
In addition to establishing interaction patterns between services, the application of client-server also supported separation of services into easily understood client and server roles.
Once asynchronous interactions were established between clients and servers, the asynchronous completion token pattern was applied to allow clients to deal effectively and efficiently with asynchronous responses. An initial attempt was made to apply this pattern to the system infrastructure, but this activity was discontinued because it was believed that the pattern was more applicable to service interactions directly, rather than for supporting infrastructure. It might have made sense to complete this activity to provide general purpose support for the pattern within the architecture. The pattern was not easily understood by service developers, so was only applied to a relatively few service interactions.
Now that services were able to communicate and interact with one another, it was evident that services were handling their own execution and were running their own message loops to process messages received over the service bus. The architecture vision indicated that services should be focused on business logic rather than execution details, so abstracting service execution was an essential next step. The executor pattern was applied to achieve this.
Given that all interactions between services would be conducted using the service bus, an executor could be created that took queued messages off the service bus, determined execution context for a service based on the received message, then passed the message to the service for handling.
Initially, a single-threaded executor was created, but this was quickly modified to introduce a thread class and a thread pool using pooling, which gave the executor the ability to support concurrent execution of services. Further modifications provided configurability, making it possible to select either a single or multithreaded execution model for each service with a configuration file.
Practically speaking, the threading configuration mechanism was not extensively used because the vast majority of services were required to handle multiple simultaneous requests to ensure acceptable system responsiveness.
Provide well-defined interfaces
Service communication, interaction, and execution abstraction were in place, so the next key step was to establish well-defined interfaces between services. The system thus far required services to process the messages they received themselves. Keeping services focused on business logic rather than communication details, such as message format, was a pivotal aspect of the architecture. The explicit interface pattern was applied to achieve support for well-defined interfaces between services, and the proxy pattern was applied to achieve decoupling of business logic from communication details.
Clients that required the capabilities of a particular interface would discover a service that supported that interface using the "service context," then make method calls against it. Service interfaces were given names to uniquely identify them. For services that could be resolved locally, the actual service object would be called using the interface. For remote services the proxy object would be called. The local invocation mechanism was quickly disabled because it bypassed the abstracted execution. However, under certain circumstances it could provide a useful way of optimizing service invocation for performance reasons, especially if it could be enabled by configuration.
The explicit interfaces were defined in pure abstract base-classes to enable both proxy and service classes to easily implement them, and to allow seamless transition between local and remote invocation. Each service or proxy that supported an interface was required to inherit from the abstract base class that defined the interface. For services to support multiple interfaces, a Java™-style approach to multiple inheritance was taken, for example with multiple, purely abstract base classes, each of which defined an interface that the service would support.
Using multiple inheritance in C++ could lead to the problem of multiple copies of a base class for a particular multiply-inheriting class. However, multiple inheritance being used only for interface implementation was thought to alleviate this problem somewhat.
Typical operations found on service interfaces include:
- Subscription to telephony-related events.
- Actions such as making a call, or changing presence status.
- Requests to access or modify configuration values or data variables.
Subsequently, this approach conflicted with the platform independence goals of the project. Each explicit interface was defined in an abstract C++ base class, which is inappropriate for service clients that do not share the same underlying platform (such as a Java-based client on a remote system). For service clients on other platforms, we had to provide an alternative definition of the interface, which was in the form of documentation of underlying message formats. On reflection, it would have been more appropriate to provide the interface definition in a platform independent form, for example using an Interface Definition Language such as CORBA IDL or Web Services Description Language (WSDL).
Symmetrical invoker counterparts to proxy objects, which were inspired by command (from the Design Patterns book) but are described formally as a pattern in Remoting Patterns, were then introduced to invoke the explicit interfaces on remote services. A one-to-one relationship was established between proxy and invoker objects, with the explicit interface being the common association.
The executor class was modified so that for each message received by a particular service, a suitable invoker class was discovered using the framework context. The executor delegated responsibility for invoking the service to the invoker object, passing it the message, and a reference to the service that was being invoked and that supported the required interface.
Both proxy and invoker components required the support of the framework context object to establish certain communication details, such as the service bus locations for return messages.
One consequence of using the explicit interface, proxy, and invoker patterns was that the complexity of service interaction was difficult to understand for developers who were new to patterns and object-oriented development. Establishing relatively simple service interactions required a significant amount of development. On reflection, it would have been better if the proxy and invoker classes had been automatically generated. A partial "Service Generator" was written that generated skeleton code for proxy, invoker, and service classes from an explicit interface. Unfortunately, due to project time pressures, it was not possible to develop this further.
Observer was then applied to support notification of events asynchronously to multiple subscribers using well-defined interfaces. The application of this pattern built upon the asynchronous behavior introduced by client-server, and the interfaces introduced by explicit interface. Notification events were communicated to observers with call-back explicit interfaces and their respective proxy and invoker objects.
Incidentally, Observer was used widely in the software system. It was applied to solve several different problems and, along with the proxy pattern, became a core project metaphor that was often discussed in design sessions. Problems such as how to notify multiple subscribers of physical device events, managed-data update events, and other telephony-related events were solved by applying this pattern. Observer was the key pattern for handling notification of events within the architecture.
Figure 2. Service invocation

Figure 2 shows the flow of service invocation.
With well-defined interfaces in place between services, there was still the problem of ensuring there were no dependencies between the clients of particular interfaces and the services that implemented those interfaces. Service transparency was required to solve the problem.
The service context provided both local and remote discovery of services supporting required explicit interfaces. Lookup was applied to achieve this. Two sources were consulted during discovery: the local "repository" of services, which was managed by the component configurator implementation, and a "service registry" in a well-known location.
If a service supporting the required explicit interface was found in the local repository, a direct reference to the service object would be returned. If no service supporting the interface was found locally or if local discovery was disabled (as described in Provide well-defined interfaces), the registry of services was consulted and a proxy was created based on the discovered information. A service-bus address representing the location of the service that supported the required interface was passed to the proxy object, along with a service-bus object so that it could send messages. A similar lookup mechanism was established to enable the executor implementation to lookup invoker objects based on the interface being invoked.
The approach ensured that service clients had no dependencies on the concrete services that implemented the explicit interfaces they invoked. It also helped to ensure that proxy and invoker objects were not associated with particular remote implementations of explicit interfaces. Rather, they simply provided a way of transforming invocations on interfaces to and from a message-based form.
One significant consequence of this approach became apparent.
The interaction between the explicit interface, proxy, invoker, and component configurator implementations relied heavily on C++ Run-Time Type Information (RTTI). Dynamic casting was required when casting objects created in different shared libraries or executable files to specific explicit interface types, such as when a client service cast a proxy object to a particular interface to make service requests. This requirement on RTTI was a restricting factor in the environments with limited resources that were being targeted on the project.
The initial service discovery mechanism was based purely on required interfaces. But, in the future it would be possible to extend available discovery options using the service context. For example, discovering services by criteria such as "number of supported database entries" or "security level" could be supported.
Figure 3. Contexts supported by component configurator implementation

Figure 3 shows the contexts supported by component configurator.
The core framework of the software system was now in place. Managed services were:
- Able to interact with each other both synchronously and asynchronously over a service bus supporting location independence
- Unaware of communication and execution details
- Abstracted from service provider details with well-defined interfaces supported by explicit interfaces, and proxy and invoker objects
The Interceptor pattern was then applied to achieve a core project goal of extensibility. It had been preselected as a pattern to be applied in the overall architecture vision. The goal was to introduce an interception point within the service bus -- the core framework component that was responsible for transporting messages between services and service clients.
To achieve a degree of consistency between different interception points, it was decided to create a set of base classes that could be subclassed for each concrete interception point. The Template Method pattern was applied to establish a common mechanism for dispatching events to interceptors, where certain concrete implementation decisions were delegated to the subclasses associated with particular interception points.
The team thought that the base classes would prove useful at a later point in the project when dynamic management of interceptors was envisaged, potentially with a component configurator. In retrospect, introduction of these classes without an explicit requirement for them might have been unnecessary when the classes were introduced. Keeping the initial implementation as simple as possible and introducing an abstraction only when needed, may have been more appropriate.
The service bus underpinned the entire software base, so the interception point was made generic to ensure maximum adaptability and applicability. It was felt, though, that this might have performance implications, especially if many concrete interceptors were plugged in. The requirements for the interception point were modest in the first usage scenario, and no performance problems have been attributed to it thus far.
Careful selection of interceptors is required to ensure performance remains acceptable in the future, with performance profiling and code efficiency enhancements potentially necessary in the worst-case scenario. A proof of concept logging interceptor was initially created to intercept and log messages being transmitted by the service bus, and a further interception point was later introduced at the point of service invocation in the executor implementation. A performance-recording interceptor that recorded performance statistics for each service invocation was written to use this interception point. Security-related interceptors are expected to be created in the future.
The "bridge" role from the broker pattern was then implemented using the template method pattern to support remote service invocation using several different underlying communication technologies. The problem of how to invoke services remotely was recognized early in the project. Services could and would be invoked with many different remote transport mechanisms and technologies (UDP/IP and USB, for example). A general purpose approach was deemed necessary to prevent complication and inconsistency being introduced into the system, and to support the platform independence goal.
A template class was introduced, which understood how to interact with the service bus locally, and which delegated the implementation details of interacting with the outside world to subclasses using template methods.
This approach provided an easy way of adding new subclasses for different types of external communication at later stages in the project. It also enabled the separation between internal and external service communication details. A consequence that emerged later in the project was that the implemented template class was not ideally suited to all types of communication technology. The initial implementation was based around the first required technology, but other technologies introduced later didn't fit perfectly with the template. It would have made more sense to establish a better understanding of the expected communication technologies in advance, and to define the template in a more generic way based on that understanding.
The core of the system, plus a number of extensibility mechanisms, were in place. The next task was to ensure that the system performed acceptably. Several patterns were applied to achieve this, although they were not applied as an afterthought. They were introduced as needed during development. We'll discuss them together for clarity and to illustrate their relationship with the other patterns previously applied.
Pooling was applied in the executor to achieve efficient use of thread resources. The application of asynchronous completion token also supports the efficient use of threads. For example, services waiting for asynchronous responses could release threads back into the thread pool.
The caching pattern was employed to store proxy and invoker objects for future use. A central cache of proxies and invokers could be queried with the framework and service contexts. Caching could also have been employed within the executor; for example, the most recent invoker object associated with a particular service could be cached.
The local resolution of services supported by the explicit interface and proxy patterns, described in Provide well-defined interfaces, contributed to acceptable performance by selectively bypassing communication and execution infrastructure.
Finally, lazy acquisition pattern was applied to the initialization of services by exposing a startup method with the framework context interface. The component configurator implementation was modified to selectively initialize services based on a configuration parameter, and those services that were not automatically initialized at startup could be started using the context. The executor implementation was modified to initialize services the first time they received a message.
Having established the system core, extensibility mechanisms, and acceptable performance, the final step was to apply cross-cutting concerns to the system.
The singleton pattern was often dismissed during design sessions as being an "anti-pattern" -- a "politically correct term for a global variable" (Wikipedia definition of the singleton design pattern). However, the project had two closely related scenarios where the singleton pattern proved useful: for logging and exception handling. Unfortunately, Aspect-Oriented Programming was not considered. It might have provided a better solution, but a C++ tool enabling Aspect-Oriented Programming was not immediately obvious so it wasn't considered.
One goal of the project was the reuse of existing software components. The team thought that the handling of certain cross-cutting concerns within the software architecture could only be provided by some form of global instance. Several large software modules from previous projects were introduced into the system during development, and a common approach to cross cutting concerns was required for both existing and new code.
Changing the existing code to introduce new mechanisms for cross-cutting concerns was too much effort, so singleton logging-handler and exception-publisher classes were created. To enable the reuse of existing code, calls to the logging singleton objects were wrapped in macros that were very similar to macros in the legacy code base. This allowed an easy transition to the new logging mechanism to take place within the existing code.
The singleton exception publisher was responsible for ensuring that exceptions were reported to a single location within the system. This approach ensured that any exception within the system could be reported to an element of the architecture which was able to provide a higher level of system availability. This follows the "Fault Tolerant Hierarchy" approach described in chapter 6 of Software Architecture In Practice.
Although the use of singletons supported the reuse goals of the project, this pattern caused all code to be dependent on particular instances of cross-cutting concern classes. It would have been better to allow multiple instances of logging and exception-handling classes. Instances of these classes could have been provided to both service and framework elements using the service and framework context interfaces, while global instances would support cross-cutting concerns in the existing code. This would have:
- Provided a more flexible approach to applying cross-cutting concerns in the system, while maintaining a degree of commonality between existing and new code
- Avoided tying all code to particular instances of cross-cutting concern classes.
The "pure" singleton implementations were subsequently watered down to allow core infrastructure management code to dynamically manage the respective object instances.
Because the executor pattern encapsulated execution for all services, a common exception-handling strategy was applied to all services. This was achieved by adding a general "catch-all" block in the Thread class. In case of an exception, the catch block simply called the exception publisher, then returned the executing thread back to the thread pool.
This section describes how the selected patterns helped solve the problems, outlined in Table 1, by summarizing how each of the forces was resolved.
- Future reuse of software components in a product-line development strategy
- Layers provided the overall architectural structure required to support a product-line development strategy by separating
reusable and platform-specific software elements into different layers.
Use of wrapper-facade pattern to encapsulate operating system, legacy, and third-party code provided a way of separating existing code from new code with suitable abstractions, which supported reuse of both sets of code.
Management of service life cycle using component configurator pattern and support for location transparency with broker, provided a way of deploying services in a wide variety of different scenarios, supporting reuse.
Client-server pattern helped establish clear roles within the system, supporting the creation of services with clearly defined roles and responsibilities. It contributed to the clarity and cohesiveness of the services, and thus to overall reusability.
Service-lifecycle was abstracted with component configurator pattern; communication was abstracted with the broker, along with proxy and invoker; execution was abstracted with executor pattern; and cross-cutting concerns were abstracted using the singleton pattern. All enabled services to focus on business logic, helping to create reusable assets.
Well-defined, discoverable interfaces supported by explicit interface pattern, encapsulated context object, and lookup also enabled services to focus on business logic because of the concentration on capabilities exposed by interfaces rather than implementation details.
Encapsulated context object provided a way of encapsulating the execution context of service and framework elements. Decoupled context interface provided a decoupling between context implementation and context user. The combination of these patterns provided coherent, decoupled access to execution context details.
Use of observer enabled the creation of reusable services that generated events for multiple subscribers, with few or no dependencies on other services, resulting in greater reuse opportunities.
Provision of a generic interceptor within the service bus provided a useful extension point. Using the template method for implementing the bridge role of the broker pattern provided a mechanism for adding alternative types of external communication in the future.
- Seamless (re)deployment of services at run time
- Component configurator provided a dynamic mechanism for managing service life cycle and deployment. It provided a way of arbitrarily assigning services to run in different processes, and a means for quickly removing services that were causing crashes or lockups. In some cases it was possible to fix defects simply by rearranging service configuration files. It also allowed for the creation of special configuration files for running tests.
- Service invocation transparency independent of deployment model
- Broker provided location transparency. Service transparency was provided by explicit interface and lookup, which provided decoupling between services and the interfaces between them.
The use of pure abstract C++ explicit interface classes worked against true service invocation transparency, because service clients on non-C++ systems required alternative interface definitions.
- A common approach to management, testing, and handling of services
- The well-defined, decoupled interfaces introduced by explicit interface enabled the creation of general purpose interfaces for management and testing, which services were able to support as required.
Singleton supported cross-cutting concerns across a wide variety of software components, at different abstraction levels, with both new and legacy code.
- Reuse and protection of existing software components
- The use of wrapper-facade to wrap complex interactions between legacy code modules provided greater reuse opportunities. It also provided a layer of protection between new and legacy code, ensuring the two types of code did not inappropriately affect one another.
Explicit interface also enabled the reuse and protection of existing software components by allowing them to be contained within services.
By providing singleton objects that supported interfaces required by legacy components, it was possible to make more effective use of legacy components.
- Acceptable performance within resource-limited environments
- The caching of proxy and invoker objects supported efficient use of resources, along with pooling of threads in the executor. Asynchronous completion token supported thread pooling by allowing services to release threads back into the pool while awaiting asynchronous responses.
By introducing well-defined interfaces with explicit interface, quick and efficient access to services by providing local resolution was ensured, albeit at the cost of breaking execution encapsulation.
Unfortunately, the interaction between the explicit interface, proxy, invoker, and component configurator patterns may have had a negative impact overall in terms of memory consumption and run-time performance due to reliance on C++ RTTI.
Lazy acquisition provided a quick and efficient service startup mechanism by enabling service startup to be delayed until required.
- Independence from specific computing platforms
- Layers gave the fundamental architectural structure required to support platform independence by separating reusable code and platform specific code into different layers.
Using wrapper-facade allowed the definition of pure OS abstractions, which could be realized for particular operating systems, and thus allowed service and service-related code to be platform independent.
The use of pure abstract C++ explicit interface classes was working against platform independence, because non-C++ service clients required alternative interface definitions.
In this article you learned how patterns can be applied to achieve service-oriented component middleware for telephony devices with limited resources. The project included key SOA characteristics of self-contained reusable software modules, well-defined interfaces, and platform independence.
Forces related to reusability, adaptability, and performance led to the application of a wide selection of patterns including component configurator, interceptor, encapsulated context object, and proxy. Patterns were central to the creation of a framework for achieving the project's architectural goals. The use of C++ RTTI, as required by the implementation of a number of patterns, was recognized as potentially causing problems in resource limited environments. The development overhead required for nongenerated invoker and proxy classes was also a cause for concern.
The entire service framework -- the life cycle management, communication, service interface, and cross-cutting concern elements, to name a few -- were developed in-house. There are advantages to developing in-house solutions, but an alternative approach would have been to use a third-party framework, such as embedded CORBA or IBM Rational Rose® RT. Several of these were considered early in the project, but were rejected because no suitable third-party framework could be identified.
The patterns applied on the project were not drawn from one particular pattern language; they were selected from several of the most popular patterns publications, were suggested by a knowledgeable source, or were already known by team members. In hindsight, applying patterns from a carefully selected pattern language rather than in a piecemeal way could have led to more appropriate pattern selections, which might have avoided some of the pitfalls. An example of a such a pattern language is Remoting Patterns, which provides patterns for solving problems in the domain of enterprise, Internet, and real-time distributed object middleware.
As of the writing of this article, several major project delivery milestones have been declared, and the project is now approaching its final delivery milestone.
- Thanks to Roman Pichler, for suggesting I write this article in the first place, for reviewing the first two drafts, and for his ongoing support and friendship.
- Thanks to Kevlin Henney, for suggesting many of the patterns discussed, for being a general font of knowledge for the project team, and for providing comments and support during the writing of this article.
- Thanks also to my fellow team members on the project described, for providing valuable input to the article, as well as an enjoyable working experience.
- In particular, I would like to thank Michael Kircher, who shepherded the article at VikingPLoP 2006 and provided numerous thought-provoking and insightful comments both before and after the conference.
- Thank you, Al Greer, for taking my photograph and for a great night out in Nottingham.
- Finally, I would like to thank the VikingPLoP 2006 conference participants for their valuable feedback.
Learn
- Wikipedia is a collaborative, community-based Web encyclopedia. Several quotations in this article
are drawn from Wikipedia.
-
More information on Pattern Stories can be found in two papers by Kevlin Henney:
- "Substitutability: Principles, Idioms and Techniques in C++"
- "Context Encapsulation: Three Stories, A Language, and Some Sequences"
-
The Pattern Oriented Software Architecture books combine the experiences
of many of software engineering's most experienced practitioners to provide a broad collection of patterns based around creating software architecture.
- Volume 1 introduces a core collection of patterns.
- Volume 2 provides patterns related to concurrency and networked objects.
- Volume 3 documents patterns related to resource management.
- Volume 4 and Volume 5 provide a pattern language for distributed computing, and discuss patterns and pattern languages in general.
- Design Patterns: Elements of Reusable Object-Oriented Software is the book that
brought patterns into the common language and understanding of the software engineering community and
distills the experience of the authors to provide a collection of widely applicable object-oriented patterns.
-
The following links provide information
for those patterns not covered by either Design Patterns or the POSA books:
- For information on encapsulated context object and decoupled context interface, see "Context Encapsulation: A Language, Three Stories, and Some Sequences," by Kevlin Henney.
- The Invoker pattern was inspired by the Command pattern (from Design Patterns, above), but is documented formally as a pattern in its own right in "Remoting Patterns: Foundations of Enterprise, Internet, and Realtime Distributed Object Middleware."
- Client-Server is a widely known pattern in software engineering, but it is rarely captured explicitly as a pattern. The Mobile Design Patterns and Architectures project captured several Patterns for Mobile Application Development, one of which was the Client-Server pattern.
- The executor pattern was presented at VikingPLoP 2002. [See below for more information on the Pattern Languages of Programs (PLoP) conferences.] The paper is in the 2002 conference proceedings.
-
"Conway's Law," broadly speaking, states that it is inevitable that system architecture will follow organizational communication structure.
Conway's original paper describing the law can be found in
"How do Committees Invent?," while associated patterns can be found in
Organizational Patterns of Agile Software Development
and "A Development Process Generative Pattern Language," which is published in
Pattern Languages of Program Design.
- Software Architecture in Practice, by Len Bass, Paul Clements, Rick Kazman. Introduces the concepts and practices of software architecture, what a software system is designed to do, and how that system's components are meant to interact with each other.
-
The Hillside group is the bedrock of the patterns community. It sponsors many PLoP conferences around the world, including PLoP
in North America, VikingPLoP in Scandinavia, and EuroPLoP
in Europe. Hillside also sponsors publication of the
Pattern Languages of Program Design books.
- Browse the
technology bookstore for books on these and other technical
topics.
Get products and technologies
-
Download IBM
product evaluation versions and get your hands on application development tools
and middleware products from DB2®, Lotus®, Rational, Tivoli®, and
WebSphere.
Discuss
-
Check out developerWorks
blogs and get involved in the developerWorks community.

