系列文章目录
哈工大软件构造课程知识点总结(一)
哈工大软件构造课程知识点总结(二)
哈工大软件构造课程知识点总结(三)
哈工大软件构造课程知识点总结(四)
哈工大软件构造课程知识点总结(五)
哈工大软件构造课程知识点总结(六)
简介
此文章是2021春哈工大软件构造课程Chapter 9 ~ Chapter 10的知识点总结。
Chapter 9:Construction for Reuse
什么是复用
定义:使用现有软件组件实现或更新软件的过程。
面向复用编程开发出可复用的软件,基于复用编程利用已有的可复用软件搭建应用系统。
复用的层次:
- 源代码级别的复用:白盒/黑盒复用
- 模块级别的复用:类/抽象类/接口
- 库级别的复用:API/包
- 系统级别的复用:框架(一组具体类、抽象类、及其之间的连接关系)
白盒复用:源代码可见,可修改和扩展(复制已有代码到正在开发的系统,进行修改)
- 优点:可定制化程度高
- 缺点:对其修改增加了软件的复杂度,且需要对其内部的充分了解
黑盒复用:源代码不可见,不能修改(只能通过API接口来使用,无法修改代码)
- 优点:简单清晰
- 缺点:适应性略差
复用的优点:
- 降低成本和开发时间
- 经过充分的测试,可靠、稳定
- 标准化,在不同应用中保持一致
复用的缺点:
- 开发成本高于一般软件——要求较高的适应性
- 性能略差——缺少足够的针对性
复用的机会和场合越多、代价越小则可复用性越好。
Liskov替换原则(LSP)
子类型多态:客户端可用统一的方式处理不同类型的对象。
Liskov替换原则内容
- 编译器静态检查的原则:
- 子类可以增加方法,但不可删
- 子类需要实现父类中的所有未实现方法
- 子类中重写的方法其返回值必须与父类相同或是父类返回值的子类型(协变)
- 子类中重写的方法其参数必须与父类相同
- 子类中重写的方法不能抛出额外的异常(协变)
- 对于特定方法(编译器不检查的原则):
- 更强的不变量(原始的不变量不要改变,可以对新增加的属性加入不变量)
- 更弱的前置条件
- 更强的后置条件
协变示例:
逆变示例:
泛型中的LSP
类型擦除:将泛型中的类型变量使用它们的边界(若有边界)或Object
(若无边界)替换,从而使生成的字节码只包含普通的类、接口和方法。
注意:虽然Integer
是Number
的子类,但Box<Integer>
不是Box<Number>
的子类!
带通配符泛型的父子关系示例:
委托(Delegation)和组合(Composition)
委托
委托:一个对象请求另一个对象的功能,是复用的一种常见形式。
一个简单的委托示例:
public class Edge {
Vertex s, t;
double weight;
...
}
public class EdgeComparator implements Comparator<Edge>{
@Override public int compare(Edge o1, Edge o2) {
if(.. > ..) return 1;
else if (.. < ..) return -1;
else return 0;
}
}
public void sort(List<Edge> edges) {
Comparator comp = new EdgeComparator();
Collections.sort(edges, comp); //把比较的功能委托给一个类
}
如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来调用所需的部分方法,从而避免大量无用的方法。
组合及CRP原则
利用委托的机制,将功能的具体实现与调用分离,在实现中又通过接口的继承树实现功能的不同实现方法,而在调用类中只需要创建具体的子类型然后调用即可。组合就是多个不同方面的委托的结合。如下图:
委托的类型:
- 依赖(Dependency):临时的委托
- 关联(Association):永久性的委托
- 组合(Composition):比关联更强,但难以变化
- 聚合(Aggregation):更弱的关联,可动态变化
这几种类型均支持一对多的委托。
具体示例:
/* Dependency 依赖 */
class Duck {
//没有保存Flyable对象的属性
void fly(Flyable f) {
f.fly();
}
}
/* Association 关联 */
class Duck {
Flyable f = new CannotFly();
void Duck(Flyable f) {
this.f = f; // 构建对象时确定
}
void Duck() {
f = new FlyWithWings();
}
void fly() { f.fly(); }
}
/* Composition 组合 */
class Duck {
Flyable f = new FlyWithWings(); // 直接在这里确定
void fly() {
f.fly();
}
}
/* Aggregation 聚合 */
class Duck {
Flyable f;
void Duck(Flyable f) {
this.f = f;
}
void setFlyBehavior(f) {
this.f = f; // 对象创建后可以修改
}
void fly() { f.fly();}
}
设计系统级可复用API库和框架
- 白盒框架:通过继承和重写实现功能的扩展,通常的设计模式是模板模式。白盒框架所执行的是框架所写好的代码,只有通过override其方法来实现新的功能,客户端启动的的是第三方开发者派生的子类型。
- 黑盒框架:通过实现接口来扩展,使用委托/组合通常的设计模式是策略模式和观察者模式。
Chapter 10:Construction for Change
软件的维护与演化
了解即可,此处略去(对应课件P4 ~ P15)。
可维护性
可维护性、可扩展性、可适应性、可管理性、支持性
一些可维护性的指标
圈复杂度、代码行数、可维护性指数、继承层次数、类之间的耦合度、单元测试覆盖度……
关于可维护性的一些问题
- 设计结构是否足够简单?
- 模块之间是否松散耦合?
- 模块内部是否高度聚合?
- 是否使用了非常深的继承树,是否使用了delegation替代继承?
- 代码的圈复杂度是否太高?
- 是否存在重复代码?
模块化编程
要求:
- 高内聚,低耦合
- 分离关注点
- 信息隐藏
内聚(cohension):模块内部功能的联系
耦合(coupling):模块间的依赖关系
评估模块化的标准:
- 可分解性
- 可组合性
- 可理解性
- 可持续性——发生变化时受影响范围最小
- 出现异常之后的保护——出现异常后受影响范围最小
设计原则:
- 直接映射
- 尽可能少的接口
- 尽可能小的接口
- 显式接口
- 信息隐藏
SOLID设计原则
- 单一责任原则(SRP)
- 开放-封闭原则(OCP)
- Liskov替换原则(LSP)
- 接口聚合原则(ISP)
- 依赖转置原则(DIP)
单一责任原则(SRP)
要求:不应该有多于1个原因让你的ADT发生变化,否则就应拆分开。
一个反例:
开放/封闭原则(OCP)
- 对扩展性的开放:模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化。
- 对修改的封闭:模块自身的代码是不应被修改,拓展模块行为的一般途径是修改模块的内部实现,如果一个模块不能被修改,那么它通常被认为是具有固定的行为。
关键的解决方案:抽象技术
典型反例是使用大量的复杂if-else
(或switch-case
)结构,这样很难维护。
应对方法是利用接口传递不同子类型执行特殊操作,而ADT中仅进行通用操作。
Liskov替换原则(LSP)
子类型必须能够替换其基类型,派生类必须能够通过其基类的接口使用,客户端无需了解二者之间的差异。
注:Chapter 9已经涉及。
接口隔离原则(ISP)
不能强迫客户端依赖于它们不需要的接口——只提供必需的接口。
客户端不应依赖于它们不需要的方法,不同的接口向不同的客户端提供服务,客户端只访问自己所需要的端口。
依赖转置原则(DIP)
具体模块应依赖于抽象模块,但抽象模块不应依赖于具体模块。
换句话说:委托的时候要通过接口建立联系,而非具体子类。
示意图:
语法驱动的构造
使用语法判断字符串是否合法,并解析成程序里使用的数据结构(通常是递归的数据结构)。
语法的组成
- 其中的常量字符串(literal strings)被称为终止节点(叶结点),它们不能再向下扩展
- 还有一类为产生式节点(非终止节点),它们既可以扩展出终止节点也可以扩展出非终止节点
- 产生式节点中有一个特殊的节点——根节点
遵循特定规则,利用操作符、终止节点和其他非终止节点可以构造新的字符串
语法中产生式的形式:nonterminal ::= expression of terminals, nonterminals, and operators
语法中的操作符
- 连接(Concatenation):
x ::= y z
, x matches y followed by z - 重复(Pepetition):
x ::= y*
, x matches zero or more y - 选择(Union):
x ::= y | z
, x matches either y or z - 可选(Optional):
x ::= y?
, x is a y or is the empty string - 至少一次出现(1 or more occurrences):
x ::= y+
, x is one or more y - 集合(Character class):
x ::= [a-c]
x ::= [abc]
, x is a or b or c - 取反(Inverted character class):
[^a-c]
, x does not include from a to c
语法树
例图:
正则表达式
正则语法:简化之后可以表达为一个产生式而不包含任何非终止节点。
正则表达式中的一些特殊运算符
.
:匹配任何单个字符\d
:任何数字,等价于[0-9]
\s
:任何空白字符,包括空格、制表符和新行符\w
:任何单词字符,也包括下划线,与[a-zA-z_0-9
等价\
:对操作符或特殊字符进行转义,以便从字面意义上匹配
在Java中使用正则表达式
注:
实际编程过程中注意使用\\
来代替\
,\
也需要被转义。
假如不进行上述代替,Eclipse IDE中可正常运行,但在线构建可能无法通过,报错为"illegal escape character"。