de_DEen_USes_ESfa_IRfr_FRhi_INid_IDjapl_PLpt_PTru_RUvizh_CNzh_TW

基于所提供的讲义材料,本指南涵盖了在UML建模背景下对象约束语言(OCL)的基本概念、语法和实际应用。

Objct Constraint Language (OCL)


1. OCL简介

我们为什么需要OCL?

虽然UML图(如类图)在可视化结构方面非常出色,但它们无法表达规范的所有相关方面。

  • 自然语言具有歧义性。

  • 传统形式语言虽然无歧义,但对于普通系统建模者来说太难阅读和编写。

  • OCL填补了这一空白:它是一种形式语言用于表达精确约束,但仍然易于阅读和编写.

OCL的核心特性

  1. 纯表达式语言:表达式没有副作用。求值OCL表达式会返回一个值,但无法改变系统状态。然而,它可以描述状态变化(例如,在后置条件中)。

  2. 不是编程语言:你无法编写程序逻辑、控制流,或调用会改变状态的过程/操作。

  3. 类型化语言:每个表达式都有类型。合法的表达式必须遵守类型一致性规则。每个UML分类器都是OCL的一种类型。

  4. 瞬时求值:对象的状态在求值OCL表达式期间不能改变。


2. 基本语法和语义

上下文和Self

每个OCL表达式都在特定UML分类器(类、接口等)的上下文中进行求值。

  • 上下文: 定义约束所适用的类。

  • self: 一个保留字,指代正在求值的上下文类的特定实例。

上下文 Person
-- 'self' 指代 Person 的一个特定实例

注释和操作符

  • 注释: 由 表示-- (例如, -- 这是一个注释).

  • 中缀操作符: 您可以使用标准的数学/逻辑操作符(+-=<)。在概念上, a + b 与 a.+(b).

访问属性

使用点号表示法来访问属性或操作。即使操作没有参数,也必须使用括号。

上下文 Person
self.age             -- 访问 'age' 属性
self.stockPrice()    -- 调用 'stockPrice' 操作

3. 类型和集合

基本预定义类型

OCL 包含具有特定操作的标准类型:

  • 布尔值: truefalse(操作:蕴含异或)

  • 整数 / 实数:数字(操作:+-*/绝对值最大值最小值整除取模)

  • 字符串:文本(操作:长度连接子字符串转为整数)

集合

集合是用于存储多个元素的抽象类型。它们通过 -> 符号来访问。

  1. 集合: 数学集合(无重复,无序)。 集合 {1, 2, 5}

  2. 有序集合: 无重复,但按位置排序。 有序集合 {5, 4, 3}

  3. 多重集合: 允许重复元素,无序。 多重集合 {1, 2, 2, 5}

  4. 序列: 允许重复元素,有序。 序列 {1, 2, 5, 10}

    • 注意: 该 .. 表示法创建一个连续整数的序列: 序列 {1..5}.

关键集合操作

  • 通用: size()isEmpty()notEmpty()includes(v)excludes(v)count(v)sum().

  • 迭代器(OCL中至关重要):

    • select(expr): 返回满足条件的元素,其中expr为真。

    • reject(expr): 返回满足条件的元素,其中expr为假。

    • collect(expr): 从所有元素中提取特定属性,形成一个新的集合。

    • forAll(expr): 返回true如果expr对至少一个元素为真。所有元素。

    • exists(expr): 返回true如果expr对至少一个元素为真。至少一个元素。

    • iterate(elem; acc = init | expr): 所有迭代器的通用基础。

迭代器语法变体:

self.employee->select(age > 50)
self.employee->select(p | p.age > 50)
self.employee->select(p: Person | p.age > 50)

4. 导航UML模型

OCL被广泛用作导航语言,用于遍历关联。

标准导航

使用相反的角色名称进行导航。返回类型取决于多重性:

  • 多重性 1 或 0..1: 返回单个对象。

  • 多重性 *: 返回一个 集合 (或 有序集合 如果标记为 {ordered}).

上下文 Company
self.manager.age > 40               -- manager 是 0..1,返回一个 Person 对象
self.employee->notEmpty()           -- employee 是 0..*,返回一个集合

如果缺少角色名称,请使用另一端类的名称的小写形式(例如, self.bank).

关联类

使用关联类的小写名称导航到关联类。

上下文 Person
self.job->size() >= 1  -- 'job' 是 Person 和 Company 之间的关联类

递归关联

当一个关联循环回到同一类时(例如,两个人之间的婚姻),必须使用方括号指定方向 [roleName].

上下文 Person
self.marriage[妻子]->select(m | m.结束 = false)->size() = 1

限定关联

使用方括号中的限定符属性来选择特定对象。

上下文 Bank
self.customer[12345]  -- 返回账户号为12345的特定人员
self.customer         -- 返回所有客户的集合

重新类型化(强制转换)

使用 oclAsType() 来访问子类型或隐藏的超类属性。

self.oclAsType(子).p       -- 访问在子类型中定义的属性 'p'
self.oclAsType(超).p     -- 访问在超类中定义的属性 'p'

5. 编写约束

1. 不变式(inv)

必须始终为真的条件始终 对于类的所有实例都必须为真。

上下文 Company 
inv: self.员工数量 <= 50
inv SME: self.股价() > 0  -- 'SME' 是约束的可选名称

2. 前置和后置条件(prepost)

与操作相关的约束。

  • pre: 必须为真 执行前 执行。

  • post: 必须为真 执行后执行。使用结果作为返回值。

  • @pre:在后置条件中用于引用属性的值操作开始前操作开始前。

上下文 Person::income(): Integer
pre adult: self.age >= 18
post resultOK: result < 5000

上下文 Company::hireEmployee(p: Person)
post: employee = employee@pre->including(p)

3. Body、初始和派生表达式

  • body:定义查询操作的结果(可以是递归的)。

  • init:定义属性的初始值。

  • derive:定义一个派生属性。

上下文 Person::income(): Integer
body: self.job.salary->sum()

上下文 Person::isMarried: Boolean
init: false

上下文 Company::noEmployees: Integer
derive: self.employee->size()

6. 高级OCL特性

Let和Def表达式

  • let:在单个表达式中定义局部变量。

    let totHours: Integer = self.worksOn->collect(hours)->sum() in totHours >= 30
    
  • def:定义与分类器关联的可重用变量或操作。

    def: hasTitle(t: String): Boolean = self.job->exists(title = t)
    

未定义值(三值逻辑)

如果子表达式未定义(例如,导航空关联),则整个表达式未定义。然而,布尔运算符具有短路例外情况:

  • true or <未定义> = true

  • false and <未定义> = 

  • 假意味着<未定义> = 

  • <未定义>意味着真 = 

类特征与预定义对象属性

  • allInstances(): 返回该类所有现有实例的集合。

    Person.allInstances()->size() <= 100
    
  • 对象属性: oclIsTypeOf(类型)oclIsKindOf(类型)oclInState(状态)oclIsNew(用于后置条件)。


7. 综合案例研究:公司模型

为了巩固这些概念,以下是一组应用于公司数据库模型(员工、部门、项目、依赖人)的完整性约束。

基本属性约束

-- 年龄必须大于等于18
上下文 员工 inv: self.age() >= 18

-- 入职日期必须在出生日期之后
上下文 员工 inv: self.hireDate > self.birthDate

-- 管理者开始日期必须在入职日期之后
上下文 员工 inv: self.manages->notEmpty() 意味着 self.manages.startDate > self.hireDate

导航与关系约束

-- 主管必须比员工年长且薪资更高
上下文 员工
inv: self.supervisor->notEmpty() 意味着 self.age() > self.supervisor.age()
inv: self.supervisor->notEmpty() 意味着 self.salary < self.supervisor.salary

-- 主管必须在下属之前被雇佣
上下文 员工
inv: self.subordinates->notEmpty() 意味着 self.subordinates->forall(e | e.hireDate > self.hireDate)

-- 部门经理必须是该部门的员工
上下文 部门
inv: self.worksFor->includes(self.manages.employee)

-- 项目地点必须是部门的地点之一
上下文 项目
inv: self.controls.locations->includes(self.location)

-- 员工只能在由其部门控制的项目上工作
上下文 员工
inv: self.worksFor.controls->includesAll(self.worksOn.project)

集合与迭代约束

-- 社会安全号码必须在所有员工中唯一
上下文 员工
inv: Employee.allInstances->forAll(e1, e2 | e1 <> e2 意味着 e1.SSN <> e2.SSN)

-- 依赖人的姓名和关系必须在每个员工中唯一
上下文 员工
inv: self.dependents->forAll(e1, e2 |
      e1 <> e2 意味着 (e1.name <> e2.name 或 e1.relationship <> e2.relationship))

-- 派生属性:部门中的员工人数
上下文 部门
inv: self.nbrEmployees = self.worksFor->size()

-- 员工最多只能参与4个项目
上下文 员工
inv: self.worksOn->size() <= 4

使用复杂逻辑选择

-- 每周总工作时间必须在30至50小时之间
上下文 员工 
约束: 让 totHours: 整数 = self.worksOn->收集(小时)->总和() 
     在 totHours >= 30 且 totHours <= 50

-- 在特定项目上,最多只能有2名员工工作少于10小时
上下文 项目 
约束: self.worksOn->选择(小时 < 10)->大小() <= 2

-- 只有部门经理才能在项目上工作少于5小时
上下文 员工 
约束: self.worksOn->选择(小时 < 5)->非空() 意味着 
     self.worksFor.manages.employee = self

-- 没有下属的员工必须在所有项目上工作 >= 10 小时
上下文 员工 
约束: self.subordinates->为空() 意味着 self.worksOn->对所有(小时 >= 10)

-- 部门经理必须在部门所控制的所有项目上工作 >= 5 小时
上下文 部门 
约束: self.controls->对所有(p: 项目 | 
      self.manages.employee.worksOn->选择(小时 >= 5)->包含(p))

防止循环(递归逻辑)

-- 员工不能监督自己
上下文 员工 约束: self.subordinates->排除(self)

-- 监督层级不能存在循环
上下文 员工 
def: 所有下属 = self.subordinates->并集(
          self.subordinates->收集(e: 员工 | e.所有下属))
约束: self.所有下属->排除(self)