大话重构连载18:最常见的问题
程序员文章站
2022-03-02 13:16:30
...
使用抽取方法,虽然道理十分简单,但实际操作起来却并不是那么容易的。完成抽取方法最大的困难,就是如何处理抽取函数与原函数的数据交换。如同将一颗大树从土壤里拔出来,盘根错节的根茎,那是剪不断理还乱。当代码还没有被抽取出来之前,它们与其它程序都是在一个函数的内部,因此各个代码段可以毫无顾忌地相互交互数据。但当我们将代码从原函数中抽取出来时,抽取出来的代码与原函数中的代码就形成了一道墙,要交换的数据只能通过参数与返回值进行交互,这将给我们带来诸多麻烦。
将代码从原函数中抽取出来,放进新的函数中,首先就会显示许多的错误,即显示许多变量未定义,这就为我们分析有哪些变量需要交互提供了线索。将所有要交互的数据都写到函数的参数中,理论上是没有问题的,却不是一个好的设计。一方面,它显得非常丑陋,长长的参数列表,使程序变得很傻而难于理解;另一方面,它也留下了一个隐患:当程序发生变更时,很可能因为增加参数而改变函数的接口,这是我们不愿看到而尽量避免的。因此,在处理数据交互方面,我更愿意选择使用值对象。值对象(Value Object)就是没有任何业务,仅仅用于传递数据的简单对象。将一堆变量杂乱无章地塞进一个值对象中,不是不可以。实际上,在重构的初期,我们往往就是这样做的。但是,随着重构的深入,值对象也在逐渐优化,最终每个值对象都应当对应现实业务中的一个事物,而它所包含的变量,就是这个事物应当拥有的属性。因此,最终函数传递的参数,应当是几个值对象,以及这些值对象以外的几个变量。建议一个函数的参数不要超过六个,最好是1~4个。
另外一个比较麻烦的问题就是返回值。在Java世界,函数的返回值只能是一个。如果我们的程序需要返回2个,甚至更多时,怎么办呢?返回一个值对象,是个好的办法。但在一个函数中,传递的参数是个值对象,返回值又是另一个值对象,如果都这样设计就会出现大量的值对象,造成值对象的泛滥。将要返回的数据直接塞进参数值对象中,是我们可以采用的另一个好的方法。在Java语言中没有形参实参的区别,如果传递进去的是一个对象,在函数中将该对象的某个属性进行了修改,即使程序已经跳出了抽取函数而回到原函数,该修改也依然起效。通过原函数对该对象的访问,就实质性地实现了抽取函数返回值的功能。从另外一个角度来说,我们的函数,就是对这些值对象中数据的处理过程。这个过程就像一个摩托车生产线,就这辆摩托,你先装上一个发动机,我再安两个*。我们的处理过程被设计成由一系列函数,依次对某个值对象进行连续处理。这样值对象就变成一个载体,在原函数与抽取函数之间交互数据。这样,前面讨论的参数与返回值的问题将得到解决。
比如,我们的系统现在从前端提交了一个表单,我们首先通过Servlet或Action获得前端传递过来的数据,填入一个值对象中。然后该表单可能需要一系列校验,因此我们将这个值对象传递给每个校验函数中。校验函数对值对象进行一些校验,可能还会对值对象写入一些标志。接着可能会调用一些处理函数,从数据库中查询一些数据填入到该值对象中。经过一系列的校验与处理,值对象中的数据被填补完整了,最后交给持久化函数去写库。所有这些函数的调用都只需要传递这个值对象就可以了,甚至可以没有返回值,问题得到解决。
除此之外,实践抽取方法中另一个总是让我们反复思考的问题,就是那个“画红线”的问题。正如前面所说,原函数中的那些块操作,如条件语句、循环语句、try语句,都是进行抽取操作比较明显的标志。但关键问题是,这并不意味着那些块操作中所有的代码都是抽取函数的范围,我们需要考虑许多的因素。当然,最初最直观地想法是将块操作中所有的代码抽取出来。但是,在放入函数,在分析它的参数列表与返回值时,我们可能会发现问题。一些参数,如request、response等,我们并不希望作为参数直接传递进来。这时候,将抽取函数中最前面或最后面的部分代码移出该函数,放回原函数,是一个比较直观的想法。还有,当一些函数功能相似、位置并列时,后续我们可能会对它们进行相应的归并处理,因此在此时抽取函数时,可能期望能统一画线在相同的位置上。以上这些问题都是我们在重构过程中需要仔细考虑的问题。
最后我们要讨论的话题是抽取方法的命名。过去我们对方法的命名常常有些令人摸不着头脑,究其原因,一方面是程序员对这方面过于随意,而另一方面是我们所站的角度不对。我们应当站在使用者的角度命名,而不是开发者的角度。我们应该告诉使用者,调用这个方法会执行什么功能,即它的操作意图是什么。这样使用者才能正确理解,并在合理的地方使用。前例中那个getBlsj()就是一个最好的明证(详见 5.1 超级大函数):起初命名为getBlsj()就是因为站在开发人员的角度它就是获取办理时间的,如果这样命名则其他使用者只能用它来获取办理时间。然而它真实的意图是什么呢?是转换日期格式,因此将其改名为transformDate()。正因为这样的改名,其他开发者才明白可以用它转换其它类似的日期,而不仅仅是办理时间。这足见方法的命名是如此重要,我们不得不察。
此外,我给大家的建议是命名不要过于专业。对于一些专业术语,查字典去找一个连自己都不认识的英语单词,你自己都不认识,怎么能够要求你的读者认识呢!命名的目的是为了增加可读性,因此专业的英语单词无助于提高可读性,应当尽量避免,而一些约定俗成的拼音未尝不可。比如纳税人识别号,有人命名为taxpayerId或者taxpayerCode。但如果整个行业都使用nsrsbh,这样写大家都看得懂,反倒taxpayerId或者taxpayerCode显得比较突兀。再比如发票,有人查了字典,命名为invoice。Invoice可以指发票,也可以指其它各种与商品清单有关的单据,因此不准确,还不如命名为fp简单明了。
大话重构连载首页:http://fangang.iteye.com/blog/2081995
特别说明:希望网友们在转载本文时,应当注明作者或出处,以示对作者的尊重,谢谢!
将代码从原函数中抽取出来,放进新的函数中,首先就会显示许多的错误,即显示许多变量未定义,这就为我们分析有哪些变量需要交互提供了线索。将所有要交互的数据都写到函数的参数中,理论上是没有问题的,却不是一个好的设计。一方面,它显得非常丑陋,长长的参数列表,使程序变得很傻而难于理解;另一方面,它也留下了一个隐患:当程序发生变更时,很可能因为增加参数而改变函数的接口,这是我们不愿看到而尽量避免的。因此,在处理数据交互方面,我更愿意选择使用值对象。值对象(Value Object)就是没有任何业务,仅仅用于传递数据的简单对象。将一堆变量杂乱无章地塞进一个值对象中,不是不可以。实际上,在重构的初期,我们往往就是这样做的。但是,随着重构的深入,值对象也在逐渐优化,最终每个值对象都应当对应现实业务中的一个事物,而它所包含的变量,就是这个事物应当拥有的属性。因此,最终函数传递的参数,应当是几个值对象,以及这些值对象以外的几个变量。建议一个函数的参数不要超过六个,最好是1~4个。
另外一个比较麻烦的问题就是返回值。在Java世界,函数的返回值只能是一个。如果我们的程序需要返回2个,甚至更多时,怎么办呢?返回一个值对象,是个好的办法。但在一个函数中,传递的参数是个值对象,返回值又是另一个值对象,如果都这样设计就会出现大量的值对象,造成值对象的泛滥。将要返回的数据直接塞进参数值对象中,是我们可以采用的另一个好的方法。在Java语言中没有形参实参的区别,如果传递进去的是一个对象,在函数中将该对象的某个属性进行了修改,即使程序已经跳出了抽取函数而回到原函数,该修改也依然起效。通过原函数对该对象的访问,就实质性地实现了抽取函数返回值的功能。从另外一个角度来说,我们的函数,就是对这些值对象中数据的处理过程。这个过程就像一个摩托车生产线,就这辆摩托,你先装上一个发动机,我再安两个*。我们的处理过程被设计成由一系列函数,依次对某个值对象进行连续处理。这样值对象就变成一个载体,在原函数与抽取函数之间交互数据。这样,前面讨论的参数与返回值的问题将得到解决。
比如,我们的系统现在从前端提交了一个表单,我们首先通过Servlet或Action获得前端传递过来的数据,填入一个值对象中。然后该表单可能需要一系列校验,因此我们将这个值对象传递给每个校验函数中。校验函数对值对象进行一些校验,可能还会对值对象写入一些标志。接着可能会调用一些处理函数,从数据库中查询一些数据填入到该值对象中。经过一系列的校验与处理,值对象中的数据被填补完整了,最后交给持久化函数去写库。所有这些函数的调用都只需要传递这个值对象就可以了,甚至可以没有返回值,问题得到解决。
除此之外,实践抽取方法中另一个总是让我们反复思考的问题,就是那个“画红线”的问题。正如前面所说,原函数中的那些块操作,如条件语句、循环语句、try语句,都是进行抽取操作比较明显的标志。但关键问题是,这并不意味着那些块操作中所有的代码都是抽取函数的范围,我们需要考虑许多的因素。当然,最初最直观地想法是将块操作中所有的代码抽取出来。但是,在放入函数,在分析它的参数列表与返回值时,我们可能会发现问题。一些参数,如request、response等,我们并不希望作为参数直接传递进来。这时候,将抽取函数中最前面或最后面的部分代码移出该函数,放回原函数,是一个比较直观的想法。还有,当一些函数功能相似、位置并列时,后续我们可能会对它们进行相应的归并处理,因此在此时抽取函数时,可能期望能统一画线在相同的位置上。以上这些问题都是我们在重构过程中需要仔细考虑的问题。
最后我们要讨论的话题是抽取方法的命名。过去我们对方法的命名常常有些令人摸不着头脑,究其原因,一方面是程序员对这方面过于随意,而另一方面是我们所站的角度不对。我们应当站在使用者的角度命名,而不是开发者的角度。我们应该告诉使用者,调用这个方法会执行什么功能,即它的操作意图是什么。这样使用者才能正确理解,并在合理的地方使用。前例中那个getBlsj()就是一个最好的明证(详见 5.1 超级大函数):起初命名为getBlsj()就是因为站在开发人员的角度它就是获取办理时间的,如果这样命名则其他使用者只能用它来获取办理时间。然而它真实的意图是什么呢?是转换日期格式,因此将其改名为transformDate()。正因为这样的改名,其他开发者才明白可以用它转换其它类似的日期,而不仅仅是办理时间。这足见方法的命名是如此重要,我们不得不察。
此外,我给大家的建议是命名不要过于专业。对于一些专业术语,查字典去找一个连自己都不认识的英语单词,你自己都不认识,怎么能够要求你的读者认识呢!命名的目的是为了增加可读性,因此专业的英语单词无助于提高可读性,应当尽量避免,而一些约定俗成的拼音未尝不可。比如纳税人识别号,有人命名为taxpayerId或者taxpayerCode。但如果整个行业都使用nsrsbh,这样写大家都看得懂,反倒taxpayerId或者taxpayerCode显得比较突兀。再比如发票,有人查了字典,命名为invoice。Invoice可以指发票,也可以指其它各种与商品清单有关的单据,因此不准确,还不如命名为fp简单明了。
大话重构连载首页:http://fangang.iteye.com/blog/2081995
特别说明:希望网友们在转载本文时,应当注明作者或出处,以示对作者的尊重,谢谢!