The Object Constraint Language: Getting Your Models Ready for MDA (2nd Edition)
This section provides some tips and hints about how to write meaningful OCL expressions. 3.10.1 Avoid Complex Navigation Expressions
Using OCL, we can write long and complex expressions that navigate through the complete object model. We could write all invariants on a class model starting from only one context, but that does not mean that it is good practice to do so. Any navigation that traverses the whole class model creates a coupling between the objects involved. An essential aspect of object orientation is encapsulation. Using a long navigation makes details of distant objects known to the object from which we started the navigation. If possible, we would like to limit the object's knowledge to only its direct surroundings, which are the properties of the type, as described in Section 8.1. Another argument against complex navigation expressions is that writing, reading, and understanding invariants becomes very difficult. It is hard to find the appropriate invariants for a specific class, and maintaining the invariants when the model changes becomes a nightmare. Consider the following expression, which specifies that a Membership does not have a loyaltyAccount if you cannot earn points in the program: context Membership inv noEarnings: programs.partners.deliveredServices-> forAll(pointsEarned = 0) implies account->isEmpty() Instead of navigating such a long way, we might want to split this constraint. We define a new attribute isSaving for LoyaltyProgram . This attribute is true if points can be earned in the program: context LoyaltyProgram def : isSaving : Boolean = partners.deliveredServices->forAll(pointsEarned = 0) The invariant for Membership can use the new attribute, rather than navigate through the model. The new invariant looks much simpler: context Membership inv noEarnings: programs.isSaving implies account->isEmpty() 3.10.2 Choose Context Wisely
By definition, invariants apply to a type, so it is important to attach an invariant to the right type. There are no strict rules that can be applied in all circumstances, but the following guidelines will help:
Sometimes it is a good exercise to describe the same invariant using different classes as context. The constraint that is the easiest to read and write is the best one to use. Attaching an invariant to the wrong context makes it more difficult to specify and more difficult to maintain. As an example, let's write an invariant in several ways. The invariant written for the diagram shown in Figure 3-18 states the following: two persons who are married to each other are not allowed to work at the same company. This can be expressed as follows , taking Person as the contextual object: context Person inv : wife.employers->intersection(self.employers)->isEmpty() and husband.employers->intersection(self.employers)->isEmpty() Figure 3-18. Persons working for Companies
This constraint states that there is no company in the set of employers of the wife or husband of the person that is also in the set of employers of the person. The constraint can also be written in the context of Company , which creates a simpler expression: context Company inv : employees.wife->intersection(self.employees)->isEmpty() In this example, the object responsible for maintaining the requirement will probably be the Company . Therefore, Company is the best candidate context for attaching the invariant. 3.10.3 Avoid allInstances
The allInstances operation is a predefined operation on any modeling element that results in the set of all instances of the modeling element and all its subtypes in the system. An invariant that is attached to a class always applies to all instances of the class. Therefore, you can often use a simple expression as invariant instead of using the allInstances predefined operation. For example, the following two invariants on class Person (which is not depicted) are equivalent, but the first is preferred: context Person inv : parents->size <= 2 context Person inv : Person.allInstances->forAll(p p. parents->size <= 2) The use of allInstances is discouraged, because it makes the invariant more complex. As you can see from the example, it hides the actual invariant. Another, more important, reason is that in most systems, apart from database systems, it is difficult to find all instances of a class. Unless an explicit tracking device keeps a record of all instances of a certain class as they are created and deleted, there is no way to find them. Thus, there is no way to implement the invariant using a programming language equivalent of the allInstances operation. In database systems, the allInstances operation can be used for types that represent a database table. In that case, the operation will result in the set of objects representing all records in the table. 3.10.4 Split and Constraints
Constraints are used during modeling, and they should be as easy to read and write as possible. People tend to write long constraints. For example, all invariants on a class can be expressed in one large invariant, or all preconditions on an operation can be written as one constraint. In general, it is much better to split a complicated constraint into several separate constraints. It is possible to split an invariant at most and operations. For example, you can write an invariant for ProgramPartner as follows: context LoyaltyProgram inv : partners.deliveredServices->forAll(pointsEarned = 0) and Membership.card->forAll(goodThru = Date.fromYMD(2000,1,1)) and participants->forAll(age() > 55) This invariant is completely valid and useful, but you can rewrite it as three separate invariants, making it easier to read: context LoyaltyProgram inv : partners.deliveredServices->forAll(pointsEarned = 0) inv : Membership.card->forAll(goodThru = Date::fromYMD(2000,1,1)) inv : participants->forAll(age() > 55) The advantages of spliting invariants are considerable:
3.10.5 Use the collect Shorthand
The shorthand for the collect operation on collections, as defined in Section 9.3.11, has been developed to streamline the process of reading navigations through the class model. You can read from left to right without being distracted by the collect keyword. We recommend that you use this shorthand whenever possible, as shown in the following: context Person inv : self.parents.brothers.children->notEmpty() This is much easier to read than context Person inv : self.parents->collect(brothers) ->collect(children)->notEmpty() Both invariants are identical, but the first one is easier to understand. 3.10.6 Always Name Association Ends
In case of multiple associations between the same classes, naming the association ends is mandatory. However, even when it is not mandatory, naming association ends is good practice. An exception can be made in the case of directed or non-navigable associations, for which only the ends that are navigable need to be named. The name of an association end, like the name of an attribute, indicates the purpose of that element for the object holding the association. Furthermore, naming association ends is helpful during the implementation, because the best name for the attribute (or class member) that represents the association is already determined. |