• Aucun résultat trouvé

CHANGING REQUIREMENTS

Adaptive Software by Example

5.1 CHANGING REQUIREMENTS

Assume that a customer asks us to write a program to compute the weight of a container.

When we ask what kind of containers they have and how their containers are structured, we get a rather confusing answer. They say that their containers contain items that have weights. The structure of the containers varies and they will know the details in an hour, but by then the program should almost be ready. This situation is quite typical; often customers keep changing their requirements, but they want to have the programs immediately.

So we are left with the task of writing a program that computes the weight of containers without knowing their structure. This task looks hard initially, but after some analysis it turns out to be the right way to approach the problem anyway. Consider an airplane, and consider that we know the weights of all its parts. The airplane is represented by some data structure that contains weight elds and all we have to do is add together the numbers in all those weight elds.

How can we nd all the weight elds, or weight objects? We traverse the airplane data structure and visit all the parts that contain weight information. When we get to weight

112

5.1. CHANGINGREQUIREMENTS 113 information, we add it to a variable that reects the weight of the section of the airplane we have already traversed.

Now go back to the generic container problem and apply the airplane idea to it. Since the data structure that we use to represent the container is not important, we may as well use a simple data structure rst and use a more complex one later. So we start with the very simple data structure that describes a container having a weight eld (see Fig. 5.1).

Container

weight Weight

DemNumber

v

Figure 5.1: Simple container: graphical representation

Container = <weight> Weight.

Weight = <v> DemNumber.

Figure 5.2: Simple container: textual representation

We use a programming-language independent data structure notation, called the class dictionary notation which has a textual as well as a graphical representation. In Fig. 5.1, we graphically dene two construction classes, one called Weight and the other called Container.

Fig. 5.2 shows the corresponding textual representation. When we use the word class in this chapter, think of a Pascal record type or a C struct. Container has a weight part and Weight has a DemNumber part. DemNumber is assumed to be a predened class, called a terminal class, for which the basic number operations, for example, addition, subtraction, etc., are dened. Later we will learn other terminal classes, such as DemReal and DemString. The prex Dem is an abbreviation of Demeter and is used to avoid name conicts with classes you might use from another class library.

Next we write a program for the data structures in Fig. 5.1 with the attitude that the data structure is very volatile and subject to frequent changes. Therefore, we solve the problem using only minimal information about the data structure. For example, we don't want to hard-code the information that a container has a part called weight. This kind of programming style is naturally called data structure-shy since the program is using as little information as possible about the data structure.

A useful way to think about the problem is to think in terms of collaborating classes for solving the weight addition problem. We need all the classes from Container to Weight to solve the problem. There could be a number of intermediate classes between Container

114 CHAPTER5. ADAPTIVESOFTWAREBYEXAMPLE

and Weight. For example, in Fig. 5.3, Item is an intermediate class between Container and

Container

weight

DemNumber

v

Weight Item

contents

Figure 5.3: One intermediate class Weight.

We need to nd a generic way to dene the group of collaborating classes.1 What about the specication in Fig. 5.4 that we call a propagation directive? It serves our purpose very

*from* Container *to* Weight

Figure 5.4: Propagation directive

well since it constrains the group of collaborating classes with minimal knowledge of the class structure. For the previous example in Fig. 5.3, the specication in Fig. 5.4 will include the class Item that is between Container and Weight. We call such a specication a propagation directive. It is a concise description of a group of collaborating classes. A propagation directive denes a group of collaborating classes using a *from* ... *to* directive.

Propagation directives play a very important role in object-oriented design. They allow us to describe object-oriented software in a highly exible form. For example, we can describe software without knowing what the detailed input objects will be.

To use an analogy from the automobile industry, with propagation directives we can build standard cars whose architecture can be changed after they are built. For example, a standard car can be customized to a sedan or a bus.

For any given container data structure, the propagation directive will select a subdata structure that describes the group of collaborating classes. We call such a subdata structure a propagation graph. In the examples in Fig. 5.1 and Fig. 5.3, all classes except DemNumber are included in the propagation graph by the propagation directive in Fig. 5.4.

1Propagation directive abstraction, page 447 (60).

5.1. CHANGINGREQUIREMENTS 115 So the propagation graph (subdata structure) denes a group of collaborating classes, but it has an additional, very useful interpretation: it denes an algorithm to traverse a given data structure instance, in our example a Container-object. In the example in Fig.

5.3, the algorithm would visit objects in the following order: Container, Item, Weight.

The traversal does most of the work for computing the weight. All we need to do is add additional code to the traversal code. Consider class Weight: its traversal code has an empty body. We want to add code that adds the current weight to a variable called return val.

*wrapper* Weight

*prefix*

(@ return_val = return_val + *v; @)

This wrapper adds code to class Weight by wrapping the C++ code between"(@"and

"@)"around the empty body of class Weight.

A wrapper can be viewed as an editing instruction for an object-oriented program. A prex wrapper, such as the one above, adds code that needs to be executed before the traversal code. When a wrapper is specied without the *prex* keyword, it is a prex wrapper by default. The above wrapper can be written in shorter form as

*wrapper* Weight

(@ return_val = return_val + *v; @)

In the rest of the chapter we will use this short form.

Why do we choose to make the Weight wrapper a prex wrapper? We could also have made it a sux wrapper, using the keyword *sux*, which would append the code at the end of the traversal code. In this case it does not matter since the traversal code is empty for class Weight. We can choose a prex or a sux wrapper.

The code between"(@"and"@)"is C++ code, which needs a little explanation because of the star appearing before v. Variable v refers to the DemNumber-object that is contained in Weight. The type of v is a pointer type to DemNumber, in C++ notation the type is DemNumber*. As a general rule, parts are implemented by pointer types unless a special notation is used in the class dictionary. This is a general and useful approach in object-oriented programming.

Since v is a pointer to a DemNumber-object, *v is a DemNumber-object; that is, * is the dereferencing operator of C++. For class DemNumber we assume (not shown) that a constructor has been dened that creates a DemNumber-object from an int value. The conversion rules of C++ then call implicitly the constructor as a conversion function during the evaluation of return val + *v. Readers not familiar with C++ can nd a transition from C to C++ for the purpose of writing adaptive software in Chapter 3, Section 3.5.2

Finally we need to agree on a signature for the weight addition function that will be attached to all the collaborating classes.

*operation* int add_weight() // signature

*init* (@ 0 @) // initialization

2It is important to notice that the adaptive software concept is independent of C++ and it can be used with many other object-oriented programming languages such as Smalltalk or CLOS.

116 CHAPTER5. ADAPTIVESOFTWAREBYEXAMPLE

The function has an implicit variable, called return val, which will be returned when the function returns. The return val variable can be initialized by giving an expression after the

*init* keyword. In this example, we initialize to 0 since we solve a counting problem.

We now have all the pieces of the program which is given in complete form in Fig. 5.5.

Such a program is called a propagation pattern. It makes few assumptions about the class

*operation* int add_weight()

*init*

(@ 0 @) // C++ code fragment

*traverse*

*from* Container *to* Weight

*wrapper* Weight

(@ return_val = return_val + *v; @) // C++ code fragment

Figure 5.5: Propagation pattern

structure with which it can be used. That is the main point we want to make here. We view Containerand Weight to be class-valued variables that will be mapped onto ordinary classes later. The algorithm is formulated in terms of the class-valued variables. We make only two assumptions about the Container and Weight class: one, that a relationship between the two exists that allows traversal from Container to Weight and two, that Weight has a part called v for which the addition operator is dened. The relationship could be a one-to-one relationship or a one-to-one-to-many relationship; we keep the algorithm independent of such details.

We further explain the propagation pattern in Fig. 5.5. To implement the add weight operation, we need the collaboration of a group of classes all having an operation with the given signature. Since we do not know the group of collaborating classes, we cannot itemize them, but we can outline them. We need all the classes that are involved in the relationship between Container and Weight. The specication *from* Container *to* Weight succinctly denes the group of collaborators. They have the important task traversing a Container-object and nding all the Weight-objects contained in it. But the traversal itself does not accomplish any addition. Therefore, we need an editing mechanism that allows us to personalize the traversal code. The editing is accomplished by wrappers that add additional code. A wrapper may contain two kinds of code fragments between (@ and @), at least one of which has to be present. The rst kind is a prex code fragment and the second kind is a sux code fragment. The prex and sux code fragments are wrapped around the traversal code. In the example above we used only a prex code fragment.