第5章 软件构建中的设计
设计中的挑战
软件设计,即构思、创造或发明一套方案,把一份计算机软件的规格说明书要求转变为可以实际运行的软件。
设计是一个险恶的问题
- 学校开发的项目和实际企业的项目大有不同
- 学校项目:从头到尾是直线式的设计开发方式
- 企业项目:需要应对变化,不断地调整完善,是迭代开发式的。
- 学校开发的项目和实际企业的项目大有不同
设计是了无章法的过程
- 难以界定做到什么地步才算足够好。设计永无止境,到没有时间优化为止。(考虑项目的周期)
设计是确定取舍和调整顺序的过程
- 去衡量彼此冲突的各项设计特性(例如,快速响应市场优于做出完整产品,可以先上线重点功能,后续再去补齐其他功能)
设计受到诸多约束
- 人力、时间、资源成本
设计是不确定的
- 解决问题的方法多种多样,不同的架构师风格也不一致,掌握的技术栈也不同
设计是一个启发式的过程
- 设计过程中总会有试验和犯错误,一项技术也不可以通用于所有应用领域
设计是自然而然形成的
- 分布式架构、云原生架构等,都是随着网络、科技不断进步以及业务需求不断变更下,逐渐萌发的技术。只有当前的技术无法满足实际的需求,才会促进大家去思考,如何解决当前面对的新问题。
关键的设计概念
软件的首要技术使命:管理复杂度
软件开发困难的两类问题
- 本质问题
- 不断地去挖掘错综复杂,相互连接的整套软件系统的具体细节。很难精确而完整地识别出各种依赖关系与例外情况,设计出的系统只能大致正确而不能保证完全正确。
- 偶然问题
- 偶然性:指一件事物碰巧拥有的属性,是附属的,任意的,非必要的或偶然出现的性质。
- 偶然性问题由于编程语言的不断演进,操作系统的进步,集成编译环境的改善,在软件开发中的重要性也随着下降。
如何去管理复杂度
项目失败大多是归根于不尽人意的需求、规划和管理。但是,也存在由于技术因素导致失败的情况,当软件系统变得复杂,没人知悉修改这处代码会对其他代码带来什么影响时,项目也快走到尽头了。
在软件架构层面,通过将系统分解成多个子系统来降低问题的复杂度。人们通常能够理解多项简单的信息,而不是一项简单的信息,对信息进行合理的拆分便于人们理解。
如何应对复杂度
高代价、低效率的设计源于以下三种原因:
- 用复杂方法解决简单问题
- 用简单但错误的方法解决复杂问题
- 用不恰当的复杂方法解决复杂问题
需要通过以下两种方法来管理复杂度
- 把任何人在同一时间需要处理的本质复杂度的量减到最少
- 不要让偶然性的复杂度无谓地快速增长
理想的设计特征
高质量的设计拥有很多常见的特征,要实现所有的目标比较困难,因为目标之间也会有相互抵触的情况,需要做出选择,确定一套折中的方案、
最小的复杂度
- 做出简单易于理解的设计
易于维护
- 能够自明的系统
松散耦合
- 程序各个组成关联最小
可扩展性
- 改动某个功能不影响其他功能
可重用性
高扇入
- 定义:大量的类使用某个给定的类。
- 意味着设计的系统能够很好地利用较低层次的工具类
低扇出
- 定义:一个类少量或适中地使用其他的类。
- 高扇出表明一个类使用大量的其他类,因此变得过于复杂。
可移植性
精简性
- 设计的系统没有多余部分
层次性
- 尽量保证系统各个分解层的层次性,从任意层面观察,都能得到某种一致性的效果。
- 编写新系统负责和旧代码交互时,要让它能隐藏旧代码的低劣质量,同时为新的层次提供一组一致的服务。
标准技术
- 使用标准化,常用的方法,让整个系统使人觉得熟悉
设计的层次
程序的设计层次如图所示:
第1层:软件系统
第2层:分解为子系统和包
- 识别出所有主要子系统,例如数据库、用户界面、业务规则、命令解释器和报表引擎。
- 需要确定不同子系统之间互相通信的规则
当没有任何限制时,热力学第二定律就会起作用,整个系统的熵会增加。
原始:
没有任何限制:
施加通信规则:
需要尽量简化子系统之间的交互关系:
- 最简单:子系统A去调用子系统B的子程序;
- 复杂一点:子系统A包含子系统B的类;
- 最复杂:子系统A继承子系统B的类。
程序中不应该有任何环形,即A调用B,B调用C,C调用A的情况。
第3层:分解为类
- 类(Class)和对象(Object)的比较
- 类似数据库术语中的模式(schema)和实例(instance)
第4层:分解成子程序
- 将每个类细分为子程序
第5层:子程序内部的设计
- 包括编写伪代码、选择算法、组织子程序内部代码块,以及用编程语言编写代码。
设计构造块:启发式方法
找出现实世界中的对象
使用对象进行设计的步骤:
- 辨识对象及其属性(方法和数据) —— 顾客拥有名称、账单、地址等数据,和修改名称、修改地址等方法
- 确定可以对各个对象进行的操作 ——
- 确认各个对象能对其他对象进行的操作 —— 关系通常有包含和继承
- 确定对象的哪些部分对其他对象可见 —— public 和 private的设置,例如,公有继承不能直接访问基类的私有方法
- 定义每个对象的公开接口
形成一致的抽象
抽象好处在于使你能够忽略无关的细节。
例如,接口里连接数据库,释放数据库连接等接口,都是抽象的,无需关注底层到底是用MySQL还是PostgreSQL。
抽象以简单视角考虑复杂问题,编程抽象,有利于站在高的角度看问题,能够更快、更稳妥地进行开发。
封装实现细节
封装 ———— 所见即所得,只能看到对外暴露的内容,不能看到对象的任何其他细节层次。
当继承能简化设计时就继承
定义对象之间的相同点和不同点。
可以利用已有的数据类型来定义新的数据类型。
隐藏秘密(信息隐藏)
设计类:关键点在于确定类的哪些特性对外暴露;哪些特性应该隐藏。
- 好的类接口就像冰山的尖儿,让类的大部分内容不会被暴露出来。
- 例如,如果订单号按照2022010100001,2022010100002方式计算,很容易能知道网站的销售量。
信息隐藏主要分为两大类:
隐藏复杂度
隐藏变化源
信息隐藏的障碍
- 信息过度分散
- 循环依赖
- 把类内数据误认为全局数据
- 性能的损耗
信息隐藏的价值
- 便于修改程序
- 有助于设计类的公开接口
找出容易改变的区域
优秀的设计师,都拥有一项特质 —— 对变化的预期能力。
好的程序设计面临的最大挑战就是适应变化。 ———— 目标:把不稳定的区域隔离出来,从而把变化所带来的影响限制在一个子程序、类或者包的内部。
应对各种变化的措施:
- 找出看起来容易变化的项目
- 把容易变化的项目分离出来
- 把看起来容易变化的项目隔离开来
- 容易变化的区域
- 业务规则
- 硬件依赖
- 输入和输出
- 数据量限制
- 容易变化的区域
保持松散耦合
耦合度:类与类之间或子程序与子程序之间关系的紧密程度。
一个模块越容易被其他模块所调用,则它们之间的耦合关系越松散。
耦合种类:
- 简单数据参数耦合
- 简单对象耦合
- 对象参数耦合
- 语义上的耦合:需要了解模块内部工作原理
设计实践
- 分而治之
- 自上而下:从一般性的问题出发,把该问题分解成可控的部分
- 自下而上:从可控的部分出发,去构造一个通用的方案
- 建立试验性原型
- 合作设计