CodeGym /Java 博客 /随机的 /重构在 Java 中是如何工作的
John Squirrels
第 41 级
San Francisco

重构在 Java 中是如何工作的

已在 随机的 群组中发布
在学习如何编程时,您会花费大量时间编写代码。大多数新手开发人员认为这就是他们将来要做的事情。这部分是正确的,但程序员的工作还包括维护和重构代码。今天我们要谈谈重构。 重构在 Java 中是如何工作的 - 1

在 CodeGym 上重构

CodeGym 课程中两次涉及重构: 大任务提供了通过实践熟悉真正重构的机会,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 设计时,就会出现这些“气味”。当他或她不完全理解 OOP 功能并且未能完全或正确地使用它们时,就会发生这种情况。

未能使用继承

如果一个子类只使用了父类函数的一小部分,那么它就会有层次结构错误的味道。发生这种情况时,通常多余的方法不会被覆盖,或者它们会抛出异常。一个类继承另一个类意味着子类几乎使用父类的所有功能。正确层次结构示例: 重构在 Java 中是如何工作的 - 2不正确层次结构示例: 重构在 Java 中是如何工作的 - 3

开关语句

声明可能有什么问题switch?当它变得非常复杂时就很糟糕了。一个相关的问题是大量的嵌套if语句。

具有不同接口的替代类

多个类做同样的事情,但它们的方法有不同的名称。

临时字段

如果一个类有一个对象只有在设置它的值时才偶尔需要的临时字段,并且它是空的,或者,上帝保佑,null其余时间,那么代码就有味道了。这是一个有问题的设计决定。

使修改变得困难的气味

这些气味更严重。其他气味主要使代码更难理解,但这些会阻止您修改它。当您尝试引入任何新功能时,一半的开发人员会退出,一半会发疯。

并行继承层次结构

当对一个类进行子类化需要您为另一个类创建另一个子类时,这个问题就会显现出来。

均匀分布的依赖

任何修改都需要您查找类的所有用途(依赖项)并进行大量小的更改。一个变化——在许多课程中进行编辑。

复杂的修改树

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

“垃圾味”

一种相当令人不愉快的气味,会引起头痛。无用的、不必要的、旧的代码。幸运的是,现代 IDE 和 linters 已经学会了警告这种气味。

一个方法中的大量注释

一个方法几乎每一行都有很多解释性注释。这通常是由于复杂的算法,所以最好将代码拆分成几个较小的方法并给它们起解释性名称。

重复代码

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

懒人班

一个类只承担很少的功能,尽管它计划很大。

未使用的代码

代码中没有使用类、方法或变量,它们是自重的。

过度连接

这类气味的特点是代码中存在大量不合理的关系。

外部方法

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

不适当的亲密

一个类依赖于另一个类的实现细节。

长课电话

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

任务经销商类

只有将任务发送到另一个类时才需要一个类。也许它应该被删除?

重构技术

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

提取一个类

一个类执行太多功能。其中一些必须转移到另一个班级。例如,假设我们有一个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) {
    // Some actions
    double yearlySalary = employee.getYearlySalary();
    double awards = employee.getAwards();
    double monthlySalary = getMonthlySalary(yearlySalary, awards);
    // Continue processing
}

public double getMonthlySalary(double yearlySalary, double awards) {
     return (yearlySalary + awards)/12;
}
employeeMethod2 整行专门用于接收值并将它们存储在原始变量中。有时这样的构造可能需要多达 10 行。传递对象本身并使用它来提取必要的数据要容易得多:
public void employeeMethod(Employee employee) {
    // Some actions
    double monthlySalary = getMonthlySalary(employee);
    // Continue processing
}

public double getMonthlySalary(Employee employee) {
    return (employee.getYearlySalary() + employee.getAwards())/12;
}

简单,简短,简洁。

逻辑上将字段分组并移动到一个单独的classDespite事实上面的例子非常简单,当你看到它们时,很多人可能会问,“这是谁做的?”,许多开发人员确实因为粗心而犯了这样的结构性错误,不愿意重构代码,或者只是一种“已经足够好了”的态度。

为什么重构是有效的

良好重构的结果是,程序的代码易于阅读,改变其逻辑的前景并不可怕,引入新功能也不会成为代码分析的地狱,而是​​几天的愉快体验. 如果从头开始编写程序会更容易,则不应进行重构。例如,假设您的团队估计理解、分析和重构代码所需的劳动量将大于从头实现相同功能所需的劳动量。或者如果要重构的代码有很多难以调试的问题。知道如何改进代码的结构是程序员工作中必不可少的。学习 Java 编程最好在 CodeGym 上完成,这是一个强调实践的在线课程。1200 多个即时验证任务,约 20 个小项目,游戏任务——所有这些都会帮助您对编码充满信心。最好的开始时间是现在:) 重构在 Java 中是如何工作的 - 4

进一步沉浸在重构中的资源

最著名的关于重构的书是 Martin Fowler 的《重构。改进现有代码的设计》。还有一本关于重构的有趣出版物,它基于之前的一本书:Joshua Kerievsky 的“Refactoring Using Patterns”。说到模式……重构时,了解基本的设计模式总是很有用的。这些优秀的书籍将对此有所帮助: 说到模式……重构时,了解基本的设计模式总是非常有用的。这些优秀的书籍将对此有所帮助:
  1. “设计模式”,作者 Eric Freeman、Elizabeth Robson、Kathy Sierra 和 Bert Bates,来自 Head First 系列
  2. Dustin Boswell 和 Trevor Foucher 的“可读代码的艺术”
  3. 史蒂夫·麦康奈尔 (Steve McConnell) 撰写的“代码大全”,阐述了编写优美优雅代码的原则。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION