软件构造:第五章
好设计的特征
- 最小的复杂度:做出简单并且易于理解的设计。即做到各部分之间的解耦,使得在设计一个部分的时候可以安心的忽视其他部分。
- 易于维护:设计的时候就考虑着维护工作的代价。
- 松散耦合:意味着在设计的时候让程序的各个组成部分之间关联最小。通过引用类接口中的合理抽象、封装性以及信息隐藏等原则,设计出相互关联尽可能最小的类。关联的减少同时可以减少继承、测试和维护时的工作。
- 可扩展性:即增强系统的功能而无需破坏其底层结构。要做到:越可能的改动,越不会给系统在成什么破坏。
- 可重用性:即系统的组成部分可能在其他系统中重复使用。
- 高扇入:高扇入是说让大量的类使用某个给定的类。意味着设计出的系统很好的利用了在较低层次上的工具类。
- 低扇出:即让一个类里少量或者适中地使用其他的类。高扇出说明一个类使用了大量其他的类,因此变得过于复杂。
- 可移植性:设计的系统应该可以很方便地移植到其他环境中。
- 精简性:设计出的系统没有多余的部分。(但是有时为了程序的可扩展性,可能会做一些现在看起来无所谓的工作)
- 层次性: 尽量保持系统各个分阶层的层次性,使得在任一层面上观察系统,并得到某种具有一致性的看法。设计出来的系统应该能在任一层次上观察而不需要进入其他层次。(例如编写新系统时使用了设计不佳的旧代码,这时候要为新系统编写一个负责和旧系统交互的层,使其可以隐藏旧代码的低质量,同时为新的层次提供一组一致的服务,这样之后重构旧代码时就不用修改交互层之上的任何代码)。
- 标准技术:用标准化的、常用的方法。
设计层次
软件系统->分解为子系统和包->分解为包中的类->分解为类中的数据和子程序->子程序内部
即按照自顶向下的设计模式,从宏观出发,一步步进行细化。
- 分解为子系统的包:识别出所有的主要子系统,确定如何把程序分为主要的子系统,并定义清楚各子系统如何使用其他子系统。从而在每个不同子系统的内部,可以使用不同的设计方法。应该限制子系统之间的通信来让每个子系统更有存在的意义这样,在修改一个子系统的时候,受到影响的子系统的个数才会更少。
为了让子系统之间的连接简单易懂并且易于维护,就要尽量简化子系统之间的交互关系。最简单的交互关系是让一个子系统去调用另一个子系统的子程序;稍微复杂一点的交互关系是在一个子系统中抱哈另一个子系统中的类;最复杂的交互关系是让一个子系统中的类继承自另一个子系统中的类。 - 分解为包中的类:
识别出系统中包含的所有的类。例如:数据库几口子系统可能会被进一步划分为数据访问类,持久化框架类等。
定义子系统的类时,也要同时定义这些类与系统的其他部分打交道的细节,尤其是确定好类的接口。
设计构造块:启发式方法
找出现实世界中的对象
设计系统之前,先问问自己该系统模仿的是现实世界中的什么!
使用对象进行设计的步骤:
- 辨识对象及其属性
- 确定可以对各个对象进行的操作
- 确定各个对象能对其他对象进行的操作:确定系统中有哪些交互关系
- 确定对象的那些部分对其他对象是可见的,(哪些是公开的,哪些是私有的)对数据和方法都要确定其可见性
- 定义每个对象的公开接口
当继承能简化设计时就继承
信息隐藏
将一个地方的设计和实现决策隐藏起来,使程序的其他部分看不到它们。
类的接口应该尽可能少地暴露其内部工作机制
找出容易改变的区域
应该将不稳定的区域隔离出来,从而把变化所带来的影响限制在一个子程序、类或者包的内部。下面是一些可以采取的面对变动的措施:
- 找出看起来容易变化的项目
- 将容易变化的项目分离出来:将这些容易变化的组件单独划分出来,或者和其他容易同时发生变化的组件划分到同一类中。
- 把看起来容易变化的项目隔离开来:设计好类的接口,将变化限制在类的内部,且不会影响类的外部。类的接口应该肩负起保护类的隐私的职责。
对硬件的依赖,输入和输出、非标准的语言特性、状态变量(表示程序的状态)等都是容易改变的区域。
保持松散耦合
耦合度表示类与类之间,或者子程序与子程序之间关系的紧密程度。耦合度设计的目标是创建出小的、直接的、清晰的类或者子程序。模块(指类和子程序)之间的好的耦合关系会松散到恰好能使一个模块可以很容易地被其他模块使用。不同模块之间的连接关系要尽可能的简单。要尽可能地使自己创建的模块不依赖或者很少依赖其他模块。
衡量模块之间耦合度的标准:
规模:指模块之间的连接数。越少越好。
可见性:指两个模块之间的连接的显著程度。通过参数表传递数据是值得提倡的,而修改全局变量从而使得另一模块可以使用数据是一种不好的做法。
灵活性:指两模块之间的链接是否容易改动。
耦合的种类
简单数据参数耦合:两模块至今通过参数来传递数据,并且所有的数据都是简单数据类型。这种耦合关系是正常的,可以接受的。
简单对象耦合:一个模块实例化一个对象,这样他们之间的耦合关系就是简单对象耦合的。
对象参数耦合:传递的是一个对象。这种耦合关系比简单对象耦合更紧密。
语义上的耦合:一个模块不仅使用了另一个模块的语法元素,并且使用了有关那个模块内部工作细节的语义知识。
尽量避免这种耦合,就需要模块化的时候,尽量当成各模块都是黑盒,隐藏内部实现的细节。
书上给出了几个例子:
- Module1向Module2传递一个控制标志,用这个标志来告诉Module2做什么,这种方法要求Module1对Module2的内部工作细节有所了解。当Module2将这个标志定义成一种特定的数据类型(枚举类型或者对象)的时候还说的过去。
- Module2在Module1修改全局数据之后使用这个全局数据。这种方式已经架设Module2知道Module1修改之后的数据满足Module2的要求,不是一种好的设计。
- Module1的接口要求它的Module1.Ini()子程序必须子它的Module1.Run()之前调用。Module2知道Module1.Run()无论如何都会调用Module1.Ini()所以Module2在实例化Module1之后不去调用Module1.Ini(),而直接调用Module1.Run()
这些例子里面都是假设一个模块已知调用模块的内部实现,严重依赖调用模块的内部实现,代码重用性十分不好,在自己编写中应该尽量避免,但是确是自己经常犯的错误
使用一些现成的设计模式可以解决很多问题,github上有一个项目,解释了很多设计模式,传送门
其他启发式方法
书中写到的其他的方法也很有指导意义:
- 高内聚,低耦合
- 分层:最通用的位于层次结构的上层,详细的放在更低的层次中
- 严格遵守规约
- 分配职责(把规约写清楚,进行职责分配,代码严格按照规约来写)