JavaRush /Java 博客 /Random-ZH /Java 中重构的工作原理

Java 中重构的工作原理

已在 Random-ZH 群组中发布
学习编程时,很多时间都花在编写代码上。大多数新手开发人员相信这是他们未来的活动。这在一定程度上是正确的,但程序员的任务还包括维护和重构代码。今天我们来谈谈重构。 Java 中重构的工作原理 - 1

JavaRush 课程中的重构

JavaRush 课程两次涵盖了重构主题: 由于这项艰巨的任务,我们有机会在实践中熟悉真正的重构,并且 IDEA 中的重构讲座将帮助您了解使生活变得异常轻松的自动化工具。

什么是重构?

这是代码结构的变化,但不改变其功能。例如,有一个方法比较 2 个数字,如果第一个数字更大则返回true ,否则返回 false
public boolean max(int a, int b) {
    if(a > b) {
        return true;
    } else if(a == b) {
        return false;
    } else {
        return false;
    }
}
结果是代码非常繁琐。即使是初学者也很少写这样的东西,但是却存在这样的风险。看起来,if-else如果你能写一个短 6 行的方法,为什么这里会有一个块:
public boolean max(int a, int b) {
     return a>b;
}
现在这个方法看起来简单而优雅,尽管它与上面的示例做了相同的事情。这就是重构的工作原理:它改变代码的结构而不影响其本质。重构方法和技术有很多,我们将更详细地考虑。

为什么需要重构?

有几个原因。比如追求代码的简单、简洁。该理论的支持者认为,代码应该尽可能简洁,即使需要数十行注释才能理解。其他开发人员认为应该重构代码,以便用最少的注释就可以理解。每个团队选择自己的立场,但我们必须记住,重构不是还原其主要目标是改进代码的结构。 这一全球目标可以包含几个目标:
  1. 重构可以提高对其他开发人员编写的代码的理解;
  2. 帮助发现并修复错误;
  3. 允许您提高软件开发速度;
  4. 总体上改进了软件组成。
如果长期不进行重构,可能会出现开发困难,甚至完全停工。

“代码有味道”

当代码需要重构时,他们会说它“有味道”。当然,不是字面上的意思,但是这样的代码看起来确实不太好看。下面我们将考虑初始阶段的主要重构技术。

不必要的大元素

有些繁琐的类和方法由于其庞大的尺寸而无法有效地使用。

大班

这样的类有大量的代码行和许多不同的方法。对于开发人员来说,向现有类添加功能通常比创建新类更容易,这就是它增长的原因。通常,此类的功能是重载的。在这种情况下,将部分功能分离到单独的类中会有所帮助。我们将在重构技术部分更详细地讨论这一点。

大方法

当开发人员向方法添加新功能时,就会出现这种“气味”。“既然可以写在这里,为什么要把参数检查放在一个单独的方法里呢?”,“为什么要把求数组最大元素的方法分开,就放在这里吧。这样代码就更清晰了”以及其他误解。 重构大型方法有两个规则:
  1. 如果在编写方法时,想要在代码中添加注释,则需要将此功能分离到单独的方法中;
  2. 如果一个方法需要超过 10-15 行代码,您应该识别它执行的任务和子任务,并尝试将子任务分离到单独的方法中。
消除大方法的几种方法:
  • 将一个方法的部分功能分离到一个单独的方法中;
  • 如果局部变量不允许您提取部分功能,您可以将整个对象传递给另一个方法。

使用许多原始数据类型

通常,当类中用于存储数据的字段数量随着时间的推移而增加时,就会出现此问题。例如,如果您使用基本类型而不是小对象来存储数据(货币、日期、电话号码等)或常量来编码任何信息。在这种情况下,一个好的做法是将字段进行逻辑分组并将它们放置在单独的类中(选择一个类)。您还可以在类中包含处理此数据的方法。

长长的选项列表

这是一个相当常见的错误,尤其是与大型方法结合使用时。如果方法的功能重载,或者方法组合了多种算法,通常会发生这种情况。长长的参数列表非常难以理解,而且这样的方法使用起来也不方便。因此,最好传输整个对象。如果对象没有足够的数据,则值得使用更通用的对象或拆分方法的功能,以便它处理逻辑相关的数据。

数据组

逻辑相关的数据组经常出现在代码中。例如,数据库的连接参数(URL、用户名、密码、架构名称等)。如果不能从元素列表中删除单个字段,则该列表是必须放置在单独的类(类选择)中的一组数据。

破坏 OOP 概念的解决方案

当开发人员违反 OOP 设计时,就会出现这种类型的“气味”。如果他没有完全理解这个范式的功能,不完全或不正确地使用它们,就会发生这种情况。

拒绝继承

如果子类使用了父类的最小部分功能,那么它听起来就像一个不正确的层次结构。通常,在这种情况下,根本不会覆盖不必要的方法或引发异常。如果一个类是从另一个类继承的,这意味着几乎完全使用了它的功能。正确层次结构示例: Java 中重构的工作原理 - 2 不正确层次结构示例: Java 中重构的工作原理 - 3

switch语句

操作员可能出了什么问题switch?当它的设计非常复杂时就很糟糕了。这还包括许多嵌套块if

具有不同接口的替代类

几个类实际上做同样的事情,但它们的方法命名不同。

临时场地

如果类包含对象偶尔需要的临时字段,当它充满值时,其余时间它是空的,或者,上帝保佑,null那么代码“有味道”,这样的设计是可疑的决定。

导致改装困难的气味

这些“气味”更严重。其余的主要损害对代码的理解,而这些并不使得修改代码成为可能。当引入任何功能时,一半的开发人员会退出,一半的开发人员会发疯。

并行继承层次结构

当你创建一个类的子类时,你必须创建另一个类的另一个子类。

统一依赖分布

执行任何修改时,您必须查找此类的所有依赖项(使用)并进行许多小的更改。一项更改 - 在许多课程中进行编辑。

复杂的修改树

这种气味与前一种气味相反:更改会影响同一类的大量方法。通常,此类代码中的依赖关系是级联的:更改了一种方法后,您需要修复另一种方法中的某些内容,然后是第三种方法,依此类推。一堂课 - 许多变化。

“垃圾味”

一种相当令人不快的气味,会导致头痛。无用的、不必要的、旧的代码。幸运的是,现代 IDE 和 linter 已经学会警告此类气味。

方法中有大量注释

该方法几乎每一行都有很多解释性注释。这通常与复杂的算法相关,因此最好将代码划分为几个较小的方法并给它们指定有意义的名称。

代码重复

不同的类或方法使用相同的代码块。

懒人班

该类承​​担的功能非常少,尽管其中很多功能都是计划好的。

未使用的代码

代码中未使用类、方法或变量,它们是“自重”。

过度耦合

这类异味的特点是代码中存在大量不必要的连接。

第三方方法

方法使用另一个对象的数据比使用自己的数据更频繁。

不恰当的亲密行为

一个类使用另一个类的服务字段和方法。

长类通话

一个类调用另一个类,后者从第三个类请求数据,从第四个类请求数据,依此类推。如此长的调用链意味着对当前类结构的高度依赖。

类别任务经销商

一个类只需要将任务传递给另一个类即可。也许应该将其删除?

重构技术

下面我们将讨论有助于消除所描述的代码异味的初始重构技术。

选课

该类执行太多功能;其中一些功能需要移至另一个类。例如,有一个类Human还包含住宅地址和提供完整地址的方法:
class Human {
   private String name;
   private String age;
   private String country;
   private String city;
   private String street;
   private String house;
   private String quarter;

   public String getFullAddress() {
       StringBuilder result = new StringBuilder();
       return result
                       .append(country)
                       .append(", ")
                       .append(city)
                       .append(", ")
                       .append(street)
                       .append(", ")
                       .append(house)
                       .append(" ")
                       .append(quarter).toString();
   }
}
将地址信息和方法(数据处理行为)放在一个单独的类中是一个好主意:
class Human {
   private String name;
   private String age;
   private Address address;

   private String getFullAddress() {
       return address.getFullAddress();
   }
}
class Address {
   private String country;
   private String city;
   private String street;
   private String house;
   private String quarter;

   public String getFullAddress() {
       StringBuilder result = new StringBuilder();
       return result
                       .append(country)
                       .append(", ")
                       .append(city)
                       .append(", ")
                       .append(street)
                       .append(", ")
                       .append(house)
                       .append(" ")
                       .append(quarter).toString();
   }
}

方法选择

如果任何功能可以分组在一个方法中,则应将其放置在单独的方法中。例如,计算二次方程根的方法:
public void calcQuadraticEq(double a, double b, double c) {
    double D = b * b - 4 * a * c;
    if (D > 0) {
        double x1, x2;
        x1 = (-b - Math.sqrt(D)) / (2 * a);
        x2 = (-b + Math.sqrt(D)) / (2 * a);
        System.out.println("x1 = " + x1 + ", x2 = " + x2);
    }
    else if (D == 0) {
        double x;
        x = -b / (2 * a);
        System.out.println("x = " + x);
    }
    else {
        System.out.println("Equation has no roots");
    }
}
让我们将所有三个可能选项的计算转移到单独的方法中:
public void calcQuadraticEq(double a, double b, double c) {
    double D = b * b - 4 * a * c;
    if (D > 0) {
        dGreaterThanZero(a, b, D);
    }
    else if (D == 0) {
        dEqualsZero(a, b);
    }
    else {
        dLessThanZero();
    }
}

public void dGreaterThanZero(double a, double b, double D) {
    double x1, x2;
    x1 = (-b - Math.sqrt(D)) / (2 * a);
    x2 = (-b + Math.sqrt(D)) / (2 * a);
    System.out.println("x1 = " + x1 + ", x2 = " + x2);
}

public void dEqualsZero(double a, double b) {
    double x;
    x = -b / (2 * a);
    System.out.println("x = " + x);
}

public void dLessThanZero() {
    System.out.println("Equation has no roots");
}
每个方法的代码变得更短、更清晰。

传输整个对象

当调用带参数的方法时,有时会看到这样的代码:
public void employeeMethod(Employee employee) {
    // Некоторые действия
    double yearlySalary = employee.getYearlySalary();
    double awards = employee.getAwards();
    double monthlySalary = getMonthlySalary(yearlySalary, awards);
    // Продолжение обработки
}

public double getMonthlySalary(double yearlySalary, double awards) {
     return (yearlySalary + awards)/12;
}
在该方法中,employeeMethod分配了多达2行用于获取值并将其存储在原始变量中。有时,此类设计最多需要 10 行。将对象本身传递给方法要容易得多,您可以从中提取必要的数据:
public void employeeMethod(Employee employee) {
    // Некоторые действия
    double monthlySalary = getMonthlySalary(employee);
    // Продолжение обработки
}

public double getMonthlySalary(Employee employee) {
    return (employee.getYearlySalary() + employee.getAwards())/12;
}
简单、简短、简洁。

对字段进行逻辑分组并将它们放在单独的类中

尽管上面的例子非常简单,很多人看到它们时可能会问“到底是谁做的?”,但许多开发人员由于疏忽、不愿意重构代码,或者只是“行了”,类似的结构错误。

为什么重构是有效的

良好重构的结果是程序代码易于阅读,程序逻辑的修改不会成为威胁,新功能的引入不会变成代码解析地狱,而是​​几天愉快的活动。如果从头开始重写程序会更容易,则不应使用重构。例如,团队估计解析、分析和重构代码的劳动力成本高于从头开始实现相同功能的劳动力成本。或者需要重构的代码有很多错误很难调试。知道如何改进代码结构是程序员工作中必须要做的事情。嗯,最好在 JavaRush 上学习 Java 编程——这是一个强调实践的在线课程。1200 多个带有即时验证的任务、大约 20 个迷你项目、游戏任务 - 所有这些都将帮助您对编码充满信心。最好的开始时间就是现在:) Java 中的重构如何工作 - 4

进一步深入重构的资源

关于重构最著名的书是《重构》。改进现有代码的设计”作者:Martin Fowler。还有一本关于重构的有趣出版物,是根据 Joshua Kiriewski 之前的书《Refactoring with Patterns》编写的。说到模板。重构时,了解基本的应用程序设计模式总是非常有用的。这些伟大的书籍将对此有所帮助:
  1. “设计模式” - 由 Eric Freeman、Elizabeth Freeman、Kathy Sierra、Bert Bates 撰写,来自 Head First 系列;
  2. “可读代码,或者说编程是一门艺术”——Dustin Boswell、Trevor Faucher。
  3. Steve McConnell 的《完美代码》,概述了美丽而优雅的代码的原则。
嗯,有几篇关于重构的文章:
  1. 任务艰巨:让我们开始重构遗留代码
  2. 重构
  3. 为大家重构
    评论
    TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
    GO TO FULL VERSION