3.10 Tips and Hints


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:

  • If the invariant restricts the value of an attribute of one class, the class containing the attribute is a clear candidate.

  • If the invariant restricts the value of attributes of more than one class, the classes containing any of the attributes are candidates.

  • If a class can be appointed the responsibility for maintaining the constraint, that class should be the context. (This guideline uses the notion of responsibility-driven design [Wirfs-Brock90].)

  • Any invariant should navigate through the smallest possible number of associations.

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

graphics/03fig18.gif

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:

  • Each invariant becomes less complex and therefore easier to read and write.

  • When you are determining whether an invariant is appropriate, the discussion can focus precisely on the invariant concerned , instead of on the complex invariant as a whole.

  • When you are checking and finding broken constraints in an implementation, it is easier to determine the part that is broken. In general, the simpler the invariant, the more localized the problem.

  • The same arguments hold for pre- and postconditions. When a precondition is broken during execution, the problem can be pinpointed much more effectively when you are using small, separate constraints.

  • Maintaining simpler invariants is easier. If you need to change one condition, then you need change only one small invariant.

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.



Object Constraint Language, The. Getting Your Models Ready for MDA
The Object Constraint Language: Getting Your Models Ready for MDA (2nd Edition)
ISBN: 0321179366
EAN: 2147483647
Year: 2003
Pages: 137

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net