Chapter 13. Separating Functional Requirements with Application-Extension Use Cases

You build a system from a core and gradually add more functionality on top of it over iterations and releases. This functionality that you add must be well modularized and separated from existing functionality. By that, we mean that the added extensions must realize different concerns than existing ones realize. This modular separation is important, otherwise, the increments will be like patchwork. Application-extension use cases provide the means to separate functional requirements into such modular extensions. Such extensions can be used to modularize enhancements to provide richer functionality, or they can be used to factor out complexities to make the core of a system simpler and more reusable. With use-case slices and aspects, you maintain the modularity of use-case extensions during design and implementation. Through the use of appropriate design techniques, you limit the impact that changes in the core can have on the extensions.

Analyzing Application-Extension Use Cases

A key benefit of aspect orientation is the ability for you to add extensions onto an existing system. This is extremely advantageous, especially for building large systems [Jacobson 1986]. In fact, it is the only way to go.

However, a word of caution: you have to make sure that each extension represents some modular unit of stakeholder concerns. Otherwise, you end up repeatedly patching the system in an ad hoc manner. This results in tangling and scattering of another kind and makes the system impossible to understand.

The use-case technique provides the means to modularize functional extensions to the system through application-extension use cases. An application-extension use case represents a modular unit of functionality defined on top of a core that is represented by some other application use case.

In this chapter, we describe how to analyze an application-extension use case and drive it all the way to design and implementation. While the underlying principles are the same, you have an additional step of identifying pointcuts and operation extensions (i.e., advices in AOP).

We use the Handle Waiting List use case as an example in this chapter that extends the Reserve Room use case (see Figure 13-1).

Extension use cases.

Figure 13-1. Extension use cases.

The Handle Waiting List use case has an extension flow, Queue for Rooms. It lets the customer queue up for rooms if none is available. The brief description of this extension flow is shown in Listing 13-1.

Example 13-1. Handle Waiting List: Queue for Room Extension Flow

This extension flow occurs after the pointcut UpdatingRoomAvailability yields No Rooms Available.

The system creates a pending reservation with a unique identifier for the selected room type. The system puts the pending reservation into a waiting list. The system displays the unique identifier of the pending reservation to the customer. The base use case terminates.

Extension Pointcuts

UpdatingRoomAvailability = Reserve Room.UpdateRoomAvailability

Identifying Classes

As in the case of analyzing peer use cases, you first identify the classes that participate in the use-case realization. The required classes are depicted in Figure 13-2. Since the Handle Waiting List use case extends the Reserve Room use case, you expect to see the classes in the Reserve Room use-case realization to be involved.

Identifying participating classes: Handle Waiting List.

Figure 13-2. Identifying participating classes: Handle Waiting List.

The waiting list capability is potentially an optional service. Take it away from the system and what remains is still a functioning hotel management system. The recommended practice according to the Unified Process is to put the classes that provide such optional services into in a separate package by themselves. Such packages are known as service packages.

The Waiting List service package is overlaid onto the application-specific layer. It contains three classes:

  • A boundary class named Waiting List Form for hotel counter staff to view the waiting list.

  • A control class named Waiting List Handler to coordinate the classes that are involved in the realization of this extension use case.

  • An entity class named Waiting List to manage and store the contents of the waiting list. The waiting list is, in essence, a list of pending reservations.

Identifying Pointcuts

You now determine how the waiting list capability will be attached into the Reserve Room use-case slice. More specifically, you must identify the specific classes and responsibilities that will invoke the Waiting List Handler to put the customer in the waiting list. The extension pointcut in the use-case specification gives an idea where you ought to extend the base use-case realization.

UpdatingRoomAvailability = Reserve Room.Update Room Availability

Recall that when defining where operation extensions will execute, you need to specify the operation extension’s structural and behavioral context. Use-case specifications describe system behaviors but do not define the internals. Thus, you can derive the behavioral context from the pointcuts defined in the use-case specification. However, you cannot derive the structural context from use cases, since the use-case model does not describe the internal structure of the system (i.e., the element structure).

Identifying the Structural Context

The interaction diagram in Figure 13-3 describes the behavioral context at the time when the system detects that there are indeed no rooms of the selected type available. This interaction diagram is an excerpt from Figure 12-3 in Chapter 12, “Separating Functional Requirements with Peer Application Use Cases,” where we demonstrated how to analyze a use case.

Interaction diagram: realization of base use case.

Figure 13-3. Interaction diagram: realization of base use case.

You must decide where you want to execute the operation extension to invoke the extension. The NoAvailableRoom condition is detected at Room.updateAvailability(). If you trace this message back to the actor, there are a total of only three messages (see Figure 13-3) where this condition can be handled. Technically, we are tracing up the call stack. Now, in design, you need to consider more extension points because you have more classes then. But for now, there are only three operations in the call stack and hence only three possible extension points for you to execute the operation extension.

Room.updateAvailability

Let’s first consider the Room class because that is where the system first detects that no rooms are available. The Room class resides in a lower layer than the other two classes. This means that it will be called more frequently than the other two classes. Do you want to put the customer into a waiting list every time you find that there are no rooms available? Probably not, because you may invoke the updateAvailability() for other purposes. For example, if a customer cancels a reservation, the availability of the room must be updated; if a customer checks out, the availability of the room must be updated, and so on. Thus, there are many reasons to update room availability, but only when a reservation is being made is it necessary to put the customer on the waiting list. Hence, if you want to execute this operation extension to put the customer on the waiting list, you need to check the execution context of this operation—that is, the reason this operation is called. Although AOP gives you the ability to check the execution context during implementation, it just makes things complicated. In short, the Room class is not a good place to execute the operation extension.

Operations in Reserve Room Handler and Reserve Room Form

Let’s move up the call stack and consider whether you should extend the operations in the Reserve Room Form or the Reserve Room Handler. The same reasoning applies: Do you want to add the customer to the waiting list whenever a call is made to the Reserve Room Form to submitReservation(), or whenever a call is made to the Reserve Room Handler to makeReservation()?

In addition, you must consider the information needed by the extension. The Waiting List Handler needs to put the customer into the waiting list, and this waiting list is for each room. Hence, it needs a reference to the Customer instance and a reference to the Room instance. You must consider whether the Reserve Room Form and Reserve Room Handler have references to these instances.

Allocating Use-Case Behavior to Classes

Let’s assume it is the Reserve Room Handler that has a reference to the Customer instance. Thus, you extend the Reserve Room Handler class and specifically the makeReservation responsibility to invoke the Waiting List Handler. Now that you have made this decision, you can describe the class interactions, as shown in Figure 13-4.

Interaction diagram: queue for room.

Figure 13-4. Interaction diagram: queue for room.

Behavioral Context

Now that you have identified which responsibility you want to extend, you define where within the responsibility you want to invoke the Waiting List Handler. This behavior executes at the point when the call to Room.updateRoomAvailability fails, specifically when a NoRoomAvailable exception is thrown. This is depicted in Figure 13-4 as a guard condition:

[after (〈updatingRoomAvailability〉) throws NoRoomsAvailable]

In UML, a guard condition is simply a Boolean expression, and in this case it determines whether the following messages in Figure 13-4 will execute. The pointcut updatingRoomAvailability refers to the point when the responsibility Room. updateAvailability is called. It is defined as follows:

updatingRoomAvailability = call (Room.updateAvailability())

Note that the pointcut identification during analysis is still at quite a high level. When you proceed to design and implementation, your responsibilities will be refined into operations, and your analysis classes may be refined into multiple design and implementation classes. Since you do not have sufficient detail in analysis, you should not be too detailed about the pointcuts. So long as you know the structural context and have some idea about the behavioral context, you have done your work.

Describe Class Responsibilities

After identifying the responsibilities of participating class instances and the pointcuts, you can consolidate the class responsibilities in a class diagram, as shown below in Figure 13-5. In Figure 13-5, you can see that the operation extension to invoke the Waiting List Handler is added to the Reserve Room Handler class. The rest of Figure 13-5 is just plain old class diagram.

Participating classes in Handle Waiting List use-case realization.

Figure 13-5. Participating classes in Handle Waiting List use-case realization.

Keeping Application-Extension Use Cases Separate

Having analyzed the extension use case, you can now update the structure of the analysis model (and that of the system). You once again consider which of the responsibilities you have identified are specific to the extension use case you are analyzing. You present the results in the use-case slice. This is exemplified for the Handle Waiting List extension use-case slice as shown in Figure 13-6.

Use-case slice: Handle Waiting List.

Figure 13-6. Use-case slice: Handle Waiting List.

As before, since the Handle Waiting List use-case slice shows only the specifics of the use case, you might also like to show what it needs to complete the entire use-case realization. To do this, you describe its dependencies on other slices, as depicted in Figure 13-7.

Use-case slice context: Handle Waiting List.

Figure 13-7. Use-case slice context: Handle Waiting List.

Figure 13-7 shows the Handle Waiting List use-case slice as an extension of the Reserve Room use-case slice. From Figure 13-6, you can see that the Handle Waiting List use-case slice extends the Reservation class in the Hotel Reservation non-use-case-specific slice. This translates to an extended dependency from the Handle Waiting List use-case slice to the Hotel Reservation non-use-case-specific slice.

Structuring Alternate Flows

In the discussion above, we explained how to analyze and structure extension flows. The operation extensions that are involved in the extension flow are modularized within an aspect. Extension flows are a special case of alternate flows. You analyze an alternate flow in the same way as you analyze extension flows, discussed earlier in this chapter. You identify pointcuts and allocate the behavior of the alternate flows to the participating classes, and so on.

You normally group alternate flows according to variables, as discussed in Chapter 7, “Capturing Concerns with Use Cases.” For example, for the Reserve Room use case, you have variables such as different customer types and different reservation periods. You might have a group of alternate flows for handling corporate customers, a group of alternate flows to handle registered customers, and so on. The groups of alternate flows are analyzed together and realized as an aspect. Hence, you have multiple aspects in a use-case slice, as exemplified by Figure 13-8.

Multiple aspects in use-case slices.

Figure 13-8. Multiple aspects in use-case slices.

Of course, the Reserve Room use-case slice will contain classes that are specific to the Reserve Room use case, but since they are not important to this discussion, we do not show them in Figure 13-8. To find these classes, you simply walk through the same steps we discussed in Section 13.1. What we want to highlight here is this: good use-case models and well-organized use-case flows help you identify good aspects.

Keeping Alternate Flows Separate

Although alternate flows and groups of alternate flows are helpful in identifying aspects, they do not have a one-to-one mapping to operation extensions. Therefore, even though there is usually one pointcut for each alternate flow, you may have more than one pointcut in analysis and in design. This is because the use cases model the system from the external perspective, whereas analysis and design deals with the internals. Let’s consider the case when an alternate flow exists in the Reserve Room use case for the customer to add his reservation preference (e.g., smoking/nonsmoking, high floor/low floor, etc.). The analysis of this alternate flow is depicted in Figure 13-9. The customer indicates that he wants to select preferences. He enters his preferences and then submits the reservation.

Entering reservation preferences.

Figure 13-9. Entering reservation preferences.

It is evident from 13-9 that you need at least two operation extensions:

  1. An operation extension to let the customer select the preference option.

  2. An operation extension to save preference details.

You will likely find more operation extensions when you proceed to design—for example, validation checks on preferences.

Note that the minimal use-case design does not care about user interfaces—that is a separate concern. You usually model user interfaces concurrently for several use cases, even for extension use cases, to achieve some consistency between them. But user interfaces are not be just about look and feel, colors, and fonts—they are also about navigability. For example, the sequence of actions arising from the Customer actor will change your user interface flow, so you need to consider how these sequences of actions will be overlaid and what user interface mechanisms are provided to achieve this.

Designing Application-Extension Use Cases

The use-case analysis steps described above help you establish a high-level organization of the analysis elements. During design, you map these analysis elements into design elements, identify further design elements, and refine the packages and design elements themselves. The approach you apply for designing extension use cases and peer use cases is very much the same. Since they are so similar, we need not repeat the discussion. Instead, we discuss design issues specific to extension use cases:

  • Designing operation extensions (i.e., advices)

  • Identifying interfaces from use-case extensions

  • Achieving extensibility

Designing Operation Extensions

A well-designed operation extension follows the same principle of well-designed operations in classes. You should avoid long parameter lists in operations. Likewise, you should avoid using long parameter lists in operation extensions and advices. Each operation should be single-minded in performing a particular action. Likewise, each operation extension should be single-minded in providing the crosscutting behavior.

Crosscutting extensions are just that—they are extensions. They should be small and they should delegate as much as possible the crosscutting behavior to additional classes that they invoke. In this way, the crosscutting behavior is localized within these additional classes and consequently is more reusable.

Operation extensions act like glue code to invoke crosscutting behaviors. They are subject to changes in the base code. Changes in the base should be prevented from propagating to the classes that perform the actual crosscutting behavior.

To keep the operation extension simple, it must execute within an appropriate context. The same principles we discussed in Section 13.1.2, Identifying Pointcuts, regarding the considerations for choosing the structural context and the behavioral context of the operation extension, apply when you proceed to design and implementation.

Identifying Component Interfaces from Use-Case Extensions

As mentioned in the previous section, operation extension ought to be small. Most of the behavior should be delegated to a new class that realizes the extension flow. In the Handle Waiting List extension use-case example, the WaitingListHandler is the new class. So, the ReserveRoomHandler class extension makes calls to the WaitingListHandler.

To limit the impact of changes in the WaitingListHandler on the ReserveRoomHandler class extension, you can encapsulate the behavior of the WaitingListHandler class behind an interface. This is depicted in Figure 13-10; the interface is named IWaitingListHandler. The IWaitingListHandler is the required interface for the ReserveRoomHandler. The same IWaitingListHandler interface is the provided interface from the WaitingListHandler.

Interfaces for extensions.

Figure 13-10. Interfaces for extensions.

You can see that there is indeed a systematic approach to achieve extensibility with both the use-case structure and the design element structure. Briefly, the approach is described as follows:

  1. Identify extension points to existing use case: In the Handle Waiting List example, the extension point is Update Room Availability.

  2. Identify the interfaces between components: In this example, the interface is named IWaitingListHandler.

  3. Overlay existing component with extension use-case slice to invoke new component: The Handle Waiting List use-case slice adds an extension to the ReserveRoomHandler component to call the WaitingListHandler.

These three steps are depicted in 13-11.

Component interfaces for use-case extensions.

Figure 13-11. Component interfaces for use-case extensions.

In the following sections, we discuss how these basic steps can help you achieve extensibility under two scenarios: extending a use case with multiple extension use cases and extending multiple existing use cases with a new use case.

Dealing with Multiple Extensions to a Use Case

An extension point in a base use case can be extended by potentially many extension use cases, each introducing a different behavior. For example, one way to handle the unavailability of rooms is through a waiting list, as we discussed in detail. Another alternative is to find a room elsewhere, perhaps in a different hotel. This implies another extension use case, which we call Find Room, depicted in Figure 13-12.

Reserve Room use case extended by Find Room use case.

Figure 13-12. Reserve Room use case extended by Find Room use case.

Both Handle Waiting List and Find Room use cases extend the base Reserve Room use case at the same extension point. This means that the interface named IWaitingListHandler described in Figure 13-10 is no longer suitable, since the name IWaitingListHandler restricts the realization to having something to do with a waiting list. A better interface name would be INoRoomAvailableHandler, as shown in Figure 13-13. This interface can be realized by the WaitingListHandler or another class, which we call RoomFinder. The purpose of RoomFinder, as the name implies, is to find an available room in another hotel.

Achieving component extensibility.

Figure 13-13. Achieving component extensibility.

In Figure 13-13, we have introduced another class, called NoRoomAvailableDelegate. This class is responsible for determining whether to invoke the WaitingListHandler or the RoomFinder. It simplifies the operation extension for the ReserveRoomHandler.

Figure 13-13 also shows the components identified for the Hotel Management System. The ReserveRoomHandler component was first introduced in Section 12.3.2. In Section 12.3.2, we only presented one class.

Now we have introduced another class to the ReserveRoomHandler component, the NoRoomAvailableDelegate. Figure 13-13 also shows two additional components: WaitingListHandler and RoomFinder.

It may seem that we are using object-oriented concepts (e.g., delegates and interfaces) to achieve extensibility. Where does aspect orientation come into play? Aspects are used to introduce class extensions onto the ReserveRoomHandler to invoke the NoRoomAvailableDelegate. The NoRoomAvailableDelegate class determines whether it will invoke the WaitingListHandler or the RoomFinder. If another realization of INoRoomAvailableHandler interface is added to the system, the NoRoomAvailableDelegate class should be able to call this new realization. This can be achieved through some configuration file that describes how the new realization can be instantiated and invoked.

Thus, what you have now is an extensible Reserve Room use-case realization, which you can easily plug in to a new realization of the INoRoomAvailableHandler interface. This is depicted in Figure 13-14, which shows the ReserveRoomHandler component having a required interface INoRoomAvailableHandler. This can be plugged in by either the WaitingListHandler or the RoomFinder component.

Achieving extensibility through use-case slices.

Figure 13-14. Achieving extensibility through use-case slices.

Note that the original ReserveRoomHandler component does not have the INoRoomAvailableHandler required interface. This interface and the NoRoomAvailableDelegate, which we mentioned earlier, are introduced by the Handle No Room use-case slice. Thus, the extensibility mechanism for you to plug in the WaitingListHandler component or the RoomFinder component is kept separate from the base ReserveRoomHandler behavior.

Figure 13-14 differs from Figure 13-11 in that the components realizing the extension use case (i.e., the WaitingListHandler and RoomFinder components) are outside the extension use-case slice. This is because the purpose of the Handle No Room use-case slice is to introduce the extensibility mechanism, not the components to be plugged in.

Extending Multiple Use Cases

In the above example, we have extension use cases extending only one use case. Let’s look at another situation in which an enhancement affects multiple use cases. For example, you want to add a new and special kind of room, like a penthouse or an executive suite for businessmen. This kind of extension affects many use cases. You identify the extension points for each of these use cases. The extension points are mapped to required interfaces to existing components that realize the existing use cases. A use-case slice then introduces this extensibility mechanism.

Figure 13-15 describes how you can design extensibility into the system one new capability at a time. In this case, you want the system to handle a new and different room type.

Introducing extensibility mechanism with use-case slices.

Figure 13-15. Introducing extensibility mechanism with use-case slices.

Designing an extensibility mechanism starts with modeling concerns—with use cases. You identify the use cases that are affected by the introduction of the new room type and the extension points in these existing use cases. As an example, the Handle New Room Type use case extends three existing use cases: Reserve Room, Check In Customer, and Check Out Customer. These are shown on the left-hand side of Figure 13-15.

The right-hand side of Figure 13-15 shows the Handle Different Room Type use-case slice. In essence, it extends the components realizing the use cases on the left with the ability to call the new room type component through the required interfaces.

In Chapter 7, we highlighted the need to consider the variability of your system. For example, the Hotel Management System needs to handle different kinds of reservation schemes, different kinds of customer types, and so on. You can treat each case as an extension use case and go through the steps in Figure 13-15. After doing that, you will have a set of extensibility use-case slices offering a set of required interfaces. What you have achieved now is a domain-specific framework (i.e., a Hotel Management System framework) by which you can plug in components to deal with different kinds of variations. In short, you have an extensible architecture.

Dealing with Changes in the Base

At this point, we want to discuss a specific issue on designing extensions. Extensions are specified on top of a base. In this chapter, we discuss an extending use case on top of a base use case, or an alternate flow on top of a basic flow. This is a technique for keeping extensions separate. Changes from the extension do not impact the base. However, changes to the base do affect the extensions.

What you want to do is limit the impact of any changes to the base on the extension. We briefly discuss some techniques to deal with possible changes in the base.

In simplistic terms, each step of a use case involves moving object attribute values from the database to the actor (i.e., retrieval of information) or moving information from the actor to the database or performing some computation on objects. This is exemplified in Figure 13-16.

Effects of changes in element structure.

Figure 13-16. Effects of changes in element structure.

You frequently need to traverse through structures of some kind, as shown in Figure 13-16.

  1. You must populate the data values in the user interface structure with values from the object structure. You might need to set the values of elements in the object structure based on the inputs provided by the user interface structure.

  2. You must traverse the object structure to perform computation. For example, to compute the bill for a customer, you might need to loop through the reservations she has made and, for each reservation check, how many rooms she has reserved, and so on.

  3. You must populate values of the object structure elements with the values held in the database structure or vice versa.

Since you will be spending the bulk of the development effort on traversing structures, it makes sense to take the effort to keep traversals separate from computation. These structures are frequently part of the same base that other use-case slices depend on. Keeping traversal separate from computation has two benefits. First, if there are changes to the structures, only the traversals get affected. The impact on the computation is limited. There are several ways to make a system resilient to such changes. We discuss them briefly. Second, even if changes to the structures do impact the computation, keeping traversal separate from the structure makes your implementation more easily understandable.

Before we go on, we like to highlight again that the minimal use-case design is not concerned with user interfaces and the database itself. User interfaces and databases are platform-specific. We explain how you overlay these on top of the minimal use-case design in Chapter 15 when we discuss keeping platform-specifics separate. Nevertheless, the minimal use-case design does have an object structure and is subject to changes in this object structure. That is why we discuss how to deal with such changes here.

Applying Reflection

One technique is to use some kind of runtime identification. The ability to find out information about objects at runtime is called reflection. Instead of hardcoding the traversal based on the data structure, you discover the data structure through reflection. Hence, if there are slight changes to the data structure, the traversal code is still valid.

Applying reflection, however, has runtime-performance penalties. Another approach is to use reflection-based code-generation techniques. Code generators reflect on the data structure and generate code based on the data structure and some template. For example, you can have templates and tools to generate database queries and present the results. So, if the underlying structure changes, then all you need to do is to regenerate the codes. Of course, anything that depends on the generated code may be affected, but at least you can generate the code automatically. With aspect orientation, since different slices are kept separate, you do not need to worry about the potential of the code generator overwriting other parts of the system, because you can keep them in separate slices.

Applying Design Patterns

Another technique is available through the Visitor and Strategy design patterns. We highlight this technique because it underpins adaptive programming—one of the research areas in aspect orientation.

To make the discussion concrete, let’s consider the example of bill computation, as illustrated in Figure 13-17 and shows the cost of the bill is the total of all reservation costs multiplied by the tax. The operation computeCost() in the Bill class sums up the costs of the individual Reservations items.

Computation of bill cost.

Figure 13-17. Computation of bill cost.

The cost of a reservation is the cost of the associated room multiplied by the duration of the reservation (i.e., how many days a person is staying multiplied by the price per day). This is computed through the computeCost() operation in the Reservation class.

The cost of the room is by default its price. If there is any promotional discount associated with the room in the duration of the reservation, the cost of the room will be the discounted price. This is computed through the computeCost() operation in the Room class.

As you can see in Figure 13-17, three operations are added into the respective classes. This is not a good idea, because bill computation is now scattered across the classes.

The Visitor and Strategy patterns provide a useful technique. A strategy class defines how you would traverse or walk through the class instances that are connected according to the relationships defined in the element structure. A visitor class defines the actions at each point in the traversal. If you apply the Visitor and Strategy patterns to the bill computation example, you need both a visitor class and a strategy class. We named them CostComputationStrategy and CostComputationVisitor, as shown in Figure 13-18.

Computation of bill cost using Visitor and Strategy pattern.

Figure 13-18. Computation of bill cost using Visitor and Strategy pattern.

The purpose of the CostComputationStrategy is to traverse the Bill containment structure. The CostComputationStrategy has an operation traverse(), which accepts a Bill instance and a Visitor instance as an input. The purpose of this operation is to walk through the items within the Bill. The relationships between these items is depicted in Figure 13-17. The traverse() operation calls the CostComputationVisitor instance to perform the actual bill computation.

Figure 13-19 depicts the pseudocode for the traverse() operation. In essence, it loops through the items in the Bill instance. Each time the traverse() operation visits an item, it calls the beforeVisit() operation with that item as a parameter, and each time it departs from the item, it calls the afterVisit() operation with the same parameter.

Example 13-19. Pseudocode illustrating traversal.

void traverse(Bill theBill, Visitor theVisitor) {
    theVisitor.beforeVisit(theBill) ;
    for each reservation in theBill {
        theVisitor.beforeVisit(theReservation) ;
        for each Room in theReservation {
            theVisitor.beforeVisit (theRoom) ;
            for each Promotion in theRoom {
                theVisitor.beforeVisit(thePromotion) ;
                theVisitor.afterVisit(thePromotion) ;
            }
            theVisitor.afterVisit(theRoom) ;
        }
        theVisitor.afterVisit(theReservation) ;
    }
    theVisitor.afterVisit(theBill) ;
}

As you can see in Figure 13-19, the CostComputationStrategy contains no computation code. Computation is all left to the CostComputationVisitor class. It contains several attributes to hold temporary values during computation:

  • The billCost attribute accumulates the total reservation cost as the strategy class loops through the reservations in the bill.

  • The reservationCost attribute is a temporary variable for the cost of the reservation being processed.

  • The roomCost attribute is a temporary variable for the room being processed. It takes the value of the cost of the room or the promotion value if applicable.

Table 13-1 describes what occurs in each operation within the CostComputationVisitor class. Basically, it is the algorithm for computing the bill value. Each operation updates the attributes in the CostComputationVisitor class accordingly. After completing the traversal, the final bill value is stored in the billCost attribute.

Table 13-1. Computation Rules Expressed in Visitor Class

Operation

Description

beforeVisit(theBill)

Set billCost = 0

beforeVisit(theReservation)

Set reservationCost = 0

beforeVisit(theRoom)

Set roomCost = theRoom.price

beforeVisit(thePromotion)

Do nothing

afterVisit(thePromotion)

If thePromotion is applicable Set roomCost = thePromotion.price

afterVisit(theRoom)

Set reservationCost = reservation.duration × roomCost

afterVisit(theReservation)

Set billCost = billCost + reservationCost

afterVisit(theBill)

Set billCost = billCost × theBill.tax

As you can see, the Visitor and Strategy patterns help you keep concerns separate in several ways. If there are changes to the Bill item structure, you only need to modify the strategy class. If there are any changes in computation rules, you just need to modify the visitor class. Even if changes affect both the strategy and visitor classes, keeping traversal in the strategy and the computation in the visitor makes the implementation more readable and understandable. In addition, temporary variables are only added to the visitor class, not to any class within the bill item structure, so it does not complicate the bill structure in any way. For this example, this technique is definitely more appropriate than using use-case slices and aspects to overlay use-case behavior onto existing classes.

Applying Adaptive Programming

The adaptive programming technique makes use of the Strategy and Visitor design patterns together with reflection. Recall that the goal of a strategy class is to traverse a data structure. For the bill computation example, the strategy class has to traverse the bill item structure. shown again in Figure 13-20.

Computation of bill cost.

Figure 13-20. Computation of bill cost.

Instead of hardcoding the traversal as a series of loops, adaptive programming uses a strategy string, which in this case is “from Bill to Promotion.” Based on this string, and using reflection, the strategy class will be able to traverse the items in the bill structure. Even if you modify the bill item structure, such as by permitting sub-bill items as shown in Figure 13-20, the traversal will still work. Basically, the strategy class will find, based on the string “from Bill to Promotion,” all possible paths from a Bill instance to a Promotion instance.

The technique of adaptive programming is a special case of aspect-orientation that attempts to separate the class structure from computation. The visitor class in adaptive programming makes use of before, after, and even around semantics, much as AOP does. Whereas AOP uses these with reference to an execution point, adaptive programming uses it with reference to a data structure traversal.

Summary and Highlights

In this chapter, we deal with a class of functional requirements called extensions. Extensions can be enhancements to existing requirements, or they can be factored out just to make the essence of a functional requirement more understandable. You keep these extensions separate from the base during requirements time, as alternate flows or extension use cases. These are gradually refined during analysis and design when you give them structural context—which class, which operation the pointcut will refer to, and so on. In this way, you keep extensions separate from the base all the way to code.

Extensions (extension use cases, class extensions, operation extensions, component extensions) are a very important concept. Extensions are used to keep additional behavior separate from the base. They ensure that the base is easy to understand and maintain. Concerns between the base and the extension have to be kept separate. With use-case slices and aspects, you keep changes in the extension from affecting the base. At the same time, you want to minimize how changes in the base impact the extension. In this chapter, we discussed how this can be achieved through various techniques such as reflection and adaptive programming.

Our discussion was limited to functional extensions. In the next two chapters, we describe how extension use case are used to introduce infrastructure and platform specifics.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.235.79