文章目录
软件构建中的设计
1、软件设计的特征与挑战
软件的设计是一个“险恶”的问题,所谓“险恶”就是你必须首先把这个问题"解决"一遍以便能够明确地定义它,然后才能再次真正地解决该问题,这是软件设计的一大挑战。
同时,软件设计无法判断设计到何种程度才是“足够好”了。而且,软件设计存在着诸多限制,比如资源等。你需要在这些限制中做出选择与平衡,所以没有好的设计或不好的设计,只有适合的设计。
合适的设计不是一蹴而就的,是不确定的、演进式的,不断探索、演练、推到、总结而形成的。
满足同一个需求,不同的人会设计出完全不同的设计,所以,设计又具有不确定性。
2、管理复杂度
人的大脑处理能力有限,无法同时关注太多东西,关注东西太多将会导致某些遗漏和混乱。比如,同时让你忙的太多东西,你肯定会丢三落四!虽然你忙的焦头烂额的,但结果却是不尽人意。
就像是玩杂耍:抛在空中的球两个是比较容易控制,增加复杂度两个以上球难度会增大很多很多。在代码设计过程中也是如此,如果关注的太多,将会越复杂,越复杂手忙脑乱势必会造成遗漏导致错误发生。需要同时关注的事情或者逻辑太多是不可取的,但是该软件中存在诸多逻辑和复杂的事情是不可避免的。为了保证代码的各方面性能,应该降低管理复杂度!
如何降低管理复杂度:
- 减少在同一时间所关注的本质复杂度的量
- 避免生成不必要的偶然复杂度
3、好的设计所具有的特征
这些目标之间有时会相互抵触,所以需要在这些目标中做出一套最好的折中方案,这正是设计的挑战与魅力。
-
最小的复杂度(Minimal complexity)
- 在设计时应该只专注于程序的某一部分,安心的忽视其他部分。
-
易于维护(Ease of maintenance)
- 在设计时要从做维护工作的程序员角度思考该设计是否易于维护
-
松散耦合(loose coupling)
- 设计时应该让程序的各个组成部分之间关联性降低最小
- 模块之间,类之间,尽可能的合理抽象、封装和信息隐藏
- 减少关联也就减少了集成、测试与维护工作量
-
可扩展性(extensibility)
- 增强系统的功能时无须破坏其底层结构
-
可重用性(reusability)
- 所设计的系统的组成部分能在其他系统中重复使用
-
高扇入(high fan-in)
- 让大量的类使用某个给定的类(比如工具类)
-
低扇出(low fan-out)
- 一个类里少量或适中地使用其他的类,一般不要超过7个,否则该类就可能会变得复杂
-
可移植性(portability)
- 设计出的系统应该很方便地移植到其他环境中
-
精简性(cleanness)
- 设计出的系统没有多余的部分。如:一个函数一个逻辑,一个类功能单一
- 任何多余的代码也需要开发、Review和测试,并且修改了其他代码后还要重新考虑这部分
-
层次性(stratification)
- 对系统进行分层设计,按层次进行访问交互;对依赖部分进行统一抽象提供一致接口
-
标准技术(Standard techniques)
- 要尽量用标准化的、常用的方法,让整个系统给人一种熟悉的感觉。比如设计模式
4、软件设计的层次
软件设计是分层次性的,软件设计从高到低,大概有如下几个层次:
5、软件设计方法
5.1 找出现实世界中的对象
- 辨识对象及其属性
- 确定可以对各个对象进行的操作
- 确定各个对象可对其他对象进行的操作
- 确定对象的哪些部分对其他对象可见
- 定义每个对象的接口
以上步骤并无特定的顺序来完成,他们经常反复的被执行。经过上述步骤之后,会得到一个高层次的、面向对象的系统组织结构,然后可以通过两种方式进行迭代:在高层次上面进行迭代,以便更好的组织类的结构;或者在每个已经定义好的类上进行迭代,把每个类的设计细化。
5.2 形成一致的抽象
抽象能够让你在关注某一个概念时可以放心的忽略其中一些细节。
抽象是一种以简化的形式来看待复杂操作的能力。
优秀的程序员会在方法的层次上、类接口的层次上以及包接口的层次上进行抽象,这样才能更快、更稳妥的进行开发。
5.3 封装实现细节
封装填补了抽象留下的空白。
抽象是说:“可以让你在高层次的细节上来看待一个对象。”
封装是说:“除此之外,你不能看到对象的任何其他细节层次”。
封装帮助你管理复杂度的方法是不让你看到那些复杂度。
5.4 有可能的情况下继承
继承是面向对象编程中最强大的工具之一。如果使用得当,它能带来极大的益处,然后如果使用过不当,它也会带来极大的弊端。当继承能简化设计时可以考虑使用继承。
5.5 信息隐藏
在软件设计中,有两种信息应该被重点隐藏:
- 隐藏复杂性,因为软件设计的首要原则就是管理复杂性,对复杂性进行隐藏,有助于在设计软件时暂时不关注太多的细节
- 隐藏变化源,这样当变化发生时,其影响就能被限定在局部范围内,这其实也就是面向对象所提倡的封装变化
5.6 找出容易改变的区域
好的程序设计的挑战之一就是适应变化。将不稳定的区域隔离出来,从而把变化的影响隔离在方法、类或包的内部,将影响范围限制在最小范围内。也就是设计模式中常说的封装变化点。
以下是一些容易发生变化的地方
- 业务规则
- 输入输出
- 对硬件的依赖
- 困难的设计区域或构建区域
- 状态变量
- 不要用布尔变量做状态变量,请换成枚举类型
- 数据量的限制
- 非标准的语言特性
5.7 保持松散耦合
耦合表示类和类之间,以及方法和方法之间关系的紧密程度。松耦合的目标是创建出小的、直接的、清晰的类或方法(也称为模块),使它们和其他的类或方法之间的关系尽可能的灵活。应该尽量使创建的模块不依赖或少依赖于其他的模块。
规模(指模块之间的链接数)、可见性、灵活性是耦合的标准。尽量构建规模小、可见性大、灵活性强的模块。
松散耦合的关键在于,一个模块提供了一层附加的抽象,一旦其Ok,你就可以想当然的去使用它。这样就降低了系统的复杂性,使你在同一时间只关注一件事情。如果在同一时间需要同时关注几件事情——内部细节、全局变量等,那么就失去了抽象的能力,模块所具有的管理复杂性的能力也削弱或完全丧失了。
5.8 使用设计模式
设计模式精炼了众多现成的解决方案,可用于解决很多软件开发中常见的问题。有些软件问题需要全新的解决方案,但大部分的问题都和过去遇到过的问题类似,因此可以用类似的解决方案或者模式加以解决。
应用设计模式时存在两大陷阱:
- 强迫代码来适应某个模式。这样做有时反而会把问题复杂化
- 为了模式而模式。不能因为想使用某个模式,而不考虑该模式是否适合就使用它
5.9 其他的设计方法
- 高类聚性
- 构造分层结构
- 严格描述类契约(接口)
- 分配责任(类)
- 为测试而设计(TDD)
- 避免失误
- 有意识的选择绑定时间(动态绑定)
- 创建中央控制点
- 考虑使用蛮力突破
- 画一个图
- 保持设计的模块化
6、软件设计实践
- 迭代(尝试的可能性越多,设计方案就会越好)
- 分而治之
- 自顶向下设计(分解;从一般性问题出发,将问题分解成可控的部分)
- 自底向上设计(合成;从可控的部分出发,构造出一个通用的方案)
- 建立实验性原型(写出用于回答特定设计问题的、量最少且能够随时扔掉的代码)
- 合作设计(找人求助或一起讨论/审查你的设计方案)
- 记录设计结果(以下任意一种方式都可以)
- 正式的设计文档
- 将设计文档插入代码中或 JavaDoc
- 用 Wiki 来记录设计讨论和决策
- 写总结邮件
- 保留设计挂图,让大家能够随时查阅和修改
- 在适当的细节层创建UML图
7、总结
不要停留于你所想到的第一套解决方案,而是去寻求合作,探求简洁性,在需要的时候做出原型,迭代,进一步迭代。好的设计都是迭代的,你尝试设计的可能性越多,你的设计能力就会越强,最终的设计方案就会越好。