软件构造复习
Posted Y-Y-R
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件构造复习相关的知识,希望对你有一定的参考价值。
面向复用的软件构造技术
复用的种类
白盒复用:源代码可见,可修改和扩展(可定制化程度高,其修改增加了软件的复杂度,且需要对其内部充分的了解)
黑盒复用:源代码不可见,不能修改(简单,清晰,适应性差些)
源代码复用
模块层面的复用
采用继承和委派
第三方库的复用
系统层面的复用——框架
框架:一组具体类、抽象类及其之间的连接关系。开发者根据framework的规约,填充自己的代码进去,形成完整系统
白盒框架:通过代码层面的继承进行框架扩展
如用swing进行可视化开发,写自定义控件时扩展JPanal类,自定义布局器扩展LayoutManager2类(以下是我实验三可视化的部分代码)
黑盒框架:通过实现特定接口/委派进行框架扩展
设计可复用的类
强行为子类型化与LSP原则
java强制要求的(静态类型检查):
- 子类型可以增加方法,但不能删
- 子类型需要实现抽象类型中所有未实现的方法
- 子类型中重写的方法必须有相同类型的返回值或符合co-variance的返回值
- 子类型中重写的方法必须使用相同类型的参数或符合contra-variance的参数(java识别为重载)
- 子类型中重写的方法不能抛出额外的异常,抛出相同或者符合co-variance的异常
java不能检查出来的:
- 更强的不变量
- 更弱的前置条件
- 更强的后置条件
协变与逆变
协变(covariance):从父类型到子类型越来越具体
逆变(contravariance):从父类型到子类型越来越抽象
数组满足协变:一个T类型的数组中的元素可以是T类型的或者是T的子类型的
上图中,引用myNumber是Number[]型的,但它所指向的对象是Integer[]型的;运行时,java知道该数组是Integer型的数组,故报错
区分“对象类型”与“引用类型”!
泛型中的LSP
泛型不满足协变!
ArrayList<String>是List<String>的子类,但List<String>不是List<Object>的子类
java编译器会在编译后清除类型信息,所以在运行时无法得知类型信息——类型擦除
List<Integer>、List<Long>、List<Double>不是List<Number>的子类
解决:使用通配符,将List<Number>改为List<? extends Number>
泛型中的LSP:
- List<Integer>不是List<Number>的子类,即使Integer是Number的子类
- List<Number>是List<?>的子类
- List<Number>是List<? extends Object>的子类
- List<Object>是List<? super String>的子类
- ArrayList<String>是List<String>的子类(ArrayList<String>不是List<Object>的子类)
(重要!)
委派
委派:一个对象请求另一个对象的功能
委派模式:通过运行时动态绑定,实现对其他类中代码的动态复用
如果子类只需要复用父类中的一小部分方法,可以不用继承,而是使用委派
委派发生在对象层面,而继承发生在类层面
CRP原则更倾向于使用委派而不是继承来实现复用
委派关系的种类
Dependency:临时性的委派
委派关系通过方法的参数传递建立起来,如下图fly方法将任务委派给其参数Flyable f实现复用
Association:永久性的委派
委派关系通过固有的field建立起来
Composition(组合): 更强的association
Composition是Association的一种特殊类型,其中Delegation关系通过类内部field初始化建立起来,无法修改
Aggregation(聚合): 更弱的association
Aggregation也是Association的一种特殊类型,其中Delegation关系通过客户端调用构造函数或专门方法建立起来
白盒框架、黑盒框架与设计模式
模板模式——白盒框架;策略模式、观察者模式——黑盒框架
面向可维护性的构造技术
可维护性的度量
圈复杂度(CyclomaticComplexity):圈复杂度可以衡量一个模块判定结构的复杂程度,其数量上表现为独立路径的条数,也可理解为覆盖所有的可能情况最少使用的测试用例个数
计算方法:根据程序的流程图计算,有两个公式——
CC=E-V+2,其中E为流程图的边数,V为流程图的顶点数(这其实就是平面图的欧拉公式,但我说不清为什么流程图一定是可平面的,希望各位能够指点)
CC=P+1,P为“判定节点”的数量,判定顶点指if/while/for/switch等语句,其中多分支(else-if, switch case1,case2,...)每个分支算一个判定节点
(感觉两个公式都跟图论关系密切)
继承的层次数
类之间的耦合度:类之间的通过参数、返回值、变量、方法调用、接口实现、继承、委派等的耦合
单元测试的覆盖度(之前章节中讲过)
模块化设计与原则
衡量模块化的标准
可分解性、可组合性、可理解性、可持续性、出现异常之后的保护
模块化设计的原则
直接映射、尽可能少的接口、尽可能小的接口、显示接口、信息隐藏
耦合与内聚
耦合表示两个模块之间的依赖
可以通过两模块间接口的数量及接口的复杂程度衡量模块间的耦合度
内聚:一个模块内部各元素彼此结合的紧密程度
好的设计应做到高内聚、低耦合
面向对象设计原则:SOLID
内容:
- SRP:单一责任原则
- OCP:开发-封闭原则
- LSP:Liskov替换原则
- DIP:依赖转置原则
- ISP:接口聚会原则
SRP 单一责任原则
不应该有多于一个原因让ADT发生变化,否则就应该分开
责任:变化的原因
一个反例:
OCP (面向变化的)开放封闭原则
对扩展性开放、对修改封闭
模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化
但模块自身的代码是不应被修改的,扩展模块行为的一般途径是修改模块的内部实现,如果一个模块不能被修改,那么它通常被认为是具有固定的行为
例:如果有多种类型的Server,那么针对每一种新出现的Server,不得不修改Server类的内部具体实现;
通过构造一个抽象的Server类:AbstractServer,该抽象类中包含针对所有类型的Server都通用的代码,从而实现了对修改的封闭;当出现新的Server类型时,只需从该抽象类中派生出具体的子类ConcreteServer即可,从而支持了对扩展的开放。
LSP Liskov替换原则
子类型必须能够替换其基类型
派生类必须能够通过其基类的接口使用,客户端无需了解二者的差异
ISP 接口隔离原则
不能强迫客户端依赖于它们不需要的接口:只提供必需的接口
客户端不应依赖于它们不需要的方法
“胖”接口可分解为多个小的接口,不同的接口向不同的客户端提供服务,客户端只访问自己所需要的端口
DIP 依赖转置原则
抽象的模块不应该依赖于具体的模块,具体应依赖于抽象
面向可复用性和可维护性的设计模式
创建型模式
工厂方法模式
当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。
定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
Client用工厂方法来创建实例,得到的实例类型是抽象接口而非具体类
静态工厂方法:将方法设置为静态
实现信息隐藏
结构器模式
适配器模式
需要使用委派
将某个类/接口转换为client期望的其他形式(主要解决接口不匹配问题)
通过增加一个接口,将已存在的子类封装起来,client面向接口编程,从而隐藏了具体子类
例如,对矩形的表示不同的问题,在原来的接口中,矩形表示为(左上坐标x,y,宽,高),而用户希望表示为(左上坐标x,y,右下坐标x,y)
用户希望通过后一种表示方式来调用原来接口中的display(int x1,int y1,int w,int h),这就出现了接口不匹配的问题
解决:在Rectangle中委派LegacyRectangle(临时性委派)
这样,客户端可以通过Shape接口以(左上坐标,右下坐标)的形式来调用LegacyRectangle的display方法
装饰器模式
这部分当时学的时候理解得不好,感觉这部分还是挺难的,这里总结一下我复习时个人的理解
需要使用委派
对于要为对象增加新特性,且要求可以随意组合的情形,如果采用继承,则组合数过多,代码量剧增
采用装饰器模式
装饰器类Decorator和被装饰的类ConcreteComponent实现同一个接口Component
装饰器类内部有一个被装饰的对象(委派)
客户端调用时,按需要添加的特性的组合来嵌套地创建对象,如Component component=new ConcreteDecoratorA(new ConcreteDecoratorB(new ConcreteComponent()));
对于想要添加特性的方法,在装饰器类进行重写,执行增加的特性并调用其被装饰对象的方法(即:执行增加的特性,并执行“内层”的方法。如此重复直到执行到最内层为止)
装饰器与继承的对比:
装饰器在运行时组合特性,继承在编译时组合特性
行为类模式
策略模式
需要使用委派
有多种不同的算法来实现同一个任务,但需要client根据需要动态切换算法,而不是写死在代码里
为不同的实现算法构造抽象接口,利用delegation,运行时动态传入client倾向的算法类实例
是黑盒框架
模板模式
做事情的步骤一样,但具体方法不同
共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现
使用继承和重写实现模板模式
模板模式在框架中广泛使用,是白盒框架
共性的顺序为final方法,防止子类重写
子类不能改变顺序,但可以改变某个步骤的具体实现
迭代器模式
客户端希望遍历被放入容器/集合类的一组ADT对象,无需关心容器的具体类型
也就是说,不管对象被放进哪里,都应该提供同样的遍历方式
Iterable接口:实现该接口的集合对象是可迭代的
Iterator接口:迭代器
迭代器模式:让自己的集合类实现Iterable接口,并实现自己的独特Iterator迭代器(hasNext, next, remove),允许客户端利用这个迭代器进行显式或隐式的迭代遍历
观察者模式
需要使用委派——双向委派
将某个功能(通过方法的参数)委派给一个外部的对象
更换visiter的具体实现即可切换算法,便于扩展(对比:对于策略模式,传入不同类型的visiter以切换算法)
查阅其他资料时,发现也有资料如此解释观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都可以得到通知并自动更新。
比较类似于被“订阅”的“创作者”更新后,通知每一个关注他的人
策略模式与观察者模式对比:
软件构造复习内容--ADT
一。可变类和不可变类(Mutability and Immutability)
可变类有Mutor方法,不可变类没有Mutor方法,一旦确定其指向的对象,不能再被改变
二。SnapShot Diagram(程序快照图)
用于描述程序运行时的内部状态。
ADT(Abstract Data Type)
以上是关于软件构造复习的主要内容,如果未能解决你的问题,请参考以下文章