2511 Design note


UNSW 2511 23T2 note (Design part)

Finished

使用UML的Domain Modelling

领域模型 Domain models

  • Domain models 被用来直观地表示重要的 领域概念(Domain concepts) 和他们之间的 关系
  • Domain models 有助于澄清(clarify)沟通(communicate)重要的特定领域概念, 并在需求收集和设计阶段中使用
  • 领域建模 (domain modelling) 是将相关领域概念表达为一个领域模型
  • 领域模型也通常被称为概念模型(conceptual models)领域对象模型(domain object models)

需求分析 & 领域建模 Requirements Analysis vs Domain modelling

  • 需求分析决定了外部行为 (external behaviour)

    未来的系统特点是什么,谁需要这些特点 (actors)

  • 领域建模决定了 内部行为 (internal behaviour)

    未来系统的元素如何互动以产生外部行为

需求分析和领域建模是 相互依赖(mutually dependent) 的,领域建模支持需求的澄清(clarification of reqs),而需求分析有助于建立模型

What is a domain

  • Domian: 与解决问题有关的知识领域

    Domain expert 领域专家:该领域的专家
    例如:在蛋糕装饰领域, 蛋糕装饰师是领域专家

无处不在的语言 Ubiquitous Language

  • 我们设计中的事物必须代表领域专家心智模型中的真正事物
  • 例如,如果领域专家称某物为 “订单order”, 则在我们的领域模型和我们的最终实现中,应该有一个叫做 订单order 的东西
  • 同样地, 我们的领域模型不应该包含 OrderHelper/OrderManager等

    技术细节不构成领域模型的一部分,因为其不属于设计

名词/动词分析 Noun/Verb analysis

  • 通过寻找需求中的名词和东西来找到领域的泛在语言
  • 名词是领域模型中可能存在的实体动词是可能存在的行为

样例分析

  • 图例: 名词 动词
  • 游客日程安排至少涉及一个甚至几个城市
  • 酒店有各种不同等级房间:标准间和高级间
  • 旅游团是以标准价或高级价预订的,表明了酒店房间等级
  • 旅游的每个城市游客都会被预订到所选等级酒店房间
  • 游客预订的每个房间都有一个到达日期和一个离开日期
  • 酒店名称(例如:墨尔本凯悦酒店)和房间编号都会被识别。
  • 游客可以在他们的旅程预订、取消或更新日程

  • Tourists have schedules that involve at least one and possibly several cities
  • Hotels have a variety of rooms of different grades: standard and premium
  • Tours are booked at either a standard or premium rate, indicating the grade of hotel room
  • In each city of their tour, a tourist is booked into a hotel room of the chosen grade
  • Each room booking made by a tourist has an arrival date and a departure date
  • Hotels are identified by a name (e.g. Melbourne Hyatt) and rooms by a number
  • Tourists may book, cancel or update schedules in their tour

UML 类图

Dependency 依赖

Dependency是最松散的关系形式, 即一个类在某种程度上依赖于另一个类

Association 关联

一个类以某种方式“使用” 一个类, 当没有箭头指向时,表达的意图为不清楚依赖关系发生在什么方向上

Directed Association 指向关联

通过指出哪个类对另一个类的了解来完善关联性

Aggregation 聚合

一个类包含另一个类(例如: 课程包含学生), 注意: 钻石型标记应该与包含的类放在一起

构成 Composition

类似于聚合,但是包含的类与被包含的类是一体的,被包含的类不能处于容器之外 (e.g. 椅子和椅子腿)

UML Diagram Types


在UML中表示类 Representing classes in UML

在UML中表示关联关系 Representing Association in UML

关联(Association) 可以建议一个”has-a“关系的模型-> 一个类包含另一个类
关联可进一步被定义为:

  • 聚合关系 Aggregation (空心钻石符号): 被包含的项是一个集合中的元素,但是其也可以单独存在, 例如大学中的讲师/学生
  • 构成关系 Composition (实心钻石符号): 被包含的项是包含项目的一个组成部分, 例如椅子和椅子腿

注: 1..* : 代表可以存在一个或多个

属性与类 Attribute vs Classes

  • 最常见的疑惑:应该是属性还是一个类?

    在创建domain model时,经常需要决定是把某个东西作为 属性Attribute 还是 概念类Conceptual Class 表示

  • 如果一个概念不能用数字或字符串表示,则它很可能是一个类
  • 例如:

    lab mark 可以用数字表示,所以应该把他列为 Attribute
    Student 不可以用数字/字符串表示,所以应该把它列为 class

契约式设计 Design by Contract

防御性编程 vs 契约式设计 Defensive Programming vs Design by contract

防御性编程 Defensive programming

试图解决不可预见的情况, 以确保软件元素的持续功能。 例如,尽管有意外的输入或用户行为,但防御性编程仍够能使软件以可预测的方式运行

  • 通常需要用于 高可用性(high availability)/安全(safety)/ 保障(security) 的地方
  • 会导致多余的检查(redundant checks)(客户端和供应端都可能进行检查),让 软件维护(complex software) 更为复杂
  • 因为没有明确的责任划分 (no clear demaration of responsibilities), 所以很难定位错误位置

契约式设计 Design by contract

在设计时,责任被明确地划分clear assigned给不同的软件元素并被明确记录下来,并在开发过程中使用单元测试(unit testing)/语言支持来执行

  • 职责的明确划分有助于防止多余的检查,从而使代码更简单/易于维护
  • 如果不满足所要求的条件,则程序会崩溃. 可能不适用于高可用性应用(not be suitable for high availability applications)

契约式设计 Design by contract (DbC)

  • 契约设计(DbC) 起源于形式化规范(formal specification), 形式化验证 (formal verification)和 Hoare逻辑方面的工作
  • E.G.:
  • 每个软件元素都应该定义一个规范(或契约), 以管理它与其他软件组件的互动(interacion), 一个契约应该解决以下三个问题:

    前提条件 Pre-condition 契约期望什么

    如果前提条件是真的,就可以避免处理前提条件之外的情况
    例如:预期的参数值为 (mark >= 0 and mark <= 100)

    后置条件 Post-condition 契约保证什么

    只要满足前提条件,就能保证有返回值
    例如:正确的返回值代表一个分数

    不变条件 Invariant 契约保持什么

    在执行前和执行后(例如方法的执行),一些值需要满足约束条件
    例如: 分数的值保持在0-100之间

契约(包括pre-condition/post-condition/invariant)应该是:

  • 声明性的(declarative),不能包括实施的细节
  • 尽可能做到 精确(precise)/正式(formal)可验证(verifiable)

契约式设计的优点 Benefits of Design by Contract (DbC)

  • 不需要为不满足前提条件的条件做错误检查
  • 防止了多余的验证任务
  • 鉴于已经满足前提条件,客户可以期待指定的后置条件得到实现
  • 职责划分明确,有助于定位错误,简易代码维护
  • 有助于 更清洁/快速(cleaner and faster) 的开发

契约式设计的实施问题 Implementation issues

  • 有一些编程语言(例如 Eifferl) 提供对于契约式设计的本地支持(native support)
  • JAVA本地并不支持契约式设计,但是有库可以支持契约式设计
  • 在语言没有本地支持的情况下,单元测试(unit tests) 被用来测试契约是否满足(即 前提条件 pre-condition/后置条件 post-condition/不变量 invariants)
  • 通常情况下, 前提条件/后置条件/不变量 都 包含在文件/注释 (documentation)
  • 如上所述,契约应该是:

    声明性的,不能包括实施细节
    尽可能做到 精确percise/正式formal/可检查 verifiable

Java中的契约式设计样例:

1
2
3
4
5
6
7
8
/**
@param value to calculate square root
@returns sqrt - square root of the value
@pre value >= 0
@post value = sqrt * sqrt
*/

public double squareRoot(double value);
1
2
3
4
5
6
7
/**
@param amount to be deposited into the account
@pre amount > 0
@post balance = old balance + amount
*/

public double deposite(double amount);
1
2
3
4
5
/**
@invarient age >= 0
*/

public class Student {};

前提条件 Pre-Conditions

  • 前提条件是一个前提条件或一个谓语,它必须在执行代码的某个部分之前始终为真
  • 如果前提条件被违反了,则这部分代码的效果就会变得 **不确定(undefined)**,从而可能不按预期执行工作
  • 由于不正确的预设条件,可能会出现安全问题
  • 通常,先决条件包括在受影响的代码部分的注释中
  • 前提条件有时会在代码本身中使用 防护措施(guards) 或用 断言(assertions) 进行测试,有部分语言有特定的语法结构来测试
  • 在契约设计中,一个软件元素可以假设前提条件得到满足,从而可以去除多余的错误检查代码

样例 Examples

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* @pre (mark >= 0) and (mark <= 100)
* @param mark
*/
public void printGradeDBC(double mark) {
if (mark < 50) {
System.out.println("Fail");
}
else {
System.out.println("Pass");
}
}
// 如果*mark*不在期望的区间内就会有不正确的结果

契约式设计(design by contract):没有针对前提条件的额外错误检查

1
2
3
4
5
6
7
8
9
10
/**
* Get Student at i'th position
* @pre i < number_of_students
* @param i -> student's position
* @return student at i'th position
*/
public Student getStudentDbC(int i) {
return students.get(i);
}
// Throws runtime exception if (i >= number_of_students)

防御性编程(defensive programming):对预设条件进行额外的错误检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* @pre (mark >= 0) and (mark <= 100)
* @param mark
*/
public void printGradeDBC(double mark) {
if((mark < 0) || (mark > 100)) {
System.out.println("Error");
}
if (mark < 50) {
System.out.println("Fail");
}
else {
System.out.println("Pass");
}
}

继承中的前置条件 Pre-conditions in Inheritance

  • 继承的方法的实现或重定义(方法覆盖/method overriding)必须遵守该方法的继承契约(inherited contract)
  • 前提条件可以在子类中被弱化(放宽),但是必须遵守继承契约
  • 一个实现或重新定义可以减少一个方法的义务,但是不能增加它

后置条件 Post-Conditions

  • 后置条件是一个条件或一个谓语,在执行完某段代码后必须保持为真
  • 任何代码段的后置条件都是在代码段执行完成后保证的属性声明
  • 通常,后置条件也会放在受影响的代码部分的文档中
  • 后置条件有时会在代码本身中使用 防护措施(guards)断言(assertions) 进行测试,在某些语言中有特定的语法结构用于测试
  • 在契约设计中,只要软件的元素在前置条件为真的情况下被调用,则后置条件所声明的属性就会得到保证

继承中的后置条件 Post-Conditions in Inheritance

  • 继承的方法的实现或重定义(方法覆盖/method overriding)必须遵守该方法的继承契约(inherited contract)
  • 后置条件可以在子类中 得到加强(更多限制), 而且必须遵守继承契约
  • 一个实现或重新定义可以增加一个方法的利益(benefits),但是不能减少它
  • 例如:

    原始契约要求返回一个集合 set
    重新定义的继承而来的方法返回排序后的集合,为方法提供更多的好处

类的不变量 Class Invariant

  • 类的不变量约束了其存储在对象(object)中的状态(即某些变量的值)
  • 类的不变量是在构造过程中建立的,并在调用公共方法(public method)之间不断维护。 类的方法必须确保类的不变性得到满足/保留
  • 在一个方法内,只要在公共方法结束前恢复了不变性,则方法内部的代码就可以破坏其不变性
  • 类的不变量帮助程序员依赖有效的状态,避免了数据不准确(inaccurate)/无效(invalid)的风险,也有助于在测试中定位错误

继承中的类的不变量 Class invariants in Inheritance

  • 类的不变性是继承的,即:

    一个类的所有父类的不变量/不变性都适用于该类本身

  • 一个子类可以访问父类的实现数据,但是必须始终满足所有父类不变量的不变性-> 防止进入无效的状态

继承与契约式设计的总结

  • PreCondition 可以放宽,不能变得更严 (loosed)
  • Post Condition 可以变的更严(更多限制)
  • Invariant 会保持继承,即父类的不变量也适用于子类

设计原则 Design principles

设计异味 Design smell

  • 是不良设计的症状
  • 往往是由于违反了关键的设计原则
  • 应该在软件层面上重构(refractoring)来消除异味

设计异味的类型

  • Rigidity

    即使是简单的更改,软件也有太难更改的倾向
    一次改动会导致其他依赖模块的连环改动

  • Fragility

    当对一处位置进行更改时,软件有在多处发生故障的倾向

  • Rigidity 和 Fragility相辅相成:在需要新功能或变化时,力求将影响降到最低

  • Immobility

    设计难以重复使用
    设计中有一部分可以用于其它系统,但是拆分系统所需的工作量和风险太大

  • Viscosity

    软件粘性(software viscosity): 通过‘黑客’而非‘保留设计的方法’ 更容易实现变更
    环境粘性(environment viscosity):开发环境缓慢且效率低下

  • Opacity

    模块具有难以理解的倾向
    代码必须编写的清楚易懂

  • Needless complexity

    包含目前无用的构造
    开发超前于需求

  • Needless repetition

    设计包含重复的结构,这些结构可能能被统一简化到一个抽象概念中
    在重复单元中发现的错误必须在每次重复中修复

优秀设计的特点 Characteristics of good design

好软件的目标是构建一个具有 loose coupling(松散耦合) and high cohesion(高度内聚) 的系统。从而让软件可以实现:

  • 可扩展 Extensible
  • 可重用 Reusable
  • 可维护 Maintainable
  • 可理解 Understandable
  • 可测试 Testable

耦合 Coupling

  • 定义为组件或类之间的 相互依赖程度
  • 当一个 组件A 依赖于另一个 组件B 的内部运作,并受到 组件B 内部变化的影响时,就会出现 高度耦合(high coupling)
  • 高度耦合会导致一个复杂的系统,其难以维护和拓展
  • 应以 松散耦合(loosly coupled) 的类为目标-> 允许组件之间独立使用和修改
  • 零耦合(zero coupled) 的类是不可用的

聚合 Cohesion

聚合代表 组件/类/模块中的所有元素 作为一个功能单元协同工作的程度

高度聚合的模块应该是:
更易于维护,更改频率更低, 可重复使用性更高

不要把所有的责任都推给一个类来避免松散聚合(low cohesion)

设计原则 和 SOLID

SOLID:

  • 单一责任原则 (Single Responsibility principle): 一个类应该只承担一个责任
  • 开-闭原则 (Open-closed principle): 软件应该对扩展开放而对更改封闭
  • 里氏替换原则 (Liskov substitution principle): 程序中的对象应该可以使用其子类型的实例替换,且不会改变程序的正确性

    如果一个类(或子类)被用作其超类的替代物,那么它应该能够完全替代超类,而不会引入不一致、不合理或不稳定的行为。

  • 接口隔离原则 (Interface segregation principle): 多个专用接口要比一个通用接口更好
  • 依赖倒置原则 (Dependency inversion principle): 应该依赖于 抽象(abstraction) 而不是 具体(concretion)

    高层模块(高级模块或策略)不应该直接依赖于低层模块(底层模块或具体实现)。
    两者都应该依赖于抽象,也就是说,高层模块和低层模块都应该基于共同的接口或抽象类。
    通过使用接口或抽象类,可以减少模块之间的耦合,提高灵活性和可替代性。

何时使用设计原则

  • 设计原则帮助消除设计异味
  • 但是当不存在设计异味时不应使用设计原则
  • 无条件遵守设计原则
  • 过度遵从原则会导致设计异味-> needless complexity

设计原则其一: 最小知识原则/Demeter定律 principle of least knowledge/ Law of Demeter

原则为:只和邻近的类产生交流

  • 类应该和尽可能少的类发生交互
  • 将类之间的交互减少到几个亲密的”朋友“ -> 直接关联的类/本地的对象(local objects)
  • 便于设计 loosely coupled 系统 -> 对系统某一部分的更改不会连带影响系统的其他部分
  • 通过一系列规则限制交互

类中的方法只能调用以下的方法:

  1. 这个类本身的
  2. 作为参数传递到这个方法中的类的
  3. 方法中实例化的对象的
  4. 任何组件对象的
  5. 不使用 返回方法中的对象

禁止以下方法的调用:
o.get(name).get(thing).remove(node)

规则1

对象O 中的 方法M 可以调用 对象O 本身的任何其他方法

1
2
3
4
5
6
7
8
9
public class M {
public void methodM() {
this.methodN();
}
public void methodN() {
// do something
}
}
// methodM() 调用了相同类中的 methodN()

规则2

对象O 中的 方法M 可以调用传递给 方法M 的参数中的任何方法

1
2
3
4
5
6
7
8
9
10
11
12
public class O {
public void M(Friend f) {
// Invoking a method on a parameter passed to the method is
// legal
f.N();
}
}
public class Friend {
public void N() {
// do something
}
}

规则3

当 方法M 实例化了 对象O,则 方法M 可以调用 对象O 中的 方法N

1
2
3
4
5
6
7
8
9
10
11
12
13
public class O {
public void M() {
Friend f = new Friend();
// Invoking a method on an object created within the
// method is legal
f.N();
}
}
public class Friend {
public void N() {
// do something
}
}

规则4

对象O 中的任何 方法M 都可以调用作为 对象O 的直接组成部分的任何对象的任何方法
-> 一个类的方法可以调用其实例变量的类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class O {
public Friend instanceVar = new Friend();
public void M4() {
// Any method can access the methods of the friend class
// F through the instance variable "instanceVar"
instanceVar.N();
}
}
public class Friend {
public void N() {
// do something
}
}

设计原则其二:里氏替换原则 LSP

如果一个类(或子类)被用作其超类的替代物,那么它应该能够完全替代超类,而不会引入不一致、不合理或不稳定的行为。/ 子类必须可以替代其超类存在

不使用继承的设计 Solve the problem without inheritance

除继承之外,还有其他的方法:

  • 委托 Delegation -> 将功能委托给另一个类
  • 组合 Composition -> 通过组合使用一个或多个类来重复行为

如果更多使用委托/组合而不是继承的话, 软件会更灵活/易于维护和拓展

方法重写规则 Rules of method overriding

  • 参数应与被override的方法的参数完全相同
  • 访问级别的限制不能超过被override的方法的访问级别

    例如,如果超类的方法为 public,则子类中的覆盖方法就不能是privateprotected

  • 声明为 final 的方法不能被 override
  • 构造函数不能被 override

静态方法和重写 static method and override

  • 可以在子类中定义具有相同签名的静态方法

    这本质上不是覆盖(override),因为静态方法不存在运行时的多态性
    子类中的静态方法会隐藏超类的方法
    当在子类中定义一个与超类中的静态方法具有相同签名的静态方法时,它实际上是在子类中创建了一个全新的静态方法,而不会影响或替换超类中的静态方法。

返回值和重写 return value and override

  • 重写的方法中的返回类型应该与超类中定义的返回类型相同属于超类中返回类型的子类
  • 重写的方法中的返回类型可以比父类的返回类型更窄
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AnimalShelter {
public Animal getAnimalForAdoption() {
return null;
}
public void putAnimal(Animal someAnimal) {

}
}

public class CatShelter extends AnimalShelter {
@Override
public Cat getAnimalForAdoption() {
// return a narrower type than parent
return new Cat();
}
}

参数和重写 arguments and override

当重写的方法中被传入的参数更宽泛/不同时,java会认为这是两个不同的方法,而并没有override父类中的方法

重构 Refactoring

重构的定义为:重组Restructuring(改变软件内部结构)软件,使其更易于理解easier to understand和使修改成本更低cheaper to modify,而不改变其外部可观察到的行为的过程

应当重构的情况:

注意:当想要添加新功能时发现代码结构对添加新功能产生阻碍,应该先重构代码然后再添加新结构

  1. 添加函数时
  2. 需要修复bug时
  3. 进行代码回顾时

常见的代码异味 common bad code smells

  1. 重复代码 Duplicated code

    同一代码结构出现在多个位置
    两个同级类中出现相同的表达式

  2. 过长的方法 Long Method
  3. 过大的类 Large class

    当一个类是土佐太多事情时,往往会表现为具有太多实例变量(instance variables)

  4. 过长的参数列表 Long parameter list
  5. 分歧变化 Divergent change

    通常发生在 一个类因为不同原因而出现不同变化时

  6. Shotgun Surgery

    通常发生在 需要对许多不同的类进行许多细小的更改

重构方法 Refractoring techniques

方法1: 提取方法 Extract Method

  1. 查找逻辑代码块,并使用提取方法Extract Method

    其中以switch代码块最为明显

  2. 查找方法中的本地变量
  3. 分辨变化和不变的局部变量
  4. 不变的变量可以作为参数被传入方法
  5. 任何被修改的变量都需要更加小心,如果只有一个变量,可以简单地返回

方法2: 重命名变量 Rename variable

变量名应当清楚易懂

方法3: 移动方法 Move method

通常来说,方法应当存在于其使用数据的对象上

方法4: 用查询替换临时变量 Replace Temp with Query

  • 一种删除不必要的局部变量和临时变量的方法
  • 在过长的方法中,临时变量会变得尤其隐蔽,很难跟踪到他们被用来做了什么
  • 有时需要付出性能的代价

方法5: 用多态性取代条件逻辑 Replacing conditional logic with Polymorphism

  • 在某些情况下,可以用继承等特性取代if逻辑

Extract Method & Move Method 使用的OO 准测

  1. 通过 封装(Encapsulation)委托(Delegation) 使代码可以被重复使用

    封装可以保护数据
    封装可以保护行为-> 当将行为从类中分离出来时,可以更改行为而无需改变类
    委托: 一个对象将操作转发给另一个对象,由另一个对象代表第一个对象执行的行为

重构(II)

软件维护 software maintenance

  • 软件系统随着时间的推移不断发展,以满足新的要求和功能
  • 软件维护包括:

    修正bug
    提升性能
    改进设计
    添加新功能

  • 大部分的软件维护都注重于后三点
  • 维护代码比从头开始编写更难
  • 大部分开发时间用于维护
  • 良好的设计、编码和规划可以减少维护的痛苦和时间
  • 避免代码气味,减少维护痛苦和时间

代码异味与可能的迹象 Code Smells: Possible Indicators

  1. Duplicate code
  2. Poor abstraction(change one place -> should change others)
  3. large loop/ method/ class/ parameter list/ deeply nested loop
  4. class with low cohesion
  5. modules with high coupling
  6. Class has poor encapsulation 封装性差
  7. A subclass doesn’t use majority of inherited functions
  8. A “data class” has little functionality
  9. Dead code
  10. Design is unnecessarily general 设计过于笼统
  11. Design is too specific

低级重构 low-level refactoring

  1. 命名

    对方法/变量重命名
    命名(提取)”神奇 “常量

  2. 程序

    将代码提取到方法中
    将常用功能(包括重复代码) 提取到类/方法中
    更改方法签名

  3. 重新排序

    将一个方法拆分成多个小方法从而提高内聚性和可读性
    将语义上属于同一语句的语句放在一起

高级重构 high-level refractoring

  1. 使用设计模式 design pattern
  2. 改变语言习语(安全,简洁)

未知 unknown

  • 存在两种困难问题

    易于理解但难于解决
    易于解决但是难于理解

  • 存在两种未知

    已知的未知 (known unknowns):知道它的存在但是不知道它是什么
    未知的未知 (unknown unknowns): 甚至没有想到需要考虑的未知

并行与并发 Parallelism & Concurrency

  1. 并行 Parallelism:同时进行计算
  2. 并发 COncurrency: 管理并行的方法

风险的概率和影响 Risk of probablity and impact

  1. 降低概率 Mitigations of probability
    • 降低不良后果发生几率的预防措施
    • 例如:过马路前要朝两边看
  2. 减轻影响 Mitigations of impact
    • 减少不良后果发生严重程度的反应性措施
    • 例如:戴自行车头盔
  • 上述通常有专有名词 质量保证 Quality Assurance

Finished