这几天在调关于Product和Offer的业务代码。
我们的系统,有应用服务器,主要是处理相关业务的和持久化;有展示层运行在ASP.NET环境下,与应用服务器之间通过WCF通讯。
~~~~~~~~~~~~~~~~~~~~~~~背景介绍完毕~~~~~~~~~~~~~~~~~~~~~~~~~~~
读代码的过程中,发现一个很神奇的对象,名叫productDetailReferencePackage,就像哆啦A梦的神奇的兜一样,这个对象里面各种信息,应有仅有Product,Offer,Agreement。这个对象,是在产品管理页面初始化的时候从应用服务器取到的,并且缓存在Web服务器上;Web服务器为了更加友好地现实信息,有时候会取出一些productDetailReferencePackage上的各种Id信息,把他转化成Description,比如OfferId=>Offer Name的映射;有时候用户在UI上点击按钮会触发一些Ajax请求,比如增加/取消product,会对该神奇对象的Products字段做些操作;总而言之,展示层,会把该对象上面的各种数据拿出来,进行各种各样的操作,最后,神奇对象会传回给App服务器,通过展示层设置的各种flag,找出用户真正想要的改动,然后持久化。
Tell don't ask
看看tell don's ask,这简直就是活生生的反面教材啊。
索取(ask),所谓ask,就是询问对象状态。很多时候,一个对象通过询问对象状态,然后再决定自己的行为,甚至直接修改询问的结果
看这段代码:
2 {
3 .....
4 public void AddOffer(int OfferId)
5 {
6 var offers = productDetailPkg.Offers;
7 offers.Add(OfferId);
8 }
9 .....
10 }
这个可以理解,在UI上有不同的事件,于是,有个Composite,封装了各种跟UI操作对应的方法,这些方法,最终会更新productDetailPkg;
但是问题也来了,不同的行为操作神奇对象,会导致对象的不一致性,比如,增加Offer,之后,productDetailPkg上的Price信息会跟真实的Offer不对应,同样,有RemoveOffer, AddProduct, RemoveProduct, 经过若干次操作后,productDetailPkg已经被改得面目全非了。 后来跟同事下了个结论,有这么个神奇对象,不如不要应用服务器,直接有Web服务器访问数据库算了。。。
痛苦的根源,在于Composite跟productDetailPkg是通过Package的状态,也就是其内部的数据的耦合,而非通过对象的行为耦合。对状态的耦合,一个对象在另一个对象不知情的情况下修改状态,很容易产生副作用,甚至错误,这种条件下,要避免错误,只能是加更多的if...else...了。
而Tell,对象之间通过命令通信(比如UI触发某个事件),对象的状态是在对象内部维护,避免了ask带来的各种副作用。
Query/Command Separation
最早由Bertrand Meyer提出来的,大概意思是,一个类的方法,要么是执行一个行为,改变对象的状态,要么执行一个查询,返回一些数据给调用者,两者而选一。这个原则可以保证“询问问题不会改变问题的答案”。
而Ask对应的就是Query,Tell是Command。有偏激者,甚至认为为了保证tell而不ask,不用给对象加setter和getter,对象所需要的数据,在在构造的时候通过构造函数传入。
我个人认为不一定要禁止类里面声明setter和getter,但我们在写代码的时候有意识的注意哪些行为该属于我们正在创作的类型,我们所创作的方法是在实现一个Command还是暴露一个查询给外界,是不是有必要让外界能够通过这个查询知道内部的状态。
End
可以想象,侧重Tell避免Ask的方式,最终会使代码里面出来很多的功能单一的小类,可能过于琐碎的类型也不一定是个很好的practice,如果对系统性能要求很苛刻的话可能这样不是个很好的办法,不过Kent Beck也强调,通过把大块的代码划分成若干个小类型,通过比较好的命名,可以带来代码的可读性和可维护性的大量提升。因此,大部分情况下,还是推荐 Tell don't ask方式。
文章有点长,不当之处,请指正。