• Nenhum resultado encontrado

Part III: Robusta

6.1.1 Defining the Service Contract

As previously mentioned, packaging classes into modules is a design decision that affects the dynamic module system. Packaging can cause undesirable coupling among component implementations that leads to the impossibility of individually removing modules (and, by consequence, individual components). In a worst-case scenario, all modules are coupled in such a way that the entire application must be stopped and reloaded for every single change, completely defeating the goal of fine-grain dynamism.

Figure 14 shows a conceptual diagram describing the logical relationship that is commonly perceived between component implementations and the service interface. Indeed, the service contract is generally reduced to simple interface that is shared between two components and is seen simply as the interaction point between them. The implementations are shown as separate entities that do not overlap (in the case of implementations, this means they do not share classes other than the service interface). If we were to modularize such a case, we would probably use three modules, one for the client, one for the provider and a final one for the service interface. Both implementation modules would depend on the interface module, but not on each other (i.e., they are expected to be decoupled).

Figure 14: Shows a common misconception of the relationship between component implementations and the service contract in centralized applications (compare to Figure 22).

However, such a relationship is idealistic and does not fit how modularity or services really function in centralized applications. In fact, the service interface is but the initial communication point between the components; communication is not limited to the service interface, it can occur through other objects as well. Indeed, the interface defines the operations a provider component

6.1 Decoupling component implementations

permits; yet, a service interface also references many other classes and interfaces (types) with which both components may continue to interact with. Figure 15 shows a generic metamodel for types (as seen in the Java programming language). A type can be either a class or an interface.

Types can reference other types. Also, interfaces can be implemented by various classes and classes can be subsequently inherited (i.e., extended in Java programming language). This shows that a single reference to an interface, e.g., the service interface, can lead to a large graph of other types being indirectly referenced.

Figure 15: Metamodel showing the relationship between interface and class

Consumer and provider components in a service interaction do not generally reference every type reachable from the service interface. In fact, which exact types are referenced by either component will undoubtedly vary; at a minimal the service interface is referenced by both provider and client, although generally a reasonably large part of the reachable type-graph is referenced by both components. Figure 16 gives us an example of a simple architecture composed of two components that communicate using a single service. The service interface is defined using a Java interface, which is referenced by the consumer component implementation class (necessary for binding and invoking the provider) and is implemented by the provider component implementation class (this is necessary for the consumer to be able to invoke operations on the provider). Note that colors are used to guide the reader into distinguishing between classes that are part of the implementations of each component, and classes that are independent of them and used in interactions (namely the Service Contract classes). Furthermore, it is important to distinguish the component implementation classes from regular classes, and to distinguish the service interface from regular interfaces.

Figure 16: Shows the dependency relationships between classes used to construct components

An interesting result of separating component implementations and the service interface is that we can calculate the entire type-graph (both classes & interfaces) that two components potentially use when communicating through service interactions. This is calculable because objects that traverse component boundaries must be defined by types that are referenced, either directly or indirectly, from the service interface38. However, not all types of all the objects that traverse component boundaries can be found in the transitive closure of referenced types starting from the service interface. This is because the transitive closure may contain interfaces or classes that are implemented or inherited by unknown types. Indeed, it is possible to know theres an object that passes through the service interface, but we cannot be sure that the object is of the type specified or if it is a subtype (i.e., the service interface can reference a super type to the object that is actually passed in an invocation)39. In the case of an interface that is referenced, we know there is a class that implements the interface (otherwise no object would be passed); however, which class is not directly known from the transitive closure40.

Nevertheless, even though there are still hidden types that can circulate between two components, those types remain inaccessible from the components themselves (components should never downcast objects to find their hidden types because this supposes they know more

38 It is possible to use e ither the root class (e.g., Object in Java) or a containe r obje ct to wrap othe r obje cts in ways that the type of the wrappe d obje cts are not known from the type -graph. This implie s that the re ceiver compone nt of the colle ction casts the objects to a known type , which, in fact, causes hidde n coupling be twe e n the compone nts and should be avoide d. In ge ne ral, a compone nt should ne ve r re quire down-casting any obje cts obtaine d through a se rvice inte raction, and, if such is the case , than the se rvice should be re -de signe d (for e xample , using ge ne rics).

39 In the worst case, the root class of the class hie rarchy is use d (e.g., class Object in Java), allowing any obje ct to pass through. This should be avoide d whe n possible .

40 It s important to note that objects defined by types that are not re ference d by a compone nt can still be indire ctly re fe renced and he ld in me mory as long as the re is an e xisting re fe re nce towards the m. This me ans that a compone nt

6.1 Decoupling component implementations

about the objects and the service than what is expressed in the contract, implying hidden coupling). Indeed, a component should only directly reference a type that is contained in the transitive closure of types starting from the service interface, not hidden types.

Understanding the relationship between the service interface and the referenced classes is particularly important because, unlike highly decoupled distributed computing approaches41, centralized applications can build services using complex objects and interactions. However, such interactions run the risk of introducing hidden coupling, which is difficult to detect and hinders dynamism. Hidden coupling exists in centralized applications because the classes that are used in the service interaction may be unknown and yet still be removed (because of dynamism itself), placing the component programmer in an untenable position where he must release objects that he was unaware of. This affects the components that are directly or indirectly using such classes, without the components (or developers of the components) having any way of knowing which classes are really being used. In short, coupled implementations are caused by the (direct or indirect) referencing of classes that belong to another components implementation and which are obtained through service interactions.

Clearly, either component, using or providing a service, can directly reference any type (class or interface) that is contained in the transitive closure starting from the service interface. As we can begin to see, the service contract must consider the service interface plus additional classes used in the service interaction. This is necessary so we can clearly identify and separate this group of classes from the component implementations. Figure 17 gives us an initial example of which classes require separation (contrast to Figure 16).

Figure 17: An example showing a graphical view of the Service Contract. The Service Contract is composed of the transitive closure or referenced types reachable from the service interface.

41Distribute d applications communicate using services with inte rfaces that de fine transferable data, generally using primitive data type s. Each inte raction be twe e n consume r and provide r passe s (i.e., copie s) the se primitive value s be twe e n the components. Complex objects, such as classes, are generally not pe rmitted be cause this causes coupling and reduces the system s flexibility e.g., this occurs with Java's RMI te chnology). Howe ver, e xclusively using primitive type s for communication is quite re strictive , unde sirable and unne ce ssary in ce ntralize d applications.

Following our explanation, we informally define the Service Contract as follows:

The Service Contract of an interface I is the set of types (interfaces &

classes) that are needed to provide the service defined by I, i.e. all types that I directly or transitively depends on.

In order to more formally define the service contract, we will start by defining the relationships between classes and interfaces. The relationship between two types may be caused by any of the following types of coupling found in source code: inheritance, abstract class implementation, interface implementation, composition, aggregation, association, dependency and exception. When considering dynamism as the only concern, we can reduce this to two types of type-coupling, namely the extends (and its reverse relationship, extendedBy) and the depends relationships, which we define as follows:

Definition 6.1 (Extends relationship): A type C extends a type D if its relationship with D is of inheritance, abstract class implementation or interface implementation. This implies that C depends on D at runtime and that C provides new functionality not existant in D. The extends relationship is directional.

Definition 6.2 (ExtendedBy relationship): We define ExtendedBy as the reverse relationship of Extends. A type D is extended by a type C if C extends D.

Definition 6.3 (Depends relationship): A type C depends on a type D if its relationship with D is of composition, aggregation, association, exception, dependency or extends. This implies that the type C requires D at runtime in order to properly function42. The depends relationship is not symmetric; C requires D to function but D does not require C to function.

We proceed by defining the Type Graph, which is composed of classes and interfaces:

Definition 6.4 (Type Graph): A type graph is a tuple TG = <Type, Extends, ExtendedBy, Depends>

where:

1. Type is a set of types (i.e., classes or interfaces);

2. ExtendsType×Type is a partial ordering relation expressing that some types Extends (as defined in 6.1) others.

3. ExtendedByType×Type is a partial ordering relation expressing that some types are ExtendedBy (as defined in 6.2) others. ExtendedBy is the reverse relation of Extends.

4. DependsType×Type is a partial ordering relation expressing that some types Depends (as defined in 6.3) on others.

We should note that the Service Contract is computed by the transitive closure of Depends and Extends relationships starting from the service interface that is used to define the service. For a graph

DEFINITION

6.1 Decoupling component implementations

G=<Node, Rel> with Node the set of nodes, and Rel the set of relationships between nodes, we note [r1, … rn]+ with r1, rn∈ � , the transitive closure of relationships r1 to rn.

And finally, using the transitive closure operator and the previous definitions, we can define the Service Contract, which is the set of types used to provide a Service.

Definition 6.5 (Service Contract): Let TG be a Type Graph. A Service Contract is a set of types defined by the tuple <ServiceInterface, Depends, Extends>.

1. SC (ServiceInterface) = { t ∈ � | <ServiceInterface, t>  [ExtendsDepends] + } 2. ServiceInterfaceType (ServiceInterface is the interface used to define the service);

3. Thus, SC(t) Type.

The Service Contract is the set of types reachable, directly or transitively, from the service interface trough an extends or depends relationship.

However, although the Service Contract has been defined, we must still handle hidden objects that can be passed from components through service interactions. In the next section we will detail how we handle such classes while ensuring that components remain consistent and continue to function properly.