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
© Copyright 2025 Paperzz