Skip to content

这里是第 16 章 "Refactoring SerialDate" (重构 SerialDate) 的中英对照翻译。


总结 (Summary)

本章记录了 Robert C. Martin 对 JCommon 库中 SerialDate 类的一次完整的重构过程。作者并非出于恶意,而是将其作为一次专业的代码审查案例。

重构的核心步骤包括:

  1. 确保测试通过 (First, Make it Work): 在重构前,作者利用 Clover 工具检查测试覆盖率,并编写了大量单元测试,修复了发现的边界条件 Bug,确保后续修改的安全性。
  2. 优化命名与结构 (Then, Make it Right):
    • 将类名从 SerialDate 改为更抽象的 DayDate
    • 将整型常量(如月份、星期)重构为 枚举 (Enum),提高类型安全和可读性。
    • 移除过时的变更日志和冗余注释。
  3. 抽象与解耦:
    • 引入 抽象工厂模式 (Abstract Factory) 来创建日期实例,隐藏具体的实现细节(如 SpreadsheetDate)。
    • 将与特定实现无关的代码上移至基类,将特定于实现的代码下移。
  4. 消除坏味道 (Code Smells):
    • 移除了死代码和未使用的变量。
    • 解决了“依恋情结” (Feature Envy),将属于其他类(如 MonthDay)的方法移动到正确的位置。
    • 使用“解释性临时变量”简化复杂的日期计算逻辑。

小计 (Key Takeaways):

  • 童子军军规 (Boy Scout Rule): 离开时的代码要比来时更干净。
  • 测试先行: 没有高覆盖率的测试,重构就是一场冒险。
  • 代码即文档: 好的命名和结构比冗余的 Javadoc 更有效。
  • 持续改进: 即使是经验丰富的程序员写出的“好代码”,也总有改进的空间。

第 16 章 Refactoring SerialDate

第 16 章 重构 SerialDate

If you go to http://www.jfree.org/jcommon/index.php, you will find the JCommon library. Deep within that library there is a package named org.jfree.date. Within that package there is a class named SerialDate. We are going to explore that class.

如果你访问 http://www.jfree.org/jcommon/index.php,你会找到 JCommon 库。在这个库深处,有一个名为 org.jfree.date 的包。在该包中有一个名为 SerialDate 的类。我们将深入探索这个类。

The author of SerialDate is David Gilbert. David is clearly an experienced and competent programmer. As we shall see, he shows a significant degree of professionalism and discipline within his code. For all intents and purposes, this is “good code.” And I am going to rip it to pieces.

SerialDate 的作者是 David Gilbert。David 显然是一位经验丰富且称职的程序员。正如我们将看到的,他在代码中表现出了高度的专业素养和纪律性。无论从哪个角度看,这都是“好代码”。而我要把它彻底拆解。

This is not an activity of malice. Nor do I think that I am so much better than David that I somehow have a right to pass judgment on his code. Indeed, if you were to find some of my code, I’m sure you could find plenty of things to complain about.

这并非恶意为之。也并非我认为自己比 David 强多少,从而有权对他的代码评头论足。实际上,如果你去找一些我写的代码,我确信你也能找到一大堆值得抱怨的地方。

No, this is not an activity of nastiness or arrogance. What I am about to do is nothing more and nothing less than a professional review. It is something that we should all be comfortable doing. And it is something we should welcome when it is done for us. It is only through critiques like these that we will learn. Doctors do it. Pilots do it. Lawyers do it. And we programmers need to learn how to do it too.

不,这不是某种恶毒或傲慢的行为。我要做的不过是一次专业的代码审查。这是我们所有人都应该能够坦然面对的事情。当别人审查我们的代码时,我们也应该表示欢迎。只有通过这样的批评,我们才能学习。医生这么做,飞行员这么做,律师也这么做。我们程序员也需要学会这么做。

One more thing about David Gilbert: David is more than just a good programmer. David had the courage and good will to offer his code to the community at large for free. He placed it out in the open for all to see and invited public usage and public scrutiny. This was well done!

关于 David Gilbert 还有一点:David 不仅仅是一位优秀的程序员。David 有勇气和善意将其代码免费提供给整个社区。他将其公开展示,邀请公众使用并接受公众的审视。这做得非常棒!

SerialDate (Listing B-1, page 349) is a class that represents a date in Java. Why have a class that represents a date, when Java already has java.util.Date and java.util.Calendar, and others? The author wrote this class in response to a pain that I have often felt myself. The comment in his opening Javadoc (line 67) explains it well. We could quibble about his intention, but I have certainly had to deal with this issue, and I welcome a class that is about dates instead of times.

SerialDate(清单 B-1,第 349 页)是一个在 Java 中表示日期的类。既然 Java 已经有了 java.util.Date 和 java.util.Calendar 等类,为什么还要有一个表示日期的类呢?作者编写这个类是为了解决我本人也经常感到的痛苦。他在开头的 Javadoc 注释(第 67 行)中解释得很清楚。我们可以对他的意图吹毛求疵,但我确实不得不处理这个问题,所以我欢迎一个只关注日期而非时间的类。

FIRST, MAKE IT WORK

第一,让它能工作

There are some unit tests in a class named SerialDateTests (Listing B-2, page 366). The tests all pass. Unfortunately a quick inspection of the tests shows that they don’t test everything [T1]. For example, doing a “Find Usages” search on the method MonthCodeToQuarter (line 334) indicates that it is not used [F4]. Therefore, the unit tests don’t test it.

在名为 SerialDateTests(清单 B-2,第 366 页)的类中有一些单元测试。这些测试全都通过了。不幸的是,快速检查一下这些测试就会发现它们并没有测试所有内容 [T1]。例如,对 method MonthCodeToQuarter(第 334 行)进行“查找用法”搜索表明它没有被使用 [F4]。因此,单元测试并没有测试它。

So I fired up Clover to see what the unit tests covered and what they didn’t. Clover reported that the unit tests executed only 91 of the 185 executable statements in SerialDate (~50 percent) [T2]. The coverage map looks like a patchwork quilt, with big gobs of unexecuted code littered all through the class.

于是我启动了 Clover,看看单元测试覆盖了什么,没覆盖什么。Clover 报告说,单元测试仅执行了 SerialDate 中 185 个可执行语句中的 91 个(约 50%)[T2]。覆盖率图看起来像一条拼布床单,类中到处散落着大块未执行的代码。

It was my goal to completely understand and also refactor this class. I couldn’t do that without much greater test coverage. So I wrote my own suite of completely independent unit tests (Listing B-4, page 374).

我的目标是完全理解并重构这个类。如果没有更高的测试覆盖率,我就无法做到这一点。所以我编写了一套完全独立的单元测试(清单 B-4,第 374 页)。

As you look through these tests, you will note that many of them are commented out. These tests didn’t pass. They represent behavior that I think SerialDate should have. So as I refactor SerialDate, I’ll be working to make these tests pass too.

当你浏览这些测试时,你会注意到其中许多被注释掉了。这些测试没有通过。它们代表了我认为 SerialDate 应该具备的行为。因此,在重构 SerialDate 时,我也会努力让这些测试通过。

Even with some of the tests commented out, Clover reports that the new unit tests are executing 170 (92 percent) out of the 185 executable statements. This is pretty good, and I think we’ll be able to get this number higher.

即使注释掉了一些测试,Clover 报告显示新的单元测试执行了 185 个可执行语句中的 170 个(92%)。这已经相当不错了,我想我们还能让这个数字更高。

The first few commented-out tests (lines 23-63) were a bit of conceit on my part. The program was not designed to pass these tests, but the behavior seemed obvious [G2] to me. I’m not sure why the testWeekdayCodeToString method was written in the first place, but because it is there, it seems obvious that it should not be case sensitive. Writing these tests was trivial [T3]. Making them pass was even easier; I just changed lines 259 and 263 to use equalsIgnoreCase.

最初几个被注释掉的测试(第 23-63 行)是我自负的表现。程序原本并非设计为通过这些测试,但在我看来这些行为是显而易见的 [G2]。我不确定当初为什么要写 testWeekdayCodeToString 方法,但既然它存在,它显然不应该区分大小写。编写这些测试是微不足道的 [T3]。让它们通过更容易;我只是将第 259 行和第 263 行改为了使用 equalsIgnoreCase。

I left the tests at line 32 and line 45 commented out because it’s not clear to me that the “tues” and “thurs” abbreviations ought to be supported.

我保留了第 32 行和第 45 行的测试注释,因为我不清楚是否应该支持 "tues" 和 "thurs" 这样的缩写。

The tests on line 153 and line 154 don’t pass. Clearly, they should [G2]. We can easily fix this, and the tests on line 163 through line 213, by making the following changes to the stringToMonthCode function.

第 153 行和第 154 行的测试没有通过。显然,它们应该通过 [G2]。我们可以通过对 stringToMonthCode 函数进行以下更改,轻松修复这个问题,以及第 163 行到第 213 行的测试。

java
   457     if ((result < 1) || (result > 12)) {
               result = -1;
   458         for (int i = 0; i < monthNames.length; i++) {
   459             if (s.equalsIgnoreCase(shortMonthNames[i])) {
   460                 result = i + 1;
   461                 break;
   462             }
   463             if (s.equalsIgnoreCase(monthNames[i])) {
   464                 result = i + 1;
   465                 break;
   466             }
   467         }
   468     }

The commented test on line 318 exposes a bug in the getFollowingDayOfWeek method (line 672). December 25th, 2004, was a Saturday. The following Saturday was January 1st, 2005. However, when we run the test, we see that getFollowingDayOfWeek returns December 25th as the Saturday that follows December 25th. Clearly, this is wrong [G3],[T1]. We see the problem in line 685. It is a typical boundary condition error [T5]. It should read as follows:

第 318 行注释掉的测试暴露了 getFollowingDayOfWeek 方法(第 672 行)中的一个 Bug。2004 年 12 月 25 日是星期六。下一个星期六是 2005 年 1 月 1 日。然而,当我们运行测试时,我们看到 getFollowingDayOfWeek 返回 12 月 25 日作为 12 月 25 日之后的星期六。显然,这是错误的 [G3],[T1]。我们在第 685 行看到了问题所在。这是一个典型的边界条件错误 [T5]。它应该修改如下:

java
   685     if (baseDOW >= targetWeekday) {

It is interesting to note that this function was the target of an earlier repair. The change history (line 43) shows that “bugs” were fixed in getPreviousDayOfWeek, getFollowingDayOfWeek, and getNearestDayOfWeek [T6].

有趣的是,这个函数是早期修复的目标。变更记录(第 43 行)显示在 getPreviousDayOfWeek、getFollowingDayOfWeek 和 getNearestDayOfWeek 中修复了“bugs” [T6]。

The testGetNearestDayOfWeek unit test (line 329), which tests the getNearestDayOfWeek method (line 705), did not start out as long and exhaustive as it currently is. I added a lot of test cases to it because my initial test cases did not all pass [T6]. You can see the pattern of failure by looking at which test cases are commented out. That pattern is revealing [T7]. It shows that the algorithm fails if the nearest day is in the future. Clearly there is some kind of boundary condition error [T5].

testGetNearestDayOfWeek 单元测试(第 329 行)用于测试 getNearestDayOfWeek 方法(第 705 行),它起初并没有现在这么长和详尽。我添加了很多测试用例,因为我最初的测试用例并没有全部通过 [T6]。你可以通过查看哪些测试用例被注释掉来看到失败的模式。这种模式很有启发性 [T7]。它表明如果最近的一天在未来,算法就会失败。显然存在某种边界条件错误 [T5]。

The pattern of test coverage reported by Clover is also interesting [T8]. Line 719 never gets executed! This means that the if statement in line 718 is always false. Sure enough, a look at the code shows that this must be true. The adjust variable is always negative and so cannot be greater or equal to 4. So this algorithm is just wrong.

Clover 报告的测试覆盖模式也很有趣 [T8]。第 719 行从未被执行!这意味着第 718 行的 if 语句总是为假。果然,看一眼代码就知道这必然是真的。adjust 变量总是负数,因此不可能大于或等于 4。所以这个算法根本就是错的。

The right algorithm is shown below:

正确的算法如下所示:

java
     int delta = targetDOW - base.getDayOfWeek();
     int positiveDelta = delta + 7;
     int adjust = positiveDelta % 7;
     if (adjust > 3)
       adjust -= 7;
 
     return SerialDate.addDays(adjust, base);

Finally, the tests at line 417 and line 429 can be made to pass simply by throwing an IllegalArgumentException instead of returning an error string from weekInMonthToString and relativeToString.

最后,第 417 行和第 429 行的测试可以通过在 weekInMonthToString 和 relativeToString 中抛出 IllegalArgumentException 而不是返回错误字符串来通过。

With these changes all the unit tests pass, and I believe SerialDate now works. So now it’s time to make it “right.”

有了这些更改,所有单元测试都通过了,我相信 SerialDate 现在可以工作了。所以现在是时候让它变得“正确”了。

THEN MAKE IT RIGHT

然后,让它变正确

We are going to walk from the top to the bottom of SerialDate, improving it as we go along. Although you won’t see this in the discussion, I will be running all of the JCommon unit tests, including my improved unit test for SerialDate, after every change I make. So rest assured that every change you see here works for all of JCommon.

我们将从上到下遍历 SerialDate,边走边改。虽然你在讨论中看不到这一点,但在我做出的每一个更改之后,我都会运行所有的 JCommon 单元测试,包括我改进的 SerialDate 单元测试。所以请放心,你在这里看到的每一个更改在整个 JCommon 中都是有效的。

Starting at line 1, we see a ream of comments with license information, copyrights, authors, and change history. I acknowledge that there are certain legalities that need to be addressed, and so the copyrights and licenses must stay. On the other hand, the change history is a leftover from the 1960s. We have source code control tools that do this for us now. This history should be deleted [C1].

从第 1 行开始,我们看到一大堆包含许可证信息、版权、作者和变更历史的注释。我承认有些法律问题需要处理,所以版权和许可证必须保留。另一方面,变更历史是 1960 年代的遗留物。我们现在有源代码控制工具来为我们做这件事。这些历史记录应该被删除 [C1]。

The import list starting at line 61 could be shortened by using java.text.* and java.util.*. [J1]

从第 61 行开始的导入列表可以通过使用 java.text.* 和 java.util.* 来缩短。[J1]

I wince at the HTML formatting in the Javadoc (line 67). Having a source file with more than one language in it troubles me. This comment has four languages in it: Java, English, Javadoc, and html [G1]. With that many languages in use, it’s hard to keep things straight. For example, the nice positioning of line 71 and line 72 are lost when the Javadoc is generated, and yet who wants to see <ul> and <li> in the source code? A better strategy might be to just surround the whole comment with <pre> so that the formatting that is apparent in the source code is preserved within the Javadoc.1

看到 Javadoc 中的 HTML 格式(第 67 行),我不禁皱眉。源文件中包含不止一种语言让我感到困扰。这个注释里有四种语言:Java、英语、Javadoc 和 HTML [G1]。使用了这么多语言,很难把事情理顺。例如,第 71 行和第 72 行漂亮的排版在生成 Javadoc 时丢失了,而且谁愿意在源代码中看到 <ul><li> 呢?更好的策略可能只是用 <pre> 包裹整个注释,这样源代码中显而易见的格式就能在 Javadoc 中保留下来。

  1. An even better solution would have been for Javadoc to present all comments as preformatted, so that comments appear the same in both code and document.
  1. 一个更好的解决方案是让 Javadoc 将所有注释都作为预格式化文本显示,这样注释在代码和文档中看起来就是一样的。

Line 86 is the class declaration. Why is this class named SerialDate? What is the significance of the word “serial”? Is it because the class is derived from Serializable? That doesn’t seem likely.

第 86 行是类声明。为什么这个类叫 SerialDate?单词“serial”(序列/串行)有什么意义?是因为这个类派生自 Serializable 吗?这看起来不太可能。

I won’t keep you guessing. I know why (or at least I think I know why) the word “serial” was used. The clue is in the constants SERIAL_LOWER_BOUND and SERIAL_UPPER_BOUND on line 98 and line 101. An even better clue is in the comment that begins on line 830. This class is named SerialDate because it is implemented using a “serial number,” which happens to be the number of days since December 30th, 1899.

我不想让你猜了。我知道(或者至少我认为我知道)为什么使用“serial”这个词。线索在于第 98 行和第 101 行的常量 SERIAL_LOWER_BOUND 和 SERIAL_UPPER_BOUND。第 830 行开始的注释提供了更好的线索。这个类之所以命名为 SerialDate,是因为它是使用“序列号”实现的,而这个序列号恰好是自 1899 年 12 月 30 日以来的天数。

I have two problems with this. First, the term “serial number” is not really correct. This may be a quibble, but the representation is more of a relative offset than a serial number. The term “serial number” has more to do with product identification markers than dates. So I don’t find this name particularly descriptive [N1]. A more descriptive term might be “ordinal.”

对此我有两个问题。首先,“序列号”这个术语并不完全正确。这可能是吹毛求疵,但这种表示法更像是一个相对偏移量,而不是序列号。“序列号”这个词更多地与产品识别标记有关,而不是日期。所以我认为这个名字不是特别具有描述性 [N1]。一个更具描述性的术语可能是“序数 (ordinal)”。

The second problem is more significant. The name SerialDate implies an implementation. This class is an abstract class. There is no need to imply anything at all about the implementation. Indeed, there is good reason to hide the implementation! So I find this name to be at the wrong level of abstraction [N2]. In my opinion, the name of this class should simply be Date.

第二个问题更为重要。SerialDate 这个名字暗示了一种实现。这是一个抽象类。完全没有必要暗示任何关于实现的信息。事实上,有充分的理由隐藏实现!所以我发现这个名字处于错误的抽象层级 [N2]。依我看,这个类的名字应该简单地叫做 Date。

Unfortunately, there are already too many classes in the Java library named Date, so this is probably not the best name to choose. Because this class is all about days, instead of time, I considered naming it Day, but this name is also heavily used in other places. In the end, I chose DayDate as the best compromise.

不幸的是,Java 库中已经有太多名为 Date 的类了,所以这可能不是最好的选择。因为这个类是关于天数而不是时间的,我考虑将其命名为 Day,但这个名字在其他地方也被大量使用。最后,我选择 DayDate 作为最佳折衷方案。

From now on in this discussion I will use the term DayDate. I leave it to you to remember that the listings you are looking at still use SerialDate.

从现在开始,在讨论中我将使用术语 DayDate。你需要记住,你看到的清单中仍然使用的是 SerialDate。

I understand why DayDate inherits from Comparable and Serializable. But why does it inherit from MonthConstants? The class MonthConstants (Listing B-3, page 372) is just a bunch of static final constants that define the months. Inheriting from classes with constants is an old trick that Java programmers used so that they could avoid using expressions like MonthConstants.January, but it’s a bad idea [J2]. MonthConstants should really be an enum.

我理解为什么 DayDate 继承自 Comparable 和 Serializable。但它为什么要继承 MonthConstants 呢?MonthConstants 类(清单 B-3,第 372 页)只是一堆定义月份的静态 final 常量。从包含常量的类继承是 Java 程序员过去使用的一个老把戏,这样他们就可以避免使用像 MonthConstants.January 这样的表达式,但这是一个坏主意 [J2]。MonthConstants 实际上应该是一个枚举 (enum)。

java
   public abstract class DayDate implements Comparable,
                                            Serializable {
     public static enum Month {
       JANUARY(1),
       FEBRUARY(2),
       MARCH(3),
       APRIL(4),
       MAY(5),
       JUNE(6),
       JULY(7),
       AUGUST(8),
       SEPTEMBER(9),
       OCTOBER(10),
       NOVEMBER(11),
       DECEMBER(12);
 
       Month(int index) {
         this.index = index;
       }

       public static Month make(int monthIndex) {
         for (Month m : Month.values()) {
           if (m.index == monthIndex)
             return m;
         }
         throw new IllegalArgumentException(“Invalid month index ” + monthIndex);
       }
       public final int index;
     }

Changing MonthConstants to this enum forces quite a few changes to the DayDate class and all it’s users. It took me an hour to make all the changes. However, any function that used to take an int for a month, now takes a Month enumerator. This means we can get rid of the isValidMonthCode method (line 326), and all the month code error checking such as that in monthCodeToQuarter (line 356) [G5].

将 MonthConstants 改为这个枚举迫使 DayDate 类及其所有用户进行大量更改。我花了一个小时才完成所有更改。然而,任何以前使用 int 表示月份的函数,现在都接受 Month 枚举器。这意味着我们可以摆脱 isValidMonthCode 方法(第 326 行),以及所有的月份代码错误检查,例如 monthCodeToQuarter 中的检查(第 356 行)[G5]。

Next, we have line 91, serialVersionUID. This variable is used to control the serializer. If we change it, then any DayDate written with an older version of the software won’t be readable anymore and will result in an InvalidClassException. If you don’t declare the serialVersionUID variable, then the compiler automatically generates one for you, and it will be different every time you make a change to the module. I know that all the documents recommend manual control of this variable, but it seems to me that automatic control of serialization is a lot safer [G4]. After all, I’d much rather debug an InvalidClassException than the odd behavior that would ensue if I forgot to change the serialVersionUID. So I’m going to delete the variable—at least for the time being.2

接下来是第 91 行,serialVersionUID。这个变量用于控制序列化器。如果我们更改它,那么任何用旧版本软件写入的 DayDate 都将无法读取,并会导致 InvalidClassException。如果你不声明 serialVersionUID 变量,编译器会自动为你生成一个,而且每次你更改模块时它都会不同。我知道所有文档都建议手动控制这个变量,但在我看来,自动控制序列化要安全得多 [G4]。毕竟,比起我忘记更改 serialVersionUID 而导致的奇怪行为,我更愿意调试 InvalidClassException。所以我打算删除这个变量——至少暂时如此。

  1. Several of the reviewers of this text have taken exception to this decision. They contend that in an open source framework it is better to assert manual control over the serial ID so that minor changes to the software don’t cause old serialized dates to be invalid. This is a fair point. However, at least the failure, inconvenient though it might be, has a clear-cut cause. On the other hand, if the author of the class forgets to update the ID, then the failure mode is undefined and might very well be silent. I think the real moral of this story is that you should not expect to deserialize across versions.
  1. 本文的几位审阅者对这一决定提出了异议。他们认为在开源框架中,最好对手动控制序列 ID,以便软件的细微更改不会导致旧的序列化日期无效。这确实是一个合理的观点。然而,这种失败虽然不便,但至少原因明确。另一方面,如果类的作者忘记更新 ID,那么失败模式是未定义的,很可能是悄无声息的。我认为这个故事的真正寓意是,你不应该期望跨版本进行反序列化。

I find the comment on line 93 redundant. Redundant comments are just places to collect lies and misinformation [C2]. So I’m going to get rid of it and its ilk.

我发现第 93 行的注释是多余的。多余的注释只是收集谎言和错误信息的地方 [C2]。所以我要摆脱它以及同类注释。

The comments at line 97 and line 100 talk about serial numbers, which I discussed earlier [C1]. The variables they describe are the earliest and latest possible dates that DayDate can describe. This can be made a bit clearer [N1].

第 97 行和第 100 行的注释谈到了序列号,我之前已经讨论过了 [C1]。它们描述的变量是 DayDate 可以描述的最早和最晚的日期。这可以表达得更清楚一些 [N1]。

java
   public static final int EARLIEST_DATE_ORDINAL = 2;     // 1/1/1900
   public static final int LATEST_DATE_ORDINAL = 2958465; // 12/31/9999

It’s not clear to me why EARLIEST_DATE_ORDINAL is 2 instead of 0. There is a hint in the comment on line 829 that suggests that this has something to do with the way dates are represented in Microsoft Excel. There is a much deeper insight provided in a derivative of DayDate called SpreadsheetDate (Listing B-5, page 382). The comment on line 71 describes the issue nicely.

我不清楚为什么 EARLIEST_DATE_ORDINAL 是 2 而不是 0。第 829 行的注释暗示这与 Microsoft Excel 表示日期的方式有关。DayDate 的一个名为 SpreadsheetDate 的派生类(清单 B-5,第 382 页)提供了更深入的见解。第 71 行的注释很好地描述了这个问题。

The problem I have with this is that the issue seems to be related to the implementation of SpreadsheetDate and has nothing to do with DayDate. I conclude from this that EARLIEST_DATE_ORDINAL and LATEST_DATE_ORDINAL do not really belong in DayDate and should be moved to SpreadsheetDate [G6].

我对此的问题在于,这个问题似乎与 SpreadsheetDate 的实现有关,而与 DayDate 无关。我由此得出结论,EARLIEST_DATE_ORDINAL 和 LATEST_DATE_ORDINAL 实际上并不属于 DayDate,应该移至 SpreadsheetDate [G6]。

Indeed, a search of the code shows that these variables are used only within SpreadsheetDate. Nothing in DayDate, nor in any other class in the JCommon framework, uses them. Therefore, I’ll move them down into SpreadsheetDate.

事实上,搜索代码显示这些变量仅在 SpreadsheetDate 中使用。DayDate 中没有任何东西,JCommon 框架中的任何其他类也没有使用它们。因此,我会将它们向下移动到 SpreadsheetDate 中。

The next variables, MINIMUM_YEAR_SUPPORTED, and MAXIMUM_YEAR_SUPPORTED (line 104 and line 107), provide something of a dilemma. It seems clear that if DayDate is an abstract class that provides no foreshadowing of implementation, then it should not inform us about a minimum or maximum year. Again, I am tempted to move these variables down into SpreadsheetDate [G6]. However, a quick search of the users of these variables shows that one other class uses them: RelativeDayOfWeekRule (Listing B-6, page 390). We see that usage at line 177 and line 178 in the getDate function, where they are used to check that the argument to getDate is a valid year. The dilemma is that a user of an abstract class needs information about its implementation.

接下来的变量,MINIMUM_YEAR_SUPPORTED 和 MAXIMUM_YEAR_SUPPORTED(第 104 行和第 107 行),带来了一些两难境地。似乎很明显,如果 DayDate 是一个不预示实现的抽象类,那么它就不应该告诉我们最小或最大年份。同样,我很想把这些变量移到 SpreadsheetDate 中 [G6]。然而,快速搜索这些变量的用户显示,还有一个类使用了它们:RelativeDayOfWeekRule(清单 B-6,第 390 页)。我们在 getDate 函数的第 177 行和第 178 行看到了这种用法,它们被用来检查 getDate 的参数是否为有效年份。困境在于,抽象类的用户需要有关其实现的信息。

What we need to do is provide this information without polluting DayDate itself. Usually, we would get implementation information from an instance of a derivative. However, the getDate function is not passed an instance of a DayDate. It does, however, return such an instance, which means that somewhere it must be creating it. Line 187 through line 205 provide the hint. The DayDate instance is being created by one of the three functions, getPreviousDayOfWeek, getNearestDayOfWeek, or getFollowingDayOfWeek. Looking back at the DayDate listing, we see that these functions (lines 638–724) all return a date created by addDays (line 571), which calls createInstance (line 808), which creates a SpreadsheetDate! [G7].

我们需要做的是在不污染 DayDate 本身的情况下提供此信息。通常,我们会从派生类的实例中获取实现信息。然而,getDate 函数没有传入 DayDate 的实例。但是,它确实返回这样一个实例,这意味着它一定在某处创建了它。第 187 行到第 205 行提供了线索。DayDate 实例是由 getPreviousDayOfWeek、getNearestDayOfWeek 或 getFollowingDayOfWeek 这三个函数之一创建的。回顾 DayDate 清单,我们看到这些函数(第 638-724 行)都返回由 addDays(第 571 行)创建的日期,而 addDays 调用了 createInstance(第 808 行),后者创建了一个 SpreadsheetDate![G7]。

It’s generally a bad idea for base classes to know about their derivatives. To fix this, we should use the ABSTRACT FACTORY3 pattern and create a DayDateFactory. This factory will create the instances of DayDate that we need and can also answer questions about the implementation, such as the maximum and minimum dates.

基类了解其派生类通常是一个坏主意。为了解决这个问题,我们应该使用 抽象工厂3 模式并创建一个 DayDateFactory。这个工厂将创建我们需要 DayDate 实例,并且还可以回答有关实现的问题,例如最大和最小日期。

  1. [GOF].
  1. [GOF](设计模式)。
java
   public abstract class DayDateFactory {
     private static DayDateFactory factory = new SpreadsheetDateFactory();
     public static void setInstance(DayDateFactory factory) {
       DayDateFactory.factory = factory;
     }
 
     protected abstract DayDate _makeDate(int ordinal);
     protected abstract DayDate _makeDate(int day, DayDate.Month month, int year);
     protected abstract DayDate _makeDate(int day, int month, int year);
     protected abstract DayDate _makeDate(java.util.Date date);
     protected abstract int _getMinimumYear();
     protected abstract int _getMaximumYear();
 
     public static DayDate makeDate(int ordinal) {
       return factory._makeDate(ordinal);
     }
     public static DayDate makeDate(int day, DayDate.Month month, int year) {
       return factory._makeDate(day, month, year);
     }
 
     public static DayDate makeDate(int day, int month, int year) {
       return factory._makeDate(day, month, year);
     }
 
     public static DayDate makeDate(java.util.Date date) {
       return factory._makeDate(date);
     }
 
     public static int getMinimumYear() {
       return factory._getMinimumYear();
     }
 
     public static int getMaximumYear() {
       return factory._getMaximumYear();
     }
   }

This factory class replaces the createInstance methods with makeDate methods, which improves the names quite a bit [N1]. It defaults to a SpreadsheetDateFactory but can be changed at any time to use a different factory. The static methods that delegate to abstract methods use a combination of the SINGLETON,4 DECORATOR,5 and ABSTRACT FACTORY patterns that I have found to be useful.

这个工厂类用 makeDate 方法替换了 createInstance 方法,这在很大程度上改进了命名 [N1]。它默认为 SpreadsheetDateFactory,但可以随时更改为使用不同的工厂。委托给抽象方法的静态方法使用了 **单例 (SINGLETON)**4、**装饰器 (DECORATOR)**5 和 抽象工厂 (ABSTRACT FACTORY) 模式的组合,我发现这非常有用。

  1. Ibid.
  2. Ibid.
  1. 同上。
  2. 同上。

The SpreadsheetDateFactory looks like this.

SpreadsheetDateFactory 看起来像这样。

java
   public class SpreadsheetDateFactory extends DayDateFactory {
     public DayDate _makeDate(int ordinal) {
       return new SpreadsheetDate(ordinal);
     }
 
     public DayDate _makeDate(int day, DayDate.Month month, int year) {
       return new SpreadsheetDate(day, month, year);
     }
 
     public DayDate _makeDate(int day, int month, int year) {
       return new SpreadsheetDate(day, month, year);
     }
 
     public DayDate _makeDate(Date date) {
       final GregorianCalendar calendar = new GregorianCalendar();
       calendar.setTime(date);
       return new SpreadsheetDate(
         calendar.get(Calendar.DATE),
         DayDate.Month.make(calendar.get(Calendar.MONTH) + 1),
         calendar.get(Calendar.YEAR));
     }

     protected int _getMinimumYear() {
       return SpreadsheetDate.MINIMUM_YEAR_SUPPORTED;
     }
 
     protected int _getMaximumYear() {
       return SpreadsheetDate.MAXIMUM_YEAR_SUPPORTED;
     }
   }

As you can see, I have already moved the MINIMUM_YEAR_SUPPORTED and MAXIMUM_YEAR_SUPPORTED variables into SpreadsheetDate, where they belong [G6].

如你所见,我已经将 MINIMUM_YEAR_SUPPORTED 和 MAXIMUM_YEAR_SUPPORTED 变量移到了它们所属的 SpreadsheetDate 中 [G6]。

The next issue in DayDate are the day constants beginning at line 109. These should really be another enum [J3]. We’ve seen this pattern before, so I won’t repeat it here. You’ll see it in the final listings.

DayDate 中的下一个问题是从第 109 行开始的日期常量。这些真的应该是另一个枚举 [J3]。我们之前见过这种模式,所以我不会在这里重复。你会在最终的清单中看到它。

Next, we see a series of tables starting with LAST_DAY_OF_MONTH at line 140. My first issue with these tables is that the comments that describe them are redundant [C3]. Their names are sufficient. So I’m going to delete the comments.

接下来,我们看到一系列从第 140 行的 LAST_DAY_OF_MONTH 开始的表格。我对这些表格的第一个问题是描述它们的注释是多余的 [C3]。它们的名字已经足够了。所以我打算删除这些注释。

There seems to be no good reason that this table isn’t private [G8], because there is a static function lastDayOfMonth that provides the same data.

似乎没有充分的理由不将这个表格设为私有 [G8],因为有一个静态函数 lastDayOfMonth 提供了相同的数据。

The next table, AGGREGATE_DAYS_TO_END_OF_MONTH, is a bit more mysterious because it is not used anywhere in the JCommon framework [G9]. So I deleted it.

下一个表格,AGGREGATE_DAYS_TO_END_OF_MONTH,有点神秘,因为它在 JCommon 框架中没有在任何地方被使用 [G9]。所以我删除了它。

The same goes for LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH.

LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH 也是如此。

The next table, AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH, is used only in Spread-sheetDate (line 434 and line 473). This begs the question of whether it should be moved to SpreadsheetDate. The argument for not moving it is that the table is not specific to any particular implementation [G6]. On the other hand, no implementation other than SpreadsheetDate actually exists, and so the table should be moved close to where it is used [G10].

下一个表格,AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH,仅在 SpreadsheetDate(第 434 行和第 473 行)中使用。这就引出了一个问题:是否应该将其移至 SpreadsheetDate。不移动它的理由是该表格并非特定于任何特定实现 [G6]。另一方面,除了 SpreadsheetDate 之外,实际上不存在其他实现,因此该表格应该移至靠近其使用位置的地方 [G10]。

What settles the argument for me is that to be consistent [G11], we should make the table private and expose it through a function like julianDateOfLastDayOfMonth. Nobody seems to need a function like that. Moreover, the table can be moved back to DayDate easily if any new implementation of DayDate needs it. So I moved it.

让我下定决心的是,为了保持一致 [G11],我们应该将表格设为私有,并通过像 julianDateOfLastDayOfMonth 这样的函数将其公开。似乎没有人需要那样的函数。此外,如果 DayDate 的任何新实现需要它,该表格可以很容易地移回 DayDate。所以我移动了它。

The same goes for the table, LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH.

LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH 表格也是如此。

Next, we see three sets of constants that can be turned into enums (lines 162–205). The first of the three selects a week within a month. I changed it into an enum named WeekInMonth.

接下来,我们看到三组可以转换为枚举的常量(第 162-205 行)。第一组选择一个月内的一周。我将其更改为一个名为 WeekInMonth 的枚举。

java
   public enum WeekInMonth {
       FIRST(1), SECOND(2), THIRD(3), FOURTH(4), LAST(0);
       public final int index;
 
       WeekInMonth(int index) {
         this.index = index;
       }
     }

The second set of constants (lines 177–187) is a bit more obscure. The INCLUDE_NONE, INCLUDE_FIRST, INCLUDE_SECOND, and INCLUDE_BOTH constants are used to describe whether the defining end-point dates of a range should be included in that range. Mathematically, this is described using the terms “open interval,” “half-open interval,” and “closed interval.” I think it is clearer using the mathematical nomenclature [N3], so I changed it to an enum named DateInterval with CLOSED, CLOSED_LEFT, CLOSED_RIGHT, and OPEN enumerators.

第二组常量(第 177-187 行)稍微晦涩一些。INCLUDE_NONE、INCLUDE_FIRST、INCLUDE_SECOND 和 INCLUDE_BOTH 常量用于描述范围的定义端点日期是否应包含在该范围内。在数学上,这使用术语“开区间”、“半开区间”和“闭区间”来描述。我认为使用数学命名法更清楚 [N3],所以我将其更改为一个名为 DateInterval 的枚举,包含 CLOSED、CLOSED_LEFT、CLOSED_RIGHT 和 OPEN 枚举器。

The third set of constants (lines 189–205) describe whether a search for a particular day of the week should result in the last, next, or nearest instance. Deciding what to call this is difficult at best. In the end, I settled for WeekdayRange with LAST, NEXT, and NEAREST enumerators.

第三组常量(第 189-205 行)描述了对特定星期的搜索结果应该是上一个、下一个还是最近的一个实例。决定如何称呼这个充其实很难。最后,我确定使用 WeekdayRange,包含 LAST、NEXT 和 NEAREST 枚举器。

You might not agree with the names I’ve chosen. They make sense to me, but they may not make sense to you. The point is that they are now in a form that makes them easy to change [J3]. They aren’t passed as integers anymore; they are passed as symbols. I can use the “change name” function of my IDE to change the names, or the types, without worrying that I missed some -1 or 2 somewhere in the code or that some int argument declaration is left poorly described.

你可能不同意我选择的名字。它们对我来说有意义,但对你可能没有意义。重点是,它们现在的形式使得它们很容易更改 [J3]。它们不再作为整数传递;它们作为符号传递。我可以使用 IDE 的“重命名”功能来更改名称或类型,而不必担心我错过了代码中某处的 -1 或 2,或者某个 int 参数声明描述不清。

The description field at line 208 does not seem to be used by anyone. I deleted it along with its accessor and mutator [G9].

第 208 行的 description 字段似乎没有被任何人使用。我将其连同其访问器和修改器一起删除了 [G9]。

I also deleted the degenerate default constructor at line 213 [G12]. The compiler will generate it for us.

我还删除了第 213 行退化的默认构造函数 [G12]。编译器会为我们生成它。

We can skip over the isValidWeekdayCode method (lines 216–238) because we deleted it when we created the Day enumeration.

我们可以跳过 isValidWeekdayCode 方法(第 216-238 行),因为我们在创建 Day 枚举时已经删除了它。

This brings us to the stringToWeekdayCode method (lines 242–270). Javadocs that don’t add much to the method signature are just clutter [C3],[G12]. The only value this Javadoc adds is the description of the -1 return value. However, because we changed to the Day enumeration, the comment is actually wrong [C2]. The method now throws an IllegalArgumentException. So I deleted the Javadoc.

这就把我们带到了 stringToWeekdayCode 方法(第 242-270 行)。不能给方法签名增加多少信息的 Javadoc 只是杂乱的干扰 [C3],[G12]。这个 Javadoc 增加的唯一价值是对 -1返回值的描述。但是,因为我们改用了 Day 枚举,所以这个注释实际上是错误的 [C2]。该方法现在抛出 IllegalArgumentException。所以我删除了这个 Javadoc。

I also deleted all the final keywords in arguments and variable declarations. As far as I could tell, they added no real value but did add to the clutter [G12]. Eliminating final flies in the face of some conventional wisdom. For example, Robert Simmons6 strongly recommends us to “. . . spread final all over your code.” Clearly I disagree. I think that there are a few good uses for final, such as the occasional final constant, but otherwise the keyword adds little value and creates a lot of clutter. Perhaps I feel this way because the kinds of errors that final might catch are already caught by the unit tests I write.

我还删除了参数和变量声明中的所有 final 关键字。据我所知,它们没有增加真正的价值,反而增加了混乱 [G12]。消除 final 与一些传统智慧背道而驰。例如,Robert Simmons6 强烈建议我们“……在代码中到处使用 final”。显然我不同意。我认为 final 有一些很好的用途,比如偶尔用于 final 常量,但除此之外,这个关键字增加的价值很小,却造成了大量的混乱。也许我这么觉得是因为 final 可能捕获的错误类型已经被我编写的单元测试捕获了。

  1. [Simmons04], p. 73.
  1. [Simmons04], 第 73 页。

I didn’t care for the duplicate if statements [G5] inside the for loop (line 259 and line 263), so I connected them into a single if statement using the || operator. I also used the Day enumeration to direct the for loop and made a few other cosmetic changes.

我不喜欢 for 循环内部重复的 if 语句 [G5](第 259 行和第 263 行),所以我使用 || 运算符将它们连接成一个 if 语句。我还使用 Day 枚举来指导 for 循环,并做了一些其他外观上的更改。

It occurred to me that this method does not really belong in DayDate. It’s really the parse function of Day. So I moved it into the Day enumeration. However, that made the Day enumeration pretty large. Because the concept of Day does not depend on DayDate, I moved the Day enumeration outside of the DayDate class into its own source file [G13].

我突然想到这个方法实际上并不属于 DayDate。它实际上是 Day 的解析函数。所以我把它移到了 Day 枚举中。然而,这使得 Day 枚举变得相当大。因为 Day 的概念不依赖于 DayDate,所以我将 Day 枚举移出了 DayDate 类,放入其自己的源文件中 [G13]。

I also moved the next function, weekdayCodeToString (lines 272–286) into the Day enumeration and called it toString.

我还将下一个函数 weekdayCodeToString(第 272-286 行)移到了 Day 枚举中,并将其命名为 toString。

java
   public enum Day {
     MONDAY(Calendar.MONDAY),
     TUESDAY(Calendar.TUESDAY),
     WEDNESDAY(Calendar.WEDNESDAY),s
     THURSDAY(Calendar.THURSDAY),
     FRIDAY(Calendar.FRIDAY),
     SATURDAY(Calendar.SATURDAY),
     SUNDAY(Calendar.SUNDAY);
 
     public final int index;
     private static DateFormatSymbols dateSymbols = new DateFormatSymbols();
 
     Day(int day) {
       index = day;
     }
 
     public static Day make(int index) throws IllegalArgumentException {
       for (Day d : Day.values())
         if (d.index == index)
           return d;
       throw new IllegalArgumentException(
         String.format(“Illegal day index: %d.”, index));
     }
 
     public static Day parse(String s) throws IllegalArgumentException {
       String[] shortWeekdayNames =
         dateSymbols.getShortWeekdays();
       String[] weekDayNames =
         dateSymbols.getWeekdays();
 
       s = s.trim();
       for (Day day : Day.values()) {
         if (s.equalsIgnoreCase(shortWeekdayNames[day.index]) ||
             s.equalsIgnoreCase(weekDayNames[day.index])) {
           return day;
         }
       }
       throw new IllegalArgumentException(
         String.format(“%s is not a valid weekday string”, s));
      }
 
      public String toString() {
        return dateSymbols.getWeekdays()[index];
      }
    }

There are two getMonths functions (lines 288–316). The first calls the second. The second is never called by anyone but the first. Therefore, I collapsed the two into one and vastly simplified them [G9],[G12],[F4]. Finally, I changed the name to be a bit more self-descriptive [N1].

有两个 getMonths 函数(第 288-316 行)。第一个调用第二个。除了第一个,没有任何人调用第二个。因此,我将这两个合并为一个,并极大地简化了它们 [G9],[G12],[F4]。最后,我更改了名称,使其更具自描述性 [N1]。

java
   public static String[] getMonthNames() {
     return dateFormatSymbols.getMonths();
   }

The isValidMonthCode function (lines 326–346) was made irrelevant by the Month enum, so I deleted it [G9].

isValidMonthCode 函数(第 326-346 行)因 Month 枚举而变得无关紧要,所以我删除了它 [G9]。

The monthCodeToQuarter function (lines 356–375) smells of FEATURE ENVY7 [G14] and probably belongs in the Month enum as a method named quarter. So I replaced it.

monthCodeToQuarter 函数(第 356-375 行)有 **依恋情结 (Feature Envy)**7 [G14] 的味道,可能应该作为名为 quarter 的方法属于 Month 枚举。所以我替换了它。

  1. [Refactoring].
  1. [Refactoring](重构)。
java
   public int quarter() {
     return 1 + (index-1)/3;
   }

This made the Month enum big enough to be in its own class. So I moved it out of DayDate to be consistent with the Day enum [G11],[G13].

这使得 Month 枚举大到足以成为一个单独的类。所以我把它从 DayDate 中移出来,以便与 Day 枚举保持一致 [G11],[G13]。

The next two methods are named monthCodeToString (lines 377–426). Again, we see the pattern of one method calling its twin with a flag. It is usually a bad idea to pass a flag as an argument to a function, especially when that flag simply selects the format of the output [G15]. I renamed, simplified, and restructured these functions and moved them into the Month enum [N1],[N3],[C3],[G14].

接下来的两个方法名为 monthCodeToString(第 377-426 行)。同样,我们看到了一个方法带标志调用其孪生方法的模式。将标志作为参数传递给函数通常是一个坏主意,尤其是当该标志仅仅选择输出格式时 [G15]。我重命名、简化并重组了这些函数,并将它们移至 Month 枚举中 [N1],[N3],[C3],[G14]。

java
   public String toString() {
     return dateFormatSymbols.getMonths()[index - 1];
   }
 
   public String toShortString() {
     return dateFormatSymbols.getShortMonths()[index - 1];
   }

The next method is stringToMonthCode (lines 428–472). I renamed it, moved it into the Month enum, and simplified it [N1],[N3],[C3],[G14],[G12].

下一个方法是 stringToMonthCode(第 428-472 行)。我重命名了它,将其移至 Month 枚举中,并对其进行了简化 [N1],[N3],[C3],[G14],[G12]。

java
   public static Month parse(String s) {
     s = s.trim();
     for (Month m : Month.values())
       if (m.matches(s))
         return m;
 
     try {
       return make(Integer.parseInt(s));
     }
     catch (NumberFormatException e) {}
     throw new IllegalArgumentException(“Invalid month ” + s);
   }

   private boolean matches(String s) {
     return s.equalsIgnoreCase(toString()) ||
            s.equalsIgnoreCase(toShortString());
   }

The isLeapYear method (lines 495–517) can be made a bit more expressive [G16].

isLeapYear 方法(第 495-517 行)可以表达得更清晰一些 [G16]。

java
   public static boolean isLeapYear(int year) {
     boolean fourth = year % 4 == 0;
     boolean hundredth = year % 100 == 0;
     boolean fourHundredth = year % 400 == 0;
     return fourth && (!hundredth || fourHundredth);
   }

The next function, leapYearCount (lines 519–536) doesn’t really belong in DayDate. Nobody calls it except for two methods in SpreadsheetDate. So I pushed it down [G6].

下一个函数 leapYearCount(第 519-536 行)实际上并不属于 DayDate。除了 SpreadsheetDate 中的两个方法外,没有人调用它。所以我把它下移了 [G6]。

The lastDayOfMonth function (lines 538–560) makes use of the LAST_DAY_OF_MONTH array. This array really belongs in the Month enum [G17], so I moved it there. I also simplified the function and made it a bit more expressive [G16].

lastDayOfMonth 函数(第 538-560 行)利用了 LAST_DAY_OF_MONTH 数组。这个数组实际上属于 Month 枚举 [G17],所以我把它移到了那里。我还简化了该函数,并使其更具表现力 [G16]。

java
   public static int lastDayOfMonth(Month month, int year) {
     if (month == Month.FEBRUARY && isLeapYear(year))
       return month.lastDay() + 1;
      else
       return month.lastDay();
   }

Now things start to get a bit more interesting. The next function is addDays (lines 562–576). First of all, because this function operates on the variables of DayDate, it should not be static [G18]. So I changed it to an instance method. Second, it calls the function toSerial. This function should be renamed toOrdinal [N1]. Finally, the method can be simplified.

现在事情变得有点有趣了。下一个函数是 addDays(第 562-576 行)。首先,因为这个函数操作 DayDate 的变量,它不应该是静态的 [G18]。所以我把它改成了一个实例方法。其次,它调用了函数 toSerial。这个函数应该重命名为 toOrdinal [N1]。最后,该方法可以被简化。

java
   public DayDate addDays(int days) {
     return DayDateFactory.makeDate(toOrdinal() + days);
   }

The same goes for addMonths (lines 578–602). It should be an instance method [G18]. The algorithm is a bit complicated, so I used EXPLAINING TEMPORARY VARIABLES8 [G19] to make it more transparent. I also renamed the method getYYY to getYear [N1].

addMonths(第 578-602 行)也是如此。它应该是一个实例方法 [G18]。算法有点复杂,所以我使用了 **解释性临时变量 (Explaining Temporary Variables)**8 [G19] 来使其更透明。我还将方法 getYYY 重命名为 getYear [N1]。

  1. [Beck97].
  1. [Beck97] (肯特·贝克的《Smalltalk Best Practice Patterns》)。
java
   public DayDate addMonths(int months) {
     int thisMonthAsOrdinal = 12 * getYear() + getMonth().index - 1;
     int resultMonthAsOrdinal = thisMonthAsOrdinal + months;
     int resultYear = resultMonthAsOrdinal / 12;
     Month resultMonth = Month.make(resultMonthAsOrdinal % 12 + 1);

     int lastDayOfResultMonth = lastDayOfMonth(resultMonth, resultYear);
     int resultDay = Math.min(getDayOfMonth(), lastDayOfResultMonth);
     return DayDateFactory.makeDate(resultDay, resultMonth, resultYear);
   }

The addYears function (lines 604–626) provides no surprises over the others.

addYears 函数(第 604-626 行)与其他函数相比没有什么意外。

java
   public DayDate plusYears(int years) {
     int resultYear = getYear() + years;
     int lastDayOfMonthInResultYear = lastDayOfMonth(getMonth(), resultYear);
     int resultDay = Math.min(getDayOfMonth(), lastDayOfMonthInResultYear);
     return DayDateFactory.makeDate(resultDay, getMonth(), resultYear);
   }

There is a little itch at the back of my mind that is bothering me about changing these methods from static to instance. Does the expression date.addDays(5) make it clear that the date object does not change and that a new instance of DayDate is returned? Or does it erroneously imply that we are adding five days to the date object? You might not think that is a big problem, but a bit of code that looks like the following can be very deceiving [G20].

将这些方法从静态改为实例方法,我心里总有一点不安。表达式 date.addDays(5) 是否清楚地表明 date 对象没有改变,并且返回了一个新的 DayDate 实例?还是它错误地暗示我们要给 date 对象增加五天?你可能不觉得这是个大问题,但像下面这样的代码可能会非常有欺骗性 [G20]。

java
   DayDate date = DateFactory.makeDate(5, Month.DECEMBER, 1952);
   date.addDays(7); // bump date by one week.

Someone reading this code would very likely just accept that addDays is changing the date object. So we need a name that breaks this ambiguity [N4]. So I changed the names to plusDays and plusMonths. It seems to me that the intent of the method is captured nicely by

阅读此代码的人很可能认为 addDays 正在更改 date 对象。所以我们需要一个打破这种歧义的名字 [N4]。所以我把名字改成了 plusDays 和 plusMonths。在我看来,方法的意图被很好地捕捉到了:

java
   DayDate date = oldDate.plusDays(5);

whereas the following doesn’t read fluidly enough for a reader to simply accept that the date object is changed:

而下面的写法读起来不够流畅,读者不会轻易认为 date 对象被改变了:

java
   date.plusDays(5);

The algorithms continue to get more interesting. getPreviousDayOfWeek (lines 628–660) works but is a bit complicated. After some thought about what was really going on [G21], I was able to simplify it and use EXPLAINING TEMPORARY VARIABLES [G19] to make it clearer. I also changed it from a static method to an instance method [G18], and got rid of the duplicate instance method [G5] (lines 997–1008).

算法继续变得更有趣。getPreviousDayOfWeek(第 628-660 行)可以工作,但有点复杂。在思考了到底发生了什么之后 [G21],我能够简化它并使用 解释性临时变量 [G19] 使其更清晰。我还将其从静态方法更改为实例方法 [G18],并去掉了重复的实例方法 [G5](第 997-1008 行)。

java
   public DayDate getPreviousDayOfWeek(Day targetDayOfWeek) {
     int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;
     if (offsetToTarget >= 0)
       offsetToTarget -= 7;
     return plusDays(offsetToTarget);
   }

The exact same analysis and result occurred for getFollowingDayOfWeek (lines 662–693).

对 getFollowingDayOfWeek(第 662-693 行)进行了同样的分析并得出了同样的结果。

java
   public DayDate getFollowingDayOfWeek(Day targetDayOfWeek) {
       int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;
       if (offsetToTarget <= 0)

         offsetToTarget += 7;
       return plusDays(offsetToTarget);
     }

The next function is getNearestDayOfWeek (lines 695–726), which we corrected back on page 270. But the changes I made back then aren’t consistent with the current pattern in the last two functions [G11]. So I made it consistent and used some EXPLAINING TEMPORARY VARIABLES [G19] to clarify the algorithm.

下一个函数是 getNearestDayOfWeek(第 695-726 行),我们在第 270 页修正过它。但我当时做的更改与最后两个函数中的当前模式不一致 [G11]。所以我让它保持一致,并使用了一些 解释性临时变量 [G19] 来阐明算法。

java
   public DayDate getNearestDayOfWeek(final Day targetDay) {
       int offsetToThisWeeksTarget = targetDay.index - getDayOfWeek().index;
       int offsetToFutureTarget = (offsetToThisWeeksTarget + 7) % 7;
       int offsetToPreviousTarget = offsetToFutureTarget - 7;
 
       if (offsetToFutureTarget > 3)
         return plusDays(offsetToPreviousTarget);
       else
         return plusDays(offsetToFutureTarget);
     }

The getEndOfCurrentMonth method (lines 728–740) is a little strange because it is an instance method that envies [G14] its own class by taking a DayDate argument. I made it a true instance method and clarified a few names.

getEndOfCurrentMonth 方法(第 728-740 行)有点奇怪,因为它是一个实例方法,却通过接受一个 DayDate 参数来“嫉妒” [G14] 它自己的类。我把它变成了一个真正的实例方法,并澄清了一些名称。

java
   public DayDate getEndOfMonth() {
       Month month = getMonth();
       int year = getYear();
       int lastDay = lastDayOfMonth(month, year);
       return DayDateFactory.makeDate(lastDay, month, year);
     }

Refactoring weekInMonthToString (lines 742–761) turned out to be very interesting indeed. Using the refactoring tools of my IDE, I first moved the method to the WeekInMonth enum that I created back on page 275. Then I renamed the method to toString. Next, I changed it from a static method to an instance method. All the tests still passed. (Can you guess where I am going?)

重构 weekInMonthToString(第 742-761 行)结果确实非常有趣。使用 IDE 的重构工具,我首先将该方法移动到我在第 275 页创建的 WeekInMonth 枚举中。然后我将该方法重命名为 toString。接下来,我将其从静态方法更改为实例方法。所有测试仍然通过。(你能猜到我要做什么吗?)

Next, I deleted the method entirely! Five asserts failed (lines 411–415, Listing B-4, page 374). I changed these lines to use the names of the enumerators (FIRST, SECOND, …). All the tests passed. Can you see why? Can you also see why each of these steps was necessary? The refactoring tool made sure that all previous callers of weekInMonthToString now called toString on the weekInMonth enumerator because all enumerators implement toString to simply return their names.…

接下来,我完全删除了这个方法!五个断言失败了(第 411-415 行,清单 B-4,第 374 页)。我将这些行更改为使用枚举器的名称(FIRST、SECOND 等)。所有测试都通过了。你能看出为什么吗?你也能看出为什么每一步都是必要的吗?重构工具确保 weekInMonthToString 的所有先前调用者现在都调用 weekInMonth 枚举器上的 toString,因为所有枚举器都实现了 toString 以简单地返回它们的名称……

Unfortunately, I was a bit too clever. As elegant as that wonderful chain of refactorings was, I finally realized that the only users of this function were the tests I had just modified, so I deleted the tests.

不幸的是,我有点太聪明了。尽管那一系列精彩的重构非常优雅,但我最终意识到这个函数的唯一用户就是我刚刚修改的测试,所以我删除了这些测试。

Fool me once, shame on you. Fool me twice, shame on me! So after determining that nobody other than the tests called relativeToString (lines 765–781), I simply deleted the function and its tests.

骗我一次,是你无耻;骗我两次,是我活该!所以在确定除了测试之外没有人调用 relativeToString(第 765-781 行)之后,我干脆删除了该函数及其测试。

We have finally made it to the abstract methods of this abstract class. And the first one is as appropriate as they come: toSerial (lines 838–844). Back on page 279 I had changed the name to toOrdinal. Having looked at it in this context, I decided the name should be changed to getOrdinalDay.

我们终于来到了这个抽象类的抽象方法。第一个方法恰如其分:toSerial(第 838-844 行)。在第 279 页,我已将名称更改为 toOrdinal。在这种上下文中审视它,我决定名称应更改为 getOrdinalDay。

The next abstract method is toDate (lines 838–844). It converts a DayDate to a java.util.Date. Why is this method abstract? If we look at its implementation in SpreadsheetDate (lines 198–207, Listing B-5, page 382), we see that it doesn’t depend on anything in the implementation of that class [G6]. So I pushed it up.

下一个抽象方法是 toDate(第 838-844 行)。它将 DayDate 转换为 java.util.Date。为什么这个方法是抽象的?如果我们查看它在 SpreadsheetDate 中的实现(第 198-207 行,清单 B-5,第 382 页),我们会发现它不依赖于该类实现中的任何内容 [G6]。所以我把它上移了。

The getYYYY, getMonth, and getDayOfMonth methods are nicely abstract. However, the getDayOfWeek method is another one that should be pulled up from SpreadSheetDate because it doesn’t depend on anything that can’t be found in DayDate [G6]. Or does it?

getYYYY、getMonth 和 getDayOfMonth 方法是很好的抽象方法。然而,getDayOfWeek 方法是另一个应该从 SpreadSheetDate 上移的方法,因为它不依赖于任何无法在 DayDate 中找到的东西 [G6]。是这样吗?

If you look carefully (line 247, Listing B-5, page 382), you’ll see that the algorithm implicitly depends on the origin of the ordinal day (in other words, the day of the week of day 0). So even though this function has no physical dependencies that couldn’t be moved to DayDate, it does have a logical dependency.

如果你仔细观察(第 247 行,清单 B-5,第 382 页),你会发现该算法隐含地依赖于序数日的原点(换句话说,第 0 天是星期几)。因此,即使此函数没有无法移动到 DayDate 的物理依赖关系,它也确实存在逻辑依赖关系。

Logical dependencies like this bother me [G22]. If something logical depends on the implementation, then something physical should too. Also, it seems to me that the algorithm itself could be generic with a much smaller portion of it dependent on the implementation [G6].

像这样的逻辑依赖让我感到困扰 [G22]。如果逻辑上的东西依赖于实现,那么物理上的东西也应该依赖于实现。此外,在我看来,算法本身可以是通用的,只有很小一部分依赖于实现 [G6]。

So I created an abstract method in DayDate named getDayOfWeekForOrdinalZero and implemented it in SpreadsheetDate to return Day.SATURDAY. Then I moved the getDayOfWeek method up to DayDate and changed it to call getOrdinalDay and getDayOfWeekForOrdinal-Zero.

因此,我在 DayDate 中创建了一个名为 getDayOfWeekForOrdinalZero 的抽象方法,并在 SpreadsheetDate 中实现了它以返回 Day.SATURDAY。然后我将 getDayOfWeek 方法上移到 DayDate,并将其更改为调用 getOrdinalDay 和 getDayOfWeekForOrdinal-Zero。

java
   public Day getDayOfWeek() {
       Day startingDay = getDayOfWeekForOrdinalZero();
       int startingOffset = startingDay.index - Day.SUNDAY.index;
       return Day.make((getOrdinalDay() + startingOffset) % 7 + 1);
     }

As a side note, look carefully at the comment on line 895 through line 899. Was this repetition really necessary? As usual, I deleted this comment along with all the others.

顺便提一下,仔细看看第 895 行到第 899 行的注释。这种重复真的有必要吗?像往常一样,我把这个注释和其他注释一起删除了。

The next method is compare (lines 902–913). Again, this method is inappropriately abstract [G6], so I pulled the implementation up into DayDate. Also, the name does not communicate enough [N1]. This method actually returns the difference in days since the argument. So I changed the name to daysSince. Also, I noted that there weren’t any tests for this method, so I wrote them.

下一个方法是 compare(第 902-913 行)。同样,这个方法不恰当地被设为抽象 [G6],所以我将实现上移到了 DayDate。此外,该名称传达的信息不够 [N1]。这个方法实际上返回的是与参数相差的天数。所以我将名称更改为 daysSince。另外,我注意到这个方法没有任何测试,所以我编写了测试。

The next six functions (lines 915–980) are all abstract methods that should be implemented in DayDate. So I pulled them all up from SpreadsheetDate.

接下来的六个函数(第 915-980 行)都是应该在 DayDate 中实现的抽象方法。所以我把它们全部从 SpreadsheetDate 上移了。

The last function, isInRange (lines 982–995) also needs to be pulled up and refactored. The switch statement is a bit ugly [G23] and can be replaced by moving the cases into the DateInterval enum.

最后一个函数 isInRange(第 982-995 行)也需要上移和重构。switch 语句有点丑陋 [G23],可以通过将 case 移动到 DateInterval 枚举中来替换。

java
   public enum DateInterval {
       OPEN {
         public boolean isIn(int d, int left, int right) {
           return d > left && d < right;
         }
       },
       CLOSED_LEFT {
         public boolean isIn(int d, int left, int right) {
           return d >= left && d < right;
         }
       },
       CLOSED_RIGHT {
         public boolean isIn(int d, int left, int right) {
           return d > left && d <= right;
         }
       },
       CLOSED {
         public boolean isIn(int d, int left, int right) {
           return d >= left && d <= right;
         }
       };
 
       public abstract boolean isIn(int d, int left, int right);
     }

   public boolean isInRange(DayDate d1, DayDate d2, DateInterval interval) {
       int left = Math.min(d1.getOrdinalDay(), d2.getOrdinalDay());
       int right = Math.max(d1.getOrdinalDay(), d2.getOrdinalDay());
       return interval.isIn(getOrdinalDay(), left, right);
     }

That brings us to the end of DayDate. So now we’ll make one more pass over the whole class to see how well it flows.

这就把我们带到了 DayDate 的结尾。所以现在我们将再过一遍整个类,看看它是否流畅。

First, the opening comment is long out of date, so I shortened and improved it [C2].

首先,开头的注释早已过时,所以我缩短并改进了它 [C2]。

Next, I moved all the remaining enums out into their own files [G12].

接下来,我将所有剩余的枚举移到了它们自己的文件中 [G12]。

Next, I moved the static variable (dateFormatSymbols) and three static methods (getMonthNames, isLeapYear, lastDayOfMonth) into a new class named DateUtil [G6].

接下来,我将静态变量 (dateFormatSymbols) 和三个静态方法 (getMonthNames, isLeapYear, lastDayOfMonth) 移到了一个名为 DateUtil 的新类中 [G6]。

I moved the abstract methods up to the top where they belong [G24].

我把抽象方法移到了它们所属的顶部 [G24]。

I changed Month.make to Month.fromInt [N1] and did the same for all the other enums. I also created a toInt() accessor for all the enums and made the index field private.

我将 Month.make 更改为 Month.fromInt [N1],并对所有其他枚举做了同样的操作。我还为所有枚举创建了一个 toInt() 访问器,并将 index 字段设为私有。

There was some interesting duplication [G5] in plusYears and plusMonths that I was able to eliminate by extracting a new method named correctLastDayOfMonth, making the all three methods much clearer.

在 plusYears 和 plusMonths 中有一些有趣的重复 [G5],我通过提取一个名为 correctLastDayOfMonth 的新方法消除了这些重复,使所有这三个方法都更加清晰。

I got rid of the magic number 1 [G25], replacing it with Month.JANUARY.toInt() or Day.SUNDAY.toInt(), as appropriate. I spent a little time with SpreadsheetDate, cleaning up the algorithms a bit. The end result is contained in Listing B-7, page 394, through Listing B-16, page 405.

我去掉了魔术数字 1 [G25],根据情况将其替换为 Month.JANUARY.toInt() 或 Day.SUNDAY.toInt()。我花了一点时间处理 SpreadsheetDate,稍微清理了一下算法。最终结果包含在清单 B-7(第 394 页)到清单 B-16(第 405 页)中。

Interestingly the code coverage in DayDate has decreased to 84.9 percent! This is not because less functionality is being tested; rather it is because the class has shrunk so much that the few uncovered lines have a greater weight. DayDate now has 45 out of 53 executable statements covered by tests. The uncovered lines are so trivial that they weren’t worth testing.

有趣的是,DayDate 中的代码覆盖率下降到了 84.9%!这不是因为被测试的功能减少了;而是因为类缩小了太多,以至于少数未覆盖的行具有更大的权重。DayDate 现在 53 个可执行语句中有 45 个被测试覆盖。未覆盖的行太微不足道了,不值得测试。

CONCLUSION

结论

So once again we’ve followed the Boy Scout Rule. We’ve checked the code in a bit cleaner than when we checked it out. It took a little time, but it was worth it. Test coverage was increased, some bugs were fixed, the code was clarified and shrunk. The next person to look at this code will hopefully find it easier to deal with than we did. That person will also probably be able to clean it up a bit more than we did.

所以,我们再次遵循了 童子军军规。我们签入的代码比签出时更干净了一点。这花了一点时间,但是值得的。测试覆盖率提高了,修复了一些 Bug,代码变得清晰且精简了。下一个查看此代码的人有望发现它比我们接手时更容易处理。那个人也许还能把它清理得比我们更干净一点。

基于 MIT 许可发布