[英]Refactor switch statement - using an object vs primitive data type
我正在阅读《重构:改进现有代码的设计》(第 1 版)一书,学习如何重构 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 和ripple)并没有清楚地解释这种重构的优势。
PS - 请注意,这不是最终代码。 本书后面有更多的重构。 我知道这是一本 1990 年代的过时书。 尽管如此,这本书似乎有很多关于设计代码的想法和概念,这些想法和概念在今天仍然有用。 但是,我不确定这本书在 2020 年会有多大帮助。而且,这本书的最新版本重构:改进现有代码的设计(第 2 版)使用了我不知道的 Javascript。
为什么根据另一个对象的属性进行切换是个坏主意?
不是。
总的来说,作者似乎指的是长期维护的代码库中可能发生的变化,以及修改该代码库的难度。 至少可以想象它们可能是正确的,但这不是软件开发的任何一般或普遍接受的原则。
为便于阅读,答案已分为两个答案。 这是第 1 部分。
根据另一个对象的属性进行切换是一个坏主意。 如果您必须使用 switch 语句,它应该使用您自己的数据,而不是其他人的数据。
我对这个声明感到惊讶,因为Movie
是Rental
类的私有变量。 因此,切换并不是真正发生在另一个对象的属性上。 它发生在私有对象的属性上。
作者重构的代码也违反了单一职责原则。 Movie
只负责电影相关信息,不加载租借信息。 因此, getCharge()
方法似乎在Rental
类中的正确位置。
... 为什么我更喜欢将租借时长传递给电影而不是电影类型传递给租借? 这是因为提议的更改都是关于添加新类型的。
好像又不对了。 根据我的经验,即使在非电影环境中Type
也是非常稳定的常量。 例如:汽车的类型非常稳定——“Car.Sedan”、“Car.SUV”等。
类型信息通常更易变。 如果我改变电影类型,我想要最小的涟漪效应,所以我更喜欢计算电影内的费用。
为了本地化类型的易变性,特定类型应该在使用它的每个类之外成为一个枚举。 示例:在Movie
和Rental
之外创建MovieType
枚举。 现在每部电影都有一个私有变量,它告诉当前电影是什么类型(或 priceCode 是什么),并且租借对象将能够从其_movie
私有变量的 getter 访问该“类型”。
无论何时添加类型,都必须解决两件事:
无论您的实施如何,您都必须同时解决这两个问题。 因此,纹波或多或少是相同的。 唯一会改变的是你做出这些改变的地方。 当类具有单一职责时,更自然地确定应该在哪里进行更改。
为什么根据另一个对象的属性进行切换是个坏主意?
因为如果该属性的类型发生更改,您必须在自己的代码中进行更改。 如果Movie.priceCode
类型从int
切换为enum
那么您还必须更改Rental
代码。 但是您将价格计算移动到Movie
不是更改Movie.priceCode
类型而不影响Rental
。
这些事情的真正含义是“类型信息通常是易变的”和“最小的涟漪效应(已实现)”? 这些模糊的词(volatile 和ripple)并没有清楚地解释这种重构的优势。
如果您决定为电影(如 HBO 系列、迪士尼系列等)添加更多价格代码,则您必须在Movie
和Rental
类中进行更改。 但是,如果您不在Rental
使用Movie.priceCode
,那么即使您从电影中删除priceCode
或重命名或更改其类型,您也根本不需要更改Rental
类。 在这种情况下,重构有助于本地化更改的范围。 假设您为Movie
编写代码,另一个团队为Rental
编写代码,并且您决定添加更多价格类型。 现在,您需要通知其他团队您添加了更多类型,为它们提供值、计算逻辑,并可能使用新版本的Movie
类构建新工件。 但是,如果您只为它们提供getCharge(daysRented)
方法,那么您可以添加任何新类型而无需通知开销。
为便于阅读,答案已分为两个答案。 这是第 2 部分。
为什么根据另一个对象的属性进行切换是个坏主意?
仅仅因为,另一个对象,是“另一个”对象。 您无法知道该对象内部发生了什么。 因此,如果这些方法依赖于内部变量,它们会更加稳定。
例如:如果Movie
的getPriceCode()
行为不同,如果Movie
的价格代码在这些调用之间发生变化,那么Rental
的getCharge()
方法将返回不同的值。
但是在这种场景下不适用,因为Movie
存储在Rental
类的私有变量_movie
中。 作者似乎忽略了这一点。
这些事情的真正含义是“类型信息通常是易变的”和“最小的涟漪效应(已实现)”? 这些模糊的词(volatile 和ripple)并没有清楚地解释这种重构的优势。
作者试图强调经常添加/删除/修改类型。 (不过,我的经验并非如此。类型在我处理过的那种代码中一直是稳定的。)并且在类型经常更新的情况下,您希望通过最小化这些更新的影响来将您的头痛降到最低。
如果对于每个“类型”更新,您必须在 2 万处修改代码,那么这不是“最小连锁效应”的好例子。
但是,我认为在这里遵循单一职责原则将比遵循作者建议的重构更有成效。 更改将是相同的,但是对于当前和未来的开发人员来说,找到必须进行这些更改的位置将更加直观。
在我看来,在 Java 中使用switch-case
语句会导致代码的可读性下降。 由于 Java 是首选的面向对象语言,因此以explicit
方式编写所有内容非常受欢迎。
在 Java 中使用switch
语句和关键字break
会导致编程为procedure
方式。 一般来说,我们可以说很难理解break
意味着什么。
因此,通过替换为If-else
语句来重构switch
语句将改善clean code
。
我只在 Java 中使用switch-case
,即使是在enum
case 中,正如Effective Java
一书(第 3 版,第 167 页)中推荐的那样。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.