The Object Constraint Language: Getting Your Models Ready for MDA (2nd Edition)
In this section, we implement the OCL Standard Library in two different parts . First, we define the implementation of the basic types, such as Integer and String . Second, we show how the OCL collection types can be implemented. 4.3.1 OCL Basic Types
The mapping of the predefined basic types and model types is, although not straightforward, relatively easy because its problems are well known. The predefined basic OCL types, and their literals and operations, should be mapped to basic types in the target language. For instance, the Java programming language offers the types float and double , whereas OCL has only one Real type. You need to choose how the Real type is to be implemented by Java code, either by float or by double . Because the basic types of most programming languages are very similar, this mapping will not constitute many problems. Table 4-1 shows the mapping that we use. Table 4-1. Mapping of basic types from OCL to Java
After defining this mapping, we need to map all operations from these OCL types to operations in Java. Because Java does not provide counterparts for all operations defined on the OCL basic types, we need to define a special library that will hold the OCL operations not provided directly in Java. This can be done by defining a library class in which each required operation is defined as a static Java method. The following code shows an example of a library class containing operations that are not available as operations in the standard Java types: class OclIntegerOperations { static public int max(int i1, int i2) { if( i1 > i2 ) { return i1; } else { return i2; } } static public int min(int i1, int i2) { if( i1 < i2 ) { return i1; } else { return i2; } } } Using these definitions, we can translate the following OCL expressions: i1.max (i2) i1.min (i2) The corresponding Java code would be as follows : OclIntegerOperations.max(i1, i2) OclIntegerOperations.min(i1, i2) In fact, the Java library includes a class, java.lang.Math , that defines the min and max operations exactly as described above. Therefore, in this case, we could have used the Math class instead. Mapping the operations defined on every OCL type, such as oclIsTypeOf and oclIsKindOf, is slightly more complicated. Usually, these operations can be mapped on similar constructs defined on the root class in the target language. For instance, in Java, the keyword instanceof can be used to implement oclIsKindOf, and the getClass method of the root class Object can be used to implement oclIsTypeOf. For example, we might define our own class OclAny as follows: class OclAny { static oclIsTypeOf(Object o, String classname) { return o.getClass().getname().equals(classname); } } Using this class, we can translate the following OCL expression: o.oclIsTypeOf(Type) The corresponding Java code would be OclAny.oclIsTypeOf(o, "Type"); Another option is to define your own class for each basic type in OCL. The preceding static operations then become straightforward nonstatic operations of such a class. This solution works along the lines of the Java Integer class, which represents a built-in Java int as a real object. The static operations min and max can then be implemented as follows: class OclInteger extends java.lang.Integer { public OclInteger(int value) { super(value); } public OclInteger max(OclInteger i2) { if( this.intValue() > i2.intValue() ) { return this; } else { return i2; } } public OclInteger min(OclInteger i2) { if( this.intValue() < i2.intValue() ) { return this; } else { return i2; } } } Using these definitions, we can translate the following OCL expression: i1.max (i2) i1.min (i2) The corresponding Java code would be i1.max(i2) i1.min(i2) The disadvantage is that this new class must be used as the type for every integer in the Java code, instead of the built-in Java classes. Many Java libraries and APIs expect standard Java types as their parameters. Wherever this is the case, we have to use the intValue() operation, defined in java.lang.Integer , to get an int out of our OclInteger . In addition, we need to transform each int that we receive ”e.g., as parameter ”to a new OclInteger object. Therefore, this solution is highly unpractical, and we have chosen not to use this approach. 4.3.2 OCL Tuples
Because tuples are types that are defined on the fly, there are no explicit type definitions for tuples in the model. Nor does the Java language need explicit tuple types. You may easily use any implementation of the Map interface from java.util. As an alternative, you can choose to define a separate Java class for each tuple type. This is safer, because it ensures proper type checking in the Java code. Additionally, this option is much faster at runtime, because fetching a field from a tuple can be done directly. Using the Map implementation, a string-based lookup needs to be performed. A disadvantage is the extra coding that needs to be added. 4.3.3 OCL Collection Types
OCL collection types should be mapped to the collections in one of the libraries of the target language; when the target language does not provide collections, we have to build our own. Java provides a large number of different collection types ”for instance, Set, Tree, and List . Choose one for each OCL collection type. It is usually best to stick to this choice for every mapping that needs to be made. That is, always use the same Java class for implementing an OCL Set , another for implementing an OCL Bag , and so on. Because there are no direct counterparts for Bag and OrderedSet , we need to choose a closely matching type to represent these. Table 4-2 shows a possible mapping. OCL collections have a large number of predefined operations. These operations come in two flavors ”the ones that loop over the collection (see Section 9.3), e.g., select, exists , and collect , and the ones that don't, e.g., union and size . Any operation in the first category is called an iterator , or collection iterator . Operations in the latter category are simple collection operations. Both categories need to be implemented differently. Table 4-2. Mapping of collection types from OCL to Java
Simple collection operations
Simple collection operations can be mapped to operations on the target language collection types. When they are not present, you have two options. The first option is to define your own classes to represent the OCL collection types by inheriting from a standard collection type. The remaining OCL operations that cannot be mapped directly can be implemented on these classes. The disadvantage of this approach was already explained in Section 4.3.1. These user -defined classes need to be used as the type for every collection object in the Java code, because the OCL expressions use collection-typed fields in the Java code. For this reason, we have not chosen this option. The approach we have taken in Appendix D is to use the Java collection classes from the standard Java libraries, adding the additional operations required for implementing OCL as static Java methods in a separate library class. In this case, you can use ordinary Java collections anywhere , and you refer to the special static methods only when you evaluate an OCL expression. A simple example of a class containing the static methods is shown in the following code: public class OclCollectionOperations { public static boolean notEmpty(Collection c) { (c == null) (!c.isEmpty() == 0); } } Note that Collection in the preceding code refers to the Java Collection interface (java.util.Collection). Using this class, the following OCL expression can be translated: someCollection->notEmpty() The corresponding Java code would be as follows: OclCollectionOperations.notEmpty(someCollection); Note that the (Java) type of someCollection must implement the Java Collection interface. Collection Iterators
The collection iterators are usually more difficult to implement. Fitting counterparts for the operations that loop over collections cannot be found easily. Preferably, the OCL standard library would be implemented by a library in the target programming language. Unfortunately, this is not possible for most languages, because the implementations of many standard operations that loop over a collection should take a piece of code as a parameter. Only a few programming languages support this. For instance, it would be very straightforward if you could translate the next OCL expression into one single Java expression: context Customer inv : cards->select( valid = true )->size() > 1 The Java expression would need to take the code that represents the valid = true part as a parameter to the select operation: // the following is incorrect Java getCards().select( valid == true ).size() > 1 In Java, the preceding code is illegal, because you cannot use a block of Java code as a parameter to a method. The (Java) definition of the select method would have to look like the following: // the following is an incorrect Java implementation // of operation 'select' on class Set Set select( JavaExpression exp ) { // body of operation // exp should repeatedly be executed here // for each element in the collection } When you work in Smalltalk or any other language that supports expressions as first-class objects, you should take advantage of this aspect of the language to define your own classes that implement the OCL standard library. If you work with a programmming language that does not support expressions as first-class objects, every use of a collection iterator requires a specially designed code fragment. In this chapter, we use the Java language; thus, we are limited in the design choices we can make. Because all collection iterators loop over a collection, the specially designed code fragment uses the looping mechanisms of the target language to implement the loop. In Java, looping is usually implemented using the Iterator class. For instance, the following OCL expression, in which source is a Set , is implemented by a rather complex piece of Java code, using a Java Iterator instance on source : source->select(a=true) The implementing Java code is as follows: Iterator it = source .iterator(); Set result = new HashSet(); while( it.hasNext() ){ ElementType elem = (ElementType) it.next(); if ( elem. a == true ){ result.add(elem); } } return result; In only two places in this code fragment can the OCL expression be recognized. These are shown in boldface. The first is a reference to the collection source ; the second is the test a == true from the body of the select operation. Furthermore, the code fragment needs to explicitly mention the type of elements in the collection source. For example, in the preceding code fragment, the type is called ElementType . When the test of the select iterator, or any of the other iterators, is more complex, it is more difficult to recognize it in the implementing code. For instance, to get all program partners for which all services have no points to be earned, from the context of a LoyaltyProgram , we can use the following OCL query: self.partners->select(deliveredServices->forAll(pointsEarned =0)) This results in the following piece of Java code. The lines are numbered to facilitate further explanation: 1. Iterator it = this. getPartners ().iterator(); 2. Set selectResult = new HashSet(); 3. while( it.hasNext() ){ 4. ProgramPartner p = (ProgramPartner) it.next(); 5. Iterator services = p. getDeliveredServices ().iterator(); 6. boolean forAllresult = true; 7. while( services.hasNext() ){ 8. Service s = (Service) services.next(); 9. forAllResult = forAllResult && (s. getPointsEarned() == 0 ); 10. } 11. if ( forAllResult ){ 12. selectResult.add(p); 13. } 14. } 15. return result; Parts of the original OCL expression can be recognized in the code; for every navigation, the corresponding get operation is called. These calls are printed in boldface in lines 1, 5, and 9. The rest of the code needs some unraveling. Lines 1 and 2 prepare the loop that implements the select operation: a Java iterator and the result set are initialized . Lines 3 through 14 represent the select loop, which embeds the forall loop. The forall loop is initialized in lines 5 and 6, and executed in lines 8 through 10. The test pointsEarned = 0 can be found in line 9, also printed in boldface. The following code illustrates a template for the implementation of collect in Java. This piece of code implements the general expression source->collect(attr) We assume that source is a Set, and attr is of type AttrType : 1. Iterator it = source .iterator(); 2. Set result = new HashSet(); 3. while( it.hasNext() ){ 4. ElementType elem = (ElementType) it.next(); 5. AttrType attr = (AttrType) elem.getAttr(); 6. if ( !( attr instanceof ClassImplementingOclCollection) ) { 7. result.add( attr ); 8. } else { 9. // repeat this template in case attr is a collection 10. } 11. } 12. }return result; Note that because the collect iterator flattens collections (see Section 9.3.10), i.e., the result is never a collection of collections but always a collection of object references or values, we have to determine whether attr itself is a collection. This is implemented in line 6, using the type ClassImplementingOclCollection . If attr is a collection, line 9 needs to be expanded using the same collect template. The template will need to be recursively repeated until elements that are not collections themselves are reached. In order to generate this code, we need to do a careful analysis to determine whether we are dealing with collections of collections, and to which depth the collections are nested. Another approach to this problem is to implement a separate flatten operation in the previously mentioned OclCollectionOperations class. We can then remove the test in line 6. After the while loop, we flatten the entire result collection: 1. Iterator it = source .iterator(); 2. Set result = new HashSet(); 3. while( it.hasNext() ){ 4. ElementType elem = (ElementType) it.next(); 5. AttrType attr = (AttrType) elem.getAttr(); 6. result.add( attr ); 7. } 8. result = OclCollectionOperations.flatten(result); 9. return result; Each iterator in OCL ( select, exists, forAll, collect , and so on) will have its own template, which needs to be filled with the details in the OCL expression to be implemented. These templates can be optimized. For example, the forAll template, as used in one of the preceding examples, can be written in such a way that the loop stops as soon as the result is found to be false. Of course, this means that the Java code will become even lengthier and more difficult to read; therefore, optimization was not used in the example. The examples in this section show that expressions dealing with collections in OCL are more comprehensive than the corresponding Java code. As a result, the OCL expressions are easier to write; more important, they are much easier to read and understand than the Java code. This is desirable because the OCL expressions are part of the software's specification, whereas the Java code is part of the implementation. Specifications should be easier to read and write. When a specification is easy to read and write, and meanwhile it is as precise and unambiguous as the implementing code, it serves as good input to the MDA process. |