面向可复用性的设计
软件复用
软件复用:将已有的软件及其有效成分用于构造新的软件
复用的优点
- 降低开发成本
- 复用的程序已经过充分测试,可靠且稳定
- 有助于实现标准化,在不同的软件中保持一致
复用的代价?
开发可复用的软件(Development for reuse)
- 由于要有足够高的适应性,开发成本高于一般软件
- 由于针对更普适的场景进行开发,缺少足够的针对性,所以性能可能会差一些
复用已有软件进行开发(Development with reuse)
-
通过软件库对可复用软件进行有效的管理
-
通常无法直接就能使用,需要进行适配
白盒复用
- 复制已有代码到正在开发的软件,并进行修改
- 优点:可定制化程度高
- 缺点:对已有代码的修改增加了软件的复杂度,且需要充分了解其内部实现
黑盒复用
- 通过接口(如类、函数调用)来复用已有的代码,无法修改代码
- 优点:简单、清晰
- 缺点:某些情况适应性差
可复用的软件来源
- 开源软件的代码
- 已有系统内的代码
- 组织内部的代码库
- 来自同学和同事的代码
- 自己日积月累的代码
- 来自教材、教程、论坛的代码示例
- 如,CppReference
- 程序设计语言自身提供的库
- 如,C++的标准库、STL
- 第三方提供的库
- 如,C++的 Boost 库
复用的形态
复制粘贴(源代码)
-
可能的问题
- 维护成本高
- 同一个代码粘贴到多处地方,改动时需同时修改多处
-
复制粘贴操作过程中出错
-
需要看懂被复制的代码
复用“函数”或“类”(模块)
-
封装性有助于复用
-
规约或文档对使用者的重要意义
-
可能的问题
- 版本更新导致后向兼容问题
- 需要将相关的类或函数链接到一起
-
复用类的两种方式
- 继承
- 委托:将某些职责由其他类来完成
库:提供可复用功能的一组类或函数
- 库提供的类和函数常被称为 API(Application Programming Interface,应用编程接口)
- 开发者在编写软件的代码时调用库提供的 API
框架:一组类及其之间的连接关系
开发者根据框架的规约,将自己编写的代码填充到框架中,形成完整的软件
设计可复用的类
回顾:对象之间的关系
面向对象将现实世界中的概念抽象为“类”,
而把符合这些概念的事物抽象为“对象”
两个对象之间可能存在各种各样的关系:
-
泛化:分类法中一般和特殊的关系
- 是(is-a):大学生是学生(使用公有继承机制可以实现“是”关系)
-
关联:静态结构上的关系,长期的、稳定的(用类的成员变量可以实现关联关系)
-
有(has-a):汽车有轮子
(成员变量使用类类型的指针可以表示“有”关系,UML 中称为“聚合”关系)
-
部分于(part-of):例如头是人体的一部分
(成员变量使用类类型对象可以表示“部分于”关系,UML 中称为“组合”关系)

-
-
依赖:动态行为上的关系,偶发的、临时性的(使用类类型的成员函数参数可以实现依赖关系)
- 使用(uses-a):程序员使用键盘
- 依赖于(depends-a):花依赖于蜜蜂传粉

通过继承实现复用
通过继承关系,子类可以继承父类已有的功能,在此基础上增加子类的特殊功能,从而实现复用
如何设计继承以减少因复用带来的 bug?
$$ 里氏替换原则(LSP) $$-
继承必须确保父类所拥有的性质在子类中仍然成立,当一个子类的对象能够替换任何其父类的对象时,它们之间才具有泛化(is-a)关系
-
里氏替换原则是继承复用的基石
只有当派生类可以替换掉其基类,而软件功能不受影响时,基类才能真正被复用,派生类也才能够在基类的基础上增加新的行为
-
LSP 本质:同一个继承体系中的对象应有共同的行为特征
-
违反 LSP 的后果:有可能需要修改调用父类的代码
通过委派实现复用
委派/委托:类 B 对象使用类 A 对象的功能,从而实现对类 A 的复用
- 通过某种方式将类 A 的对象传入类 B 中
- 在类 B 中通过调用类 A 的成员函数,实现复用
继承和委派
- 继承和委派常同时使用,来实现特定的设计目标
- 继承发生在“类”层面,委派发生在“对象”层面
- 如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来实现
- 可避免大量无用的方法
合成复用原则:
-
优先使用委派而不是继承来实现复用
-
原因:降低类与类之间的耦合度
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将父类的实现细节暴露给子类
-
实践方法:通过抽象类或接口来实现委派,兼具可复用性和可扩展性
面向复用的设计模式
广义讲,软件设计模式是可解决一类软件问题并能重复使用的软件设计方案
狭义讲,软件设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述
(是在类和对象的层次描述的、可重复使用的软件设计问题的解决方案)

适配器模式
定义
适配器(Adaptor)模式将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作
- 类适配器
- 对象适配器

适配器模式的使用过程
- 客户通过目标接口调用适配器的方法对适配器发出请求
- 适配器使用被适配者接口把请求转换成被适配者的一个或者多个调用接口
- 客户接收到调用的结果,但并未察觉这一切是适配器在起转换作用
优点:
- 方便设计者自由定义接口,不用担心匹配问题
缺点:
- 属于静态结构,不适用于多种不同的源适配到同一个目标(如果支持多继承可解决此问题)
装饰器模式
定义
对每一个特性构造子类,通过委派机制增加到对象上
装饰器(Decorator)模式动态地给一个对象添加一些额外的职责

- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
- 当不能采用生成子类的方法进行复用时。一种情况是,可能有大量独立扩展,每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况是因为类定义被隐藏,或类定义不能用于生成子类
优缺点
- 使用装饰器模式可以很容易地向对象添加职责。可以用添加和分离的方法,在运行时添加和删除职责
- 使用装饰器模式可以很容易地重复添加一个特性,而两次继承则极容易出错
- 避免在层次结构高层的类有太多的特征:可以从简单的部件组合出复杂的功能。具有低依赖性和低复杂性
- 缺点是产生了许多小对象
外观模式
Define

外观(Facade)模式提供一个统一的接口,用来访问子系统中的一群接口
- 外观定义了一个高层接口,让子系统更容易使用
- 解除客户程序与抽象类具体实现部分的依赖性,有利于移植和更改
- 外观模式的本质是让接口变得更简单
使用效果
- 对客户端屏蔽子系统组件,减少客户端使用对象数目
- 实现了子系统与客户之间松耦合的关系,使得子系统组件的变化不会影响到客户
- 不限制客户应用子系统类
策略模式

策略(Strategy)模式是指定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换
- 使得算法可独立于使用它的客户而变化
适用场景:
某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使得对象变得异常复杂;而且有时候支持不同的算法也是一个性能负担
优点
- 算法和使用算法的对象相互分离,客户程序可以在运行时动态选择算法,代码复用性好,便于修改和维护
- 消除了冗长的条件语句序列
- 提供相同行为的不同实现,客户可以根据不同的上下文从不同的策略中选择算法
缺点
- 客户必须了解不同的 Strategy
- Strategy 和 Context 之间的通信开销加大
- 增加了类和对象数目,在算法较多时更加严重
模板方法模式
define
模板方法(Template Method)模式定义操作中算法的骨架,将一些步骤的执行延迟到其子类中
- 子类不需要改变算法结构即可重定义算法的某些步骤
适用场景:
具有统一的操作步骤或操作过程,具有不同的操作细节,即存在多个具有同样操作步骤的应用场景,但某些具体的操作细节却各不相同

使用效果
- 模板方法是一种代码复用技术,模板提取了子类的公共行为
- 模板方法模式形成一种反向的控制结构(依赖倒置原则)
- 父类调用子类的操作(高层模块调用低层模块的操作),低层模块实现高层模块声明的接口
- 这样控制权在父类(高层模块),低层模块反而要依赖高层模块
- 可通过在抽象模板定义模板方法给出成熟算法步骤,同时又不限制步骤细节,具体模板实现算法细节不会改变整个算法骨架
- 在抽象模板模式中,可以通过钩子方法对某些步骤进行挂钩,具体模板通过钩子可以选择算法骨架中的某些步骤
策略模式 vs 模板方法模式
