Don’t Ask,Tell
先举一个计算工资的老例子. 不同的员工有各种不同的工资计算方法,这种情况下如何为全公司的员工计算呢? 想想现实中的情况,在公司可能有一个会计部,每到发薪的日子. 会计部就会把全公司的员工资料集中到一起,看看张三是什么类型的员工,如果是小时工就按小时工来计算,如果是办公室主任就按主任的方法来计算. 基于这样的考虑,我们在建模的时候也会相应的建立一个类似于会计部的类,其中定义了一个计算工资的方法,在方法中每计算一个员工前先会询问一个这个员工的类型,如果是A类型怎么算,如果是B类型怎么算,可以想象这个计算工资的方法将十分庞大,并充满了if else(switch case)的代码.将来如果多了一个员工类型,还要来修改这个方法.
Ask员工类型,根据类型选择相应的处理逻辑,这就是以上的方案,可以看出这不是一个好的解决办法. 好的方法是什么呢? 既然我们要根据员工的类型来判断采用何种计算方法,而员工显然知道自己是何种类型以及自己的工作量,那么为什么不交给员工自己来算呢?(具体实现的时候就会涉及到多态等等方法,这不是本文讨论的重点,在此不做详细介绍)此时会计部干什么呢? 计算工资这个活动总要有人发起,所以会计部现在的工作就是Tell所有的员工,让大家计算工资,然后把结果汇总.
现在你可以看到Don’t Ask,Tell的影子了吧.
Ask 带来的坏处:
1. 破坏对象的封装性,你不得不暴露很多的属性供别人Ask.
2. 容易使得一些对象过于复杂(会计部),而一些对象(员工)又过于简单,甚至成为了仅仅包含数据的”哑”对象.
Tell 带来的好处:
1. 更多的考虑责任分配的合理性,方法涉及的数据在哪里方法就应该在哪里. 这样对象的内聚性就大大加强了.
2. 增强了对象的封装性,对象不必暴露更多的属性.
3. 可以充分发挥面向对象的特性,比如多态,减少”哑”对象从而获得更强的可维护性,扩展性以及面对变化的能力.
上面那个计算工资的例子可以说是被用滥了,再来看看在其他场合如何运用Don’t Ask,Tell的思想. 集合的遍历操作,在日常的编程中屡见不鲜. 你是否考虑过它也在一定程度上破坏了Don’t Ask,Tell原则呢? We ask for every element in Collection,then operator on it. Why not just tell the collection to do something. 如果采用ask的办法,在我们的程序中将不断的出现遍历集合的操作. (重复代码,Bad Smell) 所以我们应该将尽量将集合遍历的操作放在集合内即Refactory away External Loops. 如果你留意了.Net2.0对集合类的最新支持ForEach,你就会发现MS也考虑到了这点.
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 List<string> strs = new List<string>();
6 strs.Add("hello");
7 strs.Add("world");
9 strs.ForEach(delegate(string str) // here use anonymous method
10 {
11 Console.WriteLine(str);
12 });
13 }
14 }
不过如我在.Net2.0的集合操作 --- What i hope? 一文中提到,似乎考虑的还不是非常完善.
当然事情没有绝对,有人会问究竟Tell到什么程度呢? 是不是Money还要负责自己的兑换操作? 这就看你把汇率放哪了. 如果汇率也在Money中,那Money可以提供兑换的功能,但是可以看出这不是一个好的设计. 汇率或许应该在Bank对象中,那Money的兑换操作还是放在Bank中比较合适. 这里就涉及到责任分配的问题,责任分配的好坏将决定对象的内聚性及耦合性. 其关键在于责任(方法)所涉及的主要状态(数据)到底在哪个对象当中.
Don’t Ask,Tell可以说是面向对象中一个非常重要的原则(Martin Fowler甚至将其称为面向对象最难理解的一个原则之一),如果将其引申到TDD中又带来什么样的新观点呢?