| Components, States and Interfaces, Oh My! |
A component is really a large-scale object used in an idiomatic way.
by Bruce Powel Douglass
In past columns, Clemens Szyperski and Bertrand Meyer have raised some interesting and important issues regarding what constitutes a component, but they haven¡¯t reached individual conclusions, let alone a common one. I won¡¯t offer the final word in this column, either; nevertheless, this is a vital debate, so I¡¯m throwing my virtual hat into the ring.
What is a component? Well, in some sense, it¡¯s whatever you want it to be. According to the "OMG Modeling Language Specification" (Revision 1.3): "a component is a physical, replaceable part of a system that packages implementation and provides the realization of a set of interfaces. A component represents a physical piece of a system¡¯s implementation, including software code (source, binary or executable) or equivalents, such as scripts or command files. As such, a component may itself conform to and provide the realization of a set of interfaces, which represent services implemented by the elements resident in the component. These services define behavior offered by instances of the component as a whole to other client component instances."
In the UML, a component is a classifier, and so may realize interfaces, have executable behavior and state. It also aggregates other model elements, and may reside on a node (processor) during execution. These things are also true of subsystems. The UML defines a subsystem as a "special kind of Package that represents a behavioral unit in the physical system, and hence in the model" ("OMG Modeling Language Specification," Revision 1.3). A subsystem is a metasubtype of both Classifier (same as a component) and Package (different than component). However, Packages are distinguished by the fact that they can contain model elements, so we are left questioning how subsystems and components differ.
In my last contribution to this column ("Components: Logical, Physical Models," Beyond Objects, Dec. 1999), I presented a taxonomy based on a scope that I have found pragmatically useful: Systems can contain subsystems, which can contain components, which can contain objects. (See Figure 1.)
| Figure 1. Architectural Elements by Scope
|
While Figure 1 reveals where components typically reside in the taxonomy of architectural things, it doesn¡¯t tell us what they are and how they are used.
Bertrand Meyer in "What to Compose" (Beyond Objects, Jan. 2000) provided the following criteria for components:
? May be used by other software elements (clients).
? May be used by clients without the intervention of the component¡¯s developers.
? Includes a specification of all dependencies (hardware and software platform, versions, other components).
? Includes a precise specification of the functionalities offered.
? Is usable on the sole basis of that specification.
? Is composable with other components.
? Can be integrated into a system quickly and smoothly.
However, this definition applies to objects of all sizes. And yes, I did say "objects," because I believe a component is really a large-scale object used in an idiomatic way. Objects, components, subsystems and even systems (if they are documented) can be used by clients without the intervention of the components¡¯ developers. In fact, objects, components, subsystems and systems should (to be reusable) specify the interfaces, system-visible dependencies and functionality. They should also fit together into collaborations to realize larger scale behavior and ought to be easily integratable. I would also add to the list executability or the ability to provide run-time behavior (even if only get/set behavior) for their clients. So what then is the distinction among objects, components and subsystems? I¡¯ll get to that in a moment: First, I want to say a few words about component behavior, interfaces and state.
Component Behavior
Much has been said about component behavior. Many think of the component¡¯s interface as a detailed set of services that can be invoked by its clients. That is indeed a reasonable view. However, the names of the operations and their parameter lists are hardly an adequate description to understand how the interface should be used, what services may be invoked under different circumstances, when one service might be more appropriate than another, and so on.
A somewhat more abstract way to think about the interface of a software architectural unit, such as a component, is the use case. A use case is a named capability of a system that does not reveal or imply the implementation of the capability¨Cin this sense, it¡¯s a high-level view of an opaque interface. (A more detailed view would be a list of operations.) However, the use case also includes the operations, their signatures, pre- and postconditions, and the set of allowable sequences (the protocol) of the interface.
In fact, we can use the notion of a use case as a high-level view of an interface to help us capture the aspects of the component behavior.
For example, a traffic control system contains a number of components: several instances of a traffic light component, a control component which manages the interaction of the traffic light components, a communication component that allows them to communicate, and so on. The traffic light component might have a small set of use cases, as shown in Figure 2.
| Figure 2. Traffic Light Component Use Cases
|
Figure 2 illustrates the interface of the traffic light component at a high level. This use case diagram is inadequate without a more detailed view, but does serve to highlight the major capabilities of the components. This aspect wouldn¡¯t be included in the more detailed view offered by merely listing the operations provided by the component.
State and Components
I find it useful to think about behavior as being in one of three different categories: simple, continuous or state (see my book Doing Hard Time: Developing Real-Time Systems with UML, Objects, Frameworks and Pattern, Addison-Wesley, 1999).
Simple behavior does not depend upon the component¡¯s history. By this definition, cos(x) is simple because I don¡¯t expect the answer to vary depending on what I called it with last time. Cos(p/2) ought to always return the same result.
Continuous behavior depends on the history of the object but does so in a nondiscrete way. Amplifiers, Proportional-Integral-Derivative (PID) control loops, digital filters, fuzzy logic and neural networks all exhibit behavior that acts in this way. The output depends on the current input and its history.
State behavior similarly depends on history, but in a discrete way. An object, when in state X, accepts a particular set of input events, does a particular set of actions and activities, and can reach a particular set of subsequent states. Different states differ in one or more of these three properties. So a sensor, when in the "waiting for data" state, might accept the "data ready" event, perform the "poll for data" action and go to the "has data" state when the "date ready" event occurs. When in the "has data" state, a sensor might then accept the "request for data" event, perform the "filter data" activity and return to "waiting for data" once the data has been read by the client.
The point is, executables are defined by behavior. They can have any or all of the three behaviors (even at the same time) in different behavioral aspects. Components, being executable, therefore may have state. A component has state behavior when its behavior depends on its history in a discrete way. One can imagine a component for a traffic light controller that has states of red, yellow, green, flashing red, flashing yellow and even flashing green. It makes perfect sense to think about the component having a set of use cases that might be described using a formal language such as Statecharts. Indeed, we are doing this on a project for NASA. Internally, there are many ways that such a component might be implemented, enhanced or extended and, as Clemens Szyperski writes in "Point, Counterpoint" (Beyond Objects, Feb. 2000), "The main point of such components it to enhance the extensibility and evolvability of deployed systems."
Interfaces and Dependencies
Based on the past columns, one might come to the conclusion that an interface is solely the signature of the set of operations provided by the component. I don¡¯t believe either Szyperski or Meyer intended this interpretation, but I want to make the point explicit. The signature of the operation is only one of three crucial aspects of the interface; the other aspects are preconditions and postconditions.
This is very similar to what Bertrand Meyer writes in Object Oriented Software Construction (2nd Edition, Prentice Hall, 1997). In order to facilitate replaceabilty, components typically provide strong encapsulation and opaque interfaces.
When the services offered by a component interface can be called at anytime, the component (externally) exhibits simple behavior. When some operations either may not be invoked or act differently, then the interface is either continuous or state-driven. It¡¯s often useful, in fact, to capture the state behavior of an interface via a statechart. In fact, this is commonly called the protocol of the interface. Formally speaking, the interface protocol is a subset of the preconditions, but practically speaking, it is a very important subset.
Consider our traffic light control component and its Control Light use case. There are a number of rules that govern its behavior:
? The component¡¯s available conditions of existence (states) are flashing yellow, flashing red, red, yellow and green.
? Once the component accepts an event to change from red to green, the transition shall take two seconds.
? Once the component accepts an event to change from green to red, it shall wait three seconds, turn yellow for two more seconds and then turn red.
? Once initialized, the component shall always accept a command to go into a "flashing red" or "flashing yellow" state.
? The component can go immediately from the "flashing red" state to a "red" state when it receives an event to do so.
? From the "flashing yellow" state, the system shall turn yellow for two seconds and then turn red when a "turn red" event is received
This component is expected, by its client, to fulfill the contract specified by these rules. We can capture these rules parsimoniously using a statechart as shown in Figure 3. Notice that the "turn green" event is ignored in all but the "red" state (in other words, the component is specifically expected to quietly discard the event if received while in any other state). This statechart specifies the expected externally visible behavior of the interface. It does not imply, as Clemens Szyperski suggests, global variables to implement the statechart, but even if globals are used, the advantage of components and their opaque interfaces is that we don¡¯t care how the components are implement¨Cglobal variables or no.
| Figure 3. Traffic Light Component States
|
An even more detailed view of the interface than the statechart is the individual operation. An interface is a named set of operations as well as the protocol that describes their usage. This detailed view can be provided in a number of ways. It is common to merely list the entire set of operations, along with preconditions, postconditions, parameters and their types, return values and the means necessary to invoke the operations (commonly by calling a member function, but other approaches are possible).
Another approach is to show a set of objects contained within the component that are accessible to the outside. These interface or boundary objects provide an way of packaging the operations into smaller coherent bundles, according to Ivar Jacobson, Grady Booch and James Rumbaugh in their book The Unified Software Development Process (Addison-Wesley, 1999). They can be shown on a class diagram representing the component while the rest of the objects inside the component remain hidden.
Components don¡¯t often stand alone. They are frequently arranged so that more abstract or client-friendly services can be offered, and the implementation uses more concrete or less client-friendly components to realize that behavior. The reason dependencies must be identified is because these services are not solely encapsulated within the component. Therefore, replacing one component with a new revision might involve replacing one the components on which it depends.
Again, this isn¡¯t unusual. Objects must do exactly the same thing when they collaborate. In my previous column, I note that objects collaborate to realize large-scale behaviors, and they can do this at various levels of abstraction. At the highest level of abstraction, the collaborating objects are the "system" and the actors that interact with it. The system object provides capabilities, identified through its use cases. The next level down is the subsystem in which large-scale architectural pieces of the system collaborate to realize the system use cases. At the next level, components collaborate to realize the use cases of the subsystems, and so on. (Positioning components on the second tier of abstraction is pragmatically useful because the second tier contains items that are usually the right size and scope for a replaceable unit.)
So What¡¯s a Component?
So here¡¯s what I think is a useful definition of a component: A large-scale executable object designed to be easily replaceable as a unit in the context of a system.
A component¡¯s other aspects exist merely to facilitate that purpose. Opaque interfaces help replaceability because they allow different components to realize a behavior differently, as long as the interface is maintained. Behavior of the component interface can be arbitrarily elaborate and specified using various means for modeling behavior. A component is large scale because it contains (via composition) smaller objects, some of which may be active in the UML sense (that is, they form the roots of threads).
The primary use or purpose of components is to facilitate construction of reusable pieces that can easily be inserted into a wide variety of systems without requiring knowledge of the internal implementation of the component, and within the context of a system, facilitate the evolution of that system with specialized, extended, elaborated or optimized versions of components that realize the same set of interfaces.
Components are objects that we treat as slightly special because we want to optimize a particular aspect of their quality of service¨Creplaceability. Because of this, we provide them with opaque interfaces and extensively document the interface¡¯s details and itsdependency on other components. We benefit by being able to construct systems in a manner similar to the process electrical engineers follow today¨Cby piecing together different large-scale pieces, our systems become easier to maintain, evolve and even construct.
A win-win scenario for everyone concerned.