Not all components work together?even if they are commercial products that claim compatibility. Components are often "almost compatible," where "almost" is a euphemism for "not." More insidious is the case where components appear to work together?the assembled code compiles and even executes?but the system produces the wrong answer because the components do not work together quite as expected. The errors can be subtle, especially in real-time or parallel systems in which the components might rely on seemingly innocuous assumptions about the timing or relative ordering of each other's operations.
In short, components that were not developed specifically for your system may not meet all of your requirements?they may not even work with the components you pair them with. Worse, you may not know if they are suitable or not until you buy them and try them because component interfaces are notoriously poor at specifying their quality attributes: How secure is the compiler you are using right now? How reliable is the mail system on your desktop? How accurate is the math library that your applications depend on? And what happens when you discover that the answer to any of these questions is "not enough"?
Garlan, Allen, and Ockerbloom coined the term architectural mismatch to describe this impediment to successfully integrating component-based systems. They state the problem as a mismatch between assumptions embodied in separately developed components, which often manifests itself architecturally, such as when two components disagree about which one invokes the other. Architectural mismatch usually shows up at system integration time?the system will not compile, will not link, or will not run.
Architectural mismatch is a special case of interface mismatch, where the interface is as Parnas defined it: the assumptions that components can make about each other. This definition goes beyond what has, unfortunately, become the standard concept of interface in current practice: a component's API (for example, a Java interface specification). An API names the programs and their parameters and may say something about their behavior, but this is only a small part of the information needed to correctly use a component. Side effects, consumption of global resources, coordination requirements, and the like, are a necessary part of an interface and are included in a complete interface specification. Interface mismatch can appear at integration time, just like architectural mismatch, but it can also precipitate the insidious runtime errors mentioned before.
These assumptions can take two forms. Provides assumptions describe the services a component provides to its users or clients. Requires assumptions detail the services or resources that a component must have in order to correctly function. Mismatch between two components occurs when their provides and requires assumptions do not match up.
What can you do about interface mismatch? Besides changing your requirements so that yesterday's bug is today's feature (which is often a viable option), there are three things:
Avoid it by carefully specifying and inspecting the components for your system.
Detect those cases you have not avoided by careful qualification of the components.
Repair those cases you have detected by adapting the components.
The rest of this section will deal with techniques for avoiding, detecting, and repairing mismatch. We begin with repair.
To date, mismatch correction (or "component/interface repair") has received little systematic attention. Terms such as "component glue" are evocative of the character of the integration code and reflect the second-class status we assign to its development. Often repairing interface mismatches is seen as a job for hackers (or sometimes junior programmers) whose sense of aesthetics is not offended by the myriad "hacks" involved in integrating off-the-shelf components. However, as is often the case, the weak link in a chain defines the chain's strength. Thus, the quality of component repair may be directly responsible for achieving?or failing to achieve?system-wide quality attributes such as availability and modifiability.
A first step toward a more disciplined approach to interface repair is to categorize the basic techniques and their qualities. One obvious repair method is to change the code of the offending component. However, this is often not possible, given that commercial products seldom arrive with their source code, an old component's source code may be lost, or the only person who understood it may be lost. Even if possible, changing a component is often not desirable. If it is used in more than one system?the whole premise of component use?it must now be maintained in multiple versions if the change to make it work renders it unusable for some of the old systems.
The alternative to changing the code of one or both mismatched components is to insert code that reconciles their interaction in a way that fixes the mismatch. There are three classes of repair code: wrappers, bridges, and mediators.
The term wrapper implies a form of encapsulation whereby some component is encased within an alternative abstraction. It simply means that clients access the wrapped component services only through an alternative interface provided by the wrapper. Wrapping can be thought of as yielding an alternative interface to the component. We can interpret interface translation as including:
Translating an element of a component interface into an alternative element
Hiding an element of a component interface
Preserving an element of a component's base interface without change
As an illustration, assume that we have a legacy component that provides programmatic access to graphics-rendering services, where the programmatic services are made available as Fortran libraries and the graphics rendering is done in terms of custom graphics primitives. We wish to make the component available to clients via CORBA, and we wish to replace the custom graphics primitives with X Window System graphics.
CORBA's interface description language (IDL) can be used to specify the new interface that makes the component services available to CORBA clients rather than through linking with Fortran libraries. The repair code for the "provides assumptions" interface is the C++ skeleton code automatically generated by an IDL compiler. Also included in the repair code is hand-written code to tie the skeleton into component functionality.
There are various options for wrapping the component's "requires assumptions" interface to accomplish the switch from custom graphics to the X system. One is to write a translator library layer whose API corresponds to the API for the custom graphics primitives; the implementation of this library translates custom graphics calls to X Window calls.
A bridge translates some requires assumptions of one arbitrary component to some provides assumptions of another. The key difference between a bridge and a wrapper is that the repair code constituting a bridge is independent of any particular component. Also, the bridge must be explicitly invoked by some external agent?possibly but not necessarily by one of the components the bridge spans. This last point should convey the idea that bridges are usually transient and that the specific translation is defined at the time of bridge construction (e.g., bridge compile time). The significance of both of these distinctions will be made clear in the discussion of mediators.
Bridges typically focus on a narrower range of interface translations than do wrappers because bridges address specific assumptions. The more assumptions a bridge tries to address, the fewer components it applies to.
Assume that we have two legacy components, one that produces PostScript output for design documents and another that displays PDF (Portable Document Format) documents. We wish to integrate these components so that the display component can be invoked on design documents.
In this scenario, a straightforward interface repair technique is a simple bridge that translates PostScript to PDF. The bridge can be written independently of specific features of the two hypothetical components?for example, the mechanisms used to extract data from one component and feed it to another. This brings to mind the use of UNIX filters, although this is not the only mechanism that can be used.
A script could be written to execute the bridge. It would need to address component-specific interface peculiarities for both integrated components. Thus, the external agent/shell script would not be a wrapper, by our definition, since it would address the interfaces of both end points of the integration relation. Alternatively, either component could launch the filter. In this case, the repair mechanism would include a hybrid wrapper and filter: The wrapper would involve the repair code necessary to detect the need to launch the bridge and to initiate the launch.
Mediators exhibit properties of both bridges and wrappers. The major distinction between bridges and mediators, however, is that mediators incorporate a planning function that in effect results in runtime determination of the translation (recall that bridges establish this translation at bridge construction time).
A mediator is also similar to a wrapper insofar as it becomes a more explicit component in the overall system architecture. That is, semantically primitive, often transient bridges can be thought of as incidental repair mechanisms whose role in a design can remain implicit; in contrast, mediators have sufficient semantic complexity and runtime autonomy (persistence) to play more of a first-class role in a software architecture. To illustrate mediators, we focus on their runtime planning function since this is the key distinction between mediators and bridges.
One scenario that illustrates mediation is intelligent data fusion. Consider a sensor that generates a high volume of high-fidelity data. At runtime, different information consumers may arise that have different operating assumptions about data fidelity. Perhaps a low-fidelity consumer requires that some information be "stripped" from the data stream. Another consumer may have similar fidelity requirements but different throughput characteristics that require temporary buffering of data. In each case, a mediator can accommodate the differences between the sensor and its consumers.
Another scenario involves the runtime assembly of sequences of bridges to integrate components whose integration requirements arise at runtime. For example, one component may produce data in format D0, while another may consume data in format D2. It may be that there is no direct D0D2 bridge, but there are separate D0D1 and D1D2 bridges that can be chained. The mediator would thus assemble the bridges to complete the D0D2 translation. This scenario covers the mundane notion of desktop integration and the more exotic runtime adaptive systems.
In order to repair mismatches, we must first detect or identify them. We present the process of identifying mismatches as an enhanced form of component qualification.
The term component qualification has been used to describe the process of determining whether a commercial component satisfies various "fitness for use" criteria. Some component qualification processes include prototype integration of candidate components as an essential step in qualifying a component. This integration step discovers subtle forms of interface mismatch that are difficult to detect, such as resource contention. The need for this step is a tacit acknowledgment of our poor understanding of component interfaces.
Carrying out this evaluation starts with the observation that, for each service offered by a component, a set of requires assumptions must be satisfied in order to provide that service. A service is just a convenient way of describing how component functionality is packaged for use by clients. Qualification, then, is the process of
discovering all of the requires assumptions of the component for each of the services that will be used by the system.
making sure that each requires assumption is satisfied by some provides assumption in the system.
To illustrate these ideas more concretely, consider the qualification of a component that provides primitive data management services for multi-threaded applications. One service it provides is the ability to write a data value into a specified location (possibly specified by a key). In order to provide a multithreaded storage service, the component might require various resources from an operating system?for example, a file system and locking primitives. This listing of the component's requires assumptions might be documented by a component provider, or it might need to be discovered by the component evaluator. In either case, this particular mapping would be useful for determining whether an upgrade of the operating system will have any impact on this particular integration relation. That is, did the new operating system change the semantics of fwrite or flock?
The list may include additional assumptions; for example, a provides assumption may stipulate that a CORBA interface be provided to the storage service. Depending on which implementation of the object request broker is used, this may or may not imply an additional provides assumption concerning the existence of a running object request broker process on the host machine that executes the storage service.
The assumptions list may reveal more interesting dependencies. For example, the same hypothetical component may allow a variable, but defined, number of clients to share a single data manager front-end process, with new processes created to accommodate overflow clients. This form of assumption can be crucial in predicting whether a component will satisfy system resource constraints.
One technique for avoiding interface mismatch is to undertake, from the earliest phases of design, a disciplined approach to specifying as many assumptions about a component's interface as feasible. Is it feasible or even possible to specify all of the assumptions a component makes about its environment, or that the components used are allowed to make about it? Of course not. Is there any evidence that it is practical to specify an important subset of assumptions, and that it pays to do so? Yes. The A-7E software design presented in Chapter 3 partitioned the system into a hierarchical tree of modules, with three modules at the highest level, decomposed into about 120 modules at the leaves. An interface specification was written for each leaf module that included the access programs (what would now be called methods in an object-based design), the parameters they required and returned, the visible effects of calling the program, the system generation parameters that allowed compile-time tailoring of the module, and a set of assumptions (about a dozen for each module).
Assumptions stated assertions about the sufficiency of the services provided by each module and the implementability of each service by identifying resources necessary to the module. Specific subject areas included the use of shared resources, effects of multiple threads of control through a module's facilities, and performance. These assumptions were meant to remain constant over the lifetime of the system, whose main design goal was modifiability. They were used by module designers to reassure themselves that they had appropriately encapsulated all areas of change within each module, by domain and application experts as a medium for evaluation, and by users of the modules to ensure suitability. Participants on the A-7 project felt that careful attention to module interfaces effectively eliminated integration as a step in the life cycle of the software. Why? Because architectural mismatch was avoided by careful specification, including the explicit assumptions lists that were reviewed for veracity by application and domain experts.
The notion of an interface as a set of assumptions, not just an API, can lead to a richer understanding of how to specify interfaces for components that work together in a variety of contexts. Private interfaces make visible only those provides and requires assumptions from a component's base interface that are relevant to its integration requirements in a particular system, or even to particular components in it. The idea is to suppress information about facilities that are not needed and whose presence may needlessly complicate the system.
There are advantages to different interfaces for the same component rather than a single omnibus base interface. The finer control over inter-component dependencies makes certain kinds of system evolution more tractable?for example, predicting the impact of upgrading a commercial component to a new version. Wrappers can be thought of as a repair strategy for introducing privacy. Additionally, architectural patterns can provide canonical forms that satisfy the provides and requires assumptions for the interface so that the number of distinct derivatives of a base interface may be relatively small in a system based on an architectural pattern that defines a small set of component types.
A parameterized interface is one whose provides and requires assumptions can be changed by changing the value of a variable before the component service is invoked. Programming languages have long possessed semantically rich parameterization techniques (e.g., Ada generics, ML polymorphism) that tailor a component's interface between the time it was designed and coded and the time its services are invoked. Commercial products also frequently provide some degree of customization via product parameterization (e.g., resource files or environment variables). Parameterized interfaces result in adaptation code that is both external to the component, where the values of the parameters are set, and within the component (to accommodate different parameter values).
Just as a mediator is a bridge with planning logic, a negotiated interface is a parameterized interface with self-repair logic. It may auto-parameterize itself, or it may be parameterized by an external agent. Self-configuring software can be thought of as involving negotiated interfaces, where the negotiation is a one-way "take-it-or-leave-it" dialog between component-building software and a host platform. Alternatively, products, such as modems, routinely use protocols to establish mutually acceptable communication parameters at runtime (rather than at install time).
Like wrappers, which can be used as a repair strategy to introduce translucency, mediators can be used as a repair strategy to introduce negotiated interfaces into a nonnegotiating component.