参数对象究竟要不要?

首先,这篇文章只是我临时写的,之前没有给文章个整体思路,所以文章可能思路会显得有些乱,连我到现在都不知道我要写到什么时候终止。

 

这篇文章的产生是因为看过了横刀天笑的这篇文章,在这篇文章中,作者给出了一个很好的关于代码重构的步骤,以及我们要如何重构代码,提高抽象层次,但是我对其中一点,是对Martin Fowler在《重构》一书中所提出的关于参数对象的观点一直抱有质疑。先别急着反对,也别急着回复,先继续向下看。

1. 先想想Python , F#之类的函数式语言

让我们先想想Python , F#之类的函数式语言(让我们暂时先把Python称之为函数式语言吧),这些函数式语言,也包括C#(从4.0开始),都有一个概念叫做Tuple,我们看看Python如何来声明一个Tuple。

 

1 = ("kym",22,"developer")

 

不知道您看到这段代码是不是感到眼熟呢?我们来写一个方法,就是横刀天笑在文章中用到的一段近似的代码:

 

1 private bool Validate(string userName,int age,string email)
2 {
3     //....Validate
4     return true;
5 }

 

恩,那我们怎么来调用这个方法:

 

1 public static void Main(string[] args)
2 {
3     bool isValid = Validate("kym",22,"developer");
4     if(isValid)
5         Console.WriteLine("Success");
6     else
7         Console.WriteLine("Failure");
8 }

 

其实方法的参数调用是不是和Tuple很类似呢?想想我们把方法改成这样的形式:

1 def Validate(userInfo):
2     bool nameIsValid = userInfo[0]==''
3     // and so on 
4     return isValid

 

然后我们就可以把最之前的Tuple传入了:

 

1 if __name__ == '__main__':
2     print Validate(t)

 

相信这个时候,也许很多人知道我想表达什么概念了。

 

2. Tuple and Class

是的,其实我想表达的就是方法参数从本质上来说就一个Tuple而已,那么接下来让我们想想Tuple和Class的区别所在吧?

从语法分类上来讲,Tuple我们把他叫做序列,和List被划分到同一分类下。而Class即使在那些弱面向对象(让我们暂且这么叫吧),比如Javascript来说,也充其量与Dictionary , Hashtable划分到同一类下,完全不同。

一个Tuple,如果不考虑思维意义的话,我们也完全可以将之用List取代。

t = ('kym',22,'developer') 和 l = ['kym',22,'developer'] 在使用上是没有什么区别的。而同样,我们将之封装成一个User对象也无可厚非:

 

1 class User(Object):
2    def __init__(name,age,work):
3        self.name = name
4        self.age = age
5        self.work = work

 

但是从思维意义上来说,这几者是完全不同的:

其实在C#之类的强类型语言来说表现的更明显,List<T>只能用来用来容纳同一类型的元素,换句话说,List<T>只能用来容纳同一种意义的元素,我们可以称之为兄弟元素。

而Tuple和Class的区别则取决于元素之间的关系如何,让我们想想单一职责原则(SRP), 一个类下应该只有一个能够引起他变化的原因,也就是说,这些元素高内聚的关系,我们才能够把他们塞到一起。而Tuple相对来说,则是更弱的内聚关系。

 

3. 要不要重构

看完了上面的内容,那就让我们想想,究竟要不要重构。或者说要不要参数对象究竟取决于什么?

我们首先,不要把方法参数当成方法参数,而要把他们当成一个Tuple,也就是说我们把要不要重构的问题转换成了是Tuple还是Class的问题。

那么Tuple还是Class取决于什么,不是取决于参数的个数,而是完全取决于参数之间的内聚关系。

 

4. 一些小引申

相信很多人都见过这样的代码:

 

 1 public void Insert(string userName, string password, bool isValid)
 2 
 3     //是否需要验证
 4     if(isValid)
 5     { 
 6         bool userNameValid = userName != '';
 7         bool passwordValid = IsStrongPassword(password);
 8         if(userNameValid && passwordValid)
 9         {
10             this.StoreIntoDatabase(username,password);
11         }
12     }
13     else
14     {
15          this.StoreIntoDatabase(username,password);
16     }
17 }

 

先说这个参数是不是很长,如果你觉得还不够长,那么就把什么Address,Job之类通通塞进去吧!够长了吧!可是我们是不是应该把这些参数封装成一个参数对象?如果是?那么这个类要怎么写?

 

1 class UserArgsForValidation
2 {
3     string userName;
4     string password;
5     bool isValid;
6 }

 

这样真的就面向对象了么?明显不是,可是这个问题到底出在哪?是方法重载的过分滥用。

如果我没有记错,Martin Fowler在书中提到过这一点,如果在方法中出现了对参数的if判断或者switch等条件选择语句,那么就让我们把这个方法拆成InsertWithValid(string userName , string password)和InsertWithoutValid(string userName, string password)这两个方法吧。

所以,在大多数情况下,如果一个方法带有了很多的参数,其实在一般的意义上,这些参数一定是有一个相对高的内聚性,如果没有,诸如上面的情况,就先想想是不是该用用其他的重构原则,如果没有,那么就想想是Tuple还是Class,而不是这个参数有四个,还有八个。

其实有个很好的区分原则,记得在设计模式的圈子有这样一个说法,说如果你设计的类名带有了这个模式的名称,比如ProductFactory, AccessAdapter,那么你就是设计模式滥用了,当然这个说法有些极端。但是我们不妨也试试把这个原则迁移到方法重构上,如果你的类名上带有了Args这样的参数,那么你也是重构的滥用了。

posted @ 2011-04-26 11:18 飞林沙 阅读(1786) 评论(25) 编辑 收藏

 回复 引用 查看   
#1楼 2011-04-26 11:41 横刀天笑      
首先我没说重构。
我想说的只是抽象层次,只是谈代码是向阅读者传递知识的。用Tuple传递参数没错,结果一样正确,但是你传递了什么?
你为什么不把所有的类型都用Hashtable代替,那样扩展性更好。你为什么不把所有的基本类型都用字符串做,免得弄出一堆转型的问题。

 回复 引用 查看   
#2楼 2011-04-26 11:42 Jerry Chou      
啥也不说,支持吧~
 回复 引用 查看   
#3楼 2011-04-26 11:45 横刀天笑      
加一句:面向对象不是将几个字段弄到一个类里面就成了,是日积月累的结果,最后的结果就是我们用代码向阅读者传递着一件关于领域内发生的故事。
 回复 引用 查看   
#4楼[楼主] 2011-04-26 11:49 飞林沙      
@横刀天笑
引用横刀天笑:
首先我没说重构。
我想说的只是抽象层次,只是谈代码是向阅读者传递知识的。用Tuple传递参数没错,结果一样正确,但是你传递了什么?
你为什么不把所有的类型都用Hashtable代替,那样扩展性更好。你为什么不把所有的基本类型都用字符串做,免得弄出一堆转型的问题。


我并不是说用Tuple来传递,Tuple本身不是基于契约的,包括Hashtable一样,都是不适合做方法传递的,我的意思是不妨把方法的参数列表看成一个Tuple。

 回复 引用 查看   
#5楼[楼主] 2011-04-26 11:51 飞林沙      
@横刀天笑

如果你见过然后还放任不管,那么你就丧失了一次提升抽象层次的机会。Robert.Martin在《Clean Code》里谈到,方法的参数不宜过多,如果有过多的参数我们就要特别审视一番。

我只是针对这句话....并没针对其他的

 回复 引用 查看   
#6楼 2011-04-26 11:55 Jerry Chou      
还是要插一杠子 :)

如果只是字段没有方法的参数对象(大多数是BO对象), 横刀天笑的方法承载了更多的信息(VS提供了智能提示,更是加重了这种重构的诱惑),当然这些信息有时很重要,有时不尽然。

还有些参数对象,其实是有方法的,常见的比如重载ToString()方法,也就是类似
class UserArgsForValidation
{
    string userName;
    string password;
    bool IsValid() { /* bla..bla  */};
}

这样的包装,Class将承载更多的信息与功能,Tuple却很难做为替代。

 回复 引用 查看   
#7楼 2011-04-26 11:57 刀 刀      
@横刀天笑
我认为你和楼主说的其实不矛盾。我猜想楼主是想表达这样一个意思:并不是所有情况下都要把参数封装起来。这点显然是正确的。但是楼主举User的例子举的不太合适。换个例子可能会好点,不如这个:
void CopyData(int start, int end, int offset)
{
//封装成void CopyData(CopyDataOption option)
//很多时候是一件蛋疼的事情(注意不是任何时候)
}

 回复 引用 查看   
#8楼 2011-04-26 12:33 吾爱孟夫子      
参数间是不是“内聚”,以前没想过,有见地,受教了。
 回复 引用 查看   
#9楼 2011-04-26 13:36 Virus-BeautyCode      
任何时候都需要分清楚是否需要内聚,不要因为有个class关键字,就随便的把一些东西组合成一个class。其实有些东西,分成几个会更好理解和阅读。

 回复 引用 查看   
#10楼 2011-04-26 13:37 Virus-BeautyCode      
这样的讨论我喜欢,有见地。
 回复 引用 查看   
#11楼 2011-04-26 13:45 Nickelzhang      
引用刀 刀:
@横刀天笑
我认为你和楼主说的其实不矛盾。我猜想楼主是想表达这样一个意思:并不是所有情况下都要把参数封装起来。这点显然是正确的。但是楼主举User的例子举的不太合适。换个例子可能会好点,不如这个:
void CopyData(int start, int end, int offset)
{
//封装成void CopyData(CopyDataOption option)
//很多时候是一件蛋疼的事情(注意不是任何时候)
}

我觉得还是看具体问题,横刀的例子中User是个具有实际意义的实体,而且可能会在多个类中传递,承载着“业务”内涵,做成一个Class也没啥不可以。刀刀这个例子中参数的实体意义不吧,非要弄个Class似乎牵强。
所以我觉得还是具体问题具体分析,没有绝对的对错。

 回复 引用 查看   
#12楼 2011-04-26 13:51 Jeff Wong      
“从语法分类上来讲,Tuple我们把他叫做序列,和List被划分到同一分类下。而Class即使在那些弱面向对象(让我们暂且这么叫吧),比如Javascript来说,也充其量与Dictionary , Hashtable划分到同一类下,完全不同。”
楼主的理解是非常值得称道的,令人眼前一亮。

 回复 引用 查看   
#13楼 2011-04-26 13:53 地狱门神      
@刀 刀
void CopyData(int start, int end, int offset)
搞成
void CopyData(CopyDataOption option)
有时候也是有理由的,比如说要方便同时使用
int start, int end

int start, int length
这时候就可以做成这种样子。
CopyDataOption可以用多种不同的参数去初始化,而CopyData本身只需要要使用它理解的参数。

 回复 引用 查看   
#14楼 2011-04-26 13:55 诺贝尔      
说什么?
 回复 引用 查看   
#15楼 2011-04-26 14:13 take it and go      
越抽象的语言越酷,就怕太多抽象的弄在一起反而使得编译器很囧
 回复 引用 查看   
#16楼 2011-04-26 14:17 刀 刀      
引用地狱门神:
@刀 刀
void CopyData(int start, int end, int offset)
搞成
void CopyData(CopyDataOption option)
有时候也是有理由的,比如说要方便同时使用
int start, int end

int start, int length
这时候就可以做成这种样子。
CopyDataOption可以用多种不同的参数去初始化,而CopyData本身只需要要使用它理解的参数。

所以我都说了“不是任何时候”

 回复 引用 查看   
#17楼 2011-04-26 15:25 深蓝医生      
横刀天笑的文章讲的是一个用抽象的思维方式不断重构代码的过程,而楼主的方式将参数抽象成 Tuple,更上了一层次。
实际上在Lisp看来,参数就是一个表而已,例如:
(+ 1 2)
>3
(+ 1 2 3)
>6
(+ 1 2 (3 4))
>10

所以Tuple 像是一个“参数委托”,实际上就是一个“表”的指针。
看来,说Python是函数式语言还是有道理啊!

都在说tuple 和 hashtable一样
我表示很头疼。 缺少参数名和缺少类型的两个不同种类的残疾怎么还就一样了?

哼哼

 回复 引用 查看   
#19楼 2011-04-26 17:34 newzhq      
我觉得横刀向天是想拿isValid作个例子,并不是说参数一定要对象化吧。
正好写的例子里面可以接受一个系统中原本有的一个User类, 并且可以把isValid放到User这个业务对象的行为里面,这样更合理。
横刀向天并没有表达参数一多就一定要对象化吧,如果这个参数聚合的对象在实际系统中并不是经常会用,那系统岂不是多了很多的类,写起来的时候,维护的人员自己看着也累。

 回复 引用 查看   
#20楼 2011-04-26 21:09 傻人傻福      
受教了
 回复 引用 查看   
#21楼 2011-04-26 21:18 横刀天笑      
@newzhq
还是你理解我~~~555

 回复 引用   
#22楼 2011-04-26 21:27 飞林沙 未登录
@韦恩卑鄙 a-zhewg @waynebaby
引用韦恩卑鄙 a-zhewg @waynebaby:
都在说tuple 和 hashtable一样
我表示很头疼。 缺少参数名和缺少类型的两个不同种类的残疾怎么还就一样了?

哼哼


嗯?我哪里说他们一样了?

 回复 引用   
#23楼 2011-04-26 21:28 飞林沙 未登录
@newzhq
引用newzhq:
我觉得横刀向天是想拿isValid作个例子,并不是说参数一定要对象化吧。
正好写的例子里面可以接受一个系统中原本有的一个User类, 并且可以把isValid放到User这个业务对象的行为里面,这样更合理。
横刀向天并没有表达参数一多就一定要对象化吧,如果这个参数聚合的对象在实际系统中并不是经常会用,那系统岂不是多了很多的类,写起来的时候,维护的人员自己看着也累。


我只是单纯地针对他引用的一句话,反对的不是横刀天笑,是Martin Fowler和Bob Martin

 回复 引用 查看   
#24楼 2011-04-26 23:19 Ivony...      
挺好的文章,跑题跑得挺厉害。
 回复 引用 查看   
#25楼 2011-04-28 15:42 take it and go      
我觉得函数式编程不比对象化的差,呵呵
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 2029093 C9RohiOAaGI=