重构第二式:搬移方法 (Refactoring 2: Move Method)

毋容置疑,搬移方法(Move Method)应该是最常用的重构手段之一,正因为太常用而且较为简单,以至于很多人并不认为它是一种很有价值的重构,但事实并非如此,在最初的代码诞生之后,有些方法可能会被放在一些不合适的地方,例如,一个方法被其他类使用比在它所在的类中的使用还要频繁或者一个方法本身就不应该放在某个类中时,我们应该考虑将它移到更合适的地方。搬移方法,顾名思义就是将方法搬移至合适的位置,如将方法搬移到更频繁地使用它的类中。与搬移方法相似的还有一种重构手段是搬移字段(Move Field),即搬移属性。

在《重构:改善既有代码的设计》一书中,多种坏味都需要使用搬移方法来进行重构,例如依恋情结(Feature Envy)、霰弹式修改(Shotgun Surgery)、平行继承结构(Parallel Inheritance Hierarchies)、异曲同工的类(Alternative Classes with DifferentInterfaces)、狎昵关系(Inappropriate Intimacy)、纯稚的数据类(Data Class)等,通过搬移方法(Move Method)或者搬移字段(Move Field),可以让某些代码待在更合适的位置。因此,Martin Fowler在《重构》一书中指出,“搬移方法”是重构理论的支柱(Moving methods is the bread and butter of refactoring.),可见该重构的重要性。

下面举一个例子来加以说明:

【重构实例】

在某银行系统中包含一个银行账户类BankAccount和账户利息类AccountInterest,重构之前的代码如下:

  1. package sunny.refactoring.two.before;
  2. class BankAccount {
  3. private int accountAge;
  4. private int creditScore;
  5. private AccountInterest accountInterest;
  6. public BankAccount(int accountAge, int creditScore, AccountInterest accountInterest) {
  7. this.accountAge = accountAge;
  8. this.creditScore = creditScore;
  9. this.accountInterest = accountInterest;
  10. }
  11. public int getAccountAge() {
  12. return this.accountAge;
  13. }
  14. public int getCreditScore() {
  15. return this.creditScore;
  16. }
  17. public AccountInterest getAccountInterest() {
  18. return this.accountInterest;
  19. }
  20. public double calculateInterestRate() {
  21. if (this.creditScore > 800) {
  22. return 0.02;
  23. }
  24. if (this.accountAge > 10) {
  25. return 0.03;
  26. }
  27. return 0.05;
  28. }
  29. }
  30. class AccountInterest {
  31. private BankAccount account;
  32. public AccountInterest(BankAccount account) {
  33. this.account = account;
  34. }
  35. public BankAccount getAccount() {
  36. return this.account;
  37. }
  38. public double getInterestRate() {
  39. return account.calculateInterestRate();
  40. }
  41. public boolean isIntroductoryRate() {
  42. return (account.calculateInterestRate() < 0.05);
  43. }
  44. }

在上述代码中,很明显,AccountInterest使用calculateInterestRate()方法更为频繁,它更希望得到该方法,因此,我们需要成人之美,,将calculateInterestRate()方法从BankAccount类搬移到AccountInterest类中。

重构之后的代码如下:

  1. package sunny.refactoring.two.after;
  2. class BankAccount {
  3. private int accountAge;
  4. private int creditScore;
  5. private AccountInterest accountInterest;
  6. public BankAccount(int accountAge, int creditScore, AccountInterest accountInterest) {
  7. this.accountAge = accountAge;
  8. this.creditScore = creditScore;
  9. this.accountInterest = accountInterest;
  10. }
  11. public int getAccountAge() {
  12. return this.accountAge;
  13. }
  14. public int getCreditScore() {
  15. return this.creditScore;
  16. }
  17. public AccountInterest getAccountInterest() {
  18. return this.accountInterest;
  19. }
  20. }
  21. class AccountInterest {
  22. private BankAccount account;
  23. public AccountInterest(BankAccount account) {
  24. this.account = account;
  25. }
  26. public BankAccount getAccount() {
  27. return this.account;
  28. }
  29. public double getInterestRate() {
  30. return calculateInterestRate();
  31. }
  32. public boolean isIntroductoryRate() {
  33. return (calculateInterestRate() < 0.05);
  34. }
  35. //将calculateInterestRate()方法从BankAccount类搬移到AccountInterest类
  36. public double calculateInterestRate() {
  37. if (account.getCreditScore() > 800) {
  38. return 0.02;
  39. }
  40. if (account.getAccountAge() > 10) {
  41. return 0.03;
  42. }
  43. return 0.05;
  44. }
  45. }

通过重构,BankAccount类更加符合单一职责原则,它负责存储银行账户信息,而对账户的操作(例如计算利息等)方法则转移到其他经常使用且适合它的类中,这样让代码变得更加合理,也有助降低类之间的耦合度,增强代码的可扩展性和可维护性。

重构心得

搬移方法是一种非常实用的重构手段。在本实例中,我们是将方法搬移到被调用次数最多的那个类中,在实际代码重构过程中,还有很多其他涉及到需要搬移方法的场景。

有一种代码味道叫做依恋情结(Feature Envy),指的是一个方法对某个类的兴趣高过对自己所处类的兴趣,例如某个方法需要访问另一个类中大量的数据成员,此时,也非常适合使用搬移方法重构。让方法能够前往它的梦想王国不是件很有意义的事情吗?如果一个方法用到了多个类的功能,那么这个方法放在哪个类中更合适呢?常用的做法是判断哪个类拥有最多被此方法使用的数据,然后将这个方法和那些数据放在一起。在这种情况下,搬移方法的时机不是判断它被哪个类调用更多,而是判断它更需要哪个类提供的数据,这跟上面的重构实例有些区别。

有时候搬移方法时,还需要将只被这个(或这些)方法使用的数据和其他方法一起搬移,需要认真检查方法中使用到的属性(字段)和其他方法,必要时同时执行搬移字段重构(Move Field)。

如果搬移后的方法需要访问原有类中的属性或者方法,可以将原有类的对象作为参数传入新类,在这个过程中可能还需要修改原有类中某些属性或方法的可见性,毕竟它们已经分家了,原有的一些私有的东西是不能再直接访问的。

如果在继承结构中,需要搬移的方法声明在抽象层中,此时要慎重使用本重构,如果太麻烦建议就不要搬移了,免得引入太多错误,毕竟我们要保证抽象层的相对稳定性。

如果有需要,可以为搬移后的函数重新取一个名字,以提高代码的可读性。

如果原有类还需要用到这个已经搬移走的方法,可以通过在原有类中提供一个委托方法的形式来实现,例如method() { TargetClass tc = new TargetClass (); tc.method();},如果原有类中的多个方法(当然也不能太多,否则就没有必要搬走了)需要使用已搬移走的方法,也可以考虑在原有类中增加一个目标类(搬移之后所在类)的对象引用,通过该引用来调用搬走的方法。

当一个类的职责太多时,为了分解类的职责,也可能需要将一些职责搬移到其他类中,此时也需要执行搬移方法重构。这样做,系统将更加满足单一职责原则,有利于提高代码的可复用性和易理解性。

虽然搬移方法是一种简单的重构手段,但是在实际使用中很多人经常会遇到一个问题,如何确定和寻找重构时机?也就是不知道什么时候该用搬移方法来进行重构,特别是当系统较为复杂,类和方法个数非常多时,要准确识别出重构时机并不是一件简单的事情。

希腊马其顿大学(University of Macedonia)学者Nikolaos Tsantalis和Alexander Chatzigeorgiou在搬移方法重构时机识别上开展了相关研究,并在软件工程国际顶级期刊IEEE Transactions on Software Engineering(PS:该期刊是Sunny的2014年目标之一,,加油!)上发表了他们的研究成果,在他们的重构时机识别过程中,使用了Jaccard距离(Jaccard distance)来计算一个待搬移的实体(方法或者属性)和一个类的距离,distance(A, B) = 1 – (|A∩B|/|A∪B|) = 1 – similarity(A,B),将实体搬移到距离最小的类中,他们实现了一个名为JDeodorant的Eclipse插件来实现重构时机识别的半自动化。JDeodorant介绍URL:http://java.uom.gr/~jdeodorant/;JDeodorant安装URL:http://marketplace.eclipse.org/content/jdeodorant。关于这篇论文Sunny就不加详细说明了,爱学习且英语好的童鞋可以自己下载看看(Identification of Move Method Refactoring Opportunities),

【Java重构系列】重构31式之搬移方法的更多相关文章

  1. Java基础系列(31)- 可变参数

    可变参数 JDK1.5开始,Java支持传递同类型的可变参数给一个方法 在方法声明中,在指定参数类型后加一个省略号(...) 一个方法中只能指定一个可变参数,它必须是方法的最后一个参数.任何普通的参数 ...

  2. 【Java多线程系列三】实现线程同步的方法

    两种实现线程同步的方法 方法 特性 synchronized  不需要显式的加锁,易实现 ReentrantLock 需要显式地加解锁,灵活性更好,性能更优秀,结合Condition可实现多种条件锁  ...

  3. 【Java重构系列】重构31式之封装集合

    2009年,Sean Chambers在其博客中发表了31 Days of Refactoring: Useful refactoring techniques you have to know系列文 ...

  4. Java基础系列-Collector和Collectors

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/10748925.html 一.概述 Collector是专门用来作为Stream的coll ...

  5. java正則表達式 match、find匹配位置

    如题.对于java正則表達式这几个方法匹配一次后的,匹配位置搞不太清楚,就写了几个样例.例如以下: String ss="ooaaoo"; Pattern pt=Pattern.c ...

  6. 重构第2天:方法搬移(Move Method)

    现在就重构来说是非常普通的,虽然我们经常会漏掉或忽略一些需要重构的地方.方法搬移,正如所定义的那样,把方法搬移到更适合他的位置.让我们看看下面这一段重构前的代码: 理解:方法搬移,正如所定义的那样,把 ...

  7. 重构改善既有代码设计--重构手法11:Move Field (搬移字段)

    你的程序中,某个字段被其所驻类之外的另一个类更多的用到.在目标类建立一个新字段,修改源字段的所有用户,令它们改用新字段.        动机:在类之间移动状态和行为,是重构过程中必不可少的措施.随着系 ...

  8. 重构改善既有代码设计--重构手法10:Move Method (搬移函数)

    你的程序中,有个函数与其所驻类之外的另一个类进行更多的交流:调用后者,或被后者调用.在该函数最常用引用的类中建立一个有着类似行为的新函数.将旧函数编程一个单纯的委托函数,或是将旧函数完全移除. 动机: ...

  9. java高并发系列 - 第31天:获取线程执行结果,这6种方法你都知道?

    这是java高并发系列第31篇. 环境:jdk1.8. java高并发系列已经学了不少东西了,本篇文章,我们用前面学的知识来实现一个需求: 在一个线程中需要获取其他线程的执行结果,能想到几种方式?各有 ...

随机推荐

  1. Avoiding “will create implicit index” NOTICE

    执行PgSql避免 notice 信息,执行之前加入以下语句调整报错级别即可: SET CLIENT_MIN_MESSAGES = ‘WARNING’;

  2. TDirectory.Move移动或更名目录

    使用函数: System.IOUtils.TDirectory.Move 定义: class procedure Move(const SourceDirName, DestDirName: stri ...

  3. cocos2dx3.4 导出节点树到XML文件

    l利用cocostudio做UI和场景时,经常要去获取某个节点,cocostudio2.1开始加入了文件的概念,可以创建场景,节点,层等文件,把公用的东西创建到文件里,然后把这个文件拖到场景里使用,达 ...

  4. 如何快速正确的安装 Ruby, Rails 运行环境-b

    对于新入门的开发者,如何安装 Ruby, Ruby Gems 和 Rails 的运行环境可能会是个问题,本页主要介绍如何用一条靠谱的路子快速安装 Ruby 开发环境.次安装方法同样适用于产品环境! 系 ...

  5. Unity给力插件之MegaFiers

    这是一个关于网格变形的插件.其中有非常多的功能. 这是它的API地址:http://www.west-racing.com/mf/ 花了2天的时间实践并整理了其中绝大多数的功能,只有一些关于特殊格式的 ...

  6. 两台主机打通ssh

    ssh打通基本概念:如果需要通过SSH进行远程登录,我们一般是需要手动输入密码,但如果将SSH之间的权限打通的话,就可以实现无密码登录.这对shell脚本的定时执行有很大的帮助. (一),生成秘钥,先 ...

  7. LightOj_1364 Expected Cards

    题目链接 题意: 一副牌, 每个花色13张牌,加上大小王,共54张. 遇到大小王可以代替其中某种花色. 给定C, D, H, S. 每次抽一张牌, 问抽到C张梅花, D张方块, H张红桃, S张黑桃所 ...

  8. jQuery实现iframe的自适应高度

    假设我们在当前页面要嵌套一个iframe 1 <iframe id="myframe" src="test.html" height="240& ...

  9. Word两端对齐问题

    主要是有英文的话,选择两端对齐后字符间距变大,这时候可以选择段落-----中文版式----------允许西文在单词中间换行,这个在最左上角的word选项里也有.遇到好几次这样的,主要是在参考文献里.

  10. 启用EXCHANGE反垃圾邮件功能和重建EXCHANGE邮件系统帐号

    How to recreate System Mailbox , FederatedEmail & DiscoverySearchMailbox in Exchange 2010 http:/ ...