使用OCL与PlantUML示例的UML全面指南
引言

尽管UML(统一建模语言)在可视化系统的结构方面表现出色,但它缺乏精确表达复杂业务规则和语义的能力。这正是对象约束语言(OCL)发挥作用的地方。OCL充当正式的“粘合剂”,将UML图与严格、无歧义的业务逻辑联系起来。
本指南解释了OCL如何与UML集成,您需要了解的关键概念,以及提供实用的PlantUML示例来可视化这些约束。
1. 协同作用:OCL如何补充UML
将UML视为系统的“骨架”(类、关联、状态),而OCL则是“神经系统”(规定骨架如何行为的规则、条件和逻辑)。
UML中的可视化表示
在标准UML中,OCL约束通常使用注释附加到相关模型元素(类、操作或状态)上表示。
-
构造型:通常,包含OCL的注释会被赋予
{约束}构造型。 -
上下文:每个OCL表达式都以
context关键字开头,声明该规则适用于哪个UML元素。
2. UML中OCL的关键概念
A. 类不变式(inv)
不变式是必须满足的条件,始终为真对于类的每个实例,无论调用什么操作,都必须成立。
关键概念:如果不变式被违反,系统就处于无效状态。

@startuml
skinparam classAttributeIconSize 0
skinparam noteBackgroundColor #FFF9C4
class BankAccount {
- accountNumber: String
- balance: Real
- isOverdraftEnabled: Boolean
+ withdraw(amount: Real)
+ deposit(amount: Real)
}
note right of BankAccount
**类不变式:**
context BankAccount
-- 余额永远不能为负,除非启用透支
inv balanceRule:
self.balance >= 0 或 self.isOverdraftEnabled
-- 透支限额不能超过500
inv overdraftLimit:
self.isOverdraftEnabled 意味着 self.balance >= -500
end note
@enduml
B. 操作契约(pre, post, body)
操作(方法)可以具有约束条件,用于定义其执行前和执行后的行为。
-
前置条件(
pre):必须为真的内容在……之前操作开始前。(调用者的责任)。 -
后置条件(
后置):将会为真的内容在……之后操作完成后。(操作的保证)。 -
主体(
主体):定义查询操作的确切返回值。

@startuml
skinparam classAttributeIconSize 0
skinparam noteBackgroundColor #E8F5E9
class ShoppingCart {
- items: Bag<Product>
- totalAmount: Real
+ checkout(): Receipt
+ getTotal(): Real
}
note right of ShoppingCart
**操作契约:**
-- 前置条件:购物车不能为空
context ShoppingCart::checkout(): Receipt
pre: self.items->notEmpty()
-- 后置条件:购物车被清空,生成收据
post: self.items->isEmpty() and result <> null
-- 主体:查询操作定义
context ShoppingCart::getTotal(): Real
body: self.items->collect(price)->sum()
end note
@enduml
C. 衍生值与初始值(衍生, 初始)
与其存储冗余数据,UML允许将属性标记为{衍生}或{初始},由OCL定义其确切值。

@startuml
skinparam classAttributeIconSize 0
skinparam noteBackgroundColor #E3F2FD
class Employee {
- firstName: String
- birthDate: Date
- {derived} age: Integer
- {initial} isActive: Boolean = true
}
note right of Employee
**派生与初始值:**
-- 动态计算年龄
context Employee::age: Integer
derive: self.birthDate.yearsSinceToday()
-- 创建时的初始状态
context Employee::isActive: Boolean
init: true
end note
@enduml
D. 状态机守卫
在状态机图中,转换可以具有守卫——必须求值为 true 才能发生的布尔表达式。OCL 是编写这些守卫的标准语言。

@startuml
skinparam stateBackgroundColor #F3E5F5
[*] --> Created
Created --> PendingApproval : submit()
state PendingApproval {
}
PendingApproval --> Approved : [total < 10000]
PendingApproval --> Rejected : [total >= 10000]
Approved --> Shipped : ship()
Shipped --> [*]
note right of PendingApproval
**状态不变式:**
context Order
inv: self.status = 'Pending' implies
self.submittedBy->notEmpty()
end note
note right of Approved
**approve() 的后置条件:**
context Order::approve()
post: self.status = 'Approved' and
self.approvedDate <> null
end note
@enduml
3. 综合案例研究:公司模型
让我们将 OCL 应用于一个涉及以下元素的复杂 UML 类图:Employee, Department,以及Project。这展示了 OCL 如何遍历关联并使用集合迭代器(select, forAll, size).

@startuml
skinparam classAttributeIconSize 0
skinparam noteBackgroundColor #FFFDE7
skinparam noteBorderColor #FBC02D
class Employee {
- SSN: String
- firstName: String
- birthDate: Date
- hireDate: Date
- salary: Float
+ age(): Integer
}
class Department {
- number: Integer
- name: String
- locations: Set<String>
- {derived} nbrEmployees: Integer
}
class Project {
- number: Integer
- name: String
- location: String
}
Employee "0..*" -- "1" Department : worksFor > department
Employee "0..*" -- "0..*" Project : worksOn > project
Employee "1" -- "0..1" Department : manages > managedDept
Department "1" -- "0..*" Project : controls > projects
Employee "1" -- "0..*" Employee : supervisor > subordinates
note right of Employee
**复杂不变式与导航:**
context Employee
-- 1. 基本属性规则
inv ageCheck: self.age() >= 18
-- 2. 遍历关联(多重性)
inv maxProjects: self.worksOn->size() <= 4
-- 3. 使用迭代器(forAll)
-- 主管必须在下属之前被雇佣
inv hireOrder: self.subordinates->forAll(
sub | sub.hireDate > self.hireDate
)
-- 4. 跨关联规则
-- 员工只能在所属部门控制的项目上工作
inv projectControl:
self.worksFor.controls->includesAll(self.worksOn)
-- 5. 唯一标识符(键)
inv uniqueSSN: Employee.allInstances()->forAll(e1, e2 |
e1 <> e2 implies e1.SSN <> e2.SSN
)
end note
note bottom of Department
**派生属性:**
context Department::nbrEmployees: Integer
derive: self.worksFor->size()
end note
note right of Project
**项目不变式:**
context Project
-- 项目位置必须是部门位置之一
inv validLocation:
self.controls.locations->includes(self.location)
end note
@enduml
4. UML 中的高级 OCL 技巧
A. 使用let 为了可读性
当OCL表达式变得过于复杂时,使用 let关键字在约束内定义局部变量。这使得UML注释更容易阅读。

@startuml
skinparam classAttributeIconSize 0
class Employee {
- worksOn: Set<Project>
}
note right of Employee
**使用 'let' 处理复杂逻辑:**
context Employee
-- 计算总工时并检查边界
inv workingHours:
let totalHours: Integer =
self.worksOn->collect(hours)->sum()
in totalHours >= 30 and totalHours <= 50
end note
@enduml
B. 在后置条件中处理先前值(@pre)
在定义后置条件时,你通常需要将 新 状态与 旧 状态进行比较。OCL使用 @pre 后缀来访问属性的值,该值恰好是操作开始前的值。

@startuml
skinparam classAttributeIconSize 0
class Company {
- employees: Set<Employee>
+ hireEmployee(p: Employee)
}
note right of Company
**在后置条件中使用 @pre:**
context Company::hireEmployee(p: Employee)
-- 新的员工集合是旧集合加上新的人
post: self.employees = self.employees@pre->including(p)
end note
@enduml
5. UML中OCL的最佳实践
-
为你的约束命名: 始终为你的不变量和条件命名(例如,
inv maxProjects:)。这使得在文档或可追溯性矩阵中引用它们更加容易。 -
有策略地使用注释: 在PlantUML和可视化UML工具中,不要在类框中塞满OCL。将OCL放在相邻的注释中。这能保持图表的整洁和可读性。
-
不要过度约束:仅当规则无法通过UML多重性(例如,
0..1,1..*)或基本类型轻松表达时,才编写OCL。如果规则能够以视觉方式展示,就应使用视觉方式。 -
利用集合迭代器:掌握
->select(),->forAll(),->exists(),以及->collect()。它们是OCL中用于遍历UML关联关系最强大的工具。 -
检查空值/空集合:在遍历可选关联关系(多重性
0..1或0..*)时,始终使用->notEmpty()或->isEmpty()在访问属性之前,以避免未定义的求值错误。














