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中又带来什么样的新观点呢?