OCL教程一为什么OCL

OCL 教程一 为什么 OCL
Alex.W 编译
OMG 的 OCL(对象约束语言)被视为精确化模型的基础,形式化模型约束的突破口,
MDA 技术乃至下一代软件开发技术的基石,已经得到了越来越多的关注。本系列教程将由浅
入深向读者呈现 OCL 的全貌。
UML 之痛
建模,特别是软件建模,过去曾经被视为一个生产图纸的过程。绝大多数模
型是由许多方框箭头图和一些附随的文本所组成,这样的模型所传达的信息是不够完整 的,
非正式的和不够精确的,甚至有些时候自相矛盾。模型中的许多缺陷都是由于所使用的图形
表达能力有限而造成的。仅仅只有一个图的时候并不能表达一些条件 陈述,而它本应该是
一个完整的规约的一部分。例如,在图一所示的 UML 模型中,类 Flight 和类 Person 之间的
关联表示一次航班的乘客是确定的一组 人,这个一对多的关联在 Person 端是多重关系
(0..*),这意味着乘客的数目是无限的。但是实际上,乘客的数目不可能超过执行这个航
班的飞机上座位 的数目。然而,在图上我们无法表达这个约束。
此主题相关图片如下:
图 1 一个航班顾客模型
在这个例子中,正确的指定多重性的方法是向图中添加如下的 OCL 约束:
context Flight
inv: passengers->size() <= plane.numberOfSeats
使用 OCL 这 样基于数学的、精确的语言写的表达式为图形表达的系统(业务的或者软
件的)模型提供了许多额外的利处,例如,这种表达是不会被不同的角色(比如分析员和程
序员)理解为不同的意思,它们是明确的,并且使得模型更为精确和详细。这些表达式也可
以被自动化工具所检查,以保证它们是正确的,并且与模型中的其它元素 一致,代码生成
变得更加有效。
然而,单纯使用表达式这种表达方式描述的模型常常是难于理解的。例如,尽管源代
码可以被认为是软件最终的模型,但是大多数人在第一次和系统打交道的时候更希望看到一
个图形化的模型,线框箭头图的好处其含义可以很容易理解。
对于软件开发者来说,结合使用 UML 和 OCL 使得两方面相得益彰。大量的不同的图
形和 OCL 表达式可以被结合起来表示模型。注意对于一个完整的模型,图和 OCL 表达式
都是不可缺少的。没有 OCL 表达式,模型可能会不够完善;没有 UML 图,OCL 表达式可
能引用不存在的元素模型——因为 OCL 中没有一种机制来表示类和关联。我们只有结合图
和约束,才能完整地表达一个模型。
OCL 带来的额外价值
你依然怀疑 OCL 为 UML 带来的好处么?图 2 显示了另外一个例子,它包含三个类:
Person、House 和 Mortgage,以及它们之间的关联。任何人看到这个模型都会毫无疑问地设
想肯定要有许多规则被应用到这个模型中,比如:
 一个人能抵押的只能是他(她)自己的房子,而不可能是他邻居或者朋友的。
 抵押的开始日期必须在结束日期之前。
 每个人的社会安全号码必须是不同的。
 只有在一个人的收入充足的情况下才允许新的抵押。
 只有在房子的估价足够的情况下才允许将它作为新的抵押。
这 个图并没有显示这些信息,也没有什么方法可以显示这些信息。如果这些规则没有
被文档化,不同的人可能会有不同的假设,这样将会导致不正确的理解和系统实 现。即使
只是像上面一样把这些规则用自然语言描述出来也是不够的,在精确性方面,自然语言是相
当模糊的,很容易被解释成不同的意思,误解和错误的系统实现 的问题依然存在。
只有通过 OCL 表达式来扩展这个系统,这些“抵押系统”的特有规则才能够完整地
精确地描述出来。OCL 是清晰的,因此这些规则不可能被误解。这些规则可以使用 OCL 描述
如下:
context Mortgage
inv: security.owner = borrower
context Mortgage
inv: startDate < endDate
context Person
inv: Person::allInstances()->isUnique(socSecNr)
context Person::getMortgage(sum : Money, security : House)
pre: self.mortgages.monthlyPayment->sum() <= self.salary * 0.30
context Person::getMortgage(sum : Money, security : House)
pre: security.value >= security.mortgages.principal->sum()
本质上,在模型中包含这些以 OCL 表达式表示的规则有许多原因。就像前面所指出的,当
人们阅读这个模型的时候不会引起误解,错误也因此可以在开发过程的前期就被发现,这时
候修复一个错误的代价是相对低廉的,而且,实现系统的程序员也可以很清楚地理解建立这
个模型的分析员的本意
当模型不是被人来阅读而是作为自动化系统地输入地时候,OCL 的使用变得更加重要
了。我们可以使用工具来生成模拟和测试、来做一致性检查、来使用 MDA 变换产生其它语
言的继承模型,来生成代码等等。如果可能的话,大多数人很乐意把这种类型的工作留给计
算机。
此主题相关图片如下:
图 2 一个抵押关系模型
然而,只有模型本身包含了所有需要的信息,自动化这种工作才有可能。一个计算机化
的工具不可能理解自然语言描述的规则。OCL 写成的规则包含了自动化的 MDA 工具所需
要的必要信息。通过这种方法,系统实现比起手工来做要快速和高效得多,而且可以保证
UML/OCL 模型与生成的工作产品之间的一致性。由此,软件开发过程的成熟度级别得到了
一个整体的提升。
后 记:
这一小节是来自 Klasse Objecten 的一篇译文,正当笔者在为如何深入浅出地介绍
OCL 存在的意义的时候,偶然发现前人已经做了如此优秀的工作,借花献佛也未尝不是一
件好事。笔者原创的后续文章将深入 OCL 语法、解析、代码生成的方方面面。欢迎访问我
们的 MDA 专题论坛(forum.mdachina.net)共同探讨。
Introduction to OCL in Together
By: Dan Massey
Abstract: Increase the precision and communication value of UML models by annotating
them with the Object Constraint Language (OCL). This tutorial provides an
introduction to OCL syntax, grammar, and idioms using new Together OCL
capabilities.�
Dan Massey is an enterprise software architect and a J2EE developer. Formerly a
TogetherSoft mentor, Dan is now the director of the Software Development Center at
Y&L Consulting in San Antonio, Texas.
[email protected]
Introduction to OCL in Together
Dan Massey - Y&L Consulting Inc.
Introduction to OCL
The OCL Specification
1
2
The Object Constraint Language (OCL) is a standard of the Object Management Group
®
(OMG ). OCL 2.0, the version described here, is part of the larger Unified Modeling
3
TM
Language (UML ) 2.0 specification. OCL adds significant expressive power to UML
models. The detail OCL adds to models makes it a key component of OMG's Model Driven
4
®
Architecture (MDA ) initiative. In this paper, I would like to give you some tips
for successfully applying OCL as you start to annotate models.
Improving Models with OCL
In my experience most teams that use UML spend much of their modeling time describing
software structures with class diagrams. Elaboration of models with interaction
diagrams or state diagrams is less common, but extremely useful. At some point during
the process of modeling, however, designers turn to informal text written in notes
on the model diagrams or in separate documents. The OMG has been working to provide
more tools for modelers to formally describe software systems. OCL 2.0 is one of
those tools.
The class diagram in Figure 1 shows a simple retail domain for a LiquidLemons lemonade
stand point of sale (POS) system. The class structure is easy to read and is readily
transformed into skeleton source code by modern modeling tools or by UML literate
developers working in a variety of languages.
Figure 1. Partial Liquid Lemons class diagram
The LiquidLemons domain class diagram is straightforward, but it leaves a
significant amount of information unspecified from the point of view of someone who
might have to implement the design. How is subtotal calcuated? How is tax calculated?
How is total calculated? What conditions are required for a sale? What is the expected
condition of the system after a sale? UML note elements with informal text blocks
could fill in the gaps with ad hoc notations, like the note telling us not to worry
about how part of the system works. Or... we could use OCL to answer all of these
questions in a standard way. Some tools can even translate the OCL into other
languages to partially fill in the skeleton code created from the class diagram.
OCL is a formal language for adding information to UML models. That information can
be divided broadly into two categories: constraints and queries. The query side of
OCL is often a surprise to developers learning OCL. In fact OCL can express any query
that can be expressed in standard SQL. Given that flexibility, OCL can provide a
variety of additions to UML class diagrams and other diagrams in a model. OCL can
be used to describe valid values for properties, pre and post conditions for
operations, calculated property values, and object queries.
Annotating a UML Model
To the typical UML literate software professional, the structure and purpose of the
Liquid Lemons domain described would be relatively clear. Also, there are myriad
business rules that are not shown in the basic class diagram. The rest of this
tutorial will use the Liquid Lemons class diagram as a base for elaboration with
OCL. There are plenty of sources for tutorials on OCL basics. I will touch on some
of the basics in this paper, but I will primarily deal with applying OCL and
constructing UML diagrams with an eye for useful elaboration in OCL.
Model Context
Context is a critical idea when it comes to annotating a single diagram or a larger
model. Every OCL expression must be assigned to an unambiguous context that defines
the base for references within the OCL. Although modeling tools like Together allow
you to graphically select context in a diagram, it is good to know how to declare
contexts textually in OCL. Practically any element in a diagram can be declared as
a context, including Classes, attributes, operations and associations.
Here are some example context statements describing contexts in the Liquid Lemons
POS model.
context Sale
context LineItem::price() : Money
context LiquidLemons::addLemonade(flavor : Flavor, size : DrinkSize)
Notice that operations must be fully qualified including parameter types and return
type.
OCL Expressions
Once you have declared a context, you can start defining constraints within that
context. OCL is a declarative language, more like SQL than imperative languages like
Java or C#. A large number of the OCL expressions you write will be constraints that
ultimately evaluate to a Boolean value. Other expressions are designed to select
single values or collections of objects or values. In support of the declarative
purpose of OCL, it supports a range of mathematical operations, boolean operations
and collection operations.
Valid chunks of OCL (when placed within proper context) include:
2 + 2 = 3
-- results in Boolean false
if 3.max(4) = 4 then
true
else
false
endif
-- results in Boolean true
self.items->size()
-- collection operation resulting in an Integer
this implies that -- Boolean assertion logic
wheels = 4 implies type = VehicleType::Car -- more complex assertion
with enumeration
As you may have guessed, "--" is the way to indicate an end-of-line comment. Hopefully,
you can also see that OCL is a relatively straightforward language for describing
business logic. You should notice that OCL statements don't have side effects. You
can use OCL to define constraints and to arrive at values, but OCL is not a means
for describing actions taken in the system. OCL describes what is required of the
system, but not how those requirements are to be implemented.
OCL Basic Types
OCL is a strongly typed language. Below is a table of OCL's basic types with
equivelant types in three popular languages and an implementation framework. This
is one suggested mapping. For example it may be more appropriate for an application
to use a mapping from OCL's Real to Java's double or BigDecimal types instead of
to float.
OCL Type
Java
C#
Delphi
EMF
*
Boolean boolean bool
Boolean
EBoolean
Integer int
int
Integer
EInt
Real
float
float Double
String
String string WideString EString
EFloat
OclAny
Object object TObject
EObject
OclType Class
Type
TTypeInfo
OclVoid null
null
Nil or Null null
*
®
EClassifier
TM
Borland Enterprise Core Objects (ECO ) and Eclipse Modeling Framework (EMF)
mappings are provided as examples of mapping OCL to a purpose-built model
implementation frameworks.
Invariants and Conditions
Many people first apply OCL following the principles of design by contract. OCL
provides a concise notation for formally specifying contracts that are often left
to implicit definition. These contracts include descriptions of valid values, state
maintainance guarantees, and class and operation responsibilities.
Some Things Never Change
Invariants are constraints that specify valid values for elements of the model. For
the entire lifecycle of the appliation, the invariants should not be violated.
Invariants are used to constrain the range of valid values model elements can take.
Invariants are Boolean expressions that should always evaluate to true in a running
application. In Figure 2 are some example Invariants from the Liquid Lemons diagram
entered in the Invariants window of the Sale class context.
Figure 2. Sales class invariants
It is a good idea to name invariants so that it is easy to refer to them. It's also
a good idea to document invariants (and other OCL expressions) using the language
provided by business users. This provides an amount of just-in-time traceability.
OCL is often easier to read than imperative source code, but it's easier if
stakeholders can see their own words side-by-side with the expressions. Hopefully,
the combination of model context and documentation make the meanings of the above
example invariants clear. Below is an example of the Invariants from Figure 2 with
textual context, names, and documentation.
context Sale
-- a Sale has zero or more items
inv numberOfSaleItems : not items->size() >= 0
-- a Sale's subtotal may not be less than zero
inv subtotalGreaterThanZero : subtotal().amount > 0.0 and
subtotal().currency = Currency::USDollars
The invariants are now named elements of the Sale class. We've added comments that
might match the original customers' descriptions of the business rules. The arrow
"->" notation is used to indicate a collection operation. OCL supports operator
overloading, so currency::Money can support a ">" operation for comparison. For
reasons having to do with OCL's support for collection sorting, the less than
operator ">" is more useful to implement than greater than, although you should
almost always implement all comparison operators if you implement one.
The Befores and Afters
OCL also provides ways to define the pre and post conditions that form the contracts
for operations. Preconditions and post conditions are assertions that must be true
either before or after the body of the context operation executes. Preconditions
and post conditions can guard the state behavior and business validity of an object.
You can also use them to describe the requirements for an operation without imposing
an implementation. In Eiffel, a language with builtin design by contract features,
an operation "requires" preconditions and "ensures" post conditions.
Figure 3. Liquid Lemons class
context LiquidLemons::addLemonade(flavor : Flavor, size : DrinkSize)
pre activeSale : currentSale->size() = 1 -- must have a current sale
pre mustHaveFlavor : not flavor.oclIsUndefined() -- must provide a
flavor
pre mustSpecifySize : not size.oclIsUndefined() -- must specify a size
post newLemonade : currentLemonade->size() = 1 -- added a lemonade
context LiquidLemons::more(ingredient : Ingredient)
pre haveLemonade : not currentLemonade.oclIsUndefined() -- must have a
current lemonade
pre mustSpecifyIngredient : not ingredient.oclIsUndefined()
The above set of preconditions and post conditions for the Liquid Lemons class define
part of the contract for the operations of the class, shown in Figure 3. Again, I
have taken advantage of OCL's ability to name constraints. This is an illustration
of the first tip I offer in using OCL, start applying OCL in the context of system
or module interface boundaries where you get the most benefit from using OCL purely
for documentation.
The preconditions and post conditions defined above are simple, essentially saying
that certain parameters or attributes must have values. It would be great if we could
use programming languages that don't allow nulls, but as long as we're using Java,
C#, or similar languages, get used to the equivelant of "<> null". In the case of
the addLemonade()method, we're saying, "In order to add a lemonade to a sale, you
must have a sale that you started previously. You must also choose a size and a flavor.
Do all that and I'll make sure you have a lemonade to modify." Assuming we have already
started the sale, the system guarantees that we can order a large raspberry lemonade.
If I just order a large lemonade through this method, I would expect it to throw
an exception. If I want to offer a default method for ordering any size of "regular"
lemonade, I would provide a new method addLemonade(size : DrinkSize) : void.
There's another tip. As much as possible allow your interface and types to specify
the contract. Use OCL to fill in the details.
Queries
In addition to syntax for enforcing contracts OCL has the requisite constructs to
define queries against an object model. In this regard OCL is as capable as SQL,
OQL, EJBQL, and XQuery. If you're used to any of these other languages, OCL's
capabilities will be familiar even if the ordering of the constructs are not.
Selecting Objects
Queries are implemented as navigations in the model. A query can be as simple as
naming an association, items for example. That query returns the elements in the
association as a Set, an unordered collection of objects with no duplicates. In the
case of our Liquid Lemons diagram, this would be all of the items in the Sale. When
navigating a single association, the result is always a Set. When traversing more
than a single association, items.modifiers, the result is usually a Bag as the second
association introduces the possibility of duplicates. Traversing associations is
useful but it doesn't rise to the query power promised at the beginning of this
section. Figure 4 introduces a simple analyzer class that the Liquid Lemons CTO might
use to start looking at various aspects of her lemonade business. Actually, the
analyzer class isn't quite that sophisticated, but it's useful for looking at query
examples.
Figure 4. Liquid Lemons Item Analyzer
context ItemAnalyzer::itemsWithModPriceOver(money : Money) :
Set(LineItem)
body: items->select(modsPrice() > money)
There's the arrow notation "->" again. It indicates a collection operation, in this
case the select() operation. The select() operation works very much like a SELECT
in other query languages. The parameter to the operation is a comma separated list
of Boolean expressions evaluated to select the correct elements. In this case, the
query is looking for all members of the items Set where the calculated price of
modifications is higher than the target passed to the query. The references inside
of the select() operation are assumed to be in the context of the individual object
in the collection. Scope works outward from there, money references the parameter
for example.
context ItemAnalyzer
def: itemsWithModPriceGreaterThanBasePrice() : Set(LineItem) =
items->select(modsPrice() > basePrice())
context ItemAnalyzer::basePrices():Bag(Money)
body: items.basePrice()
Above we have two more queries. The first is another select() query that compares
the results of two operations on each item in the items Set. The second might seem
a bit odd. It's a shorthand notation. items is a Set. It does not have a basePrice()
query operation, but ListItemdoes. This query actually executes the basePrice()
operation on every element in items, resulting in a Bag of Money objects. We could
replace this body with the longer version items->collect(basePrice()). The body
keyword is used to associate OCL with an operation. The expression included in the
body must evaluate to a value of a type compatible with the operation's type.
OCL Collection Types
OCL provides a small set of collection types distinguished by whether or not the
elements are ordered (not necessarily sorted) and whether or not they allow
duplicates. The specific semantics and operations supported by the OCL collections
prevent one-to-one mappings to most implementation languages. This table contains
one set of suggested mappings from OCL collections to base elements from example
environments. Translating OCL expressions involving collections into
implementation languages will often require wrapper classes or OCL support libraries.
Where appropriate, the mappings reference interfaces rather than specific
implementation classes. The mappings below include only core elements of each
environment. Third party libraries may provide mapping targets with semantics closer
to those of the OCL collections. As with the basic types, this is only one possible
mapping.
OCL Type
Java
C#
Delphi EMF
*
Bag
Collection IList TList EList
Sequence
List
IList TList EList
Set
Set
IList TList EList
OrderedSet List
IList TList EList
*
®
TM
Borland Enterprise Core Objects (ECO ) and Eclipse Modeling Framework (EMF)
mappings are provided as examples of mapping OCL to a purpose-built model
implementation frameworks.
Advanced Collections Operations
In addition to navigation and selection, OCL supports a wide set of collections
operations to build more sophisticated queries. Earlier we saw how we could use the
size() operator to create a query like the one needed for numberOfItems(). Let's
look at two other queries that take advantage of collections operations. The first
is a simple average calculation that combines two collections operations and an OCL
math expression.
context ItemAnalyzer::averagePrice() : Money
body: items.basePrice()->sum() / items.basePrice()->size()
The size() collections operator is already familiar to us. The sum() collection
operator does what you might expect. Provided that all members of the collection
support compatible plus "+" operators, sum() adds the values together and returns
a single result. In this case we have Money divided by Integer. As long as Money
defines a divide operation that accepts an Integer, $2.50 / 2 = $1.25, then this
query returns an average price for all of the items under analysis. This brings up
another good tip, when appropriate implement the standard operators in your classes.
OCL supports operator overloading for all standard OCL operators. Even if the target
implementation platform or language do not support operator overloading,
transformation tools should be able to translate the OCL infix operator notation
into method invocations in the target platform, ex. sumPrice.divide(size).
Now we'll look at a query that implements a straight forward idea with slightly more
complicated collection operations. We want to see the distinct set of modifier
descriptions associated with the collection of LineItems.
context ItemAnalyzer::uniqueModDescriptions() : Set(String)
body: items.modDescriptions()->flatten()->asSet()
As I mentioned earlier, one difference you might notice between OCL and other query
languages is that you work from what you have to what you want, instead of declaring
the target up front and then declaring the criteria. The ordering in OCL makes it
easy to walk through a query expression like the one above. We can look at each piece
in order to see how we get where we want.
1. items (the Set of ListItems
2. items.modDescriptions() (the Bag of Sets of Strings)
3. items.modDescriptions()->flatten() (the Bag of all Strings)
4. items.modDescriptions()->flatten()->asSet() (the Set of unique Strings)
First the query uses the collect() shorthand to invoke modDescriptions() on each
ListItem. This results in a Bag of all of the Sets returned by those calls. Then
we flatten() the Bag to combine all of the Strings in the top level container. Finally,
we use asSet() to remove any duplicate Strings. Once you get your head around the
way the collection operations work together to produce queries like those above,
it's easy to start pulling in the other operations available to you.
OCL Modularity
Let's get back to the main Liquid Lemons class diagram that started our examples.
Now that we have some feel for how OCL works from a query perspective, we can look
at how we would put the pieces together to declare more business rules in our diagram.
Compositing Queries
If we want to calculate Sale.total(), we could put a lot of code in the total operation,
probably including code copied from other places. The right thing to do, whether
implementing in C#, Scheme, or some other language is to put each chunk of business
logic in a single place and then use those blocks to construct the desired answer.
We can do this in OCL. We'll decompose down from the total() method to see how we
would accomplish this.
-- the total we want is subtotal plus tax
context Sale::total() : Money
body: subtotal() + tax()
-- tax is the subtotal times the local tax rate
context Sale::tax() : Money
body: subtotal() * Tax::localRate -- I made up a Tax class
-- the subtotal is the sum of all LineItem prices
context Sale::subtotal() : Money
body: items.price()->sum()
-- the price of an individual lemonade is the base price plus modifier
prices
context LineItem::price() : Money
body: basePrice() + modsPrice()
-- the modifications price of an individual lemonade is the sum of all
modification prices
context LineItem::modsPrice() : Money
body: modifiers.price()->sum()
Let's stop there for now. What other opportunities are there to composite query
methods in the Liquid Lemons model? Here's another tip that comes from this approach,
Divide the operations in your model into two clear categories, those that return
values and those that change the state of the system. Don't try to mix the functions
in a single operation! If you follow this rule, you are free to combine queries in
any way you want without having to worry about unexpectedly changing the state of
the system. Using OCL as the description language makes it easy to enforce this rule.
If you implement the queries directly in Java, for example, you have all of the tools
of imperative programming almost too close at hand. It looks like we're making a
lot of method calls every time we want to calculate the total, but that doesn't have
to be the case. Remember, OCL is a declarative specification language. We're defining
the rules, the "what," but not the "how." Any transformation tool should generate
code to make sure these methods always return the values defined by the rules. Caching
results in private variables, inlining operations, and other tricks that might
increase performance remain details of the implementation that we don't have to worry
about at this stage.
As an aside, notice what a workhorse that Money class is. OCL, with the collection
operations and operator overloading, should encourage you to follow the common OO
principle of using "lots of little classes" to solve design problems. OCL provides
the structure for employing these little classes as an extended vocabulary for OCL.
As a further aside, if you've read Date's and Darwen's The Third Manifesto this should
be even more appealing.
OCL Policy Objects
Picking up where we left off with the last set of queries, it looks like basePrice()
and the price of an individual Modifier could be much harder. How does the customer
define the business rules for determining base price? It might look like this:
context LineItem::basePrice() : Money
body:
if size = DrinkSize::Small then
if flavor = Flavor::Regular then
Money::newInstance(1.23, Currency::USDollars)
else
Money::newInstance(1.73, Currency::USDollars)
endif
else
if flavor = Flavor::Regular then
Money::newInstance(2.43, Currency::USDollars)
else
Money::newInstance(3.13, Currency::USDollars)
endif
endif
This is starting to look a lot like bad imperative code. It doesn't look like a very
scalable way to maintain business logic in our model. What happens when we add a
medium sized lemonade? This is where we combine OCL with object oriented design to
come up with a better way:
-- let's introduce some classes and specify their bodies
context LargeRegularLemonade::basePrice() : Money
body: Money(2.43, Currency::USDollars)
context SmallFlavoredLemonade::basePrice() : Money
body: Money(1.72, Currency::USDollars)
As shown above, we can use polymorphism to create a much more scalable version of
the rules maintained in OCL. Assume that LargeRegularLemonade and
SmallFlavoredLemonade extend the LineItem class. This moves some of the selection
code into the LiquidLemons class, but there are well known ways to solve that problem
and it is the correct division of responsibility. The top class chooses the LineItem
implementation to pass to the Sale based on the customer selection, but that class
is responsible for determining the price.
The ExtraFlavor class is another example of this way of combining OO design and OCL.
The more(), less(), and no() operations would select the correct modifier class.
Each class would implement its own policy to determine the mod price.
Tic-Tac-Toe Example
At the end of the preconference tutorial associated with this paper, the group
5
engaged in model storming a tic-tac-toe domain. Presented below is part of one
possible domain definition for a simple 3x3 tic-tac-toe game.
Tic-Tac-Toe Diagram
Figure 5. Tic-Tac-Toe Domain
Tic-Tac-Toe Constraints
The tutorial group came up with a number of constraints to annotate the tic-tac-toe
model. After the tutorial, I filled in some other examples to provide a more complete
set of constraints.
/*
*****************************************************************************
*
* Game Expressions
*
**************************************************************************** */
context Game
inv playerOneIsX: players->at(0).side = Piece::X
inv playerTwoIsO: players->at(1).side = Piece::O
-- there should be at least as many Xs as Os
inv XsAndOs: board.pieces()->count(Piece::X) >=
board.pieces()->count(Piece::O)
context Game::currentPlayer
init firstPlayerIsX: players->any(side = Piece::X)
context Game::move(position : Integer)
-- general tests that avoid restating sizes
pre validPosition: position >= 0 and position < board.size
pre noWinner: winner()->isEmpty()
pre notDraw: boardPieces()->includes(Piece::Empty)
post changedCurrentPlayer: currentPlayer = players->at(
(players->indexOf(currentPlayer@pre) +
1).mod(players->size()))
-- simpler test that tries to constrain something similar to
changedCurrentPlayer
post notPreviousPlayer: currentPlayer <> currentPlayer@pre
-- board should enforce this, but we can have the game enforce it as well
post placedCorrectPieceInCorrectPosition:
board.pieceAtPosition(position) = [email protected]
context Game::boardPieces() : Sequence(Piece)
-- using collect shorthand, could also be "board.squares->collect(piece)"
body: board.pieces()
context Game::winner() : Set(Line)
-- the game has a winner
body: lineFinder.findWinningLines(boardPieces())
/*
*****************************************************************************
*
* Board Expressions
*
**************************************************************************** */
context Board::placePiece(position : Integer, token : Piece)
-- the board only accepts a piece in an empty location
pre validPosition: position >= 0 and position < squares->size()
pre squareMustBeEmpty : squares->at(position).piece = Piece::Empty
post pieceIsPlaced : squares->at(position).piece = token
context Board::pieceAtPosition(position : Integer) : Piece
body: squares->at(position).piece
context Board::pieces() : Sequence(Piece)
body: squares->asSequence().piece
context Board::size : Integer
derive: squares->size()
/*
*****************************************************************************
*
* Square Expressions
*
**************************************************************************** */
context Square::piece
init: Piece::Empty
/*
*****************************************************************************
*
* Player Expressions
*
**************************************************************************** */
context Player
inv playerMustHaveSide: side <> Piece::Empty
Tic-Tac-Toe Winning Lines
The above constraints and queries take care of specifying most of the basic mechanics
of enforcing the "business rules" of tic-tac-toe. The challenge of determining who
has won a game of tic-tac-toe was left as an exercise for tutorial attendees. Below
is one possible solution. It uses a literal Set declaration to specify the possible
winning lines on the board. The findWinningLines() operation uses let to define a
local variable limited to the rows which contain three of one non-Empty piece. The
forAll collection operation in the let clause applies a cartesian product to each
nested Set. This construct is a powerful query tool. The actual expression, following
in, uses iteration to collection Line objects based on the winning rows.
/*
*****************************************************************************
*
* LineFinder Expressions
*
**************************************************************************** */
context LineFinder::rows : Set(Set(Integer))
-- all possible winning lines
derive: Set{ Set{0, 3, 6}, Set{1, 4, 7}, Set{2, 5, 8},
Set{0, 1, 2}, Set{3, 4, 5}, Set{6, 7, 8}, Set{0, 4, 8}, Set{6,
4, 2}}
context LineFinder::findWinningLines(board:Sequence(Piece)) : Set(Line)
-- finds all winning lines
-- it's possible to have more than one line, but if the other rules are
enforced
-- all winning lines should be for the same piece
body:
let winners : Set(Set(Integer)) = LineFinder::rows->select(row |
row->forAll(a, b : Integer | board->at(a) = board->at(b)
and board->at(a) <> Piece::Empty)
) in
winners->iterate(row : Set(Integer); lines : Set(Line) = Set{} |
lines->including(LineFinder::newLine(board->at(row->any(true)), row)))
As I wrote above, this is only one possible solution. Other approaches are possible,
including adding responsibility to the Board class or the Square to allow for
emergent discovery of winning lines.
Applying OCL
The first key to applying OCL is to start using it where it makes sense to you. That
probably means starting with design by contract and queries. From there you can start
to describe more business rules attached to your diagrams. You can also move beyond
class diagrams to annotating state diagrams and other UML elements. Remember that
OCL is not a replacement for good OO design. Proper OCL usage depends on good design
to avoid creating declarative spaghetti code. When looking at other tips for applying
OCL, reach out to functional programming, business rules, set theory, the relational
model, and other paradigms that don't necessarily cross your path every day. OCL
is a good tool for exploring other paradigms in your domain models.
References
1. The Object Constraint Language, Second Edition: Getting Your Models Ready
for MDA by Jos Warmer and Anneke Kleppe
®
2. OMG Home Page
®
TM
3. OMG UML Resource Page
4.
®
®
o OMG MDA Home Page
TM
o Model Driven Architecture : Applying MDA to Enterprise Computing by
David S. Frankel
o
MDA Explained: The Model Driven ArchitectureTM: Practice and Promise
by Anneke Kleppe, Jos Warmer and Wim Bast
o MDA Distilled by Stephen J. Mellor, Kendall Scott, Axel Uhl and Dirk
Weise
5. "The Best-Kept Secret?" Scott Ambler, Software Development, October 2004