再论对象与状态
再论对象与状态
继续上一篇博客(http://www.cnblogs.com/simonblogs/archive/2013/05/23/3095050.html)
考虑实现一个分数类,class Fraction,
class Fraction
{
Fraction Add(Fraction c);
Fraction sub (Fraction c);
Fraction multi (Fraction c);
Fraction divide (Fraction c);
}
分数的构造函数、加减乘除有个问题,就是算完之后需要简化分子分母使其不能有公约数,考虑 3/8 + 1/8 结果是4/8,应该化简为1/2。
这样加减乘除四个函数的最后应该是调用一个私有method,比如叫 Gcd().
Void Fraction::Gcd(result)
{
result.demo = xxx;
result.num = yyy;
}
Gcd()改写了入口参数result,但是这个改写的是中间结果,返回给client的是化简之后的result.所以client是意识不到这个状态的(化简前、化简后)。这里“已化简”是加减乘除的后置条件。如果后置条件总是满足,那么Fraction不是一个状态机,依然是一个值语义。
如果加减乘除不满足那个后置条件,client就需要显示调用gcd()去化简结果。这增加了client的负担,因为client需要保持result的标识。这也破坏了模块性原则,因为client需要耦合一个处理Fraction的method Gcd.
另一方面,加减乘除都满足了后置条件,加减乘除的内部实现也不需要保持一个状态,加减乘除可以说是一个事务,这个事务保证最后commit时,确保结果已经被化简。
总结好的对象设计的原则:
尽可能将状态封装,client只需要感知最少的状态。本例中, Fraction的状态数是0。
一个反面例子就是class的构造函数之后,client马上必须调用一个Init()方法,那是一个很差的设计。
Client内部也尽量减少各个method需要感知的状态,本例中,method内部也没有状态。
Method尽量实现为一个事务,如果method内部抛出异常,尽量使事务回滚,使对象依然处于可用的,逻辑一致的状态。举例说,一个Fraction的构造函数内会设置初始值并且调用gcd化简初始值,如果分母为0应该抛出异常。好的设计是抛出异常在最开始执行,而不是设置完分子值,才发现分母是非法值。回滚事务的意思就是,如果分母非法,请把分子值擦除。