欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

设计的核心任务之一:层次的控制 C++软件设计层次控制核心任务 

程序员文章站 2024-02-07 13:02:28
...


对于软件而言,层次是让人又爱又恨的东西。

 

很多问题是通过增加层次解决的,但另外一部分问题也是因为层次而导入的。我们来分别看几个例子。

 

例1:很多时候我们并不希望最终的应用绑定于某个指定平台,比如:Windows。为了达成这种跨平台的目的,就需要在OS和应用之间加入一个中间层,这个中间层负责屏蔽不同OS的差异。实际上,Java虚拟机等走的都是这样一条路线。

 

例2:当使用XML文件保存配置信息的时候,我们并不希望XML的结构在整个程序中随处可见。比如说:现在我们在Configuration/OutputFolder节点下保存了缺省保存目录,但将来很可能节点变成了Configuration/OutputFolder/Save。为了斩开与XML结构的关联,那么我们需要加入一个新的抽象层,来表征XML文件,再通过GetSaveFolder()这样的方法对缺省保存目录进行获取。

 

通过加入层次解决问题的同时,新的问题也随之发生。在眼前蒙上一层薄纱可以防止眼睛被风沙所伤害,但如果蒙上十层,那更严重的后果将会出现---你看不到路了。

 

从可理解的角度看,只有某一功能所涉及的所有层次,所关联变量的各种可能性都被澄清之后,具体的代码才可能真的被理解。在排错的时候尤其如此。我们来看一个例子:

 

在用C++创建集合类的时候,我们可能希望对集合类的内存使用方法进行更多的定制。

有时候我们可能想预先保留一块内存,接下来在这块内存上进行二次分配来存放各种小的对象。

有时候我们也可能想直接在磁盘上分配空间存放放入集合类的对象。

为了达成上面这些目的,层次又一次站出来发挥作用,我们可以建立allocator这样的类来建立一层抽象,创建集合类的时候,可以通过指定不同的allocator来控制内存使用的方法。

 

这应该是不错的设计方法,C++标准模板库里就是这么做的。

接下来我们来看一旦出了错的情形。

我们可能希望放入集合类的对象总是进行浅拷贝(swallow copy),为此重载了类的拷贝构造函数和赋值函数,但最终发现当对象被放入集合类的时候,不知道为什么总是不成功。

 

这个时候,逻辑上程序没有任何问题,因此只靠脑子想是完全解决不了问题了。为了排错,我们只能启动调试器。

调试的过程中,我们通常并不能一下就确认问题和allocator究竟有没有关联,所以为了找出问题所在,我们也要对allocator这一层次做点分析。这种分析的开销事实上就成为添加allocator这一层次的代价。

 

通过上述的例子我们可以大致体会到层次这把双刃剑的威力。

 

通过层次我们可以让软件更灵活,抽象更充分;但层次也会把达成某一功能所必须的信息进行分割,增加复杂度。所以层次的多少往往并非是一个对与错的问题,而是一个程度问题,究竟什么样的层次才合适,是需要现场的人进行判断的。

曾经有人说过这样一句话,可供我们参考,他说:如果你知道自己在做什么三层足够;如果你不知道自己在做什么,那么十七层也没用。

我个人是认同这一观点的,除非特别的情形,要努力控制层次在三层左右,否则宁愿牺牲一点抽象。

 

和层次相关的问题主要有两个:一个是层次的多少;另一个则是层次的一致性。如果说层次的多少是一个合适与否的问题,那么层次一致性则是一个是非问题。

某一个层次上所体现出来的东西应该具有层次一致性。

 

这和前面在讨论的需求中的层次问题类似。

比如说:如果有一个类叫Cat,那这个类的接口,可以有返回猫的颜色,猫的种类,这些属性是在一个抽象层面上的。但如果突然有一个接口是返回猫的个数,那么大多时候就会让人感到奇怪。

我们感觉到奇怪的本质原因是抽象的层次出现了不一致性。颜色和种类这种属性属于具体的某一只猫,而个数则属于猫的集合。

 

这种抽象层次的不一致实际上是增加耦合度的一个主要元凶。

 

很不幸的是,这又是一个要依赖于个人技能的地方。眼下还看不到自动判断抽象层次是否合适的方法。