重构 switch 语句——使用对象与原始数据类型

Refactor switch statement - using an object vs primitive data type

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

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;
  }

我们将上面的方法代码移动到已经存在的电影中class。

重构后:

 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语句,那应该是在自己的数据上,而不是在别人的数据上。

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

我的问题:

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

"type info generally tends to be volatile" 和 "least ripple effect (is achieved)" 这些东西到底是什么意思?这些模糊的词(volatile 和 ripple)并没有清楚地解释这种重构的优势。

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

Why is it a bad idea to do a switch based on an attribute of another object?

不是。

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

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


解决您的问题:

Why is it a bad idea to do a switch based on an attribute of another object ?

只是因为,另一个对象,是"other"对象。你无法知道那个对象内部发生了什么。因此,如果它们依赖于内部变量,这些方法会更稳定。

举个例子:如果 MoviegetPriceCode() 表现不同,那么 RentalgetCharge() 方法将 return 不同的值,比方说,电影的价格代码在这些调用之间发生了变化。

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


What do these things really mean "type info generally tends to be volatile" and "least ripple effect (is achieved)"? These vague words (volatile and ripple) don't clearly explain the advantage of this refactoring.

作者试图强调类型 added/removed/modified 频繁。 (但我的经验是不同的。类型在我处理过的代码中一直很稳定。)在类型经常更新的情况下,您希望通过最小化这些更新的影响来将您的头痛降到最低。

如果每次 'type' 更新都必须在 20,000 个地方修改代码,那么这不是 "least ripple effect" 的好例子。

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

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


寻址 "Author's Explanation":

It is a bad idea to do a switch based on an attribute of another object. If you must use a switch statement, it should be on your own data, not on someone else’s.

我很惊讶这个说法,因为MovieRentalclass的私有变量。因此,切换并没有真正发生在另一个对象的属性上。它发生在私有对象的属性上。

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

中的正确位置

... Why do I prefer to pass the length of rental to the movie rather than the movie type to the rental? It’s because the proposed changes are all about adding new types.

好像又不对了。根据我的经验,即使在非电影环境中 Types 也是非常稳定的常量。示例:汽车类型非常稳定 - "Car.Sedan"、"Car.SUV" 等


Type information generally tends to be more volatile. If I change the movie type, I want the least ripple effect, so I prefer to calculate the charge within the movie.

为了限制类型的波动性,特定类型应该是一个枚举,在使用它的 every class 之外。示例:在 MovieRental 之外创建 MovieType 枚举。现在每部电影都会有一个私有变量,告诉当前电影是什么类型(或 priceCode 是什么),租用对象将能够从其 [= 的 getter 访问 "type" 19=]私有变量。

无论何时添加类型,都必须解决两件事:

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

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

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

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

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

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

Why is it a bad idea to do a switch based on an attribute of another object?

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

的情况下更改 Movie.priceCode 的类型

What do these things really mean "type info generally tends to be volatile" and "least ripple effect (is achieved)"? These vague words (volatile and ripple) don't clearly explain the advantage of this refactoring.

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