The Object Constraint Language: Getting Your Models Ready for MDA (2nd Edition)
4.5 Merging Code Fragments
The code fragments that implement OCL expressions must be merged with the code that implements the part of the system specified by the UML diagrams. Each OCL expression should be used according to its purpose in the model. For most expressions, this is simple. The code for an attribute definition clearly belongs in the code for the class that is mentioned in the context definition. The code for the initial value of an attribute must be placed where the attribute is created or initialized . If the body of a query operation is given as an OCL expression, the implementing code for that expression will become the body of the operation. For other expressions, such as derivation rules, invariants, and pre- and postconditions, there are more options, which makes the merging of the code fragment for the OCL expression into the code for the contextual type slightly more complicated. 4.5.1 Derivation Rules
Derivation rules can be implemented in two different ways. The method chosen depends on the complexity and usage of the derivation rule. The most straightforward approach is to implement an attribute with a derivation rule as a query operation, where the derivation rule serves as the body of the operation. Each time the attribute value is requested , the derivation will be calculated. The example in Appendix D takes this approach. If the evaluation of the rule is expensive and the attribute is frequently used, the aforementioned strategy might not be optimal. As an alternative, the value of the attribute can be stored as an attribute. However, the object that contains the attribute needs to be notified in case any change occurs in the objects upon which the derivation depends. The listener or observer pattern can be used to set up such a notification mechanism. Each time the value of the attribute is requested, you can determine whether it needs to be recalculated. In any case, a derived attribute should never have a public set operation, because no other object should be able to change its value. 4.5.2 Invariants
The best way to implement an invariant is to write a separate operation with a boolean result that implements the check. This operation can be called whenever it is appropriate to check the invariant. It is convenient to include one operation in a class that calls all separate operations that implement invariants, because often all invariants need to be checked at the same time. The most important issue is to determine when to call these operations. This is treated in Sections 4.6.1 and 4.6.2. Note that model elements referred to in the invariant should be passed as parameters to the operation that implements the invariant. 4.5.3 Preconditions and Postconditions
Pre- and postconditions can best be implemented in the operation for which they are defined. Some languages provide an assert mechanism that can be used. For example, the following operation specification can be easily implemented using the Java assert : context LoyaltyProgram::enroll(c : Customer) pre : not participants->includes(c) post : participants = participants@pre->including(c) Assuming the operations includes and including are provided for participants , the resulting code in the class LoyaltyProgram is as follows : Void enroll(Customer c) { assert( ! participants.contains(c) ); old_participants = new ArrayList(participants); // ... // < body of operation > // ... assert( participants = old_participants.add(c) ); } When the postcondition uses precondition time values (see Section 10.1.1), the implementing code should hold those values in a temporary variable. In the preceding example, old_participants is used to hold the value of the participants association end. Note that the old value must be a true copy or clone, not a copy of the reference, because the referenced value will change during the operation. Usually, checking postconditions explicitly is not done. It does not add much value to the implementation. Postconditions are more a means to improve and clarify specifications. Implementing preconditions, conversely, is very useful. The source of a runtime error that causes a precondition to fail is much easier to find than an arbitrary runtime error. 4.5.4 Guards and Change Events in Statecharts
The implementation of guards and change events in a statechart depends on how the statechart and its transitions are implemented. When the transitions are implemented by operations, i.e., the event at the transition is actually an operation call to which the object responds, the guard should be considered part of the precondition of the operation. In that case, the start state should also be considered part of the precondition. The end state is considered part of the postcondition. Implementing them follows the rules for implementing pre- and postconditions. For example, the statechart in Figure 3-8, reprinted in Figure 4-1, may be implemented by the following operations in class Filler : void stop() { if (!pre_stop()) { system.out.println("precondition failed in stop"); } theLine.move(getMyBottle()); state = stopped; } void fill(Bottle b) { if (!pre_fill(b)) { system.out.println("precondition failed in fill(b)"); } b.filling( this ); myBottle = b; state = filling; } boolean pre_stop() { return state == filling; } boolean pre_fill(Bottle b) { return state == stopped && this.getContents() > b.getCapacity(); } Figure 4-1. Filler statechart, reprinted from Figure 3-8
When the same event occurs as a trigger for more than one transition, the situation is more complex. In this case, we must take into account all possible situations at precondition time; therefore, we must implement the precondition to be the or-ed combination of all possibilities. For example, the statechart shown in Figure 4-2, an extention of the previous example, contains the event fill(b) twice. In this case, the precondition to the operation fill may be implemented by the following code: boolean pre_fill(Bottle b) { return (state == stopped && this.getContents() > b.getCapacity()) (state == emergencystop && this.alarmOff ); } Figure 4-2. Extended Filler statechart
When a statechart is implemented by a statemachine, using the real-time interpretation of statecharts, the OCL expressions need to be implemented in a different manner. Guards should be implemented as conditions on the execution of the actions and state change specified by the transition. The following pseudocode gives an example. Note that a correct implementation of the OCL expressions depends completely on the way the statemachine is implemented. while( true ) { e = get_eventId(); if (e != null) { switch (e) { 1: if ( guard_on_1 ) { // execute actions and state change } 2: if ( guard_on_2 ) { // execute actions and state change } // etc. } } } 4.5.5 Code for Interaction Diagrams
Because interaction diagrams are instance diagrams, they are not complete specifications, but examples of interactions between instances. Interaction diagrams can be used to provide a skeleton of the code of the operations that are visible in the diagrams, but they cannot be used to generate the complete Java code. The OCL expressions used for conditions, target objects of messages, and parameters of messages all become part of the operation skeletons. For example, based on the diagrams in Figures 3-5 and 3-6, the following operation should be added to the code for class Service : Transaction makeTransaction(float amnt, Date d) { Transaction newT; if (amnt > 0 ) { newT = new Earning(); } else if ( amnt == 0 ) { newT = new Burning(); } newT.setDate(d); newT.setAmount(amount); // calculate pnt newT.setPoints(pnt); return newT; } It may very well be that this code should be completed with extra (hidden) functionality. All we know from the diagrams is that the preceding calls should be present somewhere in the operation body and that the order of calls should be as stated. |