【问题标题】:Refactor switch statement - using an object vs primitive data type重构 switch 语句 - 使用对象与原始数据类型
【发布时间】:2020-02-14 03:30:32
【问题描述】:

我正在通过Refactoring: Improving the Design of Existing Code (1st edition) 这本书学习如何重构 Java 代码。作者进行了如下所示的重构,并解释了他为什么这样做,我不清楚。有人可以帮我理解解释吗?该代码用于电影租赁软件。

public class Movie {

    //Lets call these constants "priceCode".
    public static final int  CHILDRENS = 2;
    public static final int  REGULAR = 0;
    public static final int  NEW_RELEASE = 1;

//...more code here...
}

class Rental {
    private Movie _movie;
    private int _daysRented;
//...more code here...
}

重构之前:

class Rental...
   double getCharge() {
       double result = 0;
       switch (getMovie().getPriceCode()) {
           case Movie.REGULAR:
               result += 2;
               if (getDaysRented() > 2)
                   result += (getDaysRented() - 2) * 1.5;
               break;
           case Movie.NEW_RELEASE:
               result += getDaysRented() * 3;
               break;
           case Movie.CHILDRENS:
               result += 1.5;
               if (getDaysRented() > 3)
                   result += (getDaysRented() - 3) * 1.5;
               break;
       }
       return result;
  }

我们把上面的方法代码移到已经存在的Movie类中。

重构之后:

 class Movie...
   double getCharge(int daysRented) {
       double result = 0;
       switch (getPriceCode()) {
           case Movie.REGULAR:
               result += 2;
               if (daysRented > 2)
                   result += (daysRented - 2) * 1.5;
               break;
           case Movie.NEW_RELEASE:
               result += daysRented * 3;
               break;
           case Movie.CHILDRENS:
               result += 1.5;
               if (daysRented > 3)
                   result += (daysRented - 3) * 1.5;
               break;
       }
       return result;
   }

作者的解释:

根据另一个对象的属性进行切换是个坏主意。如果你必须使用 switch 语句,它应该在你自己的数据上,而不是在别人的数据上。

为此,为了工作,我必须输入租期的长度,这当然是租期的数据。该方法有效地使用了两条数据,即租借的长度和电影的类型。 为什么我更喜欢将租借时长传递给电影而不是电影类型传递给租借?这是因为提议的更​​改都是关于添加新类型。类型信息通常更不稳定。如果我改变电影类型,我希望涟漪效应最小,所以我更喜欢计算电影中的电荷。

我的问题:

为什么基于另一个对象的属性进行切换是个坏主意?

这些事情的真正含义是“类型信息通常倾向于不稳定”和“最小的涟漪效应(已实现)”?这些模糊的词(volatile 和ipple)并不能清楚地解释这种重构的优势。

PS - 请注意,这不是最终代码。本书后面有更多的重构。我知道这是一本过时的 1990 年代的书。尽管如此,这本书看起来有很多关于设计代码的想法和概念,这些想法和概念在今天仍然有用。但是,我不确定这本书在 2020 年会有多大帮助。而且,这本书的最新版本 Refactoring: Improving the Design of Existing Code (2nd edition) 使用了我不知道的 Javascript。

【问题讨论】:

  • 这个解释就像一本关于重构的书的想法一样奇怪。此外,您正在阅读一本写于 1999 年的书,其中讲授了一堆过时的概念(在单击链接之前我想知道它是否是 Java5 之前的书……它是)。如果我是你,我会把这本书送到博物馆,然后读一些类似有效的 java 初学者的东西。这本书比eclipse 还要早
  • 重构后的代码改善了这些类之间的松散耦合,并封装了 Movie 的实现。当 Movie 的新 priceCode 到达时,它只需要在 Movie 类中处理,而不是在所有具有 Movie 作为字段的类中。但是代码前后都没有保持标准。通常是将 priceCode 设为 MovieType 枚举,这可以将 priceCode、乘法因子甚至整个 getCharge(int) 保留在枚举中 - 这可以完全消除 switch 语句的需要
  • 我认为在这个例子的上下文中“类型信息通常倾向于不稳定”意味着像电影类型这样的枚举更有可能根据业务需求而改变。因此,他更喜欢将其封装在电影类中。这样,当添加新电影类型时,他只需要更新电影类。否则,他应该跟踪其他类中对 Movie 类型的所有引用并更新它。没有绝对的方法可以说这是否是一个坏主意。他所说的“涟漪效应”可能是指一个类的变化需要修改其他几个类的情况。
  • IMO,“学习如何重构...”并不是最好的主意,最好了解设计原则、编码标准和常见的最佳实践。之后在处理一些代码时,如果某段代码需要重构,这几乎是自然而然的事情。
  • 一件事引起注意的是声明带有“_”前缀的变量 (Movie _movie),这可能从来都不是 Java 中的代码编写标准。令人欣慰的是,这本书可能涵盖了有用的主题,但与此问题类似,读者必须了解其背后的概念,并且可能在大多数情况下,书中讨论的某些代码至少可以使用 Java8 实现更简单的实现。

标签: java refactoring


【解决方案1】:

为什么基于另一个对象的属性进行切换是个坏主意?

因为如果该属性的类型发生更改,您必须在自己的代码中进行更改。 如果Movie.priceCode 的类型从int 切换到enum,那么您还必须更改Rental 中的代码。但是您将价格计算移至Movie,而不是更改Movie.priceCode 的类型而不影响Rental

这些事情的真正含义是“类型信息通常倾向于不稳定”和“最小的涟漪效应(已实现)”?这些含糊的词(volatile 和ipple)并不能清楚地解释这种重构的优势。

如果您决定为电影(如 HBO 系列、迪士尼系列等)添加更多价格代码,那么您将必须在 MovieRental 类中进行更改。但是,如果您不在Rental 中使用Movie.priceCode,那么即使您从电影中删除priceCode 或重命名或更改其类型,您也根本不需要更改Rental 类。在这种情况下,重构有助于本地化更改的范围。假设您为Movie 编写代码,另一个团队为Rental 编写代码,并且您决定添加更多价格类型。现在您需要通知其他团队您添加了更多类型,为它们提供值、计算逻辑,可能使用您的 Movie 类的新版本构建新工件。但是,如果您只为它们提供方法getCharge(daysRented),那么您可以添加任何新类型而无需通知开销。

【讨论】:

  • 我认为这个答案清楚地表明了“涟漪效应”的含义;但没有解决为什么“类型信息通常容易波动。
【解决方案2】:

为什么基于另一个对象的属性进行切换是个坏主意?

不是。

总体而言,作者似乎指的是长期维护的代码库中可能发生的变化,以及对该代码库进行修改的难度。至少可以想象它们可能是正确的,但这不是任何一种普遍或普遍接受的软件开发原则。

【讨论】:

  • Louis - 一些评论者建议我避开这本书,因为它出自 1990 年代。我不确定我是否应该这样做,因为我想知道书中的想法/概念在 2020 年代是否仍然有用(除了 Vectors 之类的东西)。有人说最好不要像这样阅读有关重构的书籍,而应阅读有关设计原则、最佳实践等的书籍。您有什么建议,尤其是对 Google 的新工程师?谢谢。
  • 信息隐藏是普遍接受的OOP原则。你不认为这就是福勒的意思吗?
【解决方案3】:

为了便于阅读,答案分为两个答案。这是第 1 部分。


解决“作者的解释”:

根据另一个对象的属性进行切换是个坏主意。如果你必须使用 switch 语句,它应该在你自己的数据上,而不是在别人的数据上。

我对这个声明感到惊讶,因为MovieRental 类的私有变量。因此,切换并没有真正发生在另一个对象的属性上。它发生在私有对象的属性上。

作者重构的代码也违反了单一职责原则。 Movie 只负责电影相关信息,不加载租借信息。因此,getCharge() 方法似乎在 Rental 类中的正确位置。


... 为什么我更喜欢将租借时长传递给电影而不是将电影类型传递给租借?这是因为提议的更​​改都是关于添加新类型。

似乎又不正确了。以我的经验,即使在非电影环境中Types 也是相当稳定的常数。示例:汽车的类型相当稳定 - “Car.Sedan”、“Car.SUV”等。


类型信息通常更不稳定。如果我改变电影类型,我希望涟漪效应最小,所以我更喜欢计算电影中的电荷。

为了本地化一个类型的易变性,特定类型应该是一个枚举,在使用它的每个类之外。示例:在MovieRental 之外创建MovieType 枚举。现在每部电影都有一个私有变量,它告诉当前电影是什么类型(或者 priceCode 是什么),并且出租对象将能够从其 _movie 私有变量的 getter 中访问该“类型”。

每当您添加一个类型时,您必须解决两件事:

  1. 在代码的某处创建新类型的表示;
  2. 添加这种新类型自带的业务逻辑。

无论您的实施如何,您都必须解决这两个问题。因此,波纹或多或少是相同的。唯一会改变的是你做出这些改变的地方。当类具有单一职责时,更自然地确定应该在哪里进行更改。

【讨论】:

    【解决方案4】:

    为了便于阅读,答案分为两个答案。这是第 2 部分。


    解决您的问题:

    为什么基于另一个对象的属性进行切换是个坏主意?

    仅仅是因为,另一个对象是“其他”对象。您无法知道该对象内部发生了什么。所以如果依赖内部变量,方法会更稳定。

    举个例子:如果MoviegetPriceCode() 的行为不同,则RentalgetCharge() 方法将返回不同的值,假设在这些调用之间更改了电影的价格代码。

    但是,它不适用于这种场景,因为Movie存储在Rental类的私有变量_movie中。作者好像忽略了。


    这些事情的真正含义是“类型信息通常倾向于不稳定”和“最小的涟漪效应(已实现)”?这些含糊的词(volatile 和ipple)并不能清楚地解释这种重构的优势。

    作者试图强调经常添加/删除/修改类型。 (不过,我的经验并非如此。在我处理过的代码中,类型一直很稳定。)在频繁更新类型的情况下,您希望通过最大限度地减少这些更新的影响来尽量减少头痛。

    如果对于每个“类型”更新,您必须在 20,000 个位置修改您的代码,那么这不是“最小涟漪效应”的一个很好的例子。

    但是,我认为这里遵循单一职责原则将比遵循作者建议的重构更有成效。更改将是相同的,但对于当前和未来的开发人员来说,找到必须进行这些更改的地方将更加直观。

    【讨论】:

      【解决方案5】:

      在我看来,在 Java 中使用 switch-case 语句会导致代码的可读性非常高。由于 Java 是首选的面向对象语言,因此非常感谢以 explicit 方式编写所有内容。

      在 Java 中使用 switch 语句和关键字 break 将导致编程为 procedure 方式。一般来说,我们可以说很难理解break 的含义。

      因此,通过替换为If-else 语句来重构switch 语句将改进clean code

      我只在 Java 中使用 switch-case,即使在 enum 的情况下也是如此,正如 Effective Java 书(第 3 版,第 167 页)中所推荐的那样。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2022-10-18
        • 1970-01-01
        • 1970-01-01
        • 2016-06-05
        • 1970-01-01
        相关资源
        最近更新 更多