The model depicted in a class diagram can be augmented with extra information in numerous places. Most of the examples given in this section augment the class diagram for the R&L system shown in Figure 2-1 of Chapter 2. 3.3.1 Derivation RulesModels often define derived attributes and associations. A derived element does not stand alone. The value of a derived element must always be determined from other (base) values in the model. Omitting the way to derive the element value results in an incomplete model. Using OCL, the derivation can be expressed in a derivation rule. In the following example, the value of a derived element usedServices is defined to be all services that have generated transactions on the account: context LoyaltyAccount::usedServices : Set(Services) derive : transactions.service->asSet() Note that the question whether usedServices is an attribute or an association end should be answered from the UML diagram(s). In this case, it was not shown in the diagram in Chapter 2, so we cannot tell. Often, derived attributes are not shown in the diagrams at all, but are defined by an OCL attribute definition (see Section 3.9.1). 3.3.2 Initial ValuesIn the model information, the initial value of an attribute or association role can be specified by an OCL expression. In the following examples, the initial value for the attribute points is , and for the association end transactions, it is an empty set: context LoyaltyAccount::points : Integer init : 0 context LoyaltyAccount::transactions : Set(Transaction) init : Set{} Note the difference between an initial value and a derivation rule. A derivation rule states an invariant: The derived element should always have the same value that the rule expresses. An initial value, however, must hold only at the moment when the contextual instance is created. After that moment, the attribute may have a different value at any point in time. 3.3.3 Body of Query OperationsThe class diagram can introduce a number of query operations. Query operations are operations that have no side effects, i.e., do not change the state of any instance in the system. Execution of a query operation results in a value or set of values, without any alterations in the state of the system. Query operations can be introduced in the class diagram, but can only be fully defined by specifying the result of the operation. Using OCL, the result can be given in a single expression, called a body expression . In fact, OCL is a full query language, comparable to SQL, as shown in [Akehurs01]. The use of body expressions is an illustration thereof. The next example states that the operation getCustomerName will always result in the name of the card owner associated with the loyalty account: context LoyaltyAccount::getCustomerName() : String body : Membership.card.owner.name 3.3.4 InvariantsAnother way to augment a class diagram is by stating an invariant. The concept invariant is defined as follows :
An invariant is described using an boolean expression that evaluates to true if the invariant is met. The invariant must be true upon completion of the constructor and completion of every public operation, but not necessarily during the execution of operations. Incorporating an invariant into a model means that any system made according to the model is faulty when the invariant is broken. How to react when an invariant is broken is explained in Section 4.6.2. In the following example, all cards that generate transactions on the loyalty account must have the same owner: context LoyaltyAccount inv oneOwner: transactions.card.owner->asSet()->size() = 1 An invariant may be named, which can be useful for reference from an accompanying text. The preceding invariant is named oneOwner . 3.3.5 Preconditions and PostconditionsPreconditions and postconditions to operations are yet another way to complete the model depicted in a class diagram. Because pre- and postconditions do not specify how the body of an operation should be implemented, they are an effective way to precisely define the interfaces in the system. The concepts are defined as follows:
Note that in contrast to invariants, which must always be true, pre- and postconditions need be true only at a certain point in time: before and after execution of an operation, respectively. We can use OCL expressions to specify either the pre- and postconditions of operations on all UML types, or the pre- and postconditions of use cases. The first option is explained in this section, the second option is explained in Section 3.8.1. In the following example, a customer that is already known in the system, perhaps because it is already participating in another loyalty program, is enrolled in a loyalty program under the condition that its name attribute is not empty. The postcondition of the enroll operation ensures that the new customer is included in the list of customers belonging to the loyalty program: context LoyaltyProgram::enroll(c : Customer) pre : c.name <> '' post : participants = participants@pre->including( c ) Note that a even a simple postcondition like myVar = 10 does not specify any statement in the body of the operation. The variable myVar may become equal to 10 in numerous ways. Here are some example implementations in Java: -- use another variable otherVar = 10; -- include other statements here myVar = otherVar; -- use a calculation myVar = 100/10; -- use another object myVar = someElement; -- where the value of someElement is 10 The principle behind the use of pre- and postconditions is often referred to as the design by contract principle. Design by contract can be used within the context of any object-oriented development method. The following paragraphs describe the principle and its advantages. Design by ContractThe definition of contract in the design by contract principle is derived from the legal notion of a contract: a univocal, lawful agreement between two parties in which both parties accept obligations, and on which both parties can ground their rights. In object-oriented terms, a contract is a means to establish the responsibilities of an object clearly and unambiguously. An object is responsible for executing services (the obligations) if and only if certain stipulations (the rights) are fulfilled. A contract is an exact specification of the interface of an object. All objects that are willing to use the services offered are called clients or consumers . The object that is offering the services is called the supplier . Although the notion of a contract is derived from law practice, it is not completely analogous when employed in object technology. A contract is offered by a supplier independently of the presence of any client. However, when a client uses the services offered in the contract, the client is bound by the conditions in the contract. A contract describes the services that are provided by an object. For each service, it specifically describes two things:
An example of a contract can be found at most mailing boxes in the Netherlands:
A contract for an express service is another example:
Table 3-1 shows the rights and obligations of both parties in the express delivery service example. Note that the rights of one party can be directly mapped to the obligations of the other party. A contract can become much more complicated ”for example, when it concerns the purchase of a house. The important point is that the rights and obligations in a contract are unambiguous. In software terms, we call this a formal specification . Both parties benefit from a clear contract:
The interface that is offered by an object consists of a number of operations that can be performed by the object. For each operation, a contract can be envisioned . The rights of the object that offers the contract are specified by the preconditions. The obligations are specified by the postconditions. If either party fails to meet the conditions in the contract, the contract is broken. When this happens, it is clear which party broke the contract: either the client did not meet the specified conditions or the supplier did not execute the service correctly. Failure of a precondition or postcondition ”that is, either condition is not true when it should be ”means that the contract is broken. In Eiffel, the only language that implements the design by contract principle, an exception is raised when a precondition or postcondition fails. In this way, the exception mechanism is an integral part of the design by contract principle. Table 3-1. Rights and obligations in a contract
3.3.6 Messaging in PostconditionsAn aspect of postconditions that is very useful for defining interfaces is the fact that a postcondition may state that a certain operation has been called. For example, in the Eclipse framework, which is an open platform for tool integration built by an open community of tool providers (www.eclipse.org), every file is contained in a project. Several builders , which are responsible for keeping associated files up to date, can be linked with the project. An example of a builder is the Java compiler, which compiles a .java file into a .class file. When a file is saved, all builders are informed by calling their incrementalBuild operation. There is no need to uncover how the framework is taking care of this, but there is a need to inform Eclipse developers about this feature. A simple postcondition to the save operation of the class File will suffice: context File::save() post : self.project.builders->forAll( b : Builder b^incrementalBuild() ) In general, when there is a need to specify dynamic aspects of a model element without revealing the actual implementation, the postcondition of the operations should contain message information. Note again that a postcondition does not specify any statement in the body of the operation. The message may have been sent in a number of different ways. The postcondition merely states that it is sent. Here are some possible example implementations in Java: -- get all builders and loop over the collection Builders[] builders = getProject().getBuilders(); for(int i=0; i<builders.size; i++) builders[i].incrementalBuild(); } -- let some other object take care of calling the builders someObject.takeCareOfBuilders( getProject() ); -- let the project take care of calling the builders getProject().callBuilders(); 3.3.7 Cycles in Class ModelsMany class models reflect cycles in the sense that you can start at an instance, navigate through various associations, and return to an instance of the same class. In the class diagram, this is shown as a cycle of classes and associations. Quite often, these cycles are the source of ambiguities . The example used in Section 1.4.2, reprinted in Figure 3-2, shows a class diagram with a cycle. In this model, a Person owns a House , which is paid for by taking a Mortgage . The Mortgage takes as security the House that is owned by the Person . Although the model looks correct at first glance, it contains a flaw. Better stated, it is imprecise and can result in ambiguities. The model allows a person to have a mortgage that takes as security a house owned by another person. This is clearly not the intention of the model. We can easily state the necessary invariant with the following piece of OCL code: context Person inv : self.mortgages.security.owner ->forAll(owner : Person owner = self) Figure 3-2. A cycle in a class model
In general, cycles in a class model should be checked carefully, and any constraints on these cycles should be stated as invariants on one of the classes in the cycle. Especially when the multiplicities in a cycle are higher than one, you need to carefully write the invariant or invariants. 3.3.8 Defining Derived ClassesA view is a well-known concept in relational database systems. In a UML/OCL model, a similar concept exists. This concept is called a derived class . A derived class is a class whose features can be derived completely from already existing classes and other derived classes. The concept of derived classes has been introduced in [Blaha98], and formalized in [Balsters03]. For instance, in the R&L system, it might be useful to define a derived class that holds a transaction report for a customer, as shown in Figure 3-3. The attributes, association ends, and query operations of this class can be defined by the following expressions: context TransactionReportLine::partnerName : String derive : transaction.generatedBy.partner.name context TransactionReportLine::serviceDesc : String derive : transaction.generatedBy.description context TransactionReportLine::points : Integer derive : transaction.points context TransactionReportLine::amount : Real derive : transaction.amount context TransactionReportLine::date : Date derive : transaction.date Figure 3-3. A derived class
The class TransactionReport is not completely derived. Its from and until attributes are normal attributes without derivation rules. The other attributes can all be derived: context TransactionReport::name : String derive : card.owner.name context TransactionReport::balance : Integer derive : card.Membership.account.points context TransactionReport::number : Integer derive : card.Membership.account.number context TransactionReport::totalEarned : Integer derive : lines.transaction->select( oclIsTypeOf( Earning ) ) .points->sum() context TransactionReport::totalBurned : Integer derive : lines.transaction->select( oclIsTypeOf( Burning ) ) .points->sum() To complete the definition of this class, some invariants are needed. The dates invariant states that the transactions in this report should be transactions between the from and until dates. The cycle invariant states that the transactions in the lines of the report should indeed be transactions for this customer card: context TransactionReport inv dates: lines.date->forAll( d d.isBefore( until ) and d.isAfter( from ) ) context TransactionReport inv cycle: card.transactions->includesAll( lines.transaction ) 3.3.9 Dynamic MultiplicityAssociations in a class diagram can sometimes be imprecise specifications of the system. This is the case when the multiplicity of the association is not fixed, but should be determined based on another value in the system. This is called dynamic multiplicity . An example of this has already been described in Section 1.4.1, where the multiplicity of the association between Flight and Passenger was indicated by the numberOfSeats on the Airplane . 3.3.10 Optional MultiplicityAn optional multiplicity of an association in a class diagram is often a partial specification of what is really intended. Sometimes the optionality is free; that is, in all circumstances there can be either one or no associated object. Quite often, an optional association is not really free. Whether an associated object can or must be present depends on the state of the objects involved. For example, in Figure 3-2, the optional association is not completely free. If a person has a mortgage, he or she must also own a house. This constraint can be specified by the following invariant: context Person inv optionality: mortgages->notEmpty() implies houses->notEmpty() In general, when an optional multiplicity is present in the class diagram, you have to use OCL invariants to describe precisely the circumstances under which the optional association may be empty or not empty. 3.3.11 Or ConstraintsThe class diagram can contain an or constraint between two associations, as shown in Figure 3-4. This constraint means that only one of the potential associations can be instantiated at one time for any single object. This is shown as a dashed line connecting two or more associations (all of which must have at least one class in common), with the string or labeling the dashed line. The multiplicity of the associations must be optional; otherwise , they cannot be empty. Figure 3-4. Or constraint
Note that the visual representation of an or constraint is ambiguous in certain situations. This is the case when two associations between the same two classes have multiplicity optional at both ends of both associations. The visual or constraint can now be read in two directions. In one direction, it can mean that one person has either a managedProject or a performedProject , but not both. In the other direction, it can mean that one project has either a projectLeader or a projectMember , but not both. Therefore, two different interpretations are possible. Although the choice of interpretation may seem obvious in this case, you can imagine the consequences if someone makes the wrong interpretation. Specifying the visual or constraint as an OCL constraint resolves the ambiguity problem. When you intend the first interpretation, you should augment the diagram with the following invariant: context Person inv : managedProject->isEmpty() or performedProject->isEmpty() The invariant stating the second interpretation is: context Project inv : projectLeader->isEmpty() or projectMember->isEmpty() This example shows the importance of formal constraints and illustrates how they ensure that a model is unambiguous. |