第五章我反复拜读了几遍,对于软件设计这项活动有了重新的认识,也重新搭建了软件设计的认知框架。我对于软件设计的认识原先只有那23个设计模式,现在看来这只是一个局部。下面介绍一下我对于这项活动的重新认识,因为不是专业的软件设计人员可能还是不对,请大家批评指正。因为这一章信息量有点大,而且我还做了一些额外的研究,我先用综述把我新认知的设计框架介绍一下,后续针对每一个部分做详细的介绍。
软件设计解决的问题
首先,软件的设计环节如此的重要,肯定是为了解决一个明确的问题,这里作者很明确地说明设计就是为了更好管理软件工程问题的复杂性。这里作者用了大量的篇幅去解释什么是复杂性问题、为什么要去管理复杂性问题,以及为什么这个问题解决起来这么有挑战性。
问题要怎么解决
对于解决复杂性问题,作者给出了两个必要的举措:
- 尽量减少一个人在同一时刻必须处理的本质复杂性数量
- 防止次要复杂性的无谓扩散
这里本质和次要复杂性引用的是《没有银弹》这篇经典文章里对于软件构造本质任务和次要任务的概念。基于这两个必要举措,作者给出了在软件设计领域的两项实际举措,或者基本的设计原则
- 将复杂系统尽可能分成简单的部分来处理:子系统越是独立,设计就越能专心与设计这个独立的模块
- 保持子程序间断,有助于减轻心智负担:从问题领域角度来写程序,而不是从低层次的实现细节角度来写
软件设计的基本原则
对应着两个解决复杂问题的举措,一个好的软件设计就应该遵循下面11个原则。这里作者特别强调有的目标之间其实有些自相矛盾,但这就是设计的挑战--从相互竞争的目标中创造出一套好的折中方案。
原则 | 鼓励什么 | 不鼓励什么 |
最小复杂化 | 简单和容易理解的设计 | “聪明”的设计 |
易于维护 | 为负责维护的程序员而设计 | 为自己而设计 |
松散耦合 | 程序间尽可能少的关联 | 程序间多的关联 |
可扩展 | 系统升级与底层结构解耦 | 系统升级与底层结构深度绑定 |
可重用 | 某些结构可以被其他系统复用 | 结构无复用价值或可行性 |
高扇入(被调用的数量) | 系统可以很好的利用较低层次的实用类 | 系统内部无法或者难以调用底层实用类 |
低或中等扇出(调用别人的数量) | 尽可能少的调用其他类 | 过多调用其他类 |
移植性 | 系统与环境解耦 | 系统高度依赖某种环境 |
精简性 | 删无可删 | 加无可加 |
层次性 | 按某种共性(比如功能、抽象程度等等)进行分层,层与层间通过接口交互(这样在实现上便可实现解耦) | 你中有我、我中有你 |
使用标准技术 | 使用官方文档的技术 | 使用新的、不成熟或者独立平台的技术 |
这些原则会渗透在设计的各个环节当中,我们在设计的任何时候都要想一想这11个准则。
软件设计的方法
前面说了设计的原则,属于道层面的东西,接下来作者介绍了软件设计的具体方法,属于术的层面。这部分的根基是两块,一个是软件设计的层次,它决定了我们设计的流程步骤,另一个是设计的各种启发式方法,它决定了我们在做任何一步设计时候的方法论。下面先介绍软件设计的层次。
软件设计的层次
软件系统需要在几个不同细节的层次上进行设计。这也就决定了软件的设计就是从粗到细,从抽象到具体的过程。在实际操作过程中,有的设计技术(就是后一节要说的启发式方法)适合所有的层次,而有的只适合某几个层次;有的系统需要把这几个层次都花充足的时间进行设计,有的系统可能对某几个层次可以合并。但是,无论怎样,这几个层次都在那里,是客观存在的。
第一层:软件系统层
就是我们要设计的软件范围,这里主要需要定义软件的边界是什么,软件要解决的问题是什么。
第二层:子系统或者包
这里需要将软件系统做第一次的拆分。一般是按照业务领域来进行拆分,因为这种拆分方式能够很好地反映现实世界的问题域,使得每个子系统都聚焦于解决特定业务领域的任务。总之,这一层重点一是定义划分的方式以及颗粒度;二是定义每个子系统如何使用其他子系统,在定义子系统间交互关系的时候需要强调松耦合的原则,尽可能减少通信。
第三层:类
将每个子系统细分为具体的类,这一层的重点是定义每个子系统类的划分方式,以及定义类的接口。
第四层:子程序
就是将类的接口实现成具体的程序,注意这里的子程序对外是不可见的,对外可见的只有接口。同时,作者指出,随着每一个接口的具体实现,程序员对于类接口的安排会有新的想法,这会促使程序员迭代修改第三层类的接口。
软件设计的启发式方法
什么是启发性方法
先说定义,启发性方法是在面对复杂问题时,采用的经验法则或直观策略,旨在通过简化问题、利用经验和直觉快速找到解决方案,而不是保证找到最优解。这里的关键是基于经验法则去制定后续的解决方案。比如,在一个寻路场景中,如果你正在为游戏中的角色寻找一条从起点到终点的最快路径,A*算法会利用对地图的理解(如直线距离作为启发式估价函数)来指引搜索,避免了穷尽所有路线的低效做法,从而高效地找到一个较优的路径。这就是一个典型的启发式算法,他基于“两点之间直线最短”这个过往的经验作为前提假设去设计相关的算法。
当然有时候你基于的过往经验不一定像这个例子一样是一个公理,或者你要同时基于若干看似矛盾的经验去构建你的解决方案,因此,你的解决方案会在某一时刻被证伪,或者不是最优,这就需要迭代和优化。作者在文中也强调启发式方法在设计中可以看成是一种“试错”指南。
为什么启发性方法适用于软件设计
因为软件设计的问题有一个特点,就是你不知道你想要产出的目标态是什么样的,同时在你到终点的过程中没有很客观的标准告诉你基于现在的状态你下一步要去哪里(没有终点、没有路牌),是一个有着极大不确定性的活动。我们有的只是前文列举的几个原则(当然这几个原则是基于大量先人们的血泪教训,目前历经了几十年依然站得住脚),所以,我们只能基于这些经验去构建我们的解决方案(至于到什么程度是终点,就要考虑外界的资源限制了)。这就是一个典型的启发性方法应用案例。
软件设计的启发性方法工具箱
这里只是做一个简单的例举,后续会详细介绍各种方法。
对应的设计阶段 | 设计方法 |
类的设计 | 找出现实世界的对象 |
子系统和包的设计 类的设计 | 形成一致的抽象 |
类的设计 | 封装实现的细节 |
类的设计 | 能简化设计就继承 |
类的设计 | 信息隐藏(感觉和封装类似) |
子程序的设计 | 识别容易改变的区域 |
子系统和包的设计 | 预测变更的不同程度 |
类的设计 子程序的设计 | 保持松散耦合 |
子程序的设计 | 设计模式(就是23个经典的设计模式) |
以上是常用的启发性方法,下面作者还介绍了一些其他的小方法:
对应的设计阶段 | 设计方法 |
类的设计 | 高内聚 |
类的设计 子程序的设计 | 建立层次结构 |
类的设计 | 正式化类的契约 |
子系统与包的设计 类的设计 | 分配职责 |
子系统与包的设计 | 为测试而设计 |
子系统与包的设计 | 避免失败 |
子程序的设计 | 有意识的选择绑定时间 |
类的设计 子程序的设计 | 建立中心控制点 |
子程序的设计 | 考虑使用蛮力 |
子系统与包的设计 类的设计 子程序的设计 | 画图 |
类的设计 子程序的设计 | 保持设计的模块化 |
使用工具箱的一些建议
在实际的设计工作中,对于这些工具的使用作者提了几条建议:
- 不要拘泥于一种方法
- 当遇到困难的时候,可以选择等待获取更多经验,不是非要在经验有限的时候就去做决定
设计实践的方法
上面介绍了设计活动的方法论,接下来作者跳出设计的细节,站在设计工作流程的角度提出了一些建议。下面先总结性的介绍一下,后续篇章再详细介绍。
方法 | 介绍 |
迭代 | 要审视自己做的设计,迭代优化 |
分而治之 | 将程序分解为不同的关注领域,在单独解决每一个领域的问题 |
自上而下和自下而上的设计 | 自上而下的设计:从抽象到具体 自下而上的设计:从具体到抽象 |
实验性的原型设计 | 用尽可能少的抛弃型代码来回答(或者验证)一个特定的设计问题 |
协作式设计 | 多与其他人交流 |
设计的颗粒度 | 这没有标准答案,这与很多因素有关。但一般来说详细的设计总要优于粗略的设计 |
记录 | 除了设计文档之外,可以通过照片、wiki记录、CRC卡片等方式记录设计过程。 |
总结
软件设计是一个基于经验主义的领域,历代先人的经验汇聚形成了设计的原则,基于这些原则形成了形形色色的设计方法,的确,这些设计原则直到今天依然屹立不倒,如今的微服务、分布式、容器、云这些设计都依然闪耀着这些几十年前思想的光芒,但是需要记住这些原则是基于经验而不是公理,是可以打破的,我们在做软件设计的时候要多保持一些警惕。