Bruce MacIsaac Component and service-oriented architectures help you to build software that is more easily understood, changed, and reused. ProblemComponents are relatively independent units that can be assembled to build larger systems. The benefits of components include the following:
Service-oriented architectures are similar to component architectures, in that systems can be composed using services. Services are published capabilities that can be discovered and used dynamically. Identifying services and designing service providers act as an extension to the basic approach for identifying interfaces and designing components that implement them. This practice describes how to identify and document components and services. BackgroundLet's discuss why components are needed and review the different kinds of components. The Need for Components and ServicesEarly on in my career as a software developer, I was asked to review some code. Sadly this was not because of my great expertise, but because a quality requirement box needed to be checked, and nobody else was available. This code was part of a highly successful commercial product, critical to the company. I looked forward to learning how professionals designed software. My first disillusionment was that there was little design documentation, and it was out of date. "Look at the code" was the recommendation. After studying the code for several hours, I went to a senior developer and expressed my confusion. "I don't understand how this software is organized. There are lots of modules, but I don't understand the grouping." His response was that the low-level modules had become numerous over time and so were grouped alphabetically. Any module could invoke any other. Large data structures were passed around or shared by different modules, and it was difficult to determine what data each module used. The result was a system that was very difficult to understand. Fixing bugs in one module often introduced new bugs as side-effects, because the dependencies between modules were not clear.[10]
Eventually, that system was completely reengineered, at great expense. Instead of having lots of small modules organized alphabetically, components with specific responsibilities were organized into subsystems and layers. Instead of large shared data structures, interfaces explicitly defined inputs and outputs. Instead of minor changes and bug fixes having broad impact across the system, most changes could be isolated to a single component, while other code, which depended only on the interfaces to the component, was unaffected. From this and other experiences with both good and bad architectures, I have learned an important lesson: components that hide complexity behind interfaces are the key to creating understandable systems.
Definition of Component, Service, and Subsystem[11]
The term "component" has been overused. It is often used generally to mean "any constituent part." RUP defines it much more narrowly as "a non-trivial, nearly independent, and replaceable part of a system that fulfills a clear function in the context of a well-defined architecture." The key term is "replaceability." Anyone who has upgraded a computer or repaired a car understands the basic principles of component parts: there are connection points that have to fit, and the part has to meet certain specifications. These specifications allow you to replace an old part with a new one, even one built by a different manufacturer.
The most basic form of replaceability is design and implementation replaceability. The component is a "black box," exposing a set of interfaces. Since clients depend only on these interfaces, the underlying component can be modified or replaced, provided it continues to support the specified interfaces. A "service," in simplified terms, is a capability that is published to a directory so that clients can dynamically discover its interface. This allows flexible composition of new capabilities from loosely coupled services. A "service component" implements one or more services much as a regular component implements interfaces. The difference is that services are dynamically discoverable.
In this practice the term "component" applies equally to service components. Note the term "design subsystem" is used interchangeably with "design component," reflecting the evolution of these terms; once different, they have evolved in the latest UML 2.0 specification to be essentially equivalent, with "subsystem" generally used to refer to larger granularity components. Applying the PracticeThis practice provides an overview of how to identify and specify components. Identify Components and Services from the Problem SpaceComponents can often be discovered from the problem that the software is intended to solve. The largest components (often called subsystems) or services can often be identified by modeling the overall business and encapsulating business rules or parts of the business process.[12]
To identify components within an application, start by identifying the "things in the system which have responsibilities and behavior." Nouns used in the requirements are a good starting point, as are domain objects (see Practice 8: Understand the Domain). A more structured approach[13] is to identify "analysis classes," including the following:
These analysis classes provide a key starting point for components. Start by grouping highly interdependent or closely related classes into components, and separate classes that are relatively independent. Identify Components and Services from Existing AssetsOften, there are existing company resources, such as databases or existing systems. Encapsulating resources with a standard interface allows the resource to be accessed in a consistent manner and enables it to be replaced or modified as needed.
Use a Layered ArchitectureComponents should encapsulate functionality, but they should also be able to take advantage of common services. A layered architecture classifies functionality into layers in such a way that upper layers use services from lower layers. This classification clarifies the dependencies and responsibilities of components. It also helps identify reusable components, as functionality that logically belongs to a lower layer can be refactored into a component at that lower layer.
Figure 6.6 shows a typical layering approach. Figure 6.6. A Typical Layering Approach.The separation of concerns in each layer makes it easier to identify components and make changes. (From RUP v7.0.)
Notice how this layered approach isolates hardware and operating system dependencies to the lowest layer, making it easier for the upper layers to migrate to other platforms. Layering is equally applicable to service-oriented architectures. Although services discussions often focus on the peer-to-peer interactions within the top layers of the architecture, services can still benefit from layering to increase reuse and reduce dependencies.[14] And even when the services themselves are not layered, the components that implement services can usually benefit from a layered architecture.
Factor Out Common BehaviorSeveral classes may have similar requirements that can be factored out and moved into another component. In particular, mechanisms such as persistent storage, interprocess communication, security, transactions, error reporting, format conversion, and so on are natural components. Look at design patterns with similar challenges (see Practice 15: Leverage Patterns). Patterns often suggest how class responsibilities can be refactored to reduce coupling in ways that create better component boundaries. Looking more broadly, consider behavior that may be reusable for other systems in the future and try to isolate such behavior into components.
Consider Likely ChangesStakeholders often have ideas about what they would like the system to do in the future. For example, they may wish a product to be configurable to support other languages, other platforms, or different working environments. You should not expend a lot of effort designing for requirements that may never materialize. However, isolating potential areas of change can be an inexpensive means of enabling easier changes in the future. For example, the simple layering approach described earlier shows how to isolate platform dependencies to ease future migration.
Separate Specification and RealizationUML provides for the separation of a component's design into specification and realization. The specification serves as a contract that defines everything a client needs to know to use the component. The realization is the detailed internal design intended to guide the implementer. You can even create two or more "realization" components for one specification component if you wish to have different implementations.[15] A similar approach is used to specify services separately from services components.
Figure 6.7 shows how an engine can be specified. The provided interface (shown as a circle) is a powertrain. The required interface (a half circle) is a power source or fuel. An engine can be reused in a car or boat, provided the right connections are present. The realizations of the engine specification are not shown in this diagram, but would be actual engines provided by various engine manufacturers. Figure 6.7. Example of Connected Components.An engine can be used in a car or a boat, provided it has the right connections. (From the OMG UML 2.0 specification.)
Figure 6.8 gives a software example based on Java Database Connectivity (JDBC). JDBC is a component that has two connections. The first provides a standard interface for client applications to access a database. The second is a driver interface that the database vendor must realize. Figure 6.8. Example of Connected Software Components.
These examples show that components both provide and require interfaces, and that the interfaces allow one component to be replaced by another compatible component.
Specifications often hide the internal complexity of a component. So while the design of a component realization can include a detailed description of interactions between internal elementsusing structure, state, activity, and interaction diagramsspecifications are usually simpler. Specifications usually focus on describing the interfaces, dependencies on other interfaces, and required quality of service. Usually a UML structure diagram, accompanied by text, is sufficient. However, you can use the rich UML notation to provide a rich and precise visual description for the specification. Such precise descriptions tend to be most useful in the following cases:
Services and service components frequently have one or more of the above characteristics, and so benefit from a precise UML specification. UML can be used to precisely specify components and services. Provide the Right Level of InterconnectionSome components encapsulate resources or capabilities that need to be accessed by multiple distributed applications. These are natural candidates for wrapping as a service, because service frameworks generally provide a standard and flexible means of managing distributed capabilities. Because accessing a distributed capability involves sending and receiving data over the network, performance and scalability are significant concerns. Message-based asynchronous protocols are often the best choice, as these allow the requestor and provider of a service to work in parallel, rather than await a response from the other. Not all components should be exposed as services, as the flexibility provided by services comes with a cost. Distribution needs to be administered, interfaces are complicated by the need to minimize the number of message interactions, and asynchronous messaging and parallel processing can be tricky. Most smaller components do not need this complexity.
Develop Components IterativelyIt can be difficult and costly to refactor components after they have been fully implemented. To validate the design of components and their interfaces, implement basic scenarios that prove the integration of the components and interfaces. Add functionality in each iteration to refine and expand upon the interfaces. Developing components iteratively enables you to find problems, such as coupling or performance issues. You can then correct these problems through refactoring, without impacting a lot of code. This issue is discussed in more detail in Practice 2: Execute Your Project in Iterations and in Practice 10: Prioritize Requirements for Implementation.
Other MethodsTraditional waterfall development can be used to design and implement components. However, this method can lead to late discovery of problems, and it is difficult and costly to refactor components after they have been fully implemented. The iterative approach validates that components integrate and perform well using partial implementations, and even stubs. The component can then be refactored as necessary without impacting a lot of code. In an XP approach, components are not a focus. Some can be expected to emerge through refactoring and general good design. The Unified Process explicitly focuses on architecture, and it recommends identifying key components and interfaces and initially implementing just enough to validate architectural decisions. Whereas the Unified Process considers components useful for organizing teams, especially on larger projects, XP recommends a flat organization in which all code is shared.
Unlike XP, RUP also extends to cover the needs of larger projects, enterprise reuse, and enterprise architecture by describing how and when to use detailed component specifications. RUP also provides technology-specific information, such as how to design and implement services using J2EE and IBM Rational Software Architect. Levels of AdoptionThis practice can be adopted at different levels:
Related Practices
In general, good practices lead to more effective component development, which in turn leads to more effective overall system development. Additional InformationInformation in the Unified ProcessOpenUP/Basic describes how to design and develop component-based systems. RUP includes more advanced techniques, such as how to apply business modeling and systems engineering techniques to identify large-scale components and how to build systems of systems using techniques such as use-case flow down. RUP and RUP plug-ins also provide technology and tool-specific guidance. For example, the service-oriented architecture plug-in to RUP includes a UML profile for service-oriented architectures and guidelines for designing and developing services using IBM Rational Software Architect. Additional ReadingFor guidance on component identification and design, see the Additional Reading section in Practice 15: Leverage Patterns (component identification and design are key concerns of many architectural patterns). For a good overall reference for object-oriented analysis and design and component-based development, see the following:
For discussion of components from the large to the small, and for guidance on adopting component-based development at an enterprise scale, see the following:
For guidance on creating detailed component specifications[16] see the following:
For some up-to-date examples for modeling components using UML 2.0:
The following is an excellent article on how to move beyond abstract theory and practically implement replaceability:
For a description of service-oriented architectures and their relationship to components, see the following:
|