『重构--改善既有代码的设计』读书笔记----Move Method
明确函数所在类的位置是很重要的。这样可以避免你的类与别的类有太多耦合。也会让你的类的内聚性变得更加牢固,让你的整个系统变得更加整洁。简单来说,如果在你的程序中,某个类的函数在使用的过程中,更多的是在和别的类进行交互,调用后者或者被后者调用,那么你就要注意了,你要去判断这个类是否真正适合他原来所在的类。
简单来说,这套手法就是在该函数最常引用的新类中建立一个有着类似行为的新函数,让旧函数变成一个单纯的委托函数或者完全删掉。
Move Method是重构理论的支柱。如果一个类的责任太多,或者一个类和别的类有太多合作,耦合太高,都需要考虑Move Method让类变得简单点。你需要多浏览你自己写的类,观察类的职责是否太多,与别的类的耦合程度问题。当然,如果你进行了Move Field,你也应该做这样的检查,因为我们都知道,函数就跟字段有关,字段都被你移动了,函数肯定要再检查一遍(如果同时需要Move Field和Move Method,一般先进行Move Filed会比较简单)。一旦发现了这样的函数,你要去分析他的调用端和被调用端,如果存在多态,你也应该去考虑继承体系中他所重定义的函数,来判断最终的移动路径。
如果你不能肯定是否该移动一个函数,你就暂且别去考虑移动这个函数,你可以转而去分析其他函数,移动其他函数往往会比你做这个决定来的简单点。如果你发现你移动了其他函数之后你仍然无法判定是否要移动这个函数,那么显然此时的这个函数移动与不移动已经不重要了。
做法:
- 检查源类中被源函数所使用的一切特性(字段和函数),考虑它们是否应该也一起搬移。如果某个特性(函数或者字段)只被你要移动的那个函数单独用到,你可以一起搬移走。如果你发现在源类中其他函数也使用了这个特性,你应该考虑将使用这些特性的所有函数一并搬移走,有时候移动一组函数比一个一个搬移函数来的简单。
- 检查源类的子类和超类,看看是否有该函数的其他声明。如果有其他声明,说明这个函数是呈多态性的,除非你的目标类也呈现相同的继承体系,不然你可能无法搬走。
- 在目标类声明这个函数,接口名称可以一样也可以不一样,由你根据语境把握。
- 将源函数的代码复制到目标函数中,调整这些代码,比如如果目标函数使用了源类中的特性(字段和函数),你就得做出决定如何让目标函数获取到源对象,如果目标类没有相应的引用机制,你应该考虑是否增加参数表,把源对象直接传过去。如果你此时使用了异常,你也应该考虑异常应该放在源类中还是放在目标类中去。
- 编译目标类。
- 决定如何从源函数去引用你新建的目标函数,如果存在一个现成的字段或者函数可以让你拿到目标对象那肯定最好,如果没有,考虑是否可以建立一个这样的取得目标类对象的函数,如果还是比较困难,你就该考虑是否应该给源类新建一个字段用来专门保存目标类对象,别去担心这会不会是一个永久性修改,因为后期的重构项目可能会让你把这些字段给删除掉。
- 修改源函数,让他变成一个单纯的委托函数去调用目标函数。
- 编译,测试。
- 如果你经常在源类中引用目标函数,那么你保留源函数接口让他变成一个单纯的委托函数显然更容易一些。
- 如果你要删除源函数,请将源类中对源函数的调用都替换为对目标函数的调用。你可以每修改一个点就编译测试一次,你也可以利用通过你的编辑器进行批量修改然后进行编译测试,这可能会更简单点。
- 编译,测试。
例子:
class Account... double overdraftCharge() { if (m_type.isPremium()) { double result = 10; if (m_daysOverdrawn > 7) { result += (m_daysOverdrawn - 7) * 0.85; return result; } else { return m_daysOverdrawn * 1.75; } } } double bankCharge() { double result = 4.5; if (m_daysOverdrawn > 0) { result += overdraftCharge(); return result; } } private: AccountType m_type; int m_daysOverdrawn;
我们发现会根据type的不同进行相应的不同运算规则,这提示我们应该把函数放到变化的类中去,即应该放到AccoountType中去。首先,我们观察搬移函数,观察其中使用的源类特性,发现只有m_daysOverdrawn是源类的特性,但我们不选择将他搬移到目标类中而是放在源类中,因为我们可以断定,他不会随着账户的种类变化而变化,注意:这个技巧还是很重要的,将不变化的东西放到固定的类中,将容易变化的东西移动到相应的变化类中,可以让我们后期重构做更好的多态处理。对于这次重构很简单,我们可以直接以传参的方式传给目标函数。接下来,我们在AccountType中新建函数,并且命名,在这里我们可以选择同样的名称,然后把函数代码全部贴过去,然后做相应的改变
class AccountType... double overdraftCharge(int daysOverdrawn) { if (isPremium()) { double result = 10; if (daysOverdrawn > 7) { result += (daysOverdrawn - 7) * 0.85; return result; } else { return daysOverdrawn * 1.75; } } }
把属于自己的isPremium()变成直接调用,以参数的形式替代源类中自己的字段。当我们需要源类的特性的时候其实有四种途径
- 将这个特性也搬移到目标类中
- 建立或使用一个从目标类到源类的引用
- 将源对象当作参数传给目标函数
- 如果所需特性只是个变量,将它作为参数传给目标函数
在这次例子中我们用了第四种方式,只是简单的传递变量参数给目标函数。调整目标函数使之通过编译之后我们进行源函数的修改,让他变成一个简单的委托,因为我们在源类中已经存在的AccountType的字段,所以改起来很简单,改完之后进行编译测试。
class Account... double overdraftCharge() { return m_type.overdraftCharge(m_daysOverdrawn); }
我们可以保留源函数现在这样的样子也可以删除,如果你要删除你就要多做一步,就是找到源函数的所有调用者,然后进行替换,比如Account中的bankCharge()函数用到了这个源函数就进行相应的替换,替换完成之后你就可以删除源函数的声明了。
double bankCharge() { double result = 4.5; if (m_daysOverdrawn > 0) { result += m_type.overdraftCharge(m_daysOverdrawn); return result; } }
如果被搬移的函数不是private,你就必须检查是否有其他类也使用了这个函数,因为C++是强类型语言,你可以简单的删除源函数的声明,让编译器来帮你进行查找。
因为在这个例子之中之用了源类的一个字段,所以我们可以简单的以传参的形式给目标函数,如果源函数中使用了源类的别的函数或者多个字段,我们就必须传源类对象给目标函数
class AccountType... double overdraftCharge(Account *account) { if (isPremium()) { double result = 10; if (account->daysOverdrawn() > 7) { result += (account->daysOverdrawn() - 7) * 0.85; return result; } else { return account->daysOverdrawn() * 1.75; } } }
比如我们要获取daysOverdrawn必须要通过源类的别的函数或者需要源类的多个特性,那么此时我们就必须传源类对象过去让目标函数中进行调用。如果目标函数中需要源类的太多特性,那么你就得进一步进行重构。通常,你需要Extract Method并将其中一部分移回源类。