Functional Modeling with OCL Contracts - Object-Oriented Analysis and Design for Information Systems: Modeling with UML, OCL, and IFML (2014)

Object-Oriented Analysis and Design for Information Systems: Modeling with UML, OCL, and IFML (2014)

CHAPTER 8

Functional Modeling with OCL Contracts

Commands and queries identified in the system sequence diagram must be implemented. Contracts are high-level specifications for those operations. They may be written in natural language or in a formal language such as OCL. This chapter presents a writing discipline for OCL contracts that avoids ambiguous interpretations and that is relatively easy to read and write. Contracts are composed of preconditions, postconditions, exceptions, and return values. Contracts for CRUDs are presented as well as contracts for other operations found in system sequence diagrams.

Keywords

System operation contract; precondition; postcondition; exception; formal specification; OCL; Object Constraint Language

Key Topics in this Chapter

• Preconditions

• Query return

• Postconditions

• Exceptions

• System operation contracts

8.1 Introduction to functional modeling

Before functional modeling, the analysis process in the Elaboration phase produces two important artifacts:

• The refined conceptual model (Chapters 6 and 7), which represents statically the information managed by the system.

• The expanded use cases or system sequence diagrams (Chapter 5), which show how potential users exchange information with the system, without mentioning how that information is processed internally by the system and without mentioning the interface technology.

When system sequence diagrams are built, the system commands and queries that must be implemented are identified. Each system command and query implies a user intention or goal; for example, the user may be providing information on which books she intends to buy, or she could be asking how much a book costs. That intention is captured by system command contracts and system query contracts, generically referred to as system operation contracts, which incorporate the functional model of the system.

A system command contract may have three sections:

Preconditions (optional): States what is assumed to be true by the command and therefore should not be checked by it.

Postconditions (mandatory): States how the command changes existing information if it is successfully executed.

Exceptions (optional): States what conditions that could prevent the command from succeeding will be checked by it.

On the other hand, a system query contract may have the following sections:

Preconditions (optional): States what is assumed to be true by the query and therefore should not be checked by it.

Results (mandatory): Defines the information that the query returns.

Exceptions (optional): States the conditions that were added by the designer to prevent the system from performing the query in some situations.

Preconditions exist in both types of contracts. They define constraints that are complementary to the ones already existing in the conceptual model. Preconditions are assumed to be true by the operation. This means that preconditions are not tested by the operation that is defined by the contract; they must be tested and assured before the operation is performed.

Postconditions exist only in system command contracts because they specify how the information kept by the system is changed. Special care must be taken to not confuse postconditions with query results. By the Command-Query Separation principle (Meyer, 1988) it is not appropriate for a command to return any result (except in special justified cases). Queries, on the other hand, must return some result, but cannot produce any change in the existing information. Therefore, system queries must have results and system commands must have postconditions.

As opposed to preconditions that must be true before the operation is called, exceptions are situations that usually cannot or should not be tested before; exceptions are tested by the operation itself. Exceptions are events that, if they occur, prevent the operation from succeeding.

Exceptions in command contracts and exceptions in query contracts may have distinct purposes. In command contracts, exceptions are used when arguments or existing information do not satisfy a given business rule (for example, trying to register a customer that is already registered).

However, in query contracts, exceptions have a different meaning. As a query does not change information, there is no invalid argument that could possibly prevent a well-defined query from being performed. If a user queries a system about a customer that does not exist, the query is not prevented from being performed: it would answer that there is no customer for the query; this is the right answer, not an exception. Exceptions in queries would be used only if by any chance the team decides that for a given condition the query must not be performed. Usually, those conditions are related to security issues, or other technological concerns such as multi-window interface, concurrency, communication, etc. As these issues are usually not addressed by functional modeling, exceptions in queries contracts are not frequent.

Normally, in an operation contract, be it a command or query, an exception can be transformed into a precondition and vice versa. The decision on designing an issue as a precondition or exception determines which part of the system is responsible for testing that issue: the calling operation or the called operation. Usually, in large multiuser systems, race conditions1 may lead to choosing to avoid preconditions; however, if the system is not subject to complex concurrency or a concurrency control mechanism that prevents data from changing during transactions exists, then a design based on preconditions may be the better choice.

For the examples in this chapter a partial refinement of the conceptual model presented in former chapters is used (Figure 8.1), as well as a stateless version of the system sequence diagram for use case 01: Order books (Figure 8.2).

image

FIGURE 8.1 Reference conceptual model.

image

FIGURE 8.2 Reference system sequence diagram.

The entimmutableent stereotype used for some attributes and roles in Figure 8.1 is explained in Section 8.7.2.

In the partial conceptual model of Figure 8.1, the concept of a shopping cart was added because a user may start shopping even if she is not yet identified. Thus, the shopping cart has an identification code (because as the stateless strategy is being adopted it must be identified in some way). Also, despite the fact that the shopping cart is not an independent concept, it should be connected to the controller because it may exist without a customer and even without items (a shopping cart may be empty).

In class Book, the attribute quantityInStock could probably be a derived attribute if stock was modeled using the account/transaction pattern (Chapter 7). But here it is kept as a simple attribute.2

The first command in the system sequence diagram creates a shopping cart and returns its identifier. This is an acceptable exception for the Command-Query Separation principle, because the interface needs a reference to the shopping cart in order to send further messages to it. If the stateful strategy was used instead of the stateless strategy, this return would not be necessary because the controller would remember which order was just created.

If the customer is not logged in, a fragment named login is called (which could be specified by a reusable separated sequence diagram). The fragment returns a customer identifier and replaces the two fragments that were mentioned in Figure 5.40: if the user has an account, she may log in; otherwise she may create a new account and log in.

The operations (commands and queries) found in Figure 8.2 are the following (the argument passing shown in the diagram was adapted to a style that is more common in programming languages):

createCart():CartId – This is a command that creates a new empty instance of Cart and assigns an identifier to it. It returns the identifier.

searchBook(keywords:Set[String]):OrderedSet[Tuple] – This is a query that returns an ordered set of tuples that contain information about the books that are relevant to the search. Each tuple has title, author’s name, price, page count, publisher name, isbn, cover image, and quantity in stock for a book.

add2Cart(aCartId:CartId, anIsbn:Isbn, aQuantity:Whole) – This is a command that creates a new instance of Item and links it to the cart and book identified by the parameters.

getCartSummary(aCartId:CartId):OrderedSet[Tuple] – This is a query that returns an ordered set of tuples with information on the cart identified by the cart ID. The information returned, as stated in the note of Figure 8.2, is title, author’s name, quantity, unit price, and total.

finishOrder(aCartId:CartId, aCustomerId:CustomerId) – This is a command that creates a new instance of Order and links it to the customer and the cart identified by the parameters.

8.2 Preconditions

Preconditions establish what must be true when a system query or command is performed. For example, considering the reference conceptual model of Figure 8.1, the operation add2Cart(aCartId:CartId, anIsbn:Isbn, aQuantity:Natural) may assume that the cart ID is valid because it was obtained by the previous operation createCart():CartId. Thus, a precondition that states that this is true could be written:

Context Livir:: add2Cart(aCartId:CartId, anIsbn:Isbn, aQuantity:Natural)

pre:

cart[aCartId]->notEmpty()

The OCL expression above states that in the context of the method add2Cart, which is a system command implemented by the controller Livir, there is a precondition that establishes that there is indeed an instance of Cart that is identified by the argument aCartId.

If the association between Livir and Cart in Figure 8.1 was not qualified, then the precondition could be written by applying select to the set of carts:

Context Livir:: add2Cart(aCartId:CartId, anIsbn:Isbn, aQuantity:Natural)

pre:

cart->select(id=aCartId)->notEmpty()

However, when an association is qualified as in Figure 8.1, a key value should be used to directly access the element mapped by the association. Thus, as the add2Cart command has an argument aCartId, the expression cart[aCartId] produces the same result as the expression cart->select(id=aCartId). Remember that the former expression is also an abbreviation of self.cart->select(aCart|aCart.id=aCartId).

In order to be useful for software development, preconditions must be expressed in a careful way. They must reflect facts that can be identified in the previously developed conceptual model, or the model has to be updated to reflect the new information just discovered. This justifies the use of formal languages such as OCL for writing contracts (Warmer & Keppe, 1998) instead of natural language.

Two families of preconditions may be identified:

Parameter guarantee: Preconditions that assure that the arguments of a command or query correspond to valid objects (for example, “there exists a shopping cart with the ID passed as an argument”).

Complementary constraint: Preconditions that further constrain the conceptual model when performing a given command or query, in order to assure that the information is in a given state when the operation is being executed (for example, “the cart has at least one item”).

8.2.1 Parameter guarantee

There is a systematic way to know if a given operation must have a parameter guarantee precondition (or exception). This technique is also related to the operation’s functional test, as seen in Chapter 13. The team must look at each parameter of the operation and ask if there are valid and invalid values considering the type of the parameter.

For example, if the parameter type is Isbn, and the command is add2Cart(…, anIsbn:Isbn, …), then, considering the business rules, there is a group of valid values (ISBNs that belong to a registered book), and a group of invalid values (ISBNs that are valid but not assigned to any book in the system).

On the other hand, if the type of the parameter is String and the command is add2Cart(…, anIsbn:String, …), then three classes should be considered: one valid (the same as before: ISBNs for books that are registered), and two invalid classes consisting of the ISBNs that are valid but do not belong to any registered books (as before), and incorrectly formed ISBNs (because the type String admits any string, not only ISBNs). That distinction between incorrectly formed and nonexistent ISBNs can be useful to prevent, for example, misunderstandings related to typos when the user is entering ISBN as a search parameter.

Thus, considering the type of the parameter and identifying the groups of invalid parameters, the rule for finding parameter guarantee preconditions is the following:

For each group of invalid values for a system operation parameter, a precondition or exception must be defined.

Regarding parameter guarantee preconditions, special care must be taken not to confuse semantic preconditions with simple syntactic verifications. Semantic verification requires that the database is consulted: for example, verifying if there is a book with a given ISBN. However, syntactic verification may be done by mechanisms other than preconditions. If a parameter has a type Natural, no database query is needed to verify if a given argument belongs to the type or not. Then, if a parameter is declared as x:Natural it is not necessary to write any preconditions to assure that x is a positive number: the type already states that.

Another example is the ISBN, which has a formation rule, and can be checked syntactically. Instead of writing preconditions to verify if a parameter is a well-formed ISBN, the team should simply create a primitive type Isbn with a verification method, and use it.

What about PrimeNumber? Could it be a type? The answer is yes, because in order to know if a given number is a prime number, a syntactic verification is sufficient.

Types must be defined in the command signature. If the type does not exist, a primitive type can be defined by the team. For example, Figure 8.1 presents primitive types such as CustomerId and OrderId that may incorporate not only identification and checksum mechanisms but also security mechanisms, because those identifiers could be encrypted to avoid unauthorized access. In both cases, primitive types could be defined to deal with the complexity of the verification mechanism.

8.2.2 Complementary constraints

A complementary constraint consists of assuring that certain constraints stronger than those of the conceptual model are obtained while a given command is performed. Thus, if the conceptual model states that a given role has multiplicity 0..1, for example, a complementary constraint could just say that when performing a given command that role is filled (1) or not (0). For example, in general a Cart may have an Orderassociated with it or not (0..1). But the command that finishes an Order must have a precondition that states that the Cart was not yet linked to any order (the role from Cart to Order must not be filled: 0).

Thus, a precondition can never contradict the conceptual model. If the conceptual model states 0..1, then no precondition could state a multiplicity of 2 or more for the same role. But if the conceptual model states 0..2, then the precondition can further restrict it, stating, for example, 0..1 or 1..2.

Complementary constraints are common in designs that use the stateful strategy, because in that case each command will need information that was left in transient memory by other commands, such as, for example, the identification of the client, which is passed by one command and used by others. Thus, those other commands would have established as a precondition that the client was adequately identified (possibly by a transient association).

It is possible to identify at least three kinds of complementary constraints:

• A specific statement about an instance or about a set of instances.

• An existential statement about a set of instances.

• A universal statement about a set of instances.

One example of a specific statement about an instance, considering again the model of Figure 8.1, could be to state that a customer with ID 123 has a birthDate equal to July 14th, 1970:

Context Livir::someCommand()

pre:

customer[123].birthDate=Date.getDate(1970,7,14)

As OCL does not define any literals for the Date type, a predefined operation Date.getDate may be used to define a given date.

An example of an existential statement would be to say that there is at least one customer born on July 14th, 1970 (does not matter who):

Context Livir::someCommand()

pre:

customer->exists(aCustomer|

aCustomer.birthDate=Date.getDate(1970,7,14))

An example of a universal statement would be to say that every customer was born on July 14th, 1970:

Customer Livir::someCommand()

pre:

customer->forAll(aCustomer|

aCustomer.birthDate=Date.getDate(1970,7,14))

Both the expressions exists and forAll may be written in simplified form as exists(birthDate=Date.getDate(1970,7,14)) and forAll(birthDate=Date.getDate(1970,7,14)), keeping the same meaning stated above.

8.2.3 Precondition assurance

As the preconditions are not tested by the command that declares them, an external mechanism should exist to assure that they will be guaranteed before the command is called.

This guarantee can be made by explicitly calling a query that verifies if the condition is true before calling the command, or by interface mechanisms that prevent the command from being called with invalid data.

For example, instead of allowing the user to type any string in an ISBN field, the user could be led to select from a list of valid ISBNs. That way, the parameter would be validated before performing the command.

Usually the flow of messages expressed in the system sequence diagram is a way to verify if preconditions are viable instead of exceptions. For example, in Figure 8.2 observe that the cart ID received by the operation add2Cart is valid because it was returned by the command createCart; the ISBN of the book is also valid because it was collected from a list returned by the searchBook query. Finally, the quantity may be accepted as valid because the interface can compare it to the quantity available in stock that was returned with other information about the book.3

8.2.4 Preconditions and exceptions versus invariants

Invariants (Section 6.7) are used in the conceptual model to represent rules that are always valid, independent of any command or query. Preconditions are used for rules that are valid only when a given command or query is being performed.

When an invariant already exists for a given situation, it is equivalent to the existence of an exception for any operation. For example, if there is an invariant that states that a student may enroll only in courses related to her career, then the invariant functions as an exception for any command. Thus, commands for enrolling a student may consider that the exception already exists in the form of the invariant and if the student tries to enroll in a course that does not belong to her career an exception would be raised.

However, an invariant does not work as a precondition. The existence of an invariant does not assure that the design does not violate it. However, a command may add a precondition that states that the invariant must be assured before it is executed.

Test mechanisms must verify if the invariants and preconditions are being respected during the system test activities. If those conditions are violated at any moment, they should raise exceptions. However, in those cases, the designer must revise the design to correct that error so that it will not happen again. When the system is delivered to the final user, it must be guaranteed that no invariant and precondition is violated at any time.

8.3 Transient associations

When the stateful strategy is used in the system sequence diagram, it is necessary for the controller to keep in memory certain information that is not persistent, but that must be stored during the execution of the use case.

Some objects, attributes, or associations that are not persistent may be defined to indicate information that is stored only in main memory during a use case, and discarded after that. Those elements may be stereotyped with enttransientent.

For example, a transient association may be used to indicate that the controller should store information about who is the current customer in the refined conceptual model, as shown in Figure 8.3.

image

FIGURE 8.3 A transient association.

Thus, a precondition of a command, for example, could state that a current customer already exists:

Context Livir::someCommand()

pre:

currentCustomer->notEmpty()

Transient attributes or classes, if needed, may be defined with this stereotype as well. In any case it means that the stereotyped information piece is not persistent.

8.4 Query return

As mentioned before, system commands change data while queries just return data to the user. The contracts for system queries must define what is returned, and this can be done in OCL by using the bodyclause.

Expressions that represent preconditions are all Boolean, but expressions that represent the return of a query may have other types. They can return strings, numbers, lists, tuples, or even more complex structures. The following examples are based on the model of Figure 8.1. Initially, a query is defined that returns the total of a given cart:

Context Livir::getCartTotal(aCartId:CartId):Money

body:

cart[aCartId].total

System queries as well as system commands always have the Controller as their context. Therefore, in the example above, cart is a property of the controller Livir: an association role from it to the class Cart.

The following query returns the name and birthdate of a given customer:

Context

Livir::getCustomerNameBirthDate(aCustomerId:CustomerId):Tuple

body:

Tuple {

name=customer[aCustomerId].name,

birthDate=customer[aCustomerId].birthDate

}

The Tuple constructor is one of the ways to represent DTOs4 in OCL; the tuple works as a Pascal record in the example with two fields: name and birthDate. The values of the fields are defined by the expressions after the “=”.

To avoid repeating expressions like customer[aCustomerId] or even more complex expressions, the def clause can be used in order to define a constant that refers to the original expression. Using the def clause, the contract above would look like the following:

Context

Livir::getCustomerNameBirthDate(aCustomerId:CustomerId):Tuple

def:

aCustomer=customer[aCustomerId]

body:

Tuple {

name=aCustomer.name,

birthDate=aCustomer.birthDate

}

The expression inside the def clause indicates that the identifier aCustomer is referring to the customer whose ID is the parameter aCustomerId received from the query.

The following expression makes a projection in the set of customers returning the names of all customers:

Context Livir::listCustomerNames():Set

body:

customer.name

The next expression applies a filter and a projection, returning the names of all customers that are less than 25 years old:

Context Livir::listYoungCustomers():Set

body:

customer->select(birthDate.addYear(25)>Date.getCurrent()).name

The idea in the example above is to add 25 years to the birthdate of the customer and compare it with the current date: if the customer reaches her 25th birthday after the current date then she is younger than 25 years. OCL also has operations to add days and months to dates: addDay and addMonth, respectively. The expression Date.currentDate returns the system’s current date.

The last example is a query that returns the cart summary, as shown in Figure 8.2.

Context Livir::getCartSummary(aCartId:CartId):Tuple

def: aCart=cart[aCartId]

body:

Tuple {

total=aCart.total,

items=aCart.item->collect(anItem|

Tuple {

title=anItem.book.title,

authorsName=anItem.book.authorsName,

quantity=anItem.quantity,

unitPrice=anItem.unitPrice,

subtotal=anItem.subtotal

}

)

}

The collect expression is a way to obtain a set whose elements are properties or transformations on properties of another set. The dot notation (“.”) itself is an abbreviated form of collect. For example, customer.name is equivalent to customer->collect(aCustomer|aCustomer.name) or customer->collect(name).

When possible, the dot notation is preferred because it is shorter. But in the getCartSummary example, the need to create a tuple instead of accessing a property of the elements of a set prevents the use of the dot, because the Tuple operator is prefixed. Thus, the collect expression has to be explicitly used in that case.

In the example, two tuple structures were built: the outer one contains the total of the cart and the ordered set of its items; the inner one contains for each item the information that is needed as mentioned in Figure 8.2: book title, author’s name, etc.

8.5 Postconditions

Postconditions establish what changes in the information managed by the system after some command is performed. Postconditions also must be carefully specified in terms that can be immediately recognized on the conceptual model structures. Thus, although contracts can be written in natural language, they should be carefully written so they can be immediately translated into OCL expressions.

An OCL post condition is written in the context of a (system) command by using the post clause:

Context Livir::someCommand()

post:

<OCL expression>

If there is more than one postcondition, then they can be combined by the use of the and operator:5

Context Livir::someCommand()

post:

<OCL expression> and

<OCL expression> and

<OCL expression>

In order to classify the types of postconditions that can be used in a contract, we must take into account that the conceptual model has three basic elements, which are concepts, attributes, and associations. Thus, considering that instances of classes can be created and destroyed, association links can be added and removed, and attribute values can be changed, only five types of postconditions are possible:

• Change an attribute value.

• Create a class instance.

• Add an association link.

• Destroy a class instance.

• Remove an association link.

These commands are the most fundamental or basic commands that may be defined for an object-oriented system. They may not be decomposed any further and all other commands are defined as combinations of them.

Thus, a basic command is a command that performs one of the five postconditions listed above. The meaning and behavior of a basic command is predefined.

Unfortunately, programming languages still do not provide a standard implementation for such basic commands. Implementing these commands sometimes is left as a difficult manual task for programmers, especially in the case of commands that add and destroy links.

The basic commands, as defined in the following subsections, are effectively basic in the sense that they do the task without performing certain consistency verifications. For example, a command that adds a link would not verify if the upper bounds for both roles of the association were reached. That kind of consistency verification is preferably made for a whole set of commands belonging to the same contract.

The discussion about keeping objects consistent in contracts is continued in Section 8.5.6.

8.5.1 Changing an attribute value

One of the postcondition types consists of indicating that the value of an attribute was changed. That is usually written in OCL as

object.attribute=value

But, as OCL is a declarative language, this expression must not be understood as an assignment. It has, in fact, three possible ways to become true:

object.attribute may be changed to become equal to value. This is usually the default interpretation.

value may be changed to become equal to object.attribute.

• Both value and object.attribute may be changed to a third value and become equal.

Therefore, in terms of code generation, this kind of expression may produce ambiguous interpretations. Cabot (2007) proposes a default semantic for interpreting such expressions so that ambiguity is reduced. However, another approach is possible, which does not require changing the original semantics of OCL. The idea is to use the “^” (caret) predicate instead of the “=” (equals sign) predicate, as explained below.

We can indicate without ambiguity that an attribute was changed just by declaring that a basic message concerning the change was sent to the object. The basic message is predefined, and its name should start with the prefix “set” followed by the name of the attribute. The new value is passed as an argument.

The message setAttribute may be sent to an instance of a class that contains the attribute. For example, if we are interested in changing the price of a given book, the following OCL expression could be used:

Context Livir::updateBookPrice(anIsbn:Isbn;newPrice:Money)

post:

book[anIsbn].price=newPrice

But we have already seen that it can be interpreted in ambiguous ways. The expression below expresses the same intention but without any ambiguity:

Context Livir::updateBookPrice(anIsbn:Isbn;newPrice:Money)

post:

book[anIsbn]^setPrice(newPrice)

The caret notation means that the message at the right was sent to the object or collection of objects at the left. Thus, it is a Boolean predicate and the result of the expression above is Boolean.

The expression book[anIsbn].price=newPrice means that two values are equal no matter how that result was accomplished. But book[anIsbn]^setPrice(newPrice) states unambiguously that the attribute price of object book[anIsbn] was changed to newPrice.

This form of writing is still considered a declarative specification, not imperative. The expression book[anIsbn]^setPrice(newPrice) just states that an object has received a message; it does not say who sent that message. The decision about which object is going to send that message to book[anIsbn] is left for dynamic modeling (Chapter 9).

8.5.2 Creating an instance

The command that creates an instance must just do that: create a class instance. Although OCL is not an imperative language, it has a constructor to indicate that a new instance of a class was created. This is expressed as a property of an object, and is referenced as follows:

object.oclIsNew()

Its meaning, when used in a postcondition, is that the object was created during the command being specified by the postcondition. This property may be used in conjunction with another that declares the class of the object:

object.isTypeOf(class)

However, as it would be a bit tedious to keep declaring the creation of an instance using two expressions, the definition of a single predicate that states that an object was created as an instance of a given class is suggested. The expression object.isNewInstanceOf(class) is therefore defined as being equivalent to

object.oclIsNew() and object.oclIsTypeOf(class)

Thus, a postcondition that declares that a new instance of Book, referred to as newBook, was created can be defined as

newBook.newInstanceOf(Book)

In this case, the caret notation is not used because it is not a message sent to an instance of Book. It is a declaration that an instance newBook of class Book was created.

The message newInstanceOf does not say anything about the instance’s attributes and links, even the mandatory ones. As before, it can be assumed that other expressions will initialize mandatory attributes and links, and that the new instance’s consistency will be checked for the contract as a whole and not for each individual basic command.

Attributes with initial values are considered automatically initialized when the instance is created. Thus, it would be redundant to specify postconditions to update the attributes with initial values, unless values different from the default are desired.

Derived attributes, on the other hand, are calculated, and cannot be directly modified. Thus, they are not initialized.

Attributes stereotyped with entoptionalent may be kept undefined at creation time; defining a value for them is not mandatory.

But what happens with other (normal and mandatory) attributes at the moment an instance is created? The basic command represented by the predicate newInstanceOf simply produces the instance. It does not initialize attributes and links except for those that are defined with default initial values. If mandatory attributes and links are not initialized the instance may be inconsistent; thus, the contract should specify further postconditions that assure that all mandatory attributes and links are initialized when an object is created.

We have seen so far postconditions to create instances and change attributes. This allows us to combine them to create a contract for a CRUD command that creates a new instance of Book. The system command createBook may have the following as an initial draft of a contract:

Context Livir::createBook(anIsbn:ISBN;

aTitle,anAuthorsName:String; aPrice:Money;

aPageCount:Natural; aCoverImage:Image)

post:

newBook.newInstanceOf(Book) and

newBook^setIsbn(anIsbn) and

newBook^setTitle(aTitle) and

newBook^setAuthorsName(anAuthorsName) and

newBook^setPrice(aPrice) and

newBook^setPageCount(aPageCount) and

newBook^setCoverImage(aCoverImage)

Note that quantityInStock is defined with an initial value and does not need to be initialized in this contract.

The coverImage attribute is an optional attribute and for that reason it is the only parameter that could accept the null value; alternatively, it could be suppressed from the parameter list of createBook.

The publisherName attribute is derived, and as it is read only it cannot be defined in the contract (instead, the derive clause in the class defines its value for every instance). However, its value depends on a link between the book and its publisher and the first version of this contract does not define how that link is added. This is discussed in the next section.

There is one more concern here: if we consider that all information is accessible by navigating links starting at the controller instance, then it would not be advisable to declare that an instance was created if it is not linked to any other instance with a path to the façade-controller. Unless another core search mechanism allows one to find instances that are not linked (directly or not) to the controller, the newly created instance is unreachable. Therefore, the creation of an instance should always happen in conjunction with the adding of a link, which will be explained in the next section.

8.5.3 Adding a link

As mentioned earlier in the chapter, another kind of basic command is one that states that a link was added between two objects. Although link additions are bounded by the multiplicity of the roles, this is only checked for the contract as a whole and not for each individual basic command.

Usually, an OCL postcondition to indicate that a link was created would look like the following:

object.role->includes(anotherObject)

But, again, this expression may be ambiguously interpreted when at the code generation stage, because there are at least four ways for it to become true:

• Include anotherObject in object.role.

• Assign to anotherObject an object that is already in object.role.

• Assign a third object to anotherObject and include it in object.role.

• Assign the empty set to anotherObject, because independently of the contents of object.role it includes the empty set.

Usually, the first option is the intended meaning. But again we suggest a notation that does not cause ambiguous interpretations and at the same time is more familiar to software developers. Once again, the “^” notation and a basic command are used to indicate that a link was created.

There are many dialects for naming commands that modify association roles. Here, we use the prefix add followed by the name of the role. Another common option would be to use the set prefix, as in the case of attributes. However, attributes have their values changed or replaced, while links are added; thus, different goals should have different names.

Considering the association between the Book and Publisher classes, and considering two instances, aBook and aPublisher, respectively, a link between the instances may be created in two different ways. First, from the point of view of a book:

aBook^addPublisher(aPublisher)

Second, from the point of view of a publisher:

aPublisher^addBook(aBook)

Both expressions are symmetric and produce exactly the same result (a link based on the association is created between the instances).

Associations with mandatory roles, such as the one from Book to Publisher, usually require that their links are created in the same contract that creates the object that must have the role filled. Thus, a link from Book to Publisher will be added when an instance of Book is created, as shown below:

Context Livir::createBook(anIsbn:ISBN; aTitle,anAuthorsName:String;

aPrice:Money; aPageCount:Natural; aPublisherName:String; aCoverImage:Image)

post:

newBook.newInstanceOf(Book) and

newBook^setIsbn(anIsbn) and

newBook^setTitle(aTitle) and

newBook^setAuthorsName(anAuthorsName) and

newBook^setPrice(aPrice) and

newBook^setPageCount(aPageCount) and

newBook^setCoverImage(aCoverImage) and

newBook^addPublisher(publisher[aPublisherName]) and

self^addBook(newBook)

In the contract above, two mandatory links were added to the new book: one from it to its publisher, and another from it to the controller Livir. Remember that as Livir is the context of the contract, self stands for a Livir instance.

8.5.4 Destroying an instance

Although destroying objects is rare in object-oriented systems, it may sometimes occur. Usually out-of-print books and deceased customers are not deleted from the records: they are simply marked as inactive or moved to an alternate place. The information must not be lost if trustable records of the company activities in the past are needed.

However, if a publisher or any other class instance was inserted in the system record twice by mistake, the instances may be merged if both have already been changed or, if no activity was registered for the second instance, it may simply be deleted from the records. In this case, an object must be destroyed.

There are two approaches to indicate that an instance was destroyed:

Explicit: It is declared that an object was destroyed by sending an explicit destruction message to it.

Implicit: All associations to the object are removed so that it becomes inaccessible. In programming languages it is possible to implement garbage collection to remove from memory objects that are not accessible anymore.

In this book, the explicit approach is chosen because it makes the modeler’s actual intention clearer. An object that has to be destroyed, then, must receive a basic message such as the following:

object^destroy()

The meaning of this expression in a system command postcondition is that the referred object was destroyed during the execution of the command.

It is assumed that all links to the destroyed object are removed as well. If any object formerly linked to it became inconsistent, then the contract must specify what happens to that object (if it is removed as well or if the role is replaced by a link to another object).

8.5.5 Removing a link

The removal of a link between two objects is made by a basic command prefixed by the word remove,6 followed by the role name and receiving as a parameter the object member of the role that must be removed. For example, for removing the last item of a given cart, the following contract could be used:

Context Livir::removeLastItem(aCartId:CartId)

def: aCart=cart[aCartId]

def: lastItem=aCart.item->last()

post:

aCart^removeItem(lastItem)

When the multiplicity of the destination role of the link to be removed is 1 or 0..1, it is optional to inform the parameter, because there is only one possible link to be removed. If instead of removing the link from the point of view of the cart it was done from the point of view of the item, the final result would be the same, but the basic command would not need a parameter:

Context Livir::removeLastItem(aCartId:CartId)

def: aCart=cart[aCartId]

def: anItem=aCart.item->last()

post:

anItem^removeCart()

However, both contracts above leave the item inconsistent, because it has a mandatory association to Cart that does not exist anymore if the contract succeeds. The contract must consider that as the role from Item to Cart is mandatory (1), the removal of this link implies the destruction of the Item or the linking of it to another Cart, which has to be completed in the same contract.

If the choice is to delete the item, then only that basic command should be used in the contract because with it, all links to the item are considered automatically removed as well:

Context Livir::removeLastItem(aCartId:CartId)

def: aCart=cart[aCartId]

def: lastItem=aCart.item->last()

post:

lastItem^destroy()

Trying to remove a nonexistent link is a design error and cannot be declared in well-formed contracts, which are discussed in the next section.

8.5.6 Well-formed postconditions

If we assume that the basic commands do not check if the objects and links are left in a consistent state after they are performed, then the designer must do the verification when she is specifying the system command contracts.

The verifications that must be done can be summarized as follows:

• A command that creates an instance must also have postconditions stating that all the instance attributes were initialized, except for (1) derived attributes (which are calculated), (2) attributes with an initial value (which are predefined by an init clause, unless a different value is desired), and (3) optional attributes, which can be null (in that case, initialization is optional).

• All mandatory links for a newly created instance must be added.

• Even if it has no links or associations, a newly created instance must be linked to at least one instance that has a link path to the controller or be linked to the controller itself.

• All created links must stay within the lower and upper bounds defined by their association roles.

• All invariants affected by changes in attributes, links, or instances must stay true.

The invariant-checking mechanism does not assure that the designer is writing a contract that keeps all invariants true. It is the responsibility of the designer to be aware of the invariants and avoid raising exceptions. If an invariant happens to raise an exception that is not handled by the system, then there is certainly a design problem.

Another issue involves attributes declared with initial values. If the definition of the initial value of an attribute uses information that may be obtained only by navigating its links, how can it be calculated before the links are created? We must consider that the initialization of attributes with initial values will be one of the last tasks in the programming code that implements a contract, that is, attributes with initial values may be defined only after all links are added and the instance is consistent. For example, the unitPrice of an item in Figure 8.1 may only be initialized after a link between the item and the book is added, because the initial value depends on the link. The contract designer does not need to worry about this because contracts are declarative, but the programmer must be concerned with it when producing code.

8.5.7 Combinations of Postconditions

Usually, a system command will have many postconditions that can be joined by the and operator, as mentioned before. But it is also possible to use the or operator, which establishes that one of the postconditions was obtained, but not necessarily the other:

post:

<postcondition1> or

<postcondition2>

The main problem with this operator is that it is inherently ambiguous in terms of code generation, and thus, its use should be avoided if deterministic expressions are possible.

Another operator that can be used to connect OCL expressions is the implies operator, which has the same meaning of the implication operator of Boolean logic. Implies is used when a postcondition is to be obtained only if a certain condition is true. For example, the following contract adds a book to a cart only if it is not there yet:

Context Livir::add2Cart(aCartId:CartId, anIsbn:Isbn,aQuantity:Natural)

def:aCart=cart[aCartId]

def:aBook=book[anIsbn]

def:anItem=aCart.item->select(book.isbn=anIsbn)

pre:

aCart->notEmpty() and

aBook->notEmpty()

post:

anItem->isEmpty() implies

newItem.isNewInstanceOf(Item) and

aCart^addItem(newItem) and

newItem^setQuantity(aQuantity) and

newItem^addBook(aBook) and

newItem^setUnitPrice(aBook.price)

The implies operator can also be replaced by an if-then-endif expression. This expression is especially useful if an alternate postcondition must be obtained when the condition is false; in this case the form if-then-else-endif should be used instead. In the case of the last example, if the book is already in the order, the quantity ordered may be added to the quantity of the existing item. The contract below shows how to use this operator:

Context Livir::add2Cart(aCartId:CartId, anIsbn:Isbn,aQuantity:Natural)

def:aCart=cart[aCartId]

def:aBook=book[anIsbn]

def:anItem=aCart.item->select(book.isbn=anIsbn)

pre:

aCart->notEmpty() and

aBook->notEmpty()

post:

if anItem->isEmpty() then

newItem.isNewInstanceOf(Item) and

aCart^addItem(newItem) and

newItem^setQuantity(aQuantity) and

newItem^addBook(aBook) and

newItem^setUnitPrice(aBook.price)

else

anItem^setQuantity(anItem.quantity@pre + aQuantity)

endif

The meaning of the @pre operator is explained in the next section.

The advantage of implies is that it is shorter. The advantage of if-then-endif or if-then-else-endif is that it is more familiar to programmers, and includes a way to specify what must be obtained for both cases of a Boolean expression that is shorter than using two implies structures.

8.5.8 Former values

Sometimes a postcondition is built upon values that attributes and roles had before the command is performed. Those former values are referred to by the function @pre to indicate that they do not correspond to the value of the attribute after the command is performed (which could be different). For example, a postcondition that establishes that a quantity in stock for a given book has increased may be expressed as

Context Livir::add2Stock(anIsbn:Isbn; aQuantity:Natural)

def:aBook=book[anIsbn]

post:

aBook^setQuantityInStock(

aBook.quantityInStock@pre + aQuantity

)

So, why was @pre used above? Because after the command is performed, the quantity in stock would already be incremented, and the argument for the setQuantityInStock basic command has to be defined as the previous quantity plus the new quantity.

Remember that OCL is declarative, just as arithmetic is. x=x+1 is not a true statement in mathematical notation, because it cannot be true for any value of x. But when you write x :=x+1 in a programming language it means the same as x=x@pre+1.

The operator @pre may also be used over the links of a given role. For example, to indicate the set of items that were linked to an order before the system command was performed, the following expression could be used:

def:anOrder=…

post:

anOrder.item@pre …

Another example with a more intense use of this operator is an expression that obtains the total value derived from an order before the system command is performed. Not only may the quantities and item prices have changed, but the list of items itself as well. Thus it is necessary to use the @pre operator three times:

def:anOrder=…

post:

anOrder.item@pre->sum(unitPrice@pre*quantity@pre) …

If, on the other hand, the analyst believes that the values of the attributes and links are not changed by the system command, then the @pre expression may be omitted, as in the following example:

def:anOrder= …

post:

anOrder.item->sum(unitPrice*quantity) …

8.5.9 Postconditions covering collections of objects

It is possible with a single OCL expression to state that a whole set of objects was changed. For example, an expression that states that the price of every book was raised by x% may be written using the forAllexpression:

Context Livir::raiseBookPrice(x:Percent)

post:

self.book->forAll(aBook|

aBook^setPrice(aBook.price@pre*(1+x))

)

If there is no need to use the former values in the equation, that is, if every object is going to be updated with the same price, then it is possible to abbreviate this notation. For example, if the price of all books has to be set to x dollars, then the following expression could be used instead:

Context Livir::setBookPrice(x:Money)

post:

self.book->forAll(aBook|

aBook^setPrice(x)

)

In this case, there is an even more abbreviated form:

Context Livir::setBookPrice(x:Money)

post:

book^setPrice(x)

Remember that in our examples, book is a role of an association from the controller to the class Book, and therefore it is equivalent to self.book, that is, it represents the set of all books that are registered in the system. As in the case of the “.” notation, the “^” notation can be used with sets as well.

8.5.10 Postconditions and real-world events

The process of creating contracts is intimately related to the expansion of the use case and the conceptual model. The conceptual model should be used as a reference at every moment because it is the source of information with which assertions can be made about the information.

Contracts should be always written with expressions that can be interpreted in terms of elements of the conceptual model. Thus, assertions like “the books were shipped” would hardly be postconditions because they do not represent information contained in the conceptual model; they describe real-world events. If the team wishes to express this in a contract, they should create a conceptual structure (attribute, role, or concept) to represent it. For example, the fact that the books were shipped could be represented by creating an instance of the Delivery class and linking it to the current instance of Order.

8.6 Exceptions

Exceptions in contracts are fail conditions that the designer cannot or does not wish to avoid before performing the command itself.

Sometimes, conditions identified as exceptions may be converted into preconditions. If it is possible to transform an exception into a precondition, it is generally advisable to do so, because it is preferable to avoid problems as soon as possible. It is preferable to keep the user from performing a mistaken action rather than allowing her to do it and reporting the error later.

OCL does not present a specific clause to manage exceptions, but they can be simulated in contracts by conditional postconditions such as

post:

if <condition> then

<raise exception>

else

<all other post conditions, including other exceptions>

endif

As it would not be easy to represent many exceptions using this nested approach, we use a new stereotyped clause, exception, which is a shorter version of the expression above:

exception:

<condition> implies <raise exception>

Now, for a contract to indicate an exception it is sufficient to identify the condition that produces the exception and name the exception.

A contract just indicates that exceptions may occur, but their handling must be defined only during the design of the interface or application tier, because if an exception is raised by the façade-controller, it should be handled by the tier immediately above: the application or interface tier.

The following example shows a contract with an exception based on Figures 8.1 and 8.2:.

Context Livir::finishOrder(aCartId:CartId, aCustomerId:CustomerId)

def: aCustomer=customer[aCustomerId]

def: aCart=cart[aCartId]

pre:

aCustomer->notEmpty() and

aCart->notEmpty()

post:

newOrder.isNewInstanceOf(Order) and

newOrder^addCart(aCart) and

newOrder^addCustomer(aCustomer) and

newOrder^setDate(Date.getCurrent()) and

newOrder^setNumber(OrderNumberGenerator.getNew())

exception:

aCart.item->isEmpty() implies Exception.throw(‘Cannot finish an empty order’)

Here, a basic throw message is used to signal the exception to a class named Exception, which is not a basic OCL class. When the code for that command is generated, the exception must be signaled to the object that called the system command (possibly an object from the application or interface tier), and we assume none of the postconditions are obtained.

A system command exception should be a condition that cannot be resolved internally by the implementation of the system command, and that requires the execution flow control to be returned to the interface tier so that the user can be asked to try other options.

Internal software exceptions that a system command can handle internally are not raised to the interface; therefore, they are simply not mentioned in contracts: they will be addressed when internal control mechanisms are designed and programmed.

It is also assumed that when an exception is raised the command cannot be executed and none of its postconditions are obtained, because the data must be kept in a consistent state even when an exception occurs.

As mentioned a bit earlier, some exceptions may be converted into preconditions if the designer devises a way to assure the condition before calling the command. Thus, the contract with an exception shown above could be converted into the following:

Context Livir::finishOrder(aCartId:CartId, aCustomerId:CustomerId)

def: aCustomer=customer[aCustomerId]

def: aCart=cart[aCartId]

pre:

aCustomer->notEmpty() and

aCart->notEmpty() and

aCart.item->notEmpty()

post:

newOrder.isNewInstanceOf(Order) and

newOrder^addCart(aCart) and

newOrder^addCustomer(aCustomer) and

newOrder^setDate(Date.getCurrent()) and

newOrder^setNumber(OrderNumberGenerator.getNew())

In this case, the command assumes that the previous operations in the system sequence diagram have checked and assured that at this point the cart is not empty; therefore, the command will not check it again. This prevents the finishOrder command from performing unnecessary verifications when called, and therefore it simplifies its code.

Only conceptual elements (concepts, attributes, and associations) may appear in the OCL expressions of the contracts. These elements are necessarily related to the business rules of the system being analyzed. The exceptions mentioned here must be exceptions related to the business rules, and not exceptions related to hardware or communication problems. Exceptions that may occur in physical storage, communication, or external devices must be addressed by specific mechanisms in the tiers where those elements belong. Usually, users are not even aware of them.

8.7 Pattern contracts for CRUD

The following subsections present models for contracts for typical CRUD commands. There are three system command contracts and one system query contract. The commands and query are performed within the Book class, defined following the model in Figure 8.1. Contracts for CRUD operations are similar and may be considered patterns.

8.7.1 Create contract

The contract for a create command usually should include the instantiation of a new object, initialization of its mandatory attributes, and addition of its mandatory links. As an example, the following is a complete contract for creating a new book:

Context Livir::createBook(anIsbn:ISBN;aTitle,anAuthorsName:String;aPrice:Money;

aPageCount:Natural;aPublisherName:String;aCoverImage:Image)

pre:

publisher[aPublisherName]->notEmpty()

post:

newBook.newInstanceOf(Book) and

newBook^setIsbn(anIsbn) and

newBook^setTitle(aTitle) and

newBook^setAuthorsName(anAuthorsName) and

newBook^setPrice(aPrice) and

newBook^setPageCount(aPageCount) and

newBook^setCoverImage(aCoverImage) and

newBook^addPublisher(publisher[aPublisherName]) and

self^addBook(newBook)

This contract assumes that the publisher’s name was already checked and is valid; otherwise that condition should be an exception instead of a precondition.

As the isbn attribute is already stereotyped with entuniqueent, it is not necessary to establish an exception related to the issue of creating a book with an existing ISBN, because that exception would be raised by the class invariant defined by the entuniqueent stereotype. The designer might want to make it explicit in the contract that such an exception exists; however, it is redundant to declare an exception that is already defined as an invariant, and as redundancy is a possible source of problems it should be avoided.

However, if the team wants to assure that the argument anIsbn passed to the command is really an ISBN that is not registered yet, they can do it by adding a precondition to the contract. This does not invalidate the exception that could be raised by the invariant, but it assures that this specific operation is not going to raise that exception:

Context Livir::createBook(anIsbn:ISBN;aTitle,anAuthorsName:String;aPrice:Money;

aPageCount:Natural;aPublisherName:String;aCoverImage:Image)

pre:

publisher[aPublisherName]->notEmpty() and

book[anIsbn]->isEmpty()

post:

newBook.newInstanceOf(Book) and

newBook^setIsbn(anIsbn) and

newBook^setTitle(aTitle) and

newBook^setAuthorsName(anAuthorsName) and

newBook^setPrice(aPrice) and

newBook^setPageCount(aPageCount) and

newBook^setCoverImage(aCoverImage) and

newBook^addPublisher(publisher[aPublisherName]) and

self^addBook(newBook)

8.7.2 Update contract

The update command usually involves just changing the value of some attributes, although sometimes it may involve adding or removing links. Before defining a contract for updating an object however, the designer must decide which attributes and links are immutable. The entimmutableent stereotype added to some attributes and association roles in Figure 8.1 declares that those properties cannot change after an object is created. It is only meaningful when one begins to consider if objects can change or not, and the time for writing contracts is just the time to take that feature into consideration. In the case of an immutable attribute, its value can be defined when an instance is created, but it cannot change after that. An example is the ISBN of a book. An immutable role refers to a link that is usually mandatory and added when the object is created. The object at that role cannot be changed. For example, an item is linked to a cart when created and cannot change to a different cart after that.

The update command cannot change immutable attributes and links. In the case of a Book in Figure 8.1 only the following attributes may be updated: price, coverImage, and quantityInStock.

The update contract in this case might look like the following:

Context Livir::updateBook(anIsbn:ISBN; aPrice:Money; aCoverImage:Image;

aQuantity: Natural)

def: aBook=book[anIsbn]

pre:

aBook->notEmpty()

post:

aBook^setPrice(aPrice) and

aBook^setCoverImage(aCoverImage) and

aBook^setQuantityInStock(aQuantity)

The ISBN is only used to find the book. But as it is immutable, it cannot be updated. If it could be updated, then two ISBN values should be passed as parameters: the old ISBN used to locate the book, and the new ISBN that would replace the old value.

Remember that unique attributes are not primary keys, as explained in Chapter 6; they are just attributes with values that do not repeat in different instances. They may or may not be immutable depending on the nature of the concept.

8.7.3 Delete contract

The command that deletes an object must consider structural rules of the conceptual model before deciding if an object can or cannot be destroyed.

In the case of Figure 8.1, for example, an instance of Book cannot be deleted without a decision on what happens to possible instances of Item eventually linked to it, because, from the point of view of an Item, a link to a Book is mandatory.

In order to perform deletion without violating any structural rules, one must choose one of the three approaches below:

• Assure by precondition that the object being deleted would not leave other objects inconsistent regarding the lower bound of their associations. With this approach, only books with no associated items may be selected for deletion. This means that only books that were never sold may be deleted.

• Use an exception to abort the delete command if it is attempted for an object that will leave other objects inconsistent. If a user tries to delete book that has items linked to it, an exception would be raised.

• Assure by postcondition that all objects that would be left inconsistent after deletion are deleted as well. In this case, items linked to a book to be deleted would be deleted as well.

The third approach is used when we want to propagate the delete command to every object that has mandatory associations to the object being deleted. This propagation is recursive: deletion will continue until no more objects with mandatory links to objects that were deleted are found.7 This approach may not always be used; there are situations in which propagating deletion is against business rules. In the case of books, for example, deleting items linked to a book that is being deleted would create problems with old orders that are already registered; it is not acceptable. However, deleting items when a cart is being deleted is acceptable, because if a cart is deleted it is expected by business rules that all information related to its items are deleted as well.

A possible contract that uses the precondition approach would look like the following:

Context Livir::deleteBook(anIsbn:ISBN)

pre:

book[anIsbn]->notEmpty() and

book[anIsbn].item->isEmpty()

post:

book[anIsbn]^destroy()

As indicated earlier, the destroy message removes the instance of Book as well as its eventual links, which do not need to be removed one by one. The preconditions assure that the book exists and that the book does not have any items linked to it. If a given book has items, then the command cannot be called.

A possible contract that uses the exception approach would be something like the following:

Context Livir::deleteBook(anIsbn:ISBN)

pre:

book[anIsbn]->notEmpty()

post:

book[anIsbn]^destroy()

exception:

book[anIsbn].item->notEmpty() implies

self^throw(‘The book cannot be deleted because copies of it have already been sold’)

A possible contract using the postcondition approach that propagates deletion to all items linked to a cart would look like the following:

Context Livir::deleteCart(aCartId:CartId)

pre:

cart[aCartId]->notEmpty()

post:

cart[aCartId].item^destroy() and

cart[aCartId]^destroy()

The postcondition above states that besides the cart, every item linked to it is deleted as well. As the Item class does not have associations with mandatory links coming from other classes, the destruction process may stop there (the books linked to the items, for example, are not destroyed). Otherwise it would be necessary to destroy other objects, and that should be established by the contract.

The postcondition approach must be chosen when it is desirable to propagate deletion to all objects that depend on the object that is being deleted. Is it also useful when the composite aggregation is deleted; by definition, all components must be deleted as well, unless previously removed from the composite.

However, as mentioned earlier, there are situations in which this propagation is not desirable. For example, removing a book from the catalog is not possible if there are already sales registered for that book. In this case, the precondition or exception approaches must be chosen. The first requires that a book that cannot be deleted is prevented from having its ISBN passed as an argument to that command. For example, the list of books available for deletion at the interface could contain only those books that could really be deleted. If this assurance is not possible or desirable, the exception approach should be chosen. In this case, the interface lets the user try to delete, and if it is not possible, an exception is raised.

Usually, information systems do not allow information to be deleted. A book that is removed from a catalog, for example, is not deleted, but marked as inactive; if the information about the book is removed, then the information about former sales would be left inconsistent with reality. For implementing this approach, one simple solution is to define a new Boolean active attribute for the class whose members may be inactivated instead of deleted. The default value for this attribute is true, because the object usually is active when it is created. In the example, for the Book class, the solution is shown in Figure 8.4.

image

FIGURE 8.4 A variation of the Book class whose instances cannot be deleted, but instead are marked as inactive.

In this case, a contract to inactivate a book would be

Context Livir::inactivateBook(anIsbn:ISBN)

pre:

book[anIsbn]->notEmpty()

post:

book[anIsbn]^setActive(false)

There is no need to check if the book has items in this case because none will be actually deleted.

Another approach is to move inactive objects to a different space, by for example using a new association to the controller that keeps inactive objects separated from active ones. Figure 8.5 shows an example of this approach for the Book class.

image

FIGURE 8.5 Alternate way to represent inactive books with two associations.

In Figure 8.5 there are two sets of books: (active) book and inactiveBook. The constraint in the Book class assures that a book belongs only to one or the other set.

Still another approach is to use the state design pattern to model the two states of an object, as shown in Figure 8.6. Again, there are two sets of books; the usual set of (active) books may be accessed directly by a derived association while the set of inactive books in the figure must be accessed by consulting the set of books linked to the instances of Inactive (however, nothing prevents the creation of another derived association to access inactive books if desired).

image

FIGURE 8.6 An alternative way to represent inactive objects using the state design pattern.

This model is a bit more complex visually but it avoids the need for a constraint. It also could be transformed into a pattern and a stereotype could be created for it.

8.7.4 Retrieve contract

The simple CRUD retrieve query returns available data about an instance of a given concept that is identified by one of its unique identifiers. These queries do not return data that is not explicitly in the class definition. Queries that return data calculated from one or more objects are reports (use cases stereotyped with entreportent). Retrieve operations do not perform calculations.

A simple retrieve query for the class Book of Figure 8.1 could be defined as follows:

Context Livir::retrieveBook(anIsbn:ISBN):Tuple

body:

Tuple {

isbn=anIsbn,

title=book[anIsbn].title,

authorsName=book[anIsbn].authorsName,

price=book[anIsbn].price,

pageCount=book[anIsbn].pageCount,

publisherName=book[anIsbn].publisherName,

coverImage=book[anIsbn].coverImage,

quantityInStock=book[anIsbn].quantityInStock,

}

The retrieve query returns a tuple or record with the data from the attributes of the object selected by the query parameter.

8.8 Pattern contracts for listing objects

Frequently it is necessary to list objects where one or more attributes are presented. A first example of this kind of contract is listing the title of all books available in the bookstore:

Context Livir::listBook():Set

body:

book.title

As the book role from the controller is a strict set in Figure 8.1, the result of the expression book.title will be a set as well. Thus, if there are two books with the same title, they will be listed just once, even if the books have a different ISBN.

If a list with multiple columns is desired (for example, author and title), then it is necessary to return a collection of tuples:

Context Livir::listBook():Set

body:

book->collect(aBook|

Tuple {

authorsName=aBook.authorsName,

title=aBook.title

}

)

Sometimes, it is necessary to apply a filter to the list, for example returning only the author and title of books that do not have any items linked to them (books that were never sold). In this case, select may be applied to the set before forming the tuples:

Context Livir::listBookNotSold():Set

body:

book->select(

item->isEmpty()

)->collect(aBook|

Tuple {

authorsName=aBook.authorsName,

title=aBook.title

}

)

Most listing queries likely have only these two constructors: select (filter) followed by collect (projection). But more complex queries may be created. In those cases, they do not necessarily fit the list pattern (entlistent) and are considered more complex reports (entreportent).

8.9 Contracts related to use cases

An expanded use case is a chain of commands and queries than will be performed in a given sequence. Usually, each operation will supply information or assure preconditions for other operations. The best approach to write contracts for this sequence of operations is to follow the use case flow that is depicted in the system sequence diagram. In this sequence, the designer must ask:

• What is the goal of each operation?

• What do they produce in terms of information?

• What do they expect their predecessors have produced?

• Which exceptions could happen during execution?

• Can their exceptions be reconfigured as preconditions?

• Do their parameters include groups of values that are invalid?

• Do the operations follow a known pattern?

When answering these questions, the designer will be building contracts that allow the commands and queries to be performed in a consistent way in the context of the use case transaction. If it is necessary to add new queries or commands to the sequence diagram in order to assure certain preconditions, then this is the right moment to do that.

This chapter shows many examples related to the system sequence diagram of Figure 8.2. The reader is encouraged to review that figure and ask the questions above for each operation to understand how they have been answered during the chapter.

8.10 The process so far

Image

Image

8.11 Questions

1. Explain and present an example on how an exception may be transformed into a precondition and vice versa.

2. What is the difference between exceptions in command contracts and in query contracts?

3. What are the five possible postconditions, and what basic commands define each of them? How can they be represented in OCL contracts?

4. Produce the contract for the searchBook query in Figure 8.2.


1For example, after a user orders a book, while the user is closing the order and providing payment, the only available book could be sold to another user. This must be avoided with concurrency control mechanisms, as explained in Chapter 13.

2The type Whole stands for the set {0, 1, 2, 3, …}.

3Remember that race conditions are not yet being considered here and they could invalidate this precondition if concurrency is an issue.

4Data Transfer Objects are a design pattern that indicates that objects passed by the façade-controller to the interface and vice versa cannot be instances of domain classes (those in the conceptual model). They must be pure data, and not domain objects with behavior. This may be implemented with objects that only have attributes and their respective getters and setters (if any), nothing else. The record type in Pascal and the OCL Tuple are examples of DTOs.

5The OCL and operator may be used also to combine preconditions, exceptions, and invariants.

6Some dialects might use unset.

7In fact, in the general case, the lower bound of the role is the information that must be considered. For example, if a role has multiplicity 3..*, than removing a link when only 3 are left makes the object inconsistent.