《软件构建中的设计》读书笔记
前言
一年左右没有写过新文章了,一方面是因为工作太忙,平时还得看书什么的,抽不出时间来写博客,另一方面也有些不知道该些什么,当然也有自己偷懒的原因
这篇博客是因为最近在读《代码大全》,本文是其中第五章《软件构建中的设计》的读书笔记,作为一个非科班出身的程序员,第五章尤其使我受益匪浅。但是又不能总带着这么厚的一本书,所以想把其中的精华总结在此,方便查阅。
1.设计中的挑战
好的高层次设计能提供一个可以稳妥容纳多个较低层次设计的结构。
我们设计代码的目标便是如此,提供一个高层次的封装与抽象,使得复杂度隐藏在其之后,让我们能够轻松应对低层次上的变化与性能的挑战,同时也可以降低程序的复杂度与提高代码复用度。
设计是一个险恶的问题
然后设计却是一个险恶的问题,这与我们的目标似乎有所冲突。这里“险恶的”定义是那种只有通过解决或部分解决才能被明确的问题。这个看似矛盾的定义其实是在暗示说,你必须首先把这个问题“解决”一遍以便能够明显地定义它,然后再次解决该问题,从而形成一个可行的方案。这一过程已经已经如影随形地在软件开发中存在数十年了。
设计是个了无章法的过程(即使它能得出清爽的成果)
软件设计的成果应该是组织良好、干净利落的,然而形成这个设计的过程却并非如此清爽。
说设计了无章法,一方面是因为在此过程中会采取很多次步入歧途,然后事实上,犯错正式设计的关键所在——即使以为设计是一个启发式的、迭代的过程,同时也因为在设计阶段犯错并改成,其代码比编码之后才发现错误要低得多。另一方面,还因为很难判断设计什么时候算是“足够好”,什么时候才算是设计完成?设计永无止境,因此对上述问题最常见的回答是“到你没时间再做了为止”。
设计就是确定取舍和调整顺序的过程
如果我们在一个理想的世界中,所有资源都是无限的,那么我们就不用去设计。然而正是因为我们的世界是有限的,才会在诸多的限制中,不断的确定取舍和调整顺序,探索更加优秀的设计方案。虽然设计充满了不确定性,每人都可能会完成一套截然不同的设计。但我们都会促成简单的方案,并最终改善这一解决方案。软件设计的目标也如此。
设计是一个启发式过程
正因为设计过程充满不确定性,因此设计技术也就趋于具体探索性——经验法则”或者“试试没准能行的办法”——而不是保证能产生预期结果的可重复的过程。设计过程中总会有试验和犯错误。在一件工作或一件工作的某个方面十分奏效的设计工具或技术,不一定在下一个项目中适用。没有任何工具是用之四海而皆灵的。
设计是自然而然形成的
把设计的这些特征综合归纳起来,我们可以说设计是“自然而然形成的”。设计不是在谁的头脑中直接跳出来的。它是在不断的设计评估、非正式讨论、写试验代码以及修改试验代码中演化和完善的。
2.关键的设计概念
好的设计源于对一小批关键设计概念的理解,比如复杂度,设计应有的特征和设计的层次。
软件的首要技术使命:管理复杂度
偶然的难题和本质的难题
Brooks 认为,两类不同的问题导致软件开发变得困难——本质的问题和偶然的问题。
本质的
属性是一件事物必须具备、如果不具备就不再是该事物的属性。
偶然的
属性则是指一件事物碰巧具有的属性,有没有这些属性都并不影响这件事物本身。
目前软件开发中大部分的偶然性难题在很久之前就已得到解决了。比如,笨拙的语法,非交互式计算机、开发工具之间无法很好地协作等等。但是在软件开发剩下的那些本质性困难上的进展将会变得相对缓慢。究其原因,从本质上来说软件开发就是不断地去挖掘错综复杂、相互连接的整套概念的所有细节。
所有这些本质性困难的根源都在于复杂性——不论是本质的,还是偶然的。
管理复杂度的重要性
有两种设计软件的方式:一种方法是让设计非常简单,看上去明显没有缺陷;另一种方法是让设计非常复杂,看上去没有明显的缺陷。 -–—C.A.R Hoare
在对导致软件项目失败的原因进行调查时,人们很少把技术原因归为项目失败的首要因素。项目失败的大多数都是由于不尽如人意的需求、规划和管理所导致的。但是,当项目确由技术因素导致失败时,其原因通常就是失控的复杂度。
管理复杂度是软件开发中最为重要的技术话题。软件的首要技术使命便是管理复杂度,它实在太重要了。
没有谁的大脑能容得下一个现代的计算机程序,也就是说,作为软件开发人员,我们不应该试着在同一时间把整个程序都塞进自己的大脑,而应该试着以某种方式去组织程序,以便能够在一个时刻可以专注于一个特定的部分。
所有软件设计技术的目标都是把复杂问题分解成简单的部分。子系统间的相互依赖越少,你就越容易在同一时间里专注问题的一个小部分。精心设计的对象关系使关注点相互分离,从而使你在每个时刻只专注于一件事情。
如何应对复杂度
高代价、低效率的设计源于下面三种根源:
- 用复杂的方法解决简单的问题;
- 用简单但错误的方法解决复杂的问题;
- 用不恰当的复杂方法解决复杂的问题;
不幸的是,无论你多努力,最终都会与存在于现实世界问题本身的某种程度的复杂性不期而遇。这就意味着要下面这两种方法来管理复杂度:
- 把任何人在同一时间需要处理的本质复杂度的量减到最少;
- 不要让偶然性的复杂度无谓地快速增长。
一旦你能理解软件开发中任何其他技术目标都不如管理复杂度重要时,众多设计上的考虑就都变得直截了当了。
理想的设计特征
高质量的设计具有很多常见的特征。这些特征之间有时会相互抵触,但这也正是设计中的挑战所在——在一系列相互竞争的目标之中做出一套最好的折中方案。
- 最小的复杂度
- 易于维护
- 松散耦合
- 可扩展性
- 可重用性
- 高扇入
- 低扇出
- 可移植性
- 精简性
- 层次性
- 标准技术
设计的层次
一个软件系统需要在不同细节层次上进行设计。有些设计技术适用于所有的层次,而有些只适用于某些层次上。
图1 design_level
第一层:软件系统
第一个层次就是整个系统。有的程序员直接从系统层次就可以是设计类,但是往往先从子系统或者包这些类的更高组织层次来次思考会更有益处。
第二层:分解为子系统或包
在这一层次上设计的主要成果是识别出所有的主要子系统。在这一层次中,有一点特别重要,即不同子系统之间互相通信的规则。如果所有的子系统都能同其他子系统通信,就完全失去了把它们分开所带来的好处。应该通过限制子系统之间的通信来让每个子系统更有存在意义。如果拿不准如何设计的话,那么就应该先对子系统之间的通信加以限制,等日后需要时再放松,这要比先不限制,等子系统之间已经有了上百个调用时再加以限制要容易得多。
常见的子系统有:业务规则、用户界面、数据库访问、对系统的依赖性。
第三层:分解为类
在这一层次上的设计包括识别出系统中所有的类。其主要设计任务是把所有的子系统进行适当的分解,并确保分解出的细节都恰到好处,能够用单个的类实现。
面向对象设计的一个核心概念就是对象(object)与类(class)的区分。对象是指运行期间在程序中实际存在的具体实体(enity),而类是指在程序源码中存在的静态事物。
第四层:分解成子程序
这一层的设计包括把每个类细分为子程序。在第三层中定义出的类接口已经定义了其中一些子程序,而第四层的设计将细化出类的私用(private)子程序。完整定义出类内部的子程序,常常会有助于对类的接口进行进一步修改,也就是说再次返回第三层的设计。
第五层:子程序内部的设计
在子程序层次上进行设计就是为每个子程序布置详细的功能。这里的设计工作包括编写伪代码、选择算法、组织子程序内部的代码块,以及用编程语言编写代码。
3.设计构造块:启发是方法
由于软件设计是非确定性的,因此,灵活熟练地运用一组有效的启发式方法,便成了合理的软件设计的核心工作。
找出现实世界中的对象
在确定设计方案时,首选且最流行的一种做法便是“常规的”面向对象设计方法,辨识现实世界中的对象(object,物体)以及人造的(synthetic)对象。
使用对象进行设计的步骤是:
- 辨识对象以及属性(方法(method)和数据(data))。
- 确定可以各个对象进行的操作。
- 确定各个对象能对其他对象进行的操作。
- 确定对象的哪些部分对其他对象可见——哪些对象可以是公用(public)的,哪些部分应该是私用(private)的。
- 定义每个对象的公开接口(public interface)。
经过上述这些步骤得到了一个高层次的、面向对象的系统组织结构之后,你可以用这两种方法来迭代:在高层次的系统组织结构上进行迭代,以便更好地组织类的结构或者在每一个已经定义好的类上进行迭代,把每个类的设计细化。
形成一致的抽象
抽象是一种能让你在关注某一概念的同时可以放心地忽略其中一些细节的能力——在不同的层次处理不同的细节。
封装实现细节
封装填补了抽象留下的空白。抽象是说:“可以让你从高层的细节来看待一个对象。”而封装则说:“除此之外,你不能看到对象的任何其他细节层次。”
当继承能简化设计时就继承
定义对象之间的相同点和不同点就叫“继承”。继承的好处在于它能很好地辅佐抽象的概念,同时继承还能简化编程的工作。
隐藏秘密(信息隐藏)
信息隐藏是结构化程序设计与面向对象设计的基础之一。在设计一个类的时候,一项关键性的决策就是确定类的哪些特性应该对外可见,而哪些特性应该隐藏起来。隐藏设计决策对于减少“改动所影响的代码量”而言是至关重要的。
两种秘密
信息隐藏中所说的秘密主要分为两大类:
- 隐藏复杂度,这样你就不用再去应付它,除非你要特别关注的时候。
- 隐藏变化源,这样当变化发生时,其影响就能被限制在局部范围内。
信息隐藏的障碍
- 信息过度分散
- 循环依赖
- 把类内数据误认为全局数据
- 可以觉察的性能损耗
信息隐藏的价值
信息隐藏有着独特的启发力,它能够激发出有效的设计方案。在设计的所有层面上,都可以通过询问该隐藏些什么来促成好的设计决策。这一问题可以在构建层面上协助你用具名常量来取代字面量,可以在类的内部生成好的子程序和参数名称,还有助于指导在系统层面上做出有关类和子系统分解以及交互设计的决策。
请养成问“我该隐藏什么?”的习惯,你会惊奇地发现,有很多很棘手的设计难题都会在你面前化解。
找出容易改变的区域
好的程序设计所面临的最重要的挑战之一就是适应变化。目标应该是把不稳定的区域隔离出来,从而把变化所带来的影响限制在一个子程序、类或者包的内部。下面是面对各种变动时应该采取的措施:
- 找出看起来容易变化的项目。
- 把容易变化的项目分离出来。
- 把看起来容易变化的项目隔离开来。
下面是一些容易发生变化的区域:业务规则、对硬件的依赖性、输入和输出、非标准的语言特性、困难的设计区域和构建区域、状态变量、数据量的限制。
预料不同程度的变化
找出容易发生变化的区域的一个好办法是:首先找出程序中可能对用户有用的最小子集。这一子集构成了系统的核心,不容易发生改变。接下来,用微小的步伐扩充这个系统。这里的增量可以非常微小,小到看似微不足道。当你考虑功能上的改变时,同时也要考虑质的变化:比如说让程序变成线性安全的,使程序能够本地化等。这些潜在的改进区域就构成了系统中的潜在变化。依照信息隐藏的原则来设计这些区域。通过首先定义清楚核心,你可以认清哪些组件属于附加功能,这时就可以把它们提取出来,并把它们的可能改进隐藏起来。
保持松散耦合
一个模块越容易被其他模块所调用,那么它们之间的耦合关系就会越松散。这种设计非常不错,因此它更灵活,并且更易于维护。因此,在创建系统架构时,请按照“尽可能缩减互相连接”的准则来分解程序。如果把程序看做是一块木材,那么就请延着木材的纹理把它劈开。
查阅常用的设计模式
设计模式精炼了众多现成的解决方案,可用于解决很多软件开发中最常见的问题。有些软件问题要求全新的解决方案,但是大多数问题都和过去遇到过的问题类似,因此可以使用类似的解决方案或者模式加以解决。常见的设计模式包括:适配器、桥接、装饰器、外观、工厂方法、观察者、单件、策略以及模板方法。《设计模式》一书是讲述设计模式的最权威著作。
与完全定制的设计方案相比,设计模式提供了以下好处:
- 设计模式通过提供现成的抽象减少复杂度。
- 设计模式通过把常见解决方案的细节予以制度化来减少出错。
- 设计模式通过提供多种那个设计方案而带来启发性的价值。
- 设计模式通过把设计对话升到一个更高的层次上来简化交流。
关于设计启发的总结
下面是对主要的设计中的启发式方法的总结:
- 寻找现实世界的对象(object,物体)
- 形成一致的抽象
- 封装实现细节
- 在可能的情况下继承
- 藏住秘密(信息隐藏)
- 找出容易改变的区域
- 保持松散耦合
- 探寻通用的设计模式
下列的启发式方法有时也很有用:
- 高内聚性
- 构造分层结构
- 严格描述类契约
- 分配职责
- 为测试而设计
- 避免失误
- 有意识地选择绑定时间
- 创建中央控制点
- 考虑使用蛮力
- 画一个图
- 保持设计模块化
使用启发式方法的原则
- 理解问题。
- 设计一个计划。找出现有数据和未知量之间的联系。如果找不出居中的联系,那么可能还得考虑些辅助性的问题。
- 执行这一计划。
- 回顾。检视整个的解。
4.设计实践
迭代
设计是一种迭代过程。你并非只能从 A 点进行到 B 点,而是可以从 A 点到达 B 点,再从 B 点返回到 A 点。
当在备选的设计方案之中循环并且尝试一些不同的做法时,你将同时从高层和低层的不同视角去审视问题。你从高层视角中得出的大范围图景会有助于你把相关的底层细节纳入考虑。你从底层视角中所获得的细节也会为你的高层决策奠定基础。这种高低层面之间的互动被认为是一种良性的原动力,它所创建的结构要远远稳定于单纯自上而下或者自下而上创建的结构。
分而治之
没有人的头脑能大到装得下一个复杂程序的全部细节,这对设计也同样有效。把程序分解为不同的关注区域,然后分别处理没一个区域。如果你在某个区域里碰上了死胡同,那么就迭代!
自上而下和自下而上的设计方法
自上而下的设计从某个很高的抽象层次开始。你定义出基类或其他不那么特殊的设计元素。在开发这一设计的过程中,你逐渐增加细节的层次,找出派生类、合作类以及其他更细节的设计元素。
自下而上的设计始于细节,向一般性延伸。这种设计通常是从寻找具体对象开始,最后从细节之中生成对象以及基类。
自上而下的论据
居于自上而下方法背后的指导原则是这样的一种观点:人的大脑在同一时间只能集中关注一定量的细节。如果你从一般的类出发,一步步地把它们分解成为更具体的类,你的大脑就不会被迫同时处理过多的细节。
自下而上的论据
有时候自上而下的方法会显得过于抽象,很难入手去做。如果你倾向于用一种更实在的方法,那么可以尝试自下而上的方法。
下面是在做自下而上合成时需要考虑的一些因素:
- 问你自己,对系统需要做的事项,你知道些什么。
- 根据上面的问题,找出具体的对象和职责。
- 找出通用的对象,把它们按照适当方式组织起来-–—子系统、包、对象组合,或者继承-–—看哪种方法合适。
- 在更上面一层继续工作,或者回到最上层尝试向下设计。
其实并没有争议
自下而上策略和自下而上策略的最关键区别在于,前者是一种分解策略而后者是一种合成策略。前者从一般性的问题除法,把问题分解成可控的部分。后者从可控的部分除法,去构造一个通用的方案。这两种方案都有各自的强项和弱项,如果你想在设计中采用它们的时候,就需要予以考虑。
建立实验性原型
使用与产品代码不同的技术,编写最少的代码,完成设计的实验性原型,以低廉的成本解决设计中本质性“险恶”的难题。一旦回答了所提出的问题,就抛弃原型代码,将风险减少到最小。
合作设计
在设计过程中,三个臭皮匠顶得上一个诸葛亮。
- 走到一名同事的办公室前,向他征求一些想法。
- 和同事坐在会议室里,在白板上画出可选的设计方案。
- 和同事在电脑前,用编程语言做出详细的设计。
- 约一名或多名同事来开会,和他们过一遍你的设计想法。
- 安排一次正式检查(21 章)。
- 如果身边没有人能检查你的工作,当你做完一些初始工作后,把它们全放进抽屉,一星期后再来回顾。这时你会把该忘的都忘了,正好可以给自己做一次不错的检查。
- 向公司以外的人求助:在某个特定的论坛或者新闻组里提问。
要做多少设计才够
如果在编码之前还判断不了应该在做多深入的设计,那么宁愿去做更详细的设计。最大的设计失误来自于我误认为自己已经做得很充分,可事后却发现还是做得不够,没能发现其他一些设计挑战。换句话说,最大的设计问题通常不是来自于那些我认为是很困难的,并且在其中做出了很不好的设计的区域;而是来自于那些我认为是简单的,而没有做出任何设计的区域。我几乎没有遇到过因为做了太多设计而受损害的项目。
记录你的设计成果
- 把设计文档插入到代码里
- 用 Wiki 来记录设计讨论和决策
- 写总结邮件
- 使用数码相机
- 保留设计挂图
- 使用 CRC(类、职责、合作者)卡片
- 在适当的细节曾创建 UML 图
对流行的设计方法的评论
正如 P.J. Plauger 所言,“你在应用某种设计方法时越教条化,你所能解决的现实问题就会越少”。请把设计看成是一个险恶的、杂乱的和启发式的过程。不要停留于你所想到的第一套解决方案,而是去寻求合作,探求简洁性,在需要的时候做出原型,迭代,并进一步迭代。你将对自己的设计成果感到满意。