使用 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. 狀態機守衛
在狀態機圖中,轉移可以具有守衛—必須評估為真的布林表達式,才能使轉移發生。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多重性輕鬆表達的規則時才撰寫OCL(例如,
0..1,1..*)或基本類型。若規則可視覺呈現,請以視覺方式展示。 -
善用集合迭代器:掌握
->select(),->forAll(),->exists(),以及->collect()。它們是用於在OCL中導航UML關聯關係最強大的工具。 -
檢查空值/空集合:在導航可選關聯關係時(多重性
0..1或0..*),請始終使用->notEmpty()或->isEmpty()在存取屬性前,以避免未定義的評估錯誤。














