目录
前言
本章主题
5.1 设计中的挑战
5.1.1 设计在软件构建中所处的角色
5.1.2 设计是一个险恶的问题
5.2 关键的设计概念
5.2.1 软件的首要技术使命:管理复杂度
1. 管理复杂度的重要性
5.2.2 理想的设计特征
5.2.3 设计的层次
第1层:软件系统
第2层:分解为子系统或者包
第3层:分解为类
第4层:分解成子程序
第5层:子程序的内部设计
5.3 设计构造块:启发式方法
找出现实中的对象
形成一致的抽象
当继承能简化设计时就继承
关于设计启发的总结
5.4设计实践
5.5 对流行的设计方法的评论
《Code_Complete_2》持续更新中......_@来杯咖啡的博客-CSDN博客这本书有意设计成使你既可以从头到尾阅读,也可以按主题阅读。1. 如果你想从头到尾阅读,那么你可以直接从第2章“用隐喻来更充分地理解软件开发”开始钻研。2. 如果你想学习特定的编程技巧,那么你可以从第6章“可以工作的类”开始,然后根据交叉引用的提示去寻找你感兴趣的主题。3. 如果你不确定哪种阅读方式更适合你,那么你可以从第3章3.2节“辦明你所从事的软件的类型”开始。.....................https://blog.csdn.net/qq_43783527/article/details/126275083
本章的所讲的设计,你可以理解是“构建活动”中的“详设(详细设计)”。
设计是个庞大的话题,而这一章只能涵盖其中的少数侧面。这里的“设计”可能就是指在编写具体代码之前先用伪代码写出一个类的接口,也可能就是在编码之前画出几个类之间的关系图,还可能就是询问另一位程序员用哪个设计模式会更好。
一个好的类或子程序的设计在很大程度上是由系统的架构所决定的,因此,请确保第3.5 节中所论述过的架构先决条件已经满足。
更多的设计工作是在个别的类和子程序这个层次上完成的,第6章“可以工作的类”,以及第7章”高质量的子程序”中会分别予以介绍。
本节主题:概述设计过程中需要面临哪些挑战。
“软件设计”一词意味着去构思、创造或发明一套方案,把一份计算机软件的规格说明书要求转变为可实际运行的软件。设计就是把需求分析和编码调试连在一起的活动。对于大、小项目都不可或缺。
1、设计是需要经过多次修订和修饰的
你在学校中所开发的程序和你在职业生涯中所开发的程序的主要差异就在于,学校里的程序所解决的设计问题很少(如果有的话)是险恶的。学校里给你的编程作业都是为了让你能从头到尾直线前进而设计的。如果有位老师给你一份编程作业,你刚完成设计时他就把作业的要求改了,然后就在你将要提交完整的程序时,他又对作业的要求再次改动,这时你肯定会十分生气。然而这一过程正是在专业编程中每日可见的真实情形。
2、设计是一个了无章法的过程
另外,说设计了无章法,还因为你很难判断设计何时算是“足够好”了。设计到什么细节才算够?有多少设计需要用形式化的设计符号完成,又有多少设计可以留到编码时再做?什么时候才算完成?因为设计永无止境,因此对上述问题最常见的回答是“到你没时间再做了为止〞。
3、设计是一个启发的过程
正因为设计过程充满了不确定性,因此设计技术也就超于具有探索性—“经验法则”或者“试试没准能行的办法”——而不是保证能产生预期结果的可重复的过程。设计过程中总会有试验和犯错误。在一件工作或一件工作的某个方面十分奏效的设计工具或技术,不一定在下一个项目中适用。没有任何工具是用之四海而皆灵的。
在对导致软件项目失败的原因进行调查时,人们很少把技术原因归为项目失败的首要因素。
1、项目的失败大多数都是由差强人意的需求、规划和管理所导致的。
2、但是,当项目确由技术因素导致失败时,其原因通常就是失控的复杂度。有关的软件变得极端复杂,让人无法知道它究竟是做什么的。当没人知道对一处代码的改动会对其他代码带来什么影响时,项目也就快停止进展了。管理复杂度是软件开发中最为重要的技术话题。在我看来,软件的首要技术使命便是管理复杂度,它实在是太重要了。
Dijkstra 还指出,没有谁的大脑能容得下一个现代的计算机程序 (Dijkstra1972),也就是说,作为软件开发人员,我们不应该试着在同一时间把整个程序都塞进自己的大脑,而应该试着以某种方式去组织程序,以便能够在一个时刻可以专注于一个特定的部分。这么做的目的是尽量减少在任一时间所要考虑的程序量。你可以把它想做是一种心理上的杂耍(边抛边接:通过轮流抛接使两个或两个以上物体同时保持于空中)——程序要求你在空中保持的(精神上的)球越多,你就越可能漏掉其中的某一个,从而导致设计或编码的错误。
在软件架构的层次上,可以通过把整个系统分解为多个子系统来降低问题的复杂度。
保持子程序的短小精悍也能帮助你减少思考的负担。从问题的领域着手,而不是从底层实现细节入手去编写程序,在最抽象的层次上工作,也能减少人的脑力负担。
1、可扩展性(extensibility)。可扩展性是说你能增强系统的功能而无须破坏其底层结构。你可以改动系统的某-部分而不会影响到其他部分。越是可能发生的改动,越不会给系统造成什么破坏。
2、可重用性 (reusability)。可重用性意味着所设计的系统的组成部分能在其他系统中重复使用。
3、高扇入(high fan-in) 。高扇入就是说让大量的类使用某个给定的类。这意味着设计出的系统很好地利用了在较低层次上的工具类 (utility classes)。
4、低扇出(low fan-out)。 低扇出就是说让一个类里少量或适中地使用其他的类。高扇出(超过约7个)说明一个类使用了大量其他的类,因此可能变得过于复杂。研究发现,无论考虑某个子程序调用其他子程序的量,还是考虑某个类使用其他类的量,低扇出的原则都是有益的(Card and Glass 1990; Basili, Briand, and Melo1996)。
5、 层次性(stratification)。层次性意味着尽量保持系统各个分解层的层次性,使你能在任意的层面上观察系统,并得到某种具有一致性的看法。设计出来的系统应该能在任意层次上观察而不需要进入其他层次。
交叉参考: 关于在旧有系统上进行工作的更多内容,请参见第24.5节“重构的策略”。
6、标准技术(Standard techniques)。一个系统所依赖的外来的、古怪的东西越多,别人在第一次想要理解它的时候就越是头疼。要尽量用标准化的、常用的方法,让整个系统给人一种熟悉的感觉。
交叉参考:一种特别有价值的标准化就是使用设计模式,在第 5.3节“查阅常用的设计模式”中会有论述。
第一个层次就是整个系统。有的程序员直接从系统层次就开始设计类,但是往往先从子系统或者包(package)这些类的更高组织层次来思考会更有益处。
在这一层次上设计的主要成果是识别出所有的主要子系统。这些子系统可能会很大,比如说数据库、用户界面、业务规则、命令解释器、报表引擎等。这一层的主要设计活动就是确定如何把程序分为主要的子系统,并定义清楚充许各子系统如何使用其他子系统。对于任何至少需要几周时间才能完成的项目,在这一层次上进行划分通常都是必需的。在每个子系统的内部可能要用到不同的设计方法——请对系统中的每一部分选用最恰当的方法。在图 5-2 中,这一层次的设计是用②注明的。
在这一层次中,有一点特别重要,即不同子系统之间相互通信的规则。如果所有的子系统都能同其他子系统通信,你就完全失去了把它们分开所带来的好处。应该通过限制子系统之间的通信来让每个子系统更有存在意义。
文叉参考:关于高质量的类的具体特性,请见第6章“可以工作的类”
在这一层次上的设计包括识别出系统中所有的类。例如,数据库接口子系统可能会被进一步划分成数据访问类、持久化框架类以及数据库元数据。图 5-2中的第3层就展示了第2层中一个子系统是如何被分解为类的,当然这也暗示着第2层的其他三个子系统也被分解为类了。
类与对象的比较。 面向对象设计的一个核心概念就是对象 (objeet)与类(class)的区分。对象是指运行期间在程序中实际存在的具体实体(enity),而类是指在程序源码中存在的静态事物。
这一层的设计包括把每个类细分为子程序。在第3层中定义出的类接口已经定义了其中一些子程序,而第 4 层的设计将细化出类的私用(private)子程序。当你查看类里面子程序的细节时,就会发现很多子程序都很简单,但也有些子程序是由更多层次化组织的子程序所组成的,这就需要更多的设计工作了。
交叉参考 :关于创建高质量的子程序的具体做法,请见第7章“高质量的子程序”和第8章“防御式编程”。
在子程序层次上进行设计就是为每个子程序布置详细的功能。子程序内部的设计工作通常是由负责该子程序的开发人员来完成的。这里的设计工作包括编写伪代码、选择算法、组织子程序内部的代码块,以及用编程语言编写代码。这一层的设计工作总是需要做的,尽管有时做得很不在意或者很差劲,有时则是经过深思熟虑而出色完成的。在图 5-2 中的第5步就是这一层的设计工作。
由于软件设计是非确定性的,因此,灵活熟练地运用一组有效的启发式方法(试探法),便成了合理的软件设计的核心工作。下面将几小节将阐述一些启发式方法,也即一些思考问题的方法,它们有时能够产生优秀设计成果。你可以把启发式方法看做是指引试错(tail and error)法中“试验”部分的指导书,你也一定曾经用过其中一些方法。因此,以下部分会依据软件的首要技术使命—管理复杂度——的原则来讲解每一种启发式方法。
在确定设计方案时,首选且最流行的一种做法便是“常规的”面向对象设计方法,此方法的要点是辦识现实世界中的对象(objeet,物体)以及人造的(synthetic)对象。使用对象进行设计的步骤是:
基类也是一种抽象,它使你能集中精力关注一组派生类所具有的共同特性,并在基类的层次上忽略各个具体派生类的细节。一个好的接口也是一种抽象,它能让你关注于接口本身而不是类的内部工作方式。一个设计良好的子程序接口也在较低的层次上提供了同样的好处,而设计良好的包(package)和子系统的接口则在更高的层次上提供了同样的好处。
封装填补了抽象留下的空白。抽象是说:“可以让你从高层的细节来看待一个对象。”而封装则说:“除此之外,你不能看到对象的任何其他细节层次。”
继续刚才关于房屋材质的比喻:封装是说,你可以从房屋的外面看,但不能靠得太近去把门的细节都看清楚。可以让你知道哪里有门,让你知道门是开着的还是关着的,但不能让你知道门是木质的、纤维玻璃的、钢质的还是其他什么材质的,当然就更不能让你看到每一根木纤维。
如图 5-8 所示,封裝帮助你管理复杂度的方法是不让你看到那些复杂度。在第 6.2 节中的“良好的封装”中还会对进一步阐述这一话题,讨论如何把它应用于类的设计。
在设计软件系统时,你经常会发现一些大同小异的对象。比如说,在一套账务系统中包含有全职员工和兼职员工,两者的大多数数据是相同的,只是某些数据不同。在面向对象编程时,你可以定义一个代表普通员工的通用(general)类型,然后把全职员工定义为普通员工——除了有一些不同之处;同样,把兼职员工也定义为普通员工——除了一些不同之处。当一项针对员工的操作与具体的员工类别无关时,这一操作就可以仅针对通用员工类型来进行。当该操作需要区别全职员工与兼职员工时,就需要按照不同的方法来处理了。
定义这种对象之间的相同点和不同点就叫“继承”,因为特殊的全职员工类型和特殊的兼职员工类型都从基本员工类型继承了某些特征。
继承的好处在于它能很好地辅佐抽象的概念。抽象是从不同的细节层次来看对象的。
下面是对主要的设计中的启发式方法的总结(以下方式详细的说明请看书):
下列的启发式方法有时也很有用: