现在的位置: 首页 > 综合 > 正文

简化函数调用之九 :Introduce Parameter Object(引入参数对象)

2014年02月23日 ⁄ 综合 ⁄ 共 4773字 ⁄ 字号 评论关闭

某些参数总是很自然地同时出现。

以一个对象取代这些参数。

动机(Motivation)

你常会看到特定的一组参数总是一起被传递。可能有好几个函数都使用这一组参数,这些函数可能隶属同一个class,也可能隶属不同的classes 。这样一组参数就是所谓的Date Clump (数据泥团)」,我们可以运用一个对象包装所有这些数据,再以该对象取代它们。哪怕只是为了把这些数据组织在一起,这样做也是值得的。本项重构的价值在于「缩短了参数列的长度」,而你知道,过长的参数列总是难以理解的。此外,新对象所定义的访问函数(accessors)还可以使代码更具一致性,这又进一步降低了代码的理解难度和修改难度。

本项重构还可以带给你更多好处。当你把这些参数组织到一起之后,往往很快可以发现一些「可被移至新建class」的行为。通常,原本使用那些参数的函数对那些参数会有一些共通措施,如果将这些共通行为移到新对象中,你可以减少很多重复代码。

作法(Mechanics)

· 新建一个class,用以表现你想替换的一组参数。将这个设为不可变的(不可被修改的,immutable)。
· 编译。
· 针对使用该组参数的所有函数,实施Add Parameter,以上述新建class 之实体对象作为新添参数,并将此一参数值设为null 。
Ø 如果你所修改的函数被其他很多函数调用,那么你可以保留修改前的旧函数,并令它调用修改后的新函数。你可以先对旧函数进行重构, 然后逐一令调用端转而调用新函数,最后再将旧函数删除。
· 对于Data Clump(数据泥团)中的每一项(在此均为参数),从函数签名式(signature)中移除之,并修改调用端和函数本体,令它们都改而通过「新建 的参数对象」取得该值。
· 每去除一个参数,编译并测试。
· 将原先的参数全部去除之后,观察有无适当函数可以运用Move Method 搬移到参数对象之中。
Ø 被搬移的可能是整个函数,也可能是函数中的一个段落。如果是后者, 首先使用Extract Method 将该段落提炼为一个独立函数,再搬移这一新建函数。

范例:(Example)

下面是一个「帐目和帐项」(account and entries)范例。表示「帐项」的Entry 实际上只是个简单的数据容器:

class Entry...

   Entry (double value, Date chargeDate) {

       _value = value;

       _chargeDate = chargeDate;

   }

   Date getDate(){

       return _chargeDate;

   }

   double getValue(){

       return _value;

   }

   private Date _chargeDate;

   private double _value;

我关注的焦点是用以表示「帐目」的Account,它保存了一组Entry 对象,并有一个函数用来计算两日期间的帐项总量:

class Account...

   double getFlowBetween (Date start, Date end) {

       double result = 0;

       Enumeration e = _entries.elements();

       while (e.hasMoreElements()) {

           Entry each = (Entry) e.nextElement();

           if (each.getDate().equals(start) ||

               each.getDate().equals(end) ||

                (each.getDate().after(start) && each.getDate().before(end)))

           {

               result += each.getValue();

           }

       }

       return result;

   }

   private Vector _entries = new Vector();

client code...

   double flow = anAccount.getFlowBetween(startDate, endDate);

我已经记不清有多少次看见代码以「一对值」表示「一个范围」,例如表示日期范围的start 和end、表示数值范围的upper 和lower 等等。我知道为什么会发生这种情况,毕竟我自己也经常这样做。不过,自从我得知Range 模式[Fowler,AP]之后,我就尽量以「范围对象」取而代之。我的第一个步骤是声明一个简单的数据容器,用以表示范围:

class DateRange {

   DateRange (Date start, Date end) {

       _start = start;

       _end = end;

   }

   Date getStart() {

       return _start;

   }

   Date getEnd() {

       return _end;

   }

   private final Date _start;

   private final Date _end;

}

我把DateRange class 设为不可变,也就是说,其中所有值域都是final ,只能由构造函数来赋值,因此没有任何函数可以修改其中任何值域值。这是一个明智的决定, 因为这样可以避免别名(aliasing)带来的困扰。Java 的函数参数都是pass by value(传值),不可变类(immutable class)正是能够模仿Java 参数的工作方式,因此这种作法对于本项重构是最合适的。

接下来我把DateRange 对象加到getFlowBetween() 函数的参数列中:

class Account...

   double getFlowBetween (Date start, Date end,
DateRange range) {

       double result = 0;

       Enumeration e = _entries.elements();

       while (e.hasMoreElements()) {

           Entry each = (Entry) e.nextElement();

           if (each.getDate().equals(start) ||

               each.getDate().equals(end) ||

               (each.getDate().after(start) && each.getDate().before(end)))

           {

               result += each.getValue();

           }

       }

       return result;

   }

client code...

   double flow = anAccount.getFlowBetween(startDate, endDate, null);

至此,我只需编译一下就行了,因为我尚未修改程序的任何行为。

下一个步骤是去除旧参数之一,以新建对象取而代之。首先我删除start 参数,并修改getFlowBetween() 函数及其调用者,让它们转而使用新对象:

class Account...

   double getFlowBetween (Date end, DateRange range) {

       double result = 0;

       Enumeration e = _entries.elements();

       while (e.hasMoreElements()) {

           Entry each = (Entry) e.nextElement();

           if (each.getDate().equals(range.getStart()) ||

               each.getDate().equals(end) ||

               (each.getDate().after(range.getStart()) && each.getDate().before(end)))

           {

               result += each.getValue();

           }

       }

       return result;

   }

client code...

   double flow = anAccount.getFlowBetween(endDate,
new DateRange (startDate, null));

然后我将参数也移除:

class Account...

   double getFlowBetween (DateRange range) {

       double result = 0;

       Enumeration e = _entries.elements();

       while (e.hasMoreElements()) {

           Entry each = (Entry) e.nextElement();

           if (each.getDate().equals(range.getStart()) ||

               each.getDate().equals(range.getEnd()) ||

               (each.getDate().after(range.getStart()) && each.getDate().before(range.getEnd())))

           {

               result += each.getValue();

           }

       }

       return result;

   }

client code...

       double flow = anAccount.getFlowBetween(new DateRange (startDate,
endDate));

现在,我已经引入了「参数对象」。我还可以将适当的行为从其他函数移到这个新建对象中,进一步从本项重构获得更大利益。这里,我选定条件式中的代码,实施Extract Method 和Move Method,最后得到如下代码:

class Account...

   double getFlowBetween (DateRange range) {

       double result = 0;

       Enumeration e = _entries.elements();

       while (e.hasMoreElements()) {

           Entry each = (Entry) e.nextElement();

           if (range.includes(each.getDate())) {

               result += each.getValue();

           }

       }

       return result;

   }

class DateRange...

   boolean includes (Date arg) {

       return (arg.equals(_start) ||

               arg.equals(_end) ||

                (arg.after(_start) && arg.before(_end)));

   }

如此单纯的提炼和搬移动作,我通常一步完成。如果在这个过程中出错,我可以回到重构前的状态,然后分成两个较小步骤重新进行。

抱歉!评论已关闭.