Facilitating an Automated Approach to Architecture-based Software

24 downloads 0 Views 204KB Size Report
This work is supported in part by National Science Foundation grants. CCR-9633391 ... for a target system, software architectures serve as frame- works into ...
in Proc. of 12th IEEE Automated Software Engineering Conf., 238-245, Incline Village, NV, Nov. 3-5, 97

Facilitating an Automated Approach to Architecture-based Software Reuse Yonghao Chen and Betty H. C. Chengy Department of Computer Science Michigan State University East Lansing, MI 48824 email: fchenyong, [email protected] Abstract Over the past several years, a number of techniques have been developed to address various issues involving software reuse, such as component classification, retrieval, and integration. However, it is not adequate to only have reuse techniques that address reuse issues separately. Instead, a seamless integration of these reuse techniques is critical to achieve effective reuse. In this paper, we present an integrated approach to software reuse. Based on software architecting techniques and formal methods, this approach addresses various reuse issues in a systematic and (semi) automatic fashion. An architecture-based software reuse and integration environment that supports this approach is also described.

1. Introduction Software reuse is the use of existing software components to construct new systems. It is recognized as having significant potential to improving software development productivity and software quality. At a high-level, software reuse consists of two types of activities: one is the management of software components, including the specification, classification, and retrieval of existing components; the other is component integration that involves the integration of the reused components into an application. Over the past several years, a large number of techniques have been developed to address these reuse issues [4, 5]. However, the lack of a seamless integration of these techniques imposes significant obstacles to achieving effective reuse, particularly with respect to component integration. Component integration typically involves conflict identification, component adaptation, and implementation of connections between components. Usually an existing component does not exactly match the requirements of a new system. Component adaptation is required in this case in  This work is supported in part by National Science Foundation grants CCR-9633391, CCR-9407318 and CCR-9209873. y Please contact this author for all correspondences.

order to resolve the conflicts (or mismatches) between an existing component and a requirement. For instance, a typical mismatch is that the order of arguments to an existing procedure may be different from that of a query specification, even though the procedure can satisfy the query functionally. In ad hoc approaches to reuse, component integration is usually considered a separate process from component specification and retrieval tasks. This separation of tasks may make component integration a time-consuming and error-prone process. First, conflicts are not easy to identify, since they are usually hidden in the implementation. Second, component adaptation may be quite difficult, particularly for conflicts that are introduced at the design level. For a given component, there typically exist assumptions about the structure of the system in which it will function. These assumptions, called architectural assumptions, constrain the way the component can be used. For example, in a client/server system, the client expects to interact with the server in a specific way that is very different from what a filter expects in a pipeline system. Thus, a filter in a pipeline system may not be used as a client in a client/server system, even though it may have the required functionality. This conflict is termed an architectural mismatch [2], recently identified as a serious obstacle in implementing reuse. Third, connections between software components are usually treated implicitly, rather than encapsulated and described as explicit entities. This approach increases the complexity of a connection implementation, since the implementation is distributed throughout the implementation of components. In this paper, we present an integrated approach to address reuse issues. This approach is based on software architecting techniques [8, 11] and formal methods [3]. By specifying the constituent components and their interactions for a target system, software architectures serve as frameworks into which existing components are assembled. Formal methods, on the other hand, enable a (semi-)automated approach to determine the reusability of software components.

The remainder of this paper is organized as follows. We first present a framework of our approach to software reuse and integration. Key techniques underlying this approach are introduced. In Section 3, we describe an experimental system implementing this approach. Section 4 compares this work with other approaches. We conclude this paper in Section 5.

2. Architecture-based Software Reuse and Integration An architecture is a collection of components, connectors, and a configuration of how the components should be connected via connectors. Figure 1 depicts the architecturebased assembly process of software components. In FigArchitecture specifies components and their inter-relations for target system

Library of existing components C1

Iy

C2 Ix Iz

C3

Component Selection and Matching

Legend C1 Existing component

Ia

A

Iy

A with interface Ia Interface

Ci

C2 Ix

Connector

Iz

Points to the interface that a component can implement I Ia

A

C3 Im

Component A is adapted to implement I

M

is invoked to integrate these components, generating code as necessary for adapting components and/or implementing connections.2 With respect to component reuse, this approach has the following potential benefits: First, component and composition (i.e., architecture) specification can be planned in a unified framework that may facilitate the consideration of design information (such as architectural properties) in component selection and matching. Second, the selection and matching process may identify the conflicts between a requirement and an existing component, thus facilitating the automation of component adaptation. Third, as a mediator of interactions between components, the connectors provide a place for recording and resolving conflict assumptions between component interfaces. Furthermore, connectors prevent the changes made to a component from implicitly propagating across the entire system. Fourth, not only can components be reused, connectors that encapsulate recurring interaction styles can also be reused, and their implementation can be automatically generated. Next, as shown in Figure 1, the adaptation of existing component M is localized by requiring the adapted component (that is, M with its wrapper) conform to the requirement Iz . This localization prevents the adaptation of M from impacting the connectors, since the connectors can only “see” the interface Iz . Finally, a significant benefit comes from the maintenance of assembled systems, since software architectures, when used as design specifications, provide a clear description of these systems and support design maintenance [1].

Packaging

2.1. Architectural Description Language Iy

Impl. of C2

Impl. of C1 Iz Ix

Wrapper for M

Im

Impl. of C3

M

Figure 1. Architecture serves as a framework into which components are assembled ure 1, the polygons labeled by Ii represent interfaces of components. The cubes represent existing components. The black bars labeled by Ci indicate connectors. The dashed arrows represent an implementation relationship between an existing component and an interface. For those cases where there are slight differences between the interface for the target and the existing components, adaptation is performed. Given an architecture specification, the assembly process consists of two steps. First, the existing components are retrieved, evaluated, and matched to the interfaces in the architecture specification.1 Then a packaging process 1 If there are interfaces

to which no existing component is matched, then

In this section, we describe a scheme for specifying components and architectures. This scheme is based on a model proposed by Shaw et al. for architectural descriptions [10]. According to this scheme, a software architecture is specified as a configuration of components and connectors. Components and connectors are two kinds of distinct, identifiable elements that constitute a software system.

2.1.1. Component A component is a computational unit that has an interface and may or may not have an implementation. The interface of a component specifies the capabilities that the component can provide and the ways the component delivers its capabilities. The implementation of a component should conform to its interface in that the implementation implements the capabilities and delivers them in the way specified in the interface. A component without an implementation we can construct them from scratch without affecting the other parts of the system. 2 In reality, a component may simultaneously fulfill multiple interfaces. For convenience of description, in this paper we assume that each interface has a component that implements the interface, as shown in Figure 1.

is known as an abstract component or simply an interface. Each existing component must have an implementation that may be in a variety of forms: executable, source code/file, object code, object library, OS built-in facility, other existing components, or as a composite component implemented as another architecture. The interface of a component is specified by the following properties:

 Type: type of the component.

 fPortg+ : one or more ports supported by the component type  Constraints: constraints imposed on the ports of the component Similar to the use of types for capturing properties of data in programming languages, we define component types as an abstraction that encapsulates certain recurring properties of components. Specifically, component types are intended to capture the architectural properties. For example, the type filter specifies that a component must be used in a pipe-and-filter system, whereas a component of type module should be used in a main program/subroutine system. We model a component as a black box with a set of ports, each of which defines a behavior that a component may exhibit in constructing a system. A port is specified by the following properties: type, MaxConn, signature, and specification. The type of a port encapsulates the behavior pattern that a component can exhibit through the port. For example, a component may define and export a procedure to its environment through a ProcDef port, whereas an InStream port in a filter means that the filter will obtain a stream of data from it. Each component type supports a specific set of port types. A component interconnects with other components through its ports. MaxConn is the maximum number of connections in which the port may simultaneously participate. If MaxConn = 1, then the port is not sharable. The signature of a port may have a different meaning depending on the type of the port. For a ProcDef port, it is the signature of the procedure defined and exported. For an InStream port, it is the type of data passed through the port. Although types and signatures capture certain properties of ports, they do not precisely specify the functionality of a port. Due to the variety of ports, it is difficult to use a single specification method to specify all kinds of ports. In order to make use of the rich formal specification languages available, the ADL allows the incorporation of other specification methods. For example, for the ports of type ProcDef, ProcInvoc (imports and invokes a procedure), ADTDef (defines and exports an ADT), and ADTUse (imports and uses an ADT), the Larch specification language [3] is used to specify their functionality.

2.1.2. Connector

Connectors encapsulate the ways that components interact. A connector is specified by the following properties:

 Type: the type of the connector.

 fRoleg+ : roles defined by the connector type.  Constraints: constraints imposed on the roles of the connector. Connector types are intended to capture recurring component interaction styles. Typical component interaction styles and the corresponding connector types include: procedure call (described by type CallProc), data access (AccessData), pipeline (Pipe), and remote procedure call (RPC). A connector defines a set of roles for the participants of the interaction specified by the connector. For example, a CallProc connector has two roles: a Definer and a Caller. Components are connected by configuring their ports to the roles of connectors. A port cannot be configured to arbitrary roles. Each role has a domain that defines a set of port types. Only the ports whose types are in the domain can be configured to the role. Furthermore, the ports configured to the roles of a connector should satisfy the constraints of the connector. For example, in a CallProc connector, the port configured to Definer should define and export a procedure (function) that can fulfill the functionality required by the port configured to Caller. These obligations provide a way to check the correctness of architecture configurations.

2.2. Component Selection and Matching By specifying constituent components and connectors, the architecture specification serves as a basis for assembling a target system. The first step in the system assembly process is to find an implementation for each interface in the architecture specification of the target system. The implementation may be constructed from scratch. However, we emphasize here the reuse of existing components for fulfilling the interface. Given an interface specification, the objective of component selection is to locate the most appropriate components that may satisfy the interface. A basic criterion for component selection is that the type of a selected component should be the same as that specified in the interface. More fine-grained approaches may consider the number of ports, port types, and port signatures, or even port specifications and constraints. The rule is that the selected component should be suitable for fulfilling the interface. Given candidate components for implementing a (query) interface, the matching process aims to establish a mapping between the ports of a candidate component and the ports specified in the interface. Only if such a mapping is established can the reusability of the existing component for implementing the interface be validated. Furthermore, the matching process identifies mismatches (conflicts) between

the existing component and the interface. The criteria for mapping a port Q to a port P is : (1) the capabilities delivered through the two ports are matched, and (2) the capabilities are delivered in the same way. Condition (2) means that the types of the two ports are the same. For the first condition, the simplest case is when the capabilities are exactly the same. However, relaxed matches should be considered. The concrete forms of these relaxed matches depend on the port types and port specifications. In the following discussion, we consider matches of procedure-related ports. We first define a generality relation between functions (procedures) that are defined or invoked by a ProcDef or ProcInvoc port. We consider both port signatures and specifications. A function is specified by a signature and a pair of pre- and postconditions. The signature of a function describes the types of its parameters and return value. The precondition states when a function is applicable, whereas the postcondition specifies what should be established by the execution of a function. Both pre- and postconditions are specified in terms of logical formula in predicate calculus. Definition 1 (Generality relation of functions) Given two functions, g and h, g is more general than h, denoted as h f g, if the following rules hold:

 Signature matching – Arguments rule. g and h have the same number of arguments. Let the list of argument types of g be (Tg1 , Tg2 , : : :, Tgn ), and that of h be (Th1 , Th2 , : : :, Thn ), then there exists a permutation of the 0 0 list (Th1 , Th2 , : : :, Thn ), denoted as (Th1 , Th2 , : : :, Thn0 ), such that for all i, 1  i  n, Thi0 is the same as Tgi . – Result rule. Either both g and h have a result or neither has one. If there is a result, then h’s result type is the same as g ’s result type.  Specification rule 3 Let pre(f ) and post(f ) be the pre- and postconditions of function f , respectively. – Precondition rule. pre(g ) ! pre(h), the precondition of g implies the precondition of h. – Postcondition rule. post(h) ! post(g ), the postcondition of h implies the postcondition of g . The signature matching requires that the two functions’ range types match and their domain types match after permutation. The specification rule requires that a more general function have a stronger precondition and a weaker postcondition. 3 Parameter renaming has been conducted based on signature matching so that the specifications of function g and h are consistent.

Intuitively, the generality relation between function specifications captures the following implementation property: let H be a function implementing function specification h, then H also implements the function specification g that is more general than h. In other words, whenever a function implementing g is needed, the function H implementing h can be used. Definition 2 (Procedure-related port matches) let P denote a port of an interface in an architecture specification, and Q a port of a candidate component.  For ProcDef ports, Q matches with P , if Qf f Pf , where Pf and Qf represent the functions defined and exported through ports P and Q, respectively.  For ProcInvoc ports, Q matches with P , if Pf f Qf , where Pf and Qf represent the functions imported and used through ports P and Q, respectively. The matches of ADT-related ports are based on the generality relation between ADTs. An ADT has a collection of methods (functions). Definition 3 (Generality relation of ADTs) Given two ADTs and , let Mx be the set of methods of x (where x is or ), is more general than (denoted as d ), if for any method m 2 M , there exists a method m0 in M , where m is more general than m0 , i.e., m0 f m. The above definition captures the behavioral property that an ADT should provide all the behavior provided by a more general ADT. In terms of implementation, the generality relation between ADTs implies that an implementation for an ADT should also be an implementation for any more general ADT. Definition 4 (ADT-related port matches) let P denote a port of an interface in an architecture specification, and Q a port of a candidate component.  For ADTDef ports, Q matches with P , if Qd d Pd , where Pd and Qd represent the ADTs defined and exported through ports P and Q, respectively.  For ADTUse ports, Q matches with P , if Pd d Qd , where Pd and Qd represent the ADTs imported and used through ports P and Q, respectively.

2.3. System Packaging After all abstract components have been matched with an implementation, the packaging process is invoked to generate a package for the target system. In our approach, we emphasize the conformance of the target system to the architecture by observing two rules:  Each interface in the architecture should physically have a conformant implementation.  The implementation of each connector in the architecture should be explicit and packaged individually.

According to the first rule, for a reused component, we should adapt the component itself to conform to the interface to which it is matched, rather than modifying those components interacting with it. As shown in Figure 1, a wrapper is generated for accomplishing this adaptation. The second rule requires the connector implementation to be localized. These rules facilitate the automation of component adaptation and connector implementation, and also ensure that the generated system has a physical structure that is well-defined (abstracted) by the architecture. System packaging consists of three steps: First, the packaging process checks the implementation of each component, ensuring that it conforms to the abstract component (interface). In the case of reusing existing components, software wrappers may be generated for adapting the reused components to conform to the abstract components (interfaces). The adaptation is based on the port mappings generated by the component matching process. Then the packaging process checks the connectors and generates their implementation. The implementation of connectors depends upon their types and role configurations. In addition to establishing connections, the implementation of a connector may need to resolve mismatches between the ports that are configured to the connector. A typical mismatch is a naming conflict. Code for resolving mismatches can be generated. Finally, construction files (such as makefiles) are generated to describe the construction process for the executables of the target system.

3. ABRIE: An Architecture-based Reuse and Integration Environment ABRIE is an experimental system designed to explore the use of software architectures as a framework for assembling software components. The design objective is to provide an integrated environment to address various reuse issues: specification of component composition, component management, and component integration. Three characteristics are particularly emphasized in the ABRIE design: visualization, multilevel abstractions, and automation. In addition to the textual form, visual representation and manipulation of components, connections, and architectures are supported. ABRIE uses three levels of abstraction in determining the reusability of existing components: types, signatures, and behavioral specifications. Automation is one main potential benefit of architecture-based reuse. In ABRIE, the component integration (packaging) process is fully automated. In component retrieval, evaluation, and matching, automated support is also provided. ABRIE is implemented for Unix and Windows’95 using C, C++, and Tcl/Tk. The targeted platform for the systems generated by ABRIE is Unix with C/C++. ABRIE consists of three functional components: architecture design, component management, and system pack-

aging. Each of these is described below in further detail.

3.1. Architecture Design The main working area of ABRIE is a canvas that provides a graphical representation and manipulation of architectural elements. Figure 2 shows a pipeline architecture for an example system pwc that counts the number of palindrome words in a file, where boxes represent components and bars represent connectors. Component PWR is a filter

Figure 2. ABRIE Architecture Design for recognizing palindrome words, and filter WC counts the words recognized by PWR. These two filters are connected by a pipe connector pipe. In ABRIE, components and connectors are associated with actions for viewing, editing, and configuring them. These actions can be activated through a menu displayed while clicking on a component or a connector. Figure 2 shows the “Connection Property” window for connector pipe and the “Component Property” window for component PWR. Component and connector properties can be edited through these windows. Architectures can also be edited using text editors. ABRIE generates the graphical representation automatically. The correctness of the connection configuration of an architecture can be analyzed by ABRIE. Specifically, ABRIE checks if each role of a connector is correctly configured, that is, if the ports configured to a connector satisfy the constraints of the connector. In addition, through built-in facilities, ABRIE ensures that each component consists of only the ports that the component type supports, and each connector only has the roles that the connector type supports.

3.2. Component Selection and Matching

An abstract component may be implemented in a variety of ways: executable, source code/document, object code, object library, OS built-in facilities, reusing existing component, or as a composite component implemented by another architecture. An implementation specification of a component specifies the implementation method and related directives for locating and/or integrating the implementation, and can be edited directly through the “Component Property” window shown in Figure 2. As shown in Figure 2, PWR is a composite component implemented by architecture pwr, depicted in Figure 3. Unlike pwc, pwr is a main program/subroutine style archi-

Figure 4. Component Selection and Matching

Figure 3. Architecture of PWR tecture, whose components are connected through CallProc and UseADT connectors. In pwr, Coordinator is the main control component that calls procedures defined in components InWord, OutWord, and Recognizer. Coordinator also defines a special port named main that is the entry point of the program. InWord inputs words from an InFile port that is configured to the standard input. OutWord writes words to a port that is configured to the standard output. Recognizer defines and exports a procedure isPalindrome that checks if a word is a palindrome word. Recognizer imports and uses a character stack through a UseADT connector UA. Component CharStack defines and exports an ADT CharStack. The implementation of each component in pwr may take various forms as discussed before. Next we discuss how ABRIE supports the reuse of existing components. ABRIE incorporates a library manager for organizing and managing existing components. Components are classified and retrieved based on their interfaces (i.e., types and ports). When implementing an abstract component in an architecture, a single click on the reuse button in the “Component Property” window (see Figure 2) triggers ABRIE to search for the current library (which is loaded through the library manager) and a component selection window is displayed as shown at the top in Figure 4. In the “Component Selection” window all candidate components that may match the abstract component are retrieved and presented to the users. For example, in Figure 4, three components (genStack, StackAsList, and List) are listed as candidates for im-

plementing CharStack of pwr. Users can browse the specification of these candidates and select an appropriate one for further matching. The “Component Matching” window shown at the bottom in Figure 4 shows the process of matching component List to CharStack. The matching process establishes a port mapping relation between the two components that will validate the reusability of the existing component for implementing the abstract component. Both List and CharStack have only one port of type ADTDef that defines and exports an ADT. An ADT has a set of methods that are either constructors (represented as “C” in Figure 4) or behavior (represented as “B” ). In order to reuse List for implementing CharStack, port list of List should be matched with port cstack of CharStack. This requires the ADT defined by port cstack to be more general than that defined by port list of List. In the following discussion, we refer to the two ADTs as cstack and list, respectively. The generality relation between ADTs requires that for each method of cstack, there is a more specific method in list. We proceed by assigning a mapping between the methods of the two ADTs. As shown in Figure 4, we assume that the constructor cstack is more general than constructor list, pushc than addAtHead, popc than detachAtHead, and etc.. Given a mapping, ABRIE automatically generates the proof obligations for justifying the mapping. Figure 5 depicts the proof obligations generated for the mapping shown in Figure 4. The proof obligations are generated based on the Larch specifications of the two components. The proof obligations are represented in terms of LSL specifications to facilitate the application of the Larch Prover (LP) for analyzing them. After preprocessing the obligations, we can invoke the Larch Prover

Figure 5. Proof Obligations (LP) from the “Component Matching” window to assist in proving these obligations. Below shows a snapshot of LP while it is discharging the proof obligations for our example. Figure 6. Implementation Status LP1.15: prove (isEmpty(self_post) => self_post = empty) .. Attempting to prove conjecture cstack_listTheorem.1: isEmpty(self_post) => self_post = empty Conjecture cstack_listTheorem.1 [] Proved by normalization. Deleted formula cstack_listTheorem.1, which reduced to ‘true’. LP1.16: prove (self_post = (x:E nprecat self_pre) => (x:E = head(self_post) ^ self_pre = tail(self_post))) .. Attempting to prove conjecture cstack_listTheorem.2: self_post = x nprecat self_pre => x = head(self_post) ^ self_pre = tail(self_post) Conjecture cstack_listTheorem.2 [] Proved by normalization. Deleted formula cstack_listTheorem.2, which reduced to ‘true’.

mapping. For example, the list port is mapped to the cstack port with its parameter T instantiated to char.

3.3. System Packaging Once all components in an architecture have a proper implementation, the packaging process can be activated to integrate all these components together to construct a system. The following is the wrapper for adapting component List for implementing CharStack. The wrapper is generated based on the port mappings recorded in the matching file. The packaging process then checks the connectors and // CharStack.h // Generated by ABRIE for adapting List to implement // CharStack

LP is an interactive theorem proving system for multisorted first-order logic. In certain cases, user interaction may be required in proving/disproving a conjecture. However, for our example, all the obligations are automatically resolved, and the user-assigned mapping is justified, thereby establishing the matching between component List and CharStack. Once the matching process is successfully completed, meaning that the existing component can be reused, a matching file will be generated for recording all the matching information. As shown in Figure 6, after the selection and matching process is finished, the implementation specification of CharStack specifies that it be implemented by using library component List. The lower window in Figure 6 shows the corresponding matching file. The file records the mapping between ports and methods of ports. For a parameterized port, the matching process also determines the appropriate instantiation of the parameters based on the port

#define addAtHead pushc #define detachAtHead popc #define head topc #include "/user/u25/chenyong/components/list/list.h" #define cstack list

generates their implementation. Finally, the implementation for the target system is generated. The following is the generated implementation for architecture pwc. The implementation is a Unix shell script together with a Unix makefile that describes the construction process for the composite component PWR. where xxxxCP1.cc, xxxxCP2.cc, and xxxxCP3.cc are software stubs generated by ABRIE for implementing connectors CP1, CP2, and CP3 respectively.

4. Related Work

#!/usr/bin/sh # pwc # Generated by ABRIE for implementing architecture pwc # Create named pipe for connection pipe mkfifo xxxxpipe # Start up filter WC wc -w xxxxpipe

Suggest Documents