在以前的文章中,我曾经提到过软件开发中充满了矛盾,一些原则本身就是彼此矛盾的,需要不断在这些矛盾中寻求折中、平衡。这里给出一个源自实际的简单例子,希望能给大家一点启示,只是不知道是否贴切(说明:程序用C++语言描述,T为某个数据类型)。
在定义某个类的接口时,需要定义两个相关变量a和b的getter/setter函数。为了使接口尽量精简,我们采用第一种方法,用一对getter/setter来处理:
getAB(T* pa, T* pb);
setAB(T a, T b); |
但是,如果有时只想对其中一个变量进行存取时,调用getAB/setAB就需要额外的工作,client方的代码就会很累赘。
为了获取a的值,不得不定义一个额外的变量b:
T a, b;
getAB(&a, &b);
cout << a; // only get a |
为了设置a的值而保留b的值,不得不额外调用getAB:
T a, b;
getAB(&a, &b);
a = 10;
setAB(a, b); // only set a |
为此,我们采用第二种方法,将之拆成两对函数:
T getA();
T getB();
void setA(T a);
void setB(T b); |
这样,接口就一下扩展了一倍。但是,事情并未就此结束。有时,像getAB/setAB这样的方式并非只起到了简化接口的作用。在调用setAB的时候,我们可以从a和b相关的角度来考察a和b的合法性,比如:a,b代表某个值域的上下限,那么假定如果a
> b时,设置就不合理,就应该拒绝。而这种合法性检查用第二种方法实现的时候就不是那么顺利了,粗看起来代码应该如下:
int setA(T a)
{
if (a > m_b)
return -1; // error
else
m_a = a;
}
int setB(T b)
{
if (b < m_a)
return -1; // error
else
m_b = b;
} |
如果只是设置a、b中的一个值,倒不会有任何麻烦,这种方法完全胜任。但是,如果同时设置呢?暂且不考虑a、b的初值应该如何取,比如某次对a、b的设置使a、b分别等于5、10,而再次试图重设a、b为15、20时,问题就产生了:
结果变成了a = 5, b = 20(completely error)。
如果要得到正确结果,则需要颠倒调用setA和setB的次序。但是,如果a、b要分别设成1、6呢?亦即,为了保证成功设置,client代码需要十分小心,不同情况,采用不同的调用约定。对于上述情况,用第二种方法很难做到正确的合法性检查,因为函数的signature决定了它无法得知相关的另一个值,从而不能做出正确判断。
所以,事情的演变过程就是:
- 为了使接口精简,我们选择方法一;
- 为了不增加额外的客户代码,我们选择方法二;
- 为了进行合法性检查,我们又不得不选择方法一。
这里总共出现了两种方法,三个原则(“为了……”)。
而当我们最终决定选择方法二时,还是有可能背负着“增加额外的客户代码”这样的罪名。而如果你确实不想如此,或许你会将两种方法结合使用,即把getA/setA,getB/setB,getAB/setAB统统定义为接口。可是,你又可能会被人指责为“接口混乱”,而且对于setA/setB而言,合法性检查仍然是个问题。
结论:在有多种方法可供选择时,存在不同的原则(选择依据),针对实际的情况,我们需要作出决定。这种决定往往不会做到满足所有原则,但一般它应该是最大限度的适合大多数情形。如果,实际的情况不能足以使你作出很肯定的判断,那么,恐怕只有习惯和直觉可以影响你的决定了。只是,或许以后你还会修改你的决定。
ps:为了说明方便,所以这里选用了一个极为简单的例子。坦白讲,可能有夸大之嫌。实际情况下,对于这样的“getter/setter”问题,你多半不会像文中说得那样处于如此为难的境地,除非你是个完美主义者。你可以很快作出决定,因为即使无法满足某条原则,其代价也不会很高。否则,那些软件开发人员,每天就不用写几行代码了,而且会深陷于矛盾的痛苦之中。但是,在这样细微的地方都会存在矛盾,可以想见,用“矛盾重重”来形容软件开发过程,可能是不算夸张的。