|Ada 95 Quality and Style Guide||Chapter 9|
9.2.2 Properties of Dispatching Operations
The implementation of the dispatching operations of each type in a derivation class rooted in a tagged type T should conform to the expected semantics of the corresponding dispatching operations of the class-wide type T'Class.
The key point of both of the alternatives in the following example is that it must be possible to use the
class-wide type Transaction.Object'Class polymorphically without having to study the implementations of each of the types derived from the root type Transaction.Object. In addition, new transactions can be added to the derivation class without invalidating the existing transaction processing code. These are the important practical consequences of the design rule captured in the guideline:with Database; package Transaction is type Object (Data : access Database.Object'Class) is abstract tagged limited record Has_Executed : Boolean := False; end record; function Is_Valid (T : Object) return Boolean; -- checks that Has_Executed is False procedure Execute (T : in out Object); -- sets Has_Executed to True Is_Not_Valid : exception; end Transaction;
The precondition of Execute(T) for all T in Transaction.Object'Class is that Is_Valid(T) is True. The postcondition is the T.Has_Executed = True. This model is trivially satisfied by the root type Transaction.Object.
Consider the following derived type:with Transaction; with Personnel; package Pay_Transaction is type Object is new Transaction.Object with record Employee : Personnel.Name; Hours_Worked : Personnel.Time; end record; function Is_Valid (T : Object) return Boolean; -- checks that Employee is a valid name, Hours_Worked is a valid -- amount of work time and Has_Executed = False procedure Has_Executed (T : in out Object); -- computes the pay earned by the Employee for the given Hours_Worked -- and updates this in the database T.Data, then sets Has_Executed to True end Pay_Transaction;
The precondition for the specific operation Pay_Transaction.Execute(T) is that Pay_Transaction.Is_Valid(T) is True, which is the same precondition as for the dispatching operation Execute on the class-wide type. (The actual validity check is different, but the statement of the "precondition" is the same.) The postcondition for Pay_Transaction.Execute(T) includes T.Has_Executed = True but also includes the appropriate condition on T.Data for computation of pay.
The class-wide transaction type can then be properly used as follows:type Transaction_Reference is access all Transaction.Object'Class; type Transaction_List is array (Positive range <>) of Transaction_Reference; procedure Process (Action : in Transaction_List) is begin for I in Action'Range loop -- Note that calls to Is_Valid and Execute are dispatching if Transaction.Is_Valid(Action(I).all) then -- the precondition for Execute is satisfied Transaction.Execute(Action(I).all); -- the postcondition Action(I).Has_Executed = True is -- guaranteed to be satisfied (as well as any stronger conditions -- depending on the specific value of Action(I)) else -- deal with the error ... end if; end loop; end Process;
If you had not defined the operation Is_Valid on transactions, then the validity condition for pay computation (valid name and hours worked) would have to directly become the precondition for Pay_Transaction.Execute. But this would be a "stronger" precondition than that on the class-wide dispatching operation, violating the guideline. As a result of this violation, there would be no way to guarantee the precondition of a dispatching call to Execute, leading to unexpected failures.
An alternative resolution to this problem is to define an exception to be raised by an Execute operation when the transaction is not valid. This behavior becomes part of the semantic model for the class-wide type: the precondition for Execute(T) becomes simply True (i.e., always valid), but the postcondition becomes "either" the exception is not raised and Has_Executed = True "or" the exception is raised and Has_Executed = False. The implementations of Execute in all derived transaction types would then need to satisfy the new postcondition. It is important that the "same" exception be raised by "all" implementations because this is part of the expected semantic model of the class-wide type.
With the alternative approach, the above processing loop becomes:procedure Process (Action : in Transaction_List) is begin for I in Action'Range loop Process_A_Transaction: begin -- there is no precondition for Execute Transaction.Execute (Action(I).all); -- since no exception was raised, the postcondition -- Action(I).Has_Executed = True is guaranteed (as well as -- any stronger condition depending on the specific value of -- Action(I)) exception when Transaction.Is_Not_Valid => -- the exception was raised, so Action(I).Has_Executed = False -- deal with the error ... end Process_A_Transaction; end loop; end Process;
All the properties expected of a class-wide type by clients of that type should be meaningful for any specific types in the derivation class of the class-wide type. This rule is related to the object-oriented programming
"substitutability principle" for consistency between the semantics of an object-oriented superclass and its subclasses (Wegner and Zdonik 1988). However, the separation of the polymorphic class-wide type T'Class from the root specific type T in Ada 95 clarifies this principle as a design rule on derivation classes rather than a correctness principle for derivation itself.
When a dispatching operation is used on a variable of a class-wide type T'Class, the actual implementation executed will depend dynamically on the actual tag of the value in the variable. In order to rationally use T'Class, it must be possible to understand the semantics of the operations on T'Class without having to study the implementation of the operations for each of the types in the derivation class rooted in T. Further, a new type added to this derivation class should not invalidate this overall understanding of T'Class because this could invalidate existing uses of the class-wide type. Thus, there needs to be an overall set of semantic properties of the operations of T'Class that is preserved by the implementations of the corresponding dispatching operations of all the types in the derivation class.
One way to capture the semantic properties of an operation is to define a "precondition" that must be true before the operation is invoked and a "postcondition" that must be true (given the precondition) after the operation has executed. You can (formally or informally) define pre- and postconditions for each operation of T'Class without reference to the implementations of dispatching operations of specific types. These semantic properties define the "minimum" set of properties common to all types in the derivation class. To preserve this minimum set of properties, the implementation of the dispatching operations of all the types in the derivation class rooted in T (including the root type T) should have (the same or) weaker preconditions than the corresponding operations of T'Class and (the same or) stronger postconditions than the T'Class operations. This means that any invocation of a dispatching operation on T'Class will result in the execution of an implementation that requires no more than what is expected of the dispatching operation in general (though it could require less) and delivers a result that is no less than what is expected (though it could do more).
Tagged types and type extension may sometimes be used primarily for type implementation reasons rather than for polymorphism and dispatching. In particular, a nontagged private type may be implemented using a type extension of a tagged type. In such cases, it may not be necessary for the implementation of the derived type to preserve the semantic properties of the class-wide type because the membership of the new type in the tagged type derivation class will not generally be known to clients of the type.
|< Previous Page||Search||Contents||Index||Next Page >|