Feeds:
Posts
Comments

Posts Tagged ‘设计’

从事软件开发二十几年了,一直想总结出一些自己应遵循的准则。受“围棋十诀”和“太极拳十要”的启发,从一些书和文章中挑出对自己最有帮助的十条。其中有些条目是相互关联的,都是从不同角度强调如何降低系统复杂度、使系统设计更趋合理。

软件编程十要:

去除冗余
名副其实
单元测试
力求简练
减少关联
重视接口
层次结构
信息隐蔽
风格统一
不断改进

1. 去除冗余
去除冗余是提高软件质量的重要途径。在去除冗余的过程中,我们要把大的函数拆成小的函数,把大的类拆成小的类,引入新的接口、新的抽象类,从而使软件结构更趋合理。

冗余不仅包括完全相同的代码,也包括重复出现的类似的逻辑。如重复出现的switch语句、或if-else if-else语句。这些语句的重复出现,通常表明我们应引入一个抽象类和若干子类,通过多态实现这些逻辑。

冗余就像人身上多余的脂肪,会影响软件的“健康”。随着功能的不断增加,软件很容易变得越来越臃肿。所以,代码复查的一项重要任务就是发现及清除冗余,使软件一直处于结结实实的健康状态。

2. 名副其实
名不正,则言不顺,言不顺,则事不成。

每一个类、每一个函数、每一个变量都应有一个恰如其分的名字。类和变量的名字应是一个名词或被一个形容词修饰的名词,函数的名字应是一个动宾词组。名字应越具体(specific)越好,最好是用现实世界里的名称。如果不能给一个类或一个函数一个具体的恰如其分的名字,那就说明我们的设计有问题,很可能是这个类或函数的聚合度不够高。一个函数应且只应做其名称所规定的工作,而不应顺带做其它工作。

检查不恰当的名字是代码复查中的重要事项。

3. 单元测试
我并没有严格遵守测试驱动开发(Test-Driven Development),但我们会在设计方案确定后,决定写哪些单元测试,并从单元测试入手调试新写的类和函数。

单元测试能在一定程度上确保新增加的代码没有破坏已有的功能,增强我们在对现有程序进行改进时的信心。如果没有单元测试,我们在做较大的调整时会畏手畏脚。

在设计时,注意提高类的可单元测试性也有助于提高设计的质量。单元测试性高的类通常与其它类的耦合度比较低。

单元测试还能在某种程度上起到文档的作用。例如,从单元测试代码中,我们能看到函数如何调用,以及函数的先决条件(pre-condition),后置条件(post-condition)以及类的恒定条件(class invariant)。

4. 力求简练
简练是很多优秀的科学家艺术家追求的目标之一。软件也应如此。简练的设计和代码易于理解、易于维护,更灵活、也很可能更高效。

去除冗余能在一定程度上使程序趋于简练,但还不够,要有意识的追求简练。能用简单的设计、简单的数据结构解决问题,就不要用复杂的设计、复杂的数据结构。更不要耍聪明,把简单问题复杂化。

PASCAL的设计者Niklaus Wirth的一个研究生在开发一个编译器时用了一个复杂的数据结构处理符号表(Symbol Table),而且效果不错。Niklaus却认为在大多数情况下,使用链表就足够好了,没有必要使用复杂的数据结构,因为一个函数的局部变量通常不会很多,而且也不应鼓励在一个函数中定义太多局部变量。结果证明链表使程序既简单易懂,总体效率又高。

所以,并不是越高深的设计越好,能解决问题的简单设计往往更合理。当然,这也并不是说我们不需了解掌握复杂的数据结构。老子说“知其雄,守其雌”,我们要了解复杂的数据结构,但只有在必要时才用。

5. 减少关联
没有关联,系统就不能成为一个整体,不能协同工作。关联太多,系统就会难于理解、难于调试、难于单元测试、难于维护,变得很僵硬。所以,关联要尽量少,尽量是单向,尤其要避免循环关联。

模块化是减少关联的重要手段。每一模块都能完成某一特定功能。模块与外界的接口要小。模块如何划分、模块之间的接口如何定义都直接影响关联的多寡。模块划分是软件设计的一项重要工作,是降低系统复杂度的重要手段,须极为慎重。

6. 重视接口
《设计模式》的前言中有一句十分重要的话“Program to an interface, not to an implementation.”要面向接口编程,而不要面向实现编程。面向接口编程会使你的程序灵活,你可以随时改换你所用的实现,而不被实现的改变所影响。致人而不致于人。

接口是类之间、模块之间的合同,因而应该只包含稳定、持久、可靠的信息。理想的接口要全而粹,既满足接口使用者的全部需求,又没有任何冗余,接口的每个方法(method)都提供独立的功能。“全而粹”说起来容易,做起来却很难。依我的经验看首先要追求粹,宁缺勿滥。由简变繁易,由繁变简难。

接口是很多设计原则的基础,如开闭原则(Open-closed principle ),无循环依赖原则(Acyclic Dependencies Principle)等。故接口设计应予以高度重视。

7. 层次结构
软件设计的目的之一是降低系统的复杂度,而层次结构是降低复杂度的重要方法。每一层完成特定的功能,层之间的关联都通过接口,从而使每一层都相对独立,提高可测试性。

领域驱动设计(Domain Driven Design)推荐的层次结构包括用户界面层、应用层、领域层和基础设施层。用户界面层为最高层,领域层为最低层,用户界面层、应用层和领域层都可调用基础设施层。(见http://dddsample.sourceforge.net/architecture.html)

Jeffrey Palermo更进一步提出了洋葱结构(Onion Architecture):把水平的层状图变成圆形层状图,把基础设施层推到最外层,领域层成为系统核心。(见http://jeffreypalermo.com/blog/the-onion-architecture-part-2/)

8. 信息隐蔽
提起信息隐蔽,很多人的理解仅限于类的成员变量应是私有的。其实David L. Parnas的本意远不限于此。David L. Parnas认为“美的设计与丑的设计的区别在于美的设计封装了那些易变的随意的信息。因为(好的)接口只包含可靠的持久的信息,它们会显得简单而给人以美感。要保持这些接口的美,就要调整系统的结构,从而把所有易变的、随意的、很细节的信息隐藏起来。”

所以,在设计类、模块以及层次时,都要注意信息隐蔽。类之间、模块之间、层次之间交换的信息应越少越好,且仅应交换稳定、可靠、持久的信息。当某项设计决策需要修改时,应最好只影响某一个类或一个模块。

9. 风格统一
统一的风格能给人以美感。统一的设计规范、代码规范、文档规范能提高文档和代码的可读性,使系统更容易理解。越容易理解的系统越容易维护、调试,越容易加新功能。
在制定规范时要十分慎重,一旦制定好就要严格遵守。

10. 不断改进
软件工程师要追求完美。这并不是说软件要到完美时才能发布,而是说在开发和维护过程中要发现问题及时解决。发现有冗余、发现有不合适的名字、发现有不合理的结构,应马上修正。否则,问题积累起来,将越来越难清理。Martin Fowler等人著的《重构:改善既有代码的设计》(Refactoring: Improving the Design of Existing Code)是一本极好的指南。

Advertisements

Read Full Post »

软件之形

软件有形状吗?我觉得有。从大的方面讲,软件有外在形状与内在形状之分。外在形状是用户界面,而内在形状是代码和各种结构图。本文只讨论内在形状。

如果有一种特殊的望远镜能让我们观察软件,那当镜头拉到最近时,我们看到的是代码;把镜头推远一点,我们看到的是程序结构图或类结构图;再远一点是模块图;推到最远是总体架构图。

软件最直观的形状是代码的形状。虽然世上没有统一的代码规范,但代码确有好看难看之分。难看的代码或挤成一团、密不透风,或充斥多余空行空格,或缩进(indent)毫无规律,或毫无注释,或注释喧宾夺主、画蛇添足;命名方式一会儿用PASCAL命名法,一会儿用CAMAL命名法,一会儿用HANGARIAN命名法,一会儿用下横杠(underscore)。难看的代码让人不忍卒读。好的代码总是简洁、清爽、疏密有致,该加空行加空行,该加空格加空格,该加注释加注释,该缩进缩进(但缩进太深也不好)。好的代码总是风格统一,类、函数、变量都有统一的命名规范。好的代码让人读来如沐春风。

比代码更抽象更高一级的形状是程序结构图或类结构图。在结构化设计方法中,程序结构图是设计阶段的成果,描述程序之间的调用关系。从程序结构图的形状大致可以看出设计的好坏。Edward Yourdon与Larry L. Constantine在他们合著的《结构化设计》中指出,他们从大量的观察中发现大多数设计良好的系统,其程序结构图的形状都大致像清真寺的穹顶一样:最高层和最底层的模块都相当较少,而中间层的模块相对较多;高层的模块具有较高的扇出,底层的模块具有较高的扇入。

尽管结构化程序设计已逐渐被面向对象程序设计取代,但结构化程序设计的一些准则依然适用,如信息隐蔽、函数和模块的聚合度要高、函数间与模块间的耦合度要低等。而且在设计具有复杂数据流的系统时,结构化程序设计方法仍值得借鉴。

在面向对象程序设计方法中,类结构图是设计阶段的重要结果。从类结构图中又能看出什么呢?首先,类之间的关联切勿过多,最好是单向,尤其要避免循环关联。根据领域驱动设计(Domain Driven Design)方法,类要分成不同的聚集体(Aggregate),聚集体之外的类只能与聚集体的根(Aggregate Root)关联。这样能有效地减少类之间的关联。另外,类的继承关系既不要宽而浅(broad and shallow),也不要过于窄而深(narrow and deep)。一个基类有20个直接子类固然不好,一个有20层深而每个类最多只有一个子类的设计也不可取。动物分类图是一个很好的例子,它既不太宽也不太窄、既不太浅也不太深。

结构图只是软件形状的一种形式。通过不同的“滤镜”,我们可以看到软件不同形式的形状。人们不断发明新方法用形象化的方式揭示软件内部的特性。如Red Gate的.NET Reflector有一个插件CodeMetrics能分析程序的各种内部特性,并用TreeMap显示出来,如下图。通过此图我们可以看出各个类的代码量、复杂度等,看他们是否超出了规定的界限,从而决定如何改善设计。

Figure 1 Code Metrics

围棋有好形愚形之分。棋子挤成一团、密不透风、子力重复的是愚形。棋子疏密有致、距离恰到好处的是好形。软件也颇有相似之处。从代码角度讲,标识符之间、标识符与操作符之间要有空格(括号除外),逻辑片段之间要有空行,不要有重复代码。从结构角度讲,函数之间、模块之间耦合要低。

比程序结构图和类结构图更高一层的是模块结构图,强调的是依赖关系和接口。依赖关系要越少越好,接口要越简单越好。避免循环依赖关系。其目的是控制系统的复杂度,对复杂的系统分而治之。

不识庐山真面目,只缘身在此山中。要真正了解一个软件的全貌,还要把镜头更推远一点,了解其总体架构。

现在,层状结构日益流行。层状结构的主要目的也是为了控制系统的复杂度。层次之间的依赖关系是单向的。层次高的依赖层次低的,层次低的不可以反过来依赖层次高的。下图是Domain Driven Design所推荐的系统结构,其核心是领域层(Domain Layer),再上一层是应用层,最高层是用户界面层。每一层都有不同的基础结构(Infrastructure)支持。层次之间分工明确。

Figure 2 Domain Driven Design推荐的层状结构 (http://dddsample.sourceforge.net/architecture.html

Jeffrey Palermo更进一步提出了洋葱结构(Onion Architecture):把水平的层状图变成圆形层状图,把基础结构层推到最外层,领域层成为系统核心。这与地球与细胞的内部结构图何其相像。

Figure 3 Onion Architecture (http://jeffreypalermo.com/blog/the-onion-architecture-part-2/)

从不同层次、不同角度我们能看到软件的不同形状。从这些形状中,我们能大致看出软件的质量。总体来说,好的形状简洁、平衡、统一,能给人以美感。跟软件之形同样重要或更重要的是软件之神,即设计原则(Design Principles)和设计理念(Design Philosophy或Design Considerations)。真正好的软件当形神兼备。

Read Full Post »