软件设计不是一次性的活动,设计师需要逐层处理,渐进完成设计方案。这使得每一次的软件设计都要兼顾设计对象的外部表现和内部结构。
外部表现是从外部(黑箱)所观察到的一个设计对象或其部件的行为,通常更简洁、抽象。内部结构是从内部(白盒)所观察到的设计对象或其部件的组元结构,通常更具体、复杂。
对于一次软件设计任务来说,外部表现是事先指定、必须满足的,更多地体现效用因素。内部结构是需要设计师自行建立的,是设计师的主要工作内容,更多地体现坚固因素。外部表现和内部结构都需要美感,但外部表现更需要简洁,内部结构更需要结构清晰,二者都需要一致。
以收音机为例(如图1-10所示),左图是它的外部表现,简洁明了;右面两个图是它的内部结构,更复杂、细致。很明显,内部结构才是决定一个收音机坚固性的关键,通过外观是无法判定其坚固性的。
图1-10 收音机示例
下面通过3个示例来说明外部表现与内部结构的区别。
1.模块设计
为一个单机版超市销售系统进行模块设计,基本功能有:销售处理、退货、商品出/入库、销售数据统计分析。
这里的基本功能需求(销售处理、退货、商品出/入库、销售数据统计分析)就是模块设计方案的对外表现,如果不能认识到内部结构不同于外部表现,那么很容易就会得出图1-11的模块设计,它为每一个功能都设计了一个独立的模块。
图1-11 外部表现与内部结构示例一(1)
内部结构要考虑坚固性,但很明显图1-11的设计方案根本没有坚固性可言。相比之下,图1-12的方案使用了分层风格,设计了固定的层间接口,使得View、Logic和Data三个层次相对独立,就有了更好的可修改性和可复用性。在图1-12方案的Logic层和Data层,不同模块之间为了消除重复冗余而增加了相互依赖,虽然结构更复杂,但提高了可维护性。
图1-12 外部表现与内部结构示例一(2)
很明显图1-12的设计方案是更好的设计,因为它并不仅仅满足了外部表现的要求,还考虑了内部结构的坚固性。
2.类结构设计
为入库管理进行面向对象静态结构设计:已有库存商品存储在文件FF.dat中,用户在界面上输入入库商品和数量,系统增加FF.dat中相应商品的数量。
如果直接、线性地反映功能要求,那么得出图1-13的设计方案是比较自然的。在图1-13的方案中,在界面接受用户请求后,直接调用Store类的add方法,add方法负责操纵FF.dat文件,把数据增加情况写入文件。
图1-13 外部表现与内部结构示例二(1)
图1-13的设计方案在质量上乏善可陈,它的界面、业务逻辑(数据增加逻辑)、持久化(读写FF.dat)三个方面的代码是混在一起的:
● 复用时,要三个方面一起复用,难以将其中一个方面(例如持久化)独立出来复用,可复用性差。
● 如果三个方面中的一个需要修改,难免会影响到其他两个,可修改性差。
● 如果三个方面中的任意一个需要灵活性,例如多个界面使用相同的业务逻辑或者数据持久化到多个不同的文件,方案难以实现,灵活性和可扩展性差。
相比之下,图1-14的设计方案是更加坚固的。图1-14的设计使用了典型的设计方法:
● 界面、业务逻辑(Store类)、持久化文件都得到了分割处理。
● 将控制逻辑独立封装为Controller,实现了界面和业务逻辑的解耦合,二者可以独立变化。
● 将持久化职责封装为StoreDao,实现了业务逻辑和持久化的解耦合,二者可以独立变化。
● Controller和Dao还可以使用接口方法,被设计得易于调整。
因为进行了解耦合处理,所以图1-14的设计方案有更好的可扩展性、可修改性和可复用性:
● 界面、业务逻辑(Store类)、持久化都很容易独立复用,调整相关的Controller和Dao即可。
● 界面、业务逻辑(Store类)、持久化都很容易修改,不影响接口的情况下完全没有修改副作用,即使修改影响了接口也仅仅是调整Controller和Dao即可。
● 调整Controller和Dao,可以实现界面、业务逻辑(Store类)、持久化的灵活性和可扩展性。
图1-14 外部表现与内部结构示例二(2)
3.程序设计
将1~7数字转变为“星期一”~“星期日”字符串。
一个比较直观的程序实现如图1-15所示。但是这个实现中使用了多项条件分支,是一个可维护性较差的方案。
图1-15 外部表现与内部结构示例三(1)
需要专门说明的是,多项分支条件都会被认为可维护性不佳。在维护工作中,如果修改涉及分支条件,就可能会因为判定条件维护不善而出现修改错误或者连锁副作用。例如:i值从1~7修改为0~6,那么就需要对getWeekDay方法内的每一个分支都做条件修改,只要有一个分支条件修改时不慎出错,就会带来修改错误。
另一个可以帮助判定多项分支条件质量不佳的是方法的复杂度度量指标:圈复杂度。圈复杂度与方法代码内部的分支数成正比,圈复杂度越高,代码质量越低。如果方法内部有多项分支条件,圈复杂度就会高,质量会低。
图1-16是一个可维护性更好的设计方案,它使用了表驱动的编程方法,它的圈复杂度大大降低了,质量提升了。图1-16的方案使得程序更不易读(分离了数据和逻辑),但更易于修改和复用(消除了多项条件分支)。
图1-16 外部表现与内部结构示例三(2)
综合上述3个示例,作为一个设计师要时刻谨记:设计师的主要任务是针对给定的外部表现要求,构造一个坚固、优雅的内部结构,虽然很多时候内部结构可以简单到与外部表现基本相同,但不要想当然地直接将外部表现映射为内部结构。
区分外部表现与内部结构的实质是正确运用抽象和分解思想。
1.抽象与外在表现
抽象是一种观察事物时归纳共性认知而忽略差异细节的方法。抽象是对一个系统的简单描述或指称,强调了系统某些细节与属性的同时抑制了另一些细节与属性。好的抽象强调了对读者或用户重要的细节,抑制了那些至少是暂时的非本质细节或枝节。抽象关注了一个事物的外部表现,可以用来分离事物的基本行为、实现等内部结构,如图1-17所示。
图1-17 抽象示意
例如,在定义函数/方法时,接口就是一种抽象,表达了函数/方法的功能,是外部表现,函数/方法的具体程序代码实现是被分离并隐藏起来的内部结构。在定义类时,类的对外接口就是抽象和外部表现,类的成员变量和成员方法的具体程序代码是被分离和隐藏起来的内部结构。在定义一个模块时,导入/导出接口集体表达了一种抽象和外部表现,模块所包括的所有子模块、类及其他抽象实体单位的组织细节是模块被分离和隐藏起来的内部结构。
2.分解与内部结构
分解是人们处理复杂问题的另一个重要方法。如图1-18所示,分解是将一个复杂事物分割为多个简单部分的组合,将一个复杂事物的理解转换为多个简单事物及其之间关系的理解,通过分而治之,将复杂问题简单化,寻找解决方案。
图1-18 分解示意
例如,在处理复杂的模块内部实现时,可以将模块内部分解为多个子模块或者多个类的协作,然后再逐一处理子模块和各个类,这样要比直接处理整个模块容易得多。在处理类的内部实现时,可以将复杂类分解为多个私有类的组合,将复杂公有接口分解为多个私有接口的组合,处理这些私有类和私有接口都比处理原来的复杂类和复杂接口容易得多。在实现复杂代码时,将代码分解为多个块,处理这些块比处理整个代码要容易得多。
3.层次结构
在处理非常复杂的事物时,一次性的抽象和分解往往不能解决问题,这时人们就会在多个层级上逐次进行抽象和分解的组合应用,建立层次结构,如图1-19所示。
图1-19 抽象和分解多层次组合形成的层次结构示意图
软件设计中人们常常会面对非常复杂的问题,就自然会形成层次结构。在每一个层次上,如果忽略了抽象的作用,认识不到外部表现的简洁性,就会把复杂的内部结构不适宜地暴露给外部,降低了上一层次的可理解性。如果忽略了分解的作用,认识不到内部结构的复杂性,就会简单地按照外部表现的形式构造内部,产生不坚固和没有美感的实现。