Often, the multiplicity of an association is greater than 1, thereby linking one object to a collection of objects of the associated class. To deal with such a collection, OCL provides a wide range of collection operations, and distinguishes between different types of collections. 2.4.1 Using Collections OperationsWhenever navigation results in a collection of objects, you can use one of the collection operations to manipulate this collection. To indicate the use of one of the predefined collection operations, you place an arrow between the rolename and the operation. When you use an operation defined in the UML model, you use a dot. The size OperationFor R&L, it would be reasonable to require that a loyalty program offers at least one service to its customers. Using the dot notation, you can navigate from the context of a loyalty program through its program partners to the services they deliver. This results in a collection of Service instances. With the arrow notation, you can apply the predefined operation size . The rule would be stated as follows : context LoyaltyProgram inv minServices: partners.deliveredServices->size() >= 1 The select OperationAnother invariant on the R&L model requires that the number of valid cards for every customer must be equal to the number of programs in which the customer participates. This constraint can be stated using the select operation on sets. The select operation takes an OCL expression as parameter. The result of select is a subset of the set on which it is applied, where the parameter expression is true for all elements of the resulting subset. In the following example, the result of select is a subset of cards , where the attribute valid is true: context Customer inv sizesAgree: programs->size() = cards->select( valid = true )->size() The forAll and isEmpty OperationsAlso relevant to the R&L model is a requirement that if none of the services offered in a LoyaltyProgram credits or debits the LoyaltyAccount instances, then these instances are useless and should not be present. We use the forAll operation on the collection of all services to state that all services comply with this condition. The forAll operation, like select , takes an expression as parameter. Its outcome is a boolean: true if the expression evaluates to true for all elements in the collection, and otherwise false. The following invariant states that when the LoyaltyProgram does not offer the possibility to earn or burn points, the members of the LoyaltyProgram do not have LoyaltyAccount s; that is, the collection of LoyaltyAccount s associated with the Membership s must be empty: context LoyaltyProgram inv noAccounts: partners.deliveredServices->forAll( pointsEarned = 0 and pointsBurned = 0 ) implies Membership.account->isEmpty() Note that defining a constraint for a class already implies that the condition holds for all instances of that class. There is no need to write the following: context LoyaltyProgram inv noAccounts: forAll( partners... ) In fact, this is an incorrect expression. The forAll operation is used when we already have a subset of all instances of a class, and we want to check on the elements of that subset. In the preceding example, all services delivered by the partners of a certain LoyaltyProgram are a subset of all instances of class Service . This subset is checked to include only those services for which pointsEarned and pointsBurned are equal to zero. (See Section 3.10.3 for more information on this topic.) The preceding example also introduces two logical operations: and and implies . The and operation is the normal logical and operation on boolean values. The implies operation states that if the first part is true, then the second part must also be true; however, if the first part is not true, it does not matter whether the second part is true; the whole expression is true. The collect OperationA collection operation that is used very often is the collect operation. For instance, it is used when you want to get the set of all values for a certain attribute of all objects in a collection. In fact, many of the examples in this chapter use this operation, because the dot notation is an abbreviation for applying the collect operation. Take, for instance, the following expression in the context of LoyaltyProgram : partners.numberOfCustomers Another way to write it is partners->collect( numberOfCustomers ) It means that for each element in the collection of partners of a loyalty program, the value of the attribute numberOfCustomers is added to a new collection, in this case, containing integer values. The resulting collection is not a subset of the original collection. In fact, in most cases, the type of the elements in the resulting collection is different from the type of the elements in the manipulated collection. In this case, the collection partners contains elements of type ProgramPartner , whereas the collection resulting from applying the collect operation contains elements of type Integer . The collect operation may be used not only to collect attribute values, but also to build a new collection from the objects held by association ends. The next expression, already used in this section, is an example of an implicit use of the collect operation on association ends: partners.deliveredServices Another way to write this expression is partners->collect( deliveredServices ) For each element in the collection of partners of a loyalty program, the value of the association end deliveredServices is added to a new collection, in this case, containing references to instances of the class Service . More Collection OperationsThis section contains more collection operations (the complete list can be found in Chapter 9):
2.4.2 Sets, Bags, OrderedSets, and SequencesWhen working with collections of objects, you should be aware of the difference between a Set , a Bag, an OrderedSet, and a Sequence . In a Set , each element may occur only once. In a Bag , elements may be present more than once. A Sequence is a bag in which the elements are ordered. An OrderedSet is a set in which the elements are ordered. Navigations Resulting in Sets and BagsTo understand why these differences are important, take a look at the attribute numberOfCustomers of class ProgramPartner . We want to state that this attribute holds the number of customers who participate in one or more loyalty programs offered by this program partner. In OCL, this would be expressed as follows: context ProgramPartner inv nrOfParticipants: numberOfCustomers = programs.participants->size() However, there is a problem with this expression. A customer can participate in more than one loyalty program. In other words, a reference to the same object of class Customer could be repeated in the collection program.participants . Therefore, this collection is a bag and not a set. In the preceding expression, these customers are counted twice, which is not what we intended. In OCL, the rule is that when you navigate through more than one association with multiplicity greater than 1, you end up with a bag. That is, when you go from A to more than one B to more than one C, the result is a bag of Cs. When you navigate just one such association you get a set. There are standard operations that transform one of the types into any of the other types. Using one of these operations you can correct the previous invariant as follows: context ProgramPartner inv nrOfParticipants: numberOfCustomers = programs.participants->asSet()->size() When you navigate an association with multiplicity greater than 1 on the target end, and from there navigate an association with multiplicity greater than 1 on the target end, you also end up with a bag. The expression transactions.generatedBy from the context of CustomerCard denotes a bag of instances of Service . Every service may be associated with more than one transaction, so when you take the services of a set of transactions, some services might be present more than once. Navigations Resulting in OrderedSets and SequencesWhen you navigate an association marked ordered , the resulting collection is of type OrderedSet; and following the rules explained above, when you navigate more than one association and one of them is marked ordered, you end up with a sequence. Several standard operations deal with the order of an ordered set or sequence: first, last, and at(index). The only ordered association in the R&L model lies between LoyaltyProgram and ServiceLevel . In the context of LoyaltyProgram, the expression serviceLevel results in an ordered set. We can state that the first element of this ordered set must be named Silver as follows: context LoyaltyProgram inv firstLevel: levels->first().name = 'Silver' |