diff --git a/README.md b/README.md index c6fde9d3..e1c45c9b 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ - [第 9 章 在一起更好还是分开更好?](docs/ch09.md) - [第 10 章 定义不存在的错误](docs/ch10.md) - [第 11 章 设计它两次](docs/ch11.md) -- [第 12 章 为什么写评论呢?四个理由](docs/ch12.md) +- [第 12 章 为什么要写注释呢?有四个理由](docs/ch12.md) - [第 13 章 注释应该描述代码中不明显的内容](docs/ch13.md) - [第 14 章 选择的名字](docs/ch14.md) -- [第 15 章 先写评论](docs/ch15.md) +- [第 15 章 先写注释](docs/ch15.md) - [第 16 章 修改现有的代码](docs/ch16.md) - [第 17 章 一致性](docs/ch17.md) - [第 18 章 代码应该是显而易见的](docs/ch18.md) diff --git a/docs/README.md b/docs/README.md index f15062ac..7509965d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,10 +16,10 @@ - [第 9 章 在一起更好还是分开更好?](ch09.md) - [第 10 章 定义不存在的错误](ch10.md) - [第 11 章 设计它两次](ch11.md) -- [第 12 章 为什么写评论呢?四个理由](ch12.md) +- [第 12 章 为什么要写注释?有四个理由](ch12.md) - [第 13 章 注释应该描述代码中不明显的内容](ch13.md) - [第 14 章 选择的名字](ch14.md) -- [第 15 章 先写评论](ch15.md) +- [第 15 章 先写注释](ch15.md) - [第 16 章 修改现有的代码](ch16.md) - [第 17 章 一致性](ch17.md) - [第 18 章 代码应该是显而易见的](ch18.md) diff --git a/docs/ch12.md b/docs/ch12.md index d0d5e02f..0edceef9 100644 --- a/docs/ch12.md +++ b/docs/ch12.md @@ -1,4 +1,4 @@ -# 第 12 章 为什么写注释?四个理由 +# 第 12 章 为什么要写注释?有四个理由 > Chapter 12 Why Write Comments? The Four Excuses diff --git a/docs/ch13.md b/docs/ch13.md index 6d643085..61a325a7 100644 --- a/docs/ch13.md +++ b/docs/ch13.md @@ -94,7 +94,7 @@ caretMemX = null; > None of these comments provide any value. For the first two comments, the code is already clear enough that it doesn’t really need comments; in the third case, a comment might be useful, but the current comment doesn’t provide enough detail to be helpful. -这些注释均未提供任何价值。对于前两个注释,代码已经很清楚了,它实际上不需要注释。在第三种情况下,注释可能有用,但是当前注释没有提供足够的细节来提供帮助。 +这些注释均未提供任何价值。对于前两个注释,代码已经很清楚了,它实际上不需要注释。第三个注释可能有用,但是当前注释没有提供足够的细节来提供帮助。 > After you have written a comment, ask yourself the following question: could someone who has never seen the code write the comment just by looking at the code next to the comment? If the answer is yes, as in the examples above, then the comment doesn’t make the code any easier to understand. Comments like these are why some people think that comments are worthless. @@ -122,7 +122,7 @@ private static final int textHorizontalPadding = 4; > These comments just take the words from the method or variable name, perhaps add a few words from argument names and types, and form them into a sentence. For example, the only thing in the second comment that isn’t in the code is the word “to”! Once again, these comments could be written just by looking at the declarations, without any understanding the methods of variables; as a result, they have no value. -这些注释只是从方法或变量名中提取单词,或者从参数名称和类型中添加几个单词,然后将它们组成一个句子。例如,第二个注释中唯一不在代码中的是单词“ to”!再说一次,这些注释可以仅通过查看声明来编写,而无需任何了解变量的方法。结果,它们没有价值。 +这些注释只是从方法或变量名中提取单词,或者从参数名称和类型中添加几个单词,然后将它们组成一个句子。例如,第二个注释中唯一不在代码中的是单词“ to”!再说一次,这些注释可以仅通过查看声明来编写,而无需任何了解变量的方法,所以它们没有价值。 > img Red Flag: Comment Repeats Code img @@ -364,7 +364,7 @@ public class Http {...} - 注释必须描述每个参数和返回值(如果有)。这些注释必须非常精确,并且必须描述对参数值的任何约束以及参数之间的依赖关系。 - 如果该方法有任何副作用,则必须在接口注释中记录这些副作用。副作用是该方法的任何结果都会影响系统的未来行为,但不属于结果的一部分。例如,如果该方法将一个值添加到内部数据结构中,可以通过将来的方法调用来检索该值,则这是副作用。写入文件系统也是一个副作用。 - 方法的接口注释必须描述该方法可能产生的任何异常。 -- 如果在调用某个方法之前必须满足任何前提条件,则必须对其进行描述(也许必须先调用其他方法;对于二进制搜索方法,必须对要搜索的列表进行排序)。尽量减少前提条件是一个好主意,但是任何保留的条件都必须记录在案。 +- 如果在调用某个方法之前必须满足任何前提条件,则必须对其进行描述(也许必须先调用其他方法;对于二分查找方法,必须对要搜索的列表进行排序)。尽量减少前提条件是一个好主意,但是任何保留的条件都必须记录在案。 > Here is the interface comment for a method that copies data out of a Buffer object: @@ -486,7 +486,7 @@ while (true) { --- - 第一段的大部分与实现有关,而不是接口。举一个例子,用户不需要知道用于与服务器通信的特定远程过程调用的名称。在第一段的后半部分中提到的配置参数都是所有私有变量,它们仅与类的维护者相关,而与类的用户无关。所有这些实现信息都应从注释中省略。 -- 该评论还包括一些显而易见的事情。例如,不需要告诉用户包括 IndexLookup.h:任何编写 C++ 代码的人都可以猜测这是必要的。另外,“通过提供所有必要的信息”一词无语,因此可以省略。 +- 该注释还包括一些显而易见的事情。例如,不需要告诉用户包括 IndexLookup.h:任何编写 C++ 代码的人都可以猜测这是必要的。另外,“通过提供所有必要的信息”一词无语,因此可以省略。 > A shorter comment for this class is sufficient (and preferable): diff --git a/docs/ch14.md b/docs/ch14.md index 1a2c6f8d..40e7f2ca 100644 --- a/docs/ch14.md +++ b/docs/ch14.md @@ -14,15 +14,15 @@ > It took six months, but I eventually found and fixed the bug. The problem was actually quite simple (as are most bugs, once you figure them out). The file system code used the variable name block for two different purposes. In some situations, block referred to a physical block number on disk; in other situations, block referred to a logical block number within a file. Unfortunately, at one point in the code there was a block variable containing a logical block number, but it was accidentally used in a context where a physical block number was needed; as a result, an unrelated block on disk got overwritten with zeroes. -花了六个月的时间,但我最终找到并修复了该错误。这个问题实际上很简单(就像大多数错误一样,一旦找出它们)。文件系统代码将变量名块用于两个不同的目的。在某些情况下,块是指磁盘上的物理块号。在其他情况下,块是指文件中的逻辑块号。不幸的是,在代码的某一点上有一个包含逻辑块号的块变量,但是在需要物理块号的情况下意外地使用了它。结果,磁盘上无关的块被零覆盖。 +花了六个月的时间,但我最终找到并修复了该错误。这个问题实际上很简单(就像大多数错误一样,一旦找出它们)。文件系统代码将变量名 “block” 用于两个不同的目的。在某些情况下,“block” 是指磁盘上的物理块号。在其他情况下,“block” 是指文件中的逻辑块号。不幸的是,在代码的某处有一个包含逻辑块号的块变量,但是在需要物理块号的情况下意外地使用了它。结果,磁盘上无关的块被重置为零了。 > While tracking down the bug, several people, including myself, read over the faulty code, but we never noticed the problem. When we saw the variable block used as a physical block number, we reflexively assumed that it really held a physical block number. It took a long process of instrumentation, which eventually showed that the corruption must be happening in a particular statement, before I was able to get past the mental block created by the name and check to see exactly where its value came from. If different variable names had been used for the different kinds of blocks, such as fileBlock and diskBlock, it’s unlikely that the error would have happened; the programmer would have known that fileBlock couldn’t be used in that situation. -在跟踪该错误时,包括我自己在内的几个人阅读了错误的代码,但我们从未注意到问题所在。当我们看到可变块用作物理块号时,我们反身地假设它确实拥有物理块号。经过很长时间的检测,最终显示出腐败一定是在特定的语句中发生的,然后我才能越过该名称所创建的思维障碍,并查看其价值的确切来源。如果对不同种类的块(例如 fileBlock 和 diskBlock)使用了不同的变量名,则错误不太可能发生;程序员会知道在那种情况下不能使用 fileBlock。 +在跟踪该错误时,包括我自己在内的几个人阅读了错误的代码,但我们从未注意到问题所在。当我们看到变量 “block” 用作物理块号时,我们本能地假设它确实拥有物理块号。经过很长时间的检测,最终表明损坏必须发生在特定的语句中,然后我才能越过该名称所创建的思维障碍,并检查它的值究竟来自何处。如果对不同种类的块(例如 fileBlock 和 diskBlock)使用不同的变量名,则错误不太可能发生;程序员会知道在那种情况下不能使用 fileBlock。 > Unfortunately, most developers don’t spend much time thinking about names. They tend to use the first name that comes to mind, as long as it’s reasonably close to matching the thing it names. For example, block is a pretty close match for both a physical block on disk and a logical block within a file; it’s certainly not a horrible name. Even so, it resulted in a huge expenditure of time to track down a subtle bug. Thus, you shouldn’t settle for names that are just “reasonably close”. Take a bit of extra time to choose great names, which are precise, unambiguous, and intuitive. The extra attention will pay for itself quickly, and over time you’ll learn to choose good names quickly. -不幸的是,大多数开发人员没有花太多时间在思考名字。他们倾向于使用想到的名字,只要它与匹配的名字相当接近即可。例如,块与磁盘上的物理块和文件内的逻辑块非常接近;这肯定不是一个可怕的名字。即使这样,它仍然要花费大量时间来查找一个细微的错误。因此,您不应该只选择“合理接近”的名称。花一些额外的时间来选择准确,明确且直观的好名字。额外的注意力将很快收回成本,随着时间的流逝,您将学会快速选择好名字。 +不幸的是,大多数开发人员没有花太多时间在思考名字上面。他们倾向于使用想到的第一个名字,只要它与命名的事件合理接近即可。例如,块与磁盘上的物理块和文件内的逻辑块非常接近;这肯定不是一个可怕的名字。即使如此,它还是导致花费了大量时间来追踪一个细微的错误。因此,您不应该只选择“合理接近”的名称。花一些额外的时间来选择准确,明确且直观的好名字。额外的注意力将很快收回成本,随着时间的流逝,您将学会快速选择好名字。 ## 14.2 Create an image 创建图像 @@ -38,7 +38,7 @@ > Good names have two properties: precision and consistency. Let’s start with precision. The most common problem with names is that they are too generic or vague; as a result, it’s hard for readers to tell what the name refers to; the reader may assume that the name refers to something different from reality, as in the block bug above. Consider the following method declaration: -良好名称具有两个属性:精度和一致性。让我们从精度开始。名称最常见的问题是名称太笼统或含糊不清。结果,读者很难说出这个名字指的是什么。读者可能会认为该名称所指的是与现实不符的事物,如上面的代码错误所示。考虑以下方法声明: +良好名称具有两个属性:精度和一致性。让我们从精度开始。名称最常见的问题是名称太笼统或含糊不清。结果,读者很难说出这个名字指的是什么。读者可能会认为该名称所指的是与现实不符的事物,如上面的 “block” bug 所示。考虑以下方法声明: ```java /** @@ -70,7 +70,7 @@ int IndexletManager::getCount() {...} > The name blinkStatus doesn’t convey enough information. The word “status” is too vague for a boolean value: it gives no clue about what a true or false value means. The word “blink” is also vague, since it doesn’t indicate what is blinking. The following alternative is better: - blinkStatus (这个命名)无法传达足够的信息。“status”一词对于布尔值来说太含糊了:它不提供关于真值或假值含义的任何线索。“blink”一词也含糊不清,因为它并不表示闪烁的内容。以下替代方法更好: + blinkStatus (这个命名)无法传达足够的信息。“status”一词对于布尔值来说太含糊了:它不提供关于真值或假值含义的任何线索。“blink”一词也含糊不清,因为它并没有将 “blinking” 表述清楚。以下替代方法更好: ```java // Controls cursor blinking: true means the cursor is visible, @@ -108,7 +108,7 @@ int IndexletManager::getCount() {...} > Like all rules, the rule about choosing precise names has a few exceptions. For example, it’s fine to use generic names like i and j as loop iteration variables, as long as the loops only span a few lines of code. If you can see the entire range of usage of a variable, then the meaning of the variable will probably be obvious from the code so you don’t need a long name. For example, consider the following code: -像所有规则一样,有关选择精确名称的规则也有一些例外。例如,只要循环仅跨越几行代码,就可以将通用名称(如 i 和 j)用作循环迭代变量。如果您可以看到一个变量的整个用法范围,那么该变量的含义在代码中就很明显了,因此您不需要长名称。例如,考虑以下代码: +像所有规则一样,有关选择精确名称的规则也有一些例外。例如,只要循环仅跨越几行代码,就可以将通用名称(如 i 和 j)用作循环迭代变量。如果您可以看到一个变量的整个用法范围,那么该变量的含义在代码中就很明显了,因此您不需要长名称。例如以下代码: ```java for (i = 0; i < numLines; i++) { @@ -134,13 +134,13 @@ void delete(Range selection) {...} > If you find it difficult to come up with a name for a particular variable that is precise, intuitive, and not too long, this is a red flag. It suggests that the variable may not have a clear definition or purpose. When this happens, consider alternative factorings. For example, perhaps you are trying to use a single variable to represent several things; if so, separating the representation into multiple variables may result in a simpler definition for each variable. The process of choosing good names can improve your design by identifying weaknesses. -如果您发现很难为精确,直观且时间不长的特定变量命名,那么这是一个危险信号。这表明该变量可能没有明确的定义或目的。发生这种情况时,请考虑其他因素。例如,也许您正在尝试使用单个变量来表示几件事;如果是这样,将表示形式分成多个变量可能会导致每个变量的定义更简单。选择好名字的过程可以通过识别弱点来改善您的设计。 +如果您发现很难为特定变量想出一个准确、直观且不太长的名字,那么这是一个危险信号。这表明该变量可能没有明确的定义或目的。发生这种情况时,请考虑替代因素。例如,也许您正在尝试使用单个变量来表示多个事物;如果是这样,将表示分成多个变量可能会让每个变量的定义更简单。选择好名字的过程可以通过识别弱点来改善您的设计。 > img Red Flag: Hard to Pick Name img > If it’s hard to find a simple name for a variable or method that creates a clear image of the underlying object, that’s a hint that the underlying object may not have a clean design. -如果很难为创建基础对象清晰图像的变量或方法找到简单的名称,则表明基础对象可能没有简洁的设计。 +如果很难为创建基础对象清晰图像的变量或方法找到简单的名称,则暗示底层对象的设计可能不够简洁。 ## 14.4 Use names consistently 一致使用名称(命名要确保一致性) @@ -212,7 +212,7 @@ Go 文化鼓励在多个不同的事物上使用相同的短名称:ch 用于 > Overall, I would argue that readability must be determined by readers, not writers. If you write code with short variable names and the people who read it find it easy to understand, then that’s fine. If you start getting complaints that your code is cryptic, then you should consider using longer names (a Web search for “go language short names” will identify several such complaints). Similarly, if I start getting complaints that long variable names make my code harder to read, then I’ll consider using shorter ones. -总的来说,我认为可读性必须由读者而不是作家来决定。如果您使用简短的变量名编写代码,并且阅读该代码的人很容易理解,那么很好。如果您开始抱怨代码很含糊,那么您应该考虑使用更长的名称(在网络上搜索“ go language short name”(使用语言简称)会识别出几种此类抱怨)。同样,如果我开始抱怨长变量名使我的代码难以阅读,那么我会考虑使用较短的变量名。 +总的来说,我认为可读性必须由读者而不是作家来决定。如果您使用简短的变量名编写代码,并且阅读该代码的人很容易理解,那么很好。如果您开始抱怨代码很含糊,那么您应该考虑使用更长的名称(在网络上搜索“ go language short name”(使用语言简称)会发现一些这样的抱怨)。同样,如果我开始抱怨长变量名使我的代码难以阅读,那么我会考虑使用较短的变量名。 > Gerrand makes one comment that I agree with: “The greater the distance between a name’s declaration and its uses, the longer the name should be.” The earlier discussion about using loop variables named i and j is an example of this rule. @@ -222,6 +222,6 @@ Gerrand 发表一个我同意的评论:“名称声明与使用之间的距离 > Well chosen names help to make code more obvious; when someone encounters the variable for the first time, their first guess about its behavior, made without much thought, will be correct. Choosing good names is an example of the investment mindset discussed in Chapter 3: if you take a little extra time up front to select good names, it will be easier for you to work on the code in the future. In addition, you will be less likely to introduce bugs. Developing a skill for naming is also an investment. When you first decide to stop settling for mediocre names, you may find it frustrating and time-consuming to come up with good names. However, as you get more experience you’ll find that it becomes easier; eventually, you’ll get to the point where it takes almost no extra time to choose good names, so you will get the benefits almost for free. -精心选择的名称有助于使代码更明显。当某人第一次遇到该变量时,他们对行为的第一次猜测是正确的。选择好名字是第 3 章讨论的投资思维方式的一个示例:如果您花一些额外的时间来选择好名字,那么将来您将更容易处理代码。此外,您不太可能引入错误。培养命名技巧也是一项投资。当您第一次决定停止为平庸的名字定居时,您会发现想出好名字的过程既令人沮丧又耗时。但是,随着您获得更多的经验,您会发现它变得更加容易。最终,您将几乎不需要花费额外的时间来选择好名字,因此您几乎可以免费获得好处。 +精心选择的名称有助于使代码更明显。当某人第一次遇到该变量时,他们对行为的第一次猜测是正确的。选择好名字是第 3 章讨论的投资思维方式的一个示例:如果您花一些额外的时间来选择好名字,那么将来您将更容易处理代码。此外,您不太可能引入错误。培养命名技巧也是一项投资。当您第一次决定不再满足于平庸的名字时,您会发现想出好名字的过程既令人沮丧又耗时。但是,随着您获得更多的经验,您会发现它变得更加容易。最终,您将几乎不需要花费额外的时间来选择好名字,因此您几乎可以免费获得好处。 1 https://talks.golang.org/2014/names.slide#1 diff --git a/docs/ch15.md b/docs/ch15.md index 52ab9781..d673f673 100644 --- a/docs/ch15.md +++ b/docs/ch15.md @@ -4,21 +4,21 @@ > Many developers put off writing documentation until the end of the development process, after coding and unit testing are complete. This is one of the surest ways to produce poor quality documentation. The best time to write comments is at the beginning of the process, as you write the code. Writing the comments first makes documentation part of the design process. Not only does this produce better documentation, but it also produces better designs and it makes the process of writing documentation more enjoyable. -在完成编码和单元测试之后,许多开发人员推迟编写文档,直到开发过程结束。这是产生质量差的文档的最可靠方法之一。编写注释的最佳时间是在过程开始时。首先编写注释使文档成为设计过程的一部分。这不仅可以产生更好的文档,还可以产生更好的设计,并使编写文档的过程更加愉快。 +许多开发人员推迟编写文档,直到开发过程结束,编码和单元测试完成之后。这是产生质量差的文档的最可靠方法之一。编写注释的最佳时间是在过程开始时。首先编写注释使文档成为设计过程的一部分。这不仅可以产生更好的文档,还可以产生更好的设计,并使编写文档的过程更加愉快。 ## 15.1 Delayed comments are bad comments 迟到的注释不是好注释 > Almost every developer I have ever met puts off writing comments. When asked why they don’t write documentation earlier, they say that the code is still changing. If they write documentation early, they say, they’ll have to rewrite it when the code changes; better to wait until the code stabilizes. However, I suspect that there is also another reason, which is that they view documentation as drudge work; thus, they put it off as long as possible. -我见过的几乎每个开发人员都会推迟编写注释。当被问及为什么不更早编写文档时,他们说代码仍在更改。他们说,如果他们尽早编写文档,则必须在代码更改时重新编写文档。最好等到代码稳定下来。但是,我怀疑还有另一个原因,那就是他们将文档视为繁琐的工作。因此,他们尽可能地推迟了。 +我见过的几乎每个开发人员都会推迟编写注释。当被问及为什么不更早编写文档时,他们说代码仍在更改。他们说,如果他们尽早编写文档,则必须在代码更改时重新编写文档。最好等到代码稳定下来。但是,我怀疑还有另一个原因,那就是他们将文档视为苦差事。因此,他们尽可能地推迟了。 > Unfortunately, this approach has several negative consequences. First, delaying documentation often means that it never gets written at all. Once you start delaying, it’s easy to delay a bit more; after all, the code will be even more stable in a few more weeks. By the time the code has inarguably stabilized, there is a lot of it, which means the task of writing documentation has become huge and even less attractive. There’s never a convenient time to stop for a few days and fill in all of the missing comments, and it’s easy to rationalize that the best thing for the project is to move on and fix bugs or write the next new feature. This will create even more undocumented code. -不幸的是,这种方法有几个负面影响。首先,延迟文档通常意味着根本无法编写文档。一旦开始延迟,就容易再延迟一些。毕竟,代码将在几周后变得更加稳定。到了代码毫无疑问地稳定下来的时候,代码已经很多了,这意味着编写文档的任务变得越来越庞大,吸引力也越来越小。从来没有一个方便的时间可以停下来几天并填写所有遗漏的注释,并且很容易使该项目的最佳选择合理化,那就是继续并修复错误或编写下一个新功能。这将创建更多未记录的代码。 +不幸的是,这种方法有几个负面影响。首先,延迟文档通常意味着根本无法编写文档。一旦开始延迟,就容易再延迟一些。毕竟,代码将在几周后变得更加稳定。到了代码毫无疑问地稳定下来的时候,代码已经很多了,这意味着编写文档的任务变得越来越庞大,甚至没有了吸引力。从来没有一个合适的时间可以停下来几天并填写所有缺失的注释,并且很容易合理化项目最好的事情是继续前进并修复错误或编写下一个新功能。这将导致更多没有注释的代码。 > Even if you do have the self-discipline to go back and write the comments (and don’t fool yourself: you probably don’t), the comments won’t be very good. By this time in the process, you have checked out mentally. In your mind, this piece of code is done; you are eager to move on to your next project. You know that writing comments is the right thing to do, but it’s no fun. You just want to get through it as quickly as possible. Thus, you make a quick pass over the code, adding just enough comments to look respectable. By now, it’s been a while since you designed the code, so your memories of the design process are becoming fuzzy. You look at the code as you are writing the comments, so the comments repeat the code. Even if you try to reconstruct the design ideas that aren’t obvious from the code, there will be things you don’t remember. Thus, the comments are missing some of the most important things they should describe. -即使你有自律性回去写注释(不要欺骗你自己:你可能没有),注释也不会很好。在这个过程的这个时候,你已经在精神上离开了。在你的脑海中,这段代码已经完成了;你急于开始下一个项目。你知道写注释是正确的事情,但它没有乐趣。你只想尽快度过难关。因此,您可以快速地浏览代码,添加足够的注释以使其看起来令人满意。到目前为止,您已经有一段时间没有设计代码了,所以您对设计过程的记忆变得模糊了。您在编写注释时查看代码,因此注释重复了代码。即使您试图重构代码中不明显的设计思想,也会有您不记得的事情。因此,这些注释忽略了他们应该描述的一些最重要的事情。 +即使您有自律性回去写注释(不要欺骗您自己:您可能没有),注释也不会很好。在这个过程的这个时候,你已经在精神上离开了。在你的脑海中,这段代码已经完成了;你急于开始下一个项目。你知道写注释是正确的事情,但它没有乐趣。你只想尽快度过难关。因此,您快速地浏览代码,添加足够的注释以使其看起来令人满意。到目前为止,您设计代码已经有一段时间了,所以您对设计过程的记忆变得模糊了。您查看代码完成注释,因此注释重复了代码(comments repeat the code)。即使您试图重构代码中不明显的设计思想,也会有您不记得的事情。因此,这些注释遗漏了一些他们应该描述的最重要的事情。 ## 15.2 Write the comments first 首先写注释 @@ -38,9 +38,9 @@ - 对于新类,我首先编写类接口注释。 - 接下来,我为最重要的公共方法编写接口注释和签名,但将方法主体保留为空。 - 我对这些注释进行了迭代,直到基本结构感觉正确为止。 -- 在这一点上,我为类中最重要的类实例变量编写了声明和注释。 +- 此时我为类中最重要的类实例变量编写了声明和注释。 - 最后,我填写方法的主体,并根据需要添加实现注释。 -- 在编写方法主体时,我通常会发现需要其他方法和实例变量。对于每个新方法,我在方法主体之前编写接口注释。例如变量,我在编写变量声明的同时填写了注释。 +- 在编写方法主体时,我通常会发现需要其他方法和实例变量。对于每个新方法,我在方法主体之前编写接口注释。对于每个变量,我在编写其声明的同时填写了注释。 > When the code is done, the comments are also done. There is never a backlog of unwritten comments. @@ -48,13 +48,13 @@ > The comments-first approach has three benefits. First, it produces better comments. If you write the comments as you are designing the class, the key design issues will be fresh in your mind, so it’s easy to record them. It’s better to write the interface comment for each method before its body, so you can focus on the method’s abstraction and interface without being distracted by its implementation. During the coding and testing process you will notice and fix problems with the comments. As a result, the comments improve over the course of development. -注释优先的方法具有三个好处。首先,它会产生更好的注释。如果您在设计课程时写注释,那么关键的设计问题将在您的脑海中浮现,因此很容易记录下来。最好在每个方法的主体之前编写接口注释,这样您就可以专注于方法的抽象和接口,而不会因其实现而分心。在编码和测试过程中,您会注意到并修复注释问题。结果,注释在开发过程中得到了改善。 +注释优先的方法具有三个好处。首先,它会产生更好的注释。如果您在设计类时写注释,那么关键的设计问题将在您的脑海中浮现,因此很容易记录下来。最好在每个方法的主体之前编写接口注释,这样您就可以专注于方法的抽象和接口,而不会因其实现而分心。在编码和测试过程中,您会注意到并修复注释中的问题。结果,注释在开发过程中得到了改善。 ## 15.3 Comments are a design tool 注释是一种设计工具 > The second, and most important, benefit of writing the comments at the beginning is that it improves the system design. Comments provide the only way to fully capture abstractions, and good abstractions are fundamental to good system design. If you write comments describing the abstractions at the beginning, you can review and tune them before writing implementation code. To write a good comment, you must identify the essence of a variable or piece of code: what are the most important aspects of this thing? It’s important to do this early in the design process; otherwise you are just hacking code. -在开始时编写注释的第二个也是最重要的好处是可以改善系统设计。注释提供了完全捕获抽象的唯一方法,好的抽象是好的系统设计的基础。如果您在一开始就写了描述抽象的注释,则可以在编写实现代码之前对其进行检查和调整。要写一个好的注释,您必须确定一个变量或一段代码的本质:这件事最重要的方面是什么?在设计过程的早期进行此操作很重要;否则,您只是在破解代码。 +在开始时编写注释的第二个也是最重要的好处是可以改善系统设计。注释提供了完全捕获抽象的唯一方法,好的抽象是好的系统设计的基础。如果您在一开始就写了描述抽象的注释,则可以在编写实现代码之前对其进行检查和调整。要写一个好的注释,您必须确定一个变量或一段代码的本质:这件事最重要的方面是什么?在设计过程的早期进行此操作很重要;否则,您只是在破解代码(hacking code 未找到合适的翻译)。 > Comments serve as a canary in the coal mine of complexity. If a method or variable requires a long comment, it is a red flag that you don’t have a good abstraction. Remember from Chapter 4 that classes should be deep: the best classes have very simple interfaces yet implement powerful functions. The best way to judge the complexity of an interface is from the comments that describe it. If the interface comment for a method provides all the information needed to use the method and is also short and simple, that indicates that the method has a simple interface. Conversely, if there’s no way to describe a method completely without a long and complicated comment, then the method has a complex interface. You can compare a method’s interface comment with the implementation to get a sense of how deep the method is: if the interface comment must describe all the major features of the implementation, then the method is shallow. The same idea applies to variables: if it takes a long comment to fully describe a variable, it’s a red flag that suggests you may not have chosen the right variable decomposition. Overall, the act of writing comments allows you to evaluate your design decisions early, so you can discover and fix problems. @@ -68,23 +68,23 @@ > Of course, comments are only a good indicator of complexity if they are complete and clear. If you write a method interface comment that doesn’t provide all the information needed to invoke the method, or one that is so cryptic that it’s hard to understand, then that comment doesn’t provide a good measure of the method’s depth. -当然,如果注释完整而清晰,那么它们仅是复杂性的良好指标。如果编写的方法接口注释未提供调用该方法所需的全部信息,或者编写的注释过于神秘以至于难以理解,则该注释不能很好地衡量该方法的深度。 +当然,如果注释完整而清晰,那么它们仅是复杂性的良好指标。如果编写的方法接口注释未提供调用该方法所需的全部信息,或者编写的注释太过晦涩难懂,那么则该注释不能很好地衡量该方法的深度。 ## 15.4 Early comments are fun comments 早期注释很有趣 > The third and final benefit of writing comments early is that it makes comment-writing more fun. For me, one of the most enjoyable parts of programming is the early design phase for a new class, where I’m fleshing out the abstractions and structure for the class. Most of my comments are written during this phase, and the comments are how I record and test the quality of my design decisions. I’m looking for the design that can be expressed completely and clearly in the fewest words. The simpler the comments, the better I feel about my design, so finding simple comments is a source of pride. If you are programming strategically, where your main goal is a great design rather than just writing code that works, then writing comments should be fun, since that’s how you identify the best designs. -尽早编写注释的第三个也是最后一个好处是,它使编写注释更加有趣。对我来说,编程中最有趣的部分之一是新类的早期设计阶段,在那里,我将充实该类的抽象和结构。我的大部分注释都是在此阶段编写的,这些注释是我记录和测试设计决策质量的方式。我正在寻找可以用最少的词来完整而清晰地表达的设计。注释越简单,我对设计的感觉就越好,因此找到简单的注释是一种自豪感。如果您是策略性编程,而您的主要目标是一个出色的设计,而不仅仅是编写有效的代码,那么编写注释应该很有趣,因为这是您确定最佳设计的方式。 +尽早编写注释的第三个也是最后一个好处是,它使编写注释更加有趣。对我来说,编程中最有趣的部分之一是新类的早期设计阶段,我在这个阶段充实类的抽象和结构。我的大部分注释都是在此阶段编写的,这些注释是我记录和测试设计决策质量的方式。我正在寻找可以用最少的词来完整而清晰地表达的设计。注释越简单,我对设计的感觉就越好,因此找到简单的注释是一种自豪感。如果您是策略性编程,而您的主要目标是一个出色的设计,而不仅仅是编写有效的代码,那么编写注释应该很有趣,因为这是您确定最佳设计的方式。 ## 15.5 Are early comments expensive? 早期注释是否昂贵? > Now let’s revisit the argument for delaying comments, which is that it avoids the cost of reworking the comments as the code evolves. A simple back-of-the-envelope calculation will show that this doesn’t save much. First, estimate the total fraction of development time that you spend typing in code and comments together, including time to revise code and comments; it’s unlikely that this will be more than about 10% of all development time. Even if half of your total code lines are comments, writing comments probably doesn’t account for more than about 5% of your total development time. Delaying the comments until the end will save only a fraction of this, which isn’t very much. -现在,让我们重新讨论延迟注释的参数,这是因为它避免了在代码演变时重新处理注释的开销。一个简单的信封计算将显示这并不能节省很多。首先,估算您一起键入代码和注释所花费的开发时间的总和,包括修改代码和注释的时间;这不太可能超过所有开发时间的 10%。即使您的全部代码行中有一半是注释,编写注释也可能不会占开发总时间的 5%以上。将注释延迟到最后只会节省其中的一小部分,这不是很多。 +现在,让我们重新审视延迟注释的论点,它避免了在代码演变时重新处理注释的成本。一个简单的粗略计算会表明这并没有节省多少。首先,估算您一起键入代码和注释所花费的开发时间的总和,包括修改代码和注释的时间;这不太可能超过所有开发时间的 10%。即使您的全部代码行中有一半是注释,编写注释也可能不会占开发总时间的 5%以上。将注释延迟到最后只会节省其中的一小部分,这不是很多。 > Writing the comments first will mean that the abstractions will be more stable before you start writing code. This will probably save time during coding. In contrast, if you write the code first, the abstractions will probably evolve as you code, which will require more code revisions than the comments-first approach. When you consider all of these factors, it’s possible that it might be faster overall to write the comments first. -首先编写注释将意味着在开始编写代码之前,抽象将更加稳定。这可能会节省编码时间。相反,如果您首先编写代码,则抽象可能会随代码的发展而变化,与注释优先方法相比,将需要更多的代码修订。当您考虑所有这些因素时,可能首先整体编写注释可能会更快。 +首先编写注释将意味着在开始编写代码之前,抽象将更加稳定。这可能会节省编码时间。相反,如果您首先编写代码,则抽象可能会随代码的发展而变化,与注释优先方法相比,将需要更多的代码修订。当您考虑所有这些因素时,首先编写注释可能总体上更快。 ## 15.6 Conclusion 结论 diff --git a/docs/ch16.md b/docs/ch16.md index 02cf3dab..8bbaa922 100644 --- a/docs/ch16.md +++ b/docs/ch16.md @@ -4,39 +4,39 @@ > Chapter 1 described how software development is iterative and incremental. A large software system develops through a series of evolutionary stages, where each stage adds new capabilities and modifies existing modules. This means that a system’s design is constantly evolving. It isn’t possible to conceive the right design for a system at the outset; the design of a mature system is determined more by changes made during the system’s evolution than by any initial conception. Previous chapters described how to squeeze out complexity during the initial design and implementation; this chapter discusses how to keep complexity from creeping in as the system evolves. -第 1 章介绍了软件开发是如何迭代和增量的。大型软件系统是通过一系列演化阶段开发的,其中每个阶段都添加了新功能并修改了现有模块。这意味着系统的设计在不断发展。一开始就不可能为系统设计正确的设计。一个成熟的系统的设计更多地取决于系统演化过程中所做的更改,而不是任何初始概念。前面的章节描述了如何在初始设计和实现过程中降低复杂性。本章讨论如何防止随着系统的发展而增加复杂性。 +第 1 章介绍了软件开发是如何迭代和增量的。大型软件系统是通过一系列演化阶段开发的,其中每个阶段都添加了新功能并修改了现有模块。这意味着系统的设计在不断发展。不可能从一开始就为系统构思出正确的设计。一个成熟的系统的设计更多地取决于系统演化过程中所做的更改,而不是任何初始概念。前面的章节描述了如何在初始设计和实现过程中降低复杂性。本章讨论如何防止随着系统的发展而蔓延。 -## 16.1 Stay strategic 保持战略 +## 16.1 Stay strategic 保持战略性 > Chapter 3 introduced the distinction between tactical programming and strategic programming: in tactical programming, the primary goal is to get something working quickly, even if that results in additional complexity; in strategic programming, the most important goal is to produce a great system design. The tactical approach very quickly leads to a messy system design. If you want to have a system that is easy to maintain and enhance, then “working” isn’t a high enough standard; you have to prioritize design and think strategically. This idea also applies when you are modifying existing code. -第 3 章介绍了战术编程和战略编程之间的区别:在战术编程中,主要目标是使某些事物快速工作,即使这会导致额外的复杂性;在战略编程中,最重要的目标是进行出色的系统设计。战术方法很快导致系统设计混乱。如果您想要一个易于维护和增强的系统,那么“工作”还不够高。您必须优先考虑设计并进行战略思考。当您修改现有代码时,此想法也适用。 +第 3 章介绍了战术编程和战略编程之间的区别:在战术编程中,主要目标是使某些事物快速工作,即使这会导致额外的复杂性;在战略编程中,最重要的目标是进行出色的系统设计。战术方法很快导致系统设计混乱。如果您想要一个易于维护和增强的系统,那么能“工作”并不是一个足够高的标准。您必须优先考虑设计并进行战略思考。当您修改现有代码时,此想法也适用。 > Unfortunately, when developers go into existing code to make changes such as bug fixes or new features, they don’t usually think strategically. A typical mindset is “what is the smallest possible change I can make that does what I need?” Sometimes developers justify this because they are not comfortable with the code being modified; they worry that larger changes carry a greater risk of introducing new bugs. However, this results in tactical programming. Each one of these minimal changes introduces a few special cases, dependencies, or other forms of complexity. As a result, the system design gets just a bit worse, and the problems accumulate with each step in the system’s evolution. -不幸的是,当开发人员进入现有代码以进行更改(例如错误修复或新功能)时,他们通常不会从战略角度进行思考。一个典型的心态是“我能做出我需要做的最小的改变是什么?” 有时,开发人员证明这是合理的,因为他们对修改的代码不满意。他们担心较大的更改会带来更大的引入新错误的风险。但是,这导致了战术编程。这些最小的变化中的每一个都会引入一些特殊情况,依赖性或其他形式的复杂性。结果,系统设计变得更糟,并且问题随着系统发展的每个步骤而累积。 +不幸的是,当开发人员进入现有代码以进行更改(例如错误修复或新功能)时,他们通常不会从战略角度进行思考。一个典型的心态是“我能做出我需要做的最小的改变是什么?” 有时开发人员认为这是合理的,因为他们对修改的代码不放心。他们担心较大的更改会带来更大的风险,会引入新的错误。然而,这导致了战术编程。每一个最小的变化都会引入一些特殊情况,依赖性或其他形式的复杂性。结果,系统设计变得更糟,并且问题随着系统演进的每一步骤而累积。 > If you want to maintain a clean design for a system, you must take a strategic approach when modifying existing code. Ideally, when you have finished with each change, the system will have the structure it would have had if you had designed it from the start with that change in mind. To achieve this goal, you must resist the temptation to make a quick fix. Instead, think about whether the current system design is still the best one, in light of the desired change. If not, refactor the system so that you end up with the best possible design. With this approach, the system design improves with every modification. -如果要维护系统的简洁设计,则在修改现有代码时必须采取战略性方法。理想情况下,当您完成每次更改时,如果您从一开始就考虑到更改就设计了系统,那么系统将具有它应该具有的结构。为了实现此目标,您必须抵制诱惑以快速解决问题。相反,请根据所需的更改来考虑当前的系统设计是否仍然是最佳的。如果不是,请重构系统,以便最终获得最佳设计。通过这种方法,每次修改都会改善系统设计。 +如果要保持系统的简洁设计,则在修改现有代码时必须采取战略性方法。理想情况下,当您完成每次更改时,系统将具有如果你在一开始设计时就考虑这一变化而具有的结构。为了实现此目标,您必须抵制诱惑以快速解决问题。相反,请根据所需的更改来考虑当前的系统设计是否仍然是最佳的。如果不是,请重构系统,以便最终获得最佳设计。通过这种方法,每次修改都会改善系统设计。 > This is also an example of the investment mindset introduced on page 15: if you invest a little extra time to refactor and improve the system design, you’ll end up with a cleaner system. This will speed up development, and you will recoup the effort that you invested in the refactoring. Even if your particular change doesn’t require refactoring, you should still be on the lookout for design imperfections that you can fix while you’re in the code. Whenever you modify any code, try to find a way to improve the system design at least a little bit in the process. If you’re not making the design better, you are probably making it worse. -这也是第 15 页介绍的投资心态的一个示例:如果您花费一些额外的时间来重构和改善系统设计,您将得到一个更干净的系统。这将加快开发速度,您将收回在重构方面投入的精力。即使您的特定更改不需要重构,您仍然应该注意在代码中可以修复的设计缺陷。每当您修改任何代码时,都尝试在该过程中至少找到一点方法来改进系统设计。如果您没有使设计更好,则可能会使它变得更糟。 +这也是第 15 页介绍的投资心态的一个示例:如果您花费一些额外的时间来重构和改善系统设计,您将得到一个更干净的系统。这将加快开发速度,您将收回在重构方面投入的精力。即使您的特定更改不需要重构,您仍然应该注意在代码中可以修复的设计缺陷。每当您修改任何代码时,都尝试在该过程中至少找到一点方法来改进系统设计。如果您没有使设计变得更好,则可能会使它变得更糟。 > As discussed in Chapter 3, an investment mindset sometimes conflicts with the realities of commercial software development. If refactoring the system “the right way” would take three months but a quick and dirty fix would take only two hours, you may have to take the quick and dirty approach, particularly if you are working against a tight deadline. Or, if refactoring the system would create incompatibilities that affect many other people and teams, then the refactoring may not be practical. -如第 3 章所述,投资心态有时与商业软件开发的现实相冲突。如果“正确的方式”重构系统需要三个月,而快速且肮脏的修复仅需两个小时,则您可能必须采取快速而肮脏的方法,尤其是在紧迫的期限内工作时。或者,如果重构系统会造成影响许多其他人员和团队的不兼容性,则重构可能不切实际。 +如第 3 章所述,投资心态有时与商业软件开发的现实相冲突。如果“正确的方式”重构系统需要三个月,而快速且肮脏的修复仅需两个小时,则您可能必须采取快速而肮脏的方法,尤其是在紧迫的期限内工作时。或者,如果重构系统会造成不兼容,从而影响许多其他人员和团队,则重构可能不切实际。 > Nonetheless, you should resist these compromises as much as possible. Ask yourself “Is this the best I can possibly do to create a clean system design, given my current constraints?” Perhaps there’s an alternative approach that would be almost as clean as the 3-month refactoring but could be done in a couple of days? Or, if you can’t afford to do a large refactoring now, get your boss to allocate time for you to come back to it after the current deadline. Every development organization should plan to spend a small fraction of its total effort on cleanup and refactoring; this work will pay for itself over the long run. -但是,您应尽可能抵制这些妥协。问问自己:“考虑到我目前的限制,这是否是我能做的最好的工作来创建一个干净的系统设计?” 也许有一种替代方法几乎可以像 3 个月的重构一样干净,但是可以在几天内完成?或者,如果您现在负担不起大型重构,请让您的老板为您分配时间,让您在当前截止日期之后恢复到原来的水平。每个开发组织都应计划将其全部工作的一小部分用于清理和重构;从长远来看,这项工作将收回成本。 +尽管如此,您应尽可能抵制这些妥协。问问自己:“考虑到我目前的限制,这是否是我能做的最好的工作来创建一个干净的系统设计?” 也许有一种替代方法几乎可以像 3 个月的重构一样干净,但是可以在几天内完成?或者,如果您现在没有能力做大规模的重构,请让您的老板为您分配时间,让您在当前截止日期之后再来做。每个开发组织都应计划将其全部工作的一小部分用于清理和重构;从长远来看,这项工作将为自己带来回报。 ## 16.2 Maintaining comments: keep the comments near the code 维护注释:将注释保留在代码附近 > When you change existing code, there’s a good chance that the changes will invalidate some of the existing comments. It’s easy to forget to update comments when you modify code, which results in comments that are no longer accurate. Inaccurate comments are frustrating to readers, and if there are very many of them, readers begin to distrust all of the comments. Fortunately, with a little discipline and a couple of guiding rules, it’s possible to keep comments up-to-date without a huge effort. This section and the following ones put forth some specific techniques. -当您更改现有代码时,更改很有可能会使某些现有注释无效。修改代码时,很容易忘记更新注释,从而导致注释不再准确。不准确的注释使读者感到沮丧,如果注释太多,读者就会开始不信任所有注释。幸运的是,只要有一点纪律和一些指导规则,就可以在不付出巨大努力的情况下使注释保持最新。本节及随后的部分提出了一些特定的技术。 +当您更改现有代码时,更改很有可能会使某些现有注释无效。修改代码时,很容易忘记更新注释,从而导致注释不再准确。不准确的注释使读者感到沮丧,如果有很多这样的注释,读者就会开始不信任所有注释。幸运的是,只要有一点纪律和一些指导规则,就可以在不费吹灰之力使注释保持更新。本节及随后的部分提出了一些具体的技巧。 > The best way to ensure that comments get updated is to position them close to the code they describe, so developers will see them when they change the code. The farther a comment is from its associated code, the less likely it is that it will be updated properly. For example, the best place for a method’s interface comment is in the code file, right next to the body of the method. Any changes to the method will involve this code, so the developer is likely to see the interface comments and update them if needed. @@ -44,11 +44,11 @@ > An alternative for languages like C and C++ that have separate code and header files, is to place the interface comments next to the method’s declaration in the .h file. However, this is a long way from the code; developers won’t see those comments when modifying the method’s body, and it takes additional work to open a different file and find the interface comments to update them. Some might argue that interface comments should go in header files so that users can learn how to use an abstraction without having to look at the code file. However, users should not need to read either code or header files; they should get their information from documentation compiled by tools such as Doxygen or Javadoc. In addition, many IDEs will extract and present documentation to users, such as by displaying a method’s documentation when the method’s name is typed. Given tools such as these, the documentation should be located in the place that is most convenient for developers working on the code. -对于 C 和 C++ 等具有单独的代码和头文件的语言,一种替代方法是将接口注释放在.h 文件中方法声明的旁边。但是,这距离代码还有很长的路要走。开发人员在修改方法的主体时将看不到这些注释,因此需要打开其他文件并查找接口注释来更新它们,这需要额外的工作。有人可能会争辩说接口注释应该放在头文件中,以便用户可以不必看代码文件就可以学习如何使用抽象。但是,用户无需读取代码或头文件;他们应该从由 Doxygen 或 Javadoc 等工具编译的文档中获取信息。此外,许多 IDE 都会提取文档并将其呈现给用户,例如在键入方法名称时显示方法的文档。给定诸如此类的工具,文档应位于对开发人员进行代码开发最方便的位置。 +对于 C 和 C++ 等具有单独的代码和头文件的语言,一种替代方法是将接口注释放在.h 文件中方法声明的旁边。但是,这距离代码还有很长的路要走。开发人员在修改方法的主体时将看不到这些注释,因此需要打开其他文件并查找接口注释来更新它们,这需要额外的工作。有人可能会争辩说接口注释应该放在头文件中,以便用户可以学习如何使用一个抽象概念,而不需要查看代码。然而,用户不应该阅读代码或头文件;他们应该从由 Doxygen 或 Javadoc 等工具编译的文档中获取信息。此外,许多 IDE 都会提取文档并将其呈现给用户,例如在键入方法名称时显示方法的文档。鉴于这样的工具,文档应位于对开发人员进行代码开发最方便的位置。 > When writing implementation comments, don’t put all the comments for an entire method at the top of the method. Spread them out, pushing each comment down to the narrowest scope that includes all of the code referred to by the comment. For example, if a method has three major phases, don’t write one comment at the top of the method that describes all of the phases in detail. Instead, write a separate comment for each phase and position that comment just above the first line of code in that phase. On the other hand, it can also be helpful to have a comment at the top of a method’s implementation that describes the overall strategy, like this: -在编写实现注释时,不要将整个方法的所有注释放在方法的顶部。展开它们,将每个注释推到最狭窄的范围,其中包括该注释所引用的所有代码。例如,如果一种方法具有三个主要阶段,则不要在方法的顶部写一个详细描述所有阶段的注释。而是为每个阶段编写一个单独的注释,并将该注释放置在该阶段的第一行代码的正上方。另一方面,在描述总体策略的方法实现的顶部添加注释也可能会有所帮助,例如: +在编写实现注释时,不要将整个方法的所有注释放在方法的顶部。把他们分解开来,将每个注释向下写到最合适范围,即包括该注释所引用的所有代码。例如,如果一种方法具有三个主要阶段,则不要在方法的顶部写一个详细描述所有阶段的注释。而是为每个阶段编写一个单独的注释,并将该注释放置在该阶段的第一行代码的正上方。另一方面,在描述总体策略的方法实现的顶部添加注释也可能会有所帮助,例如: ```java // We proceed in three phases: @@ -69,25 +69,25 @@ > A common mistake when modifying code is to put detailed information about the change in the commit message for the source code repository, but then not to document it in the code. Although commit messages can be browsed in the future by scanning the repository’s log, a developer who needs the information is unlikely to think of scanning the repository log. Even if they do scan the log, it will be tedious to find the right log message. -修改代码时,常见的错误是将有关更改的详细信息放入源代码存储库的提交消息中,而不是将其记录在代码中。尽管将来可以通过扫描存储库的日志来浏览提交消息,但是需要该信息的开发人员不太可能考虑扫描存储库的日志。即使他们确实扫描了日志,也很难找到正确的日志消息。 +修改代码时,常见的错误是将有关更改的详细信息放入源代码存储库的提交消息中,而不是将其记录在代码中。尽管将来可以通过扫描存储库的日志来浏览提交消息,但是需要该信息的开发人员不太可能考虑扫描存储库的日志。即使他们确实扫描了日志,也很难找到正确的日志也会很乏味。 > When writing a commit message, ask yourself whether developers will need to use that information in the future. If so, then document this information in the code. An example is a commit message describing a subtle problem that motivated a code change. If this isn’t documented in the code, then a developer might come along later and undo the change without realizing that they have re-created a bug. If you want to include a copy of this information in the commit message as well, that’s fine, but the most important thing is to get it in the code. This illustrates the principle of placing documentation in the place where developers are most likely to see it; the commit log is rarely that place. -在编写提交消息时,请问自己将来开发人员是否需要使用该信息。如果是这样,则在代码中记录此信息。一个示例是提交消息,描述了导致代码更改的细微问题。如果代码中未对此进行记录,则开发人员可能会稍后再提出并撤消更改,而不会意识到他们已经重新创建了错误。如果您也想在提交消息中包含此信息的副本,那很好,但是最重要的是在代码中获取它。这说明了将文档放置在开发人员最有可能看到它的地方的原理;提交日志很少在那个地方。 +在编写提交消息时,请问自己将来开发人员是否需要使用该信息。如果是这样,则在代码中记录此信息。一个示例是提交消息,描述了导致代码更改的细微问题。如果代码中未对此进行记录,那么一个开发人员可能会稍后撤消这个更改,而没有意识到他们已经重新创建了错误。如果您也想在提交消息中包含此信息的副本,那也可以,但是最重要的是把他放在代码中。这说明了将文档放置在开发人员最有可能看到它的地方的原则;尽量少放在提交日志中。 ## 16.4 Maintaining comments: avoid duplication 维护注释:避免重复 > The second technique for keeping comments up to date is to avoid duplication. If documentation is duplicated, it is more difficult for developers to find and update all of the relevant copies. Instead, try to document each design decision exactly once. If there are multiple places in the code that are affected by a particular decision, don’t repeat the documentation at each of these points. Instead, find the most obvious single place to put the documentation. For example, suppose there is tricky behavior related to a variable, which affects several different places where the variable is used. You can document that behavior in the comment next to the variable’s declaration. This is a natural place that developers are likely to check if they’re having trouble understanding code that uses the variable. -保持注释最新的第二种技术是避免重复。如果文档重复,那么开发人员将很难找到并更新所有相关副本。相反,请尝试仅一次记录每个设计决策。如果代码中有多个地方受某个特定决定的影响,请不要在所有这些地方重复文档。相反,找到放置文档最明显的位置。例如,假设存在与变量相关的棘手行为,这会影响使用变量的几个不同位置。您可以在变量声明旁边的注释中记录该行为。这是很自然的地方,开发人员可能会检查他们是否在理解使用该变量的代码时遇到麻烦。 +保持注释最新的第二种技术是避免重复。如果文档重复,那么开发人员将很难找到并更新所有相关副本。因此尽量将每个设计决策精确的记录一次。如果代码中有多个地方受某个特定决定的影响,请不要在所有这些地方重复注释。所以找到放置注释最明显的位置。例如,假设存在与变量相关的棘手行为,这会影响使用变量的几个不同地方。您可以在变量声明旁边的注释中记录该行为。如果开发人员在理解使用该变量的代码时遇到麻烦,他们自然会在这里进行检查。 > If there is no “obvious” single place to put a particular piece of documentation where developers will find it, create a designNotes file as described in Section 13.7. Or, pick the best of the available places and put the documentation there. In addition, add short comments in the other places that refer to the central location: “See the comment in xyz for an explanation of the code below.” If the reference becomes obsolete because the master comment was moved or deleted, this inconsistency will be self-evident because developers won’t find the comment at the indicated place; they can use revision control history to find out what happened to the comment and then update the reference. In contrast, if the documentation is duplicated and some of the copies don’t get updated, there will be no indication to developers that they are using stale information. -如果没有一个“明显的”地方来放置特定的文档,开发人员可以找到它,那么创建一个 designNotes 文件,如第 13.7 节所述。或者,选择最好的地方,把文档放在那里。另外,在引用中心位置的其他地方添加简短的注释:“查看 xyz 中的注释以了解下面代码的解释。“如果引用因为主注释被移动或删除而变得过时,这种不一致性将是不言而喻的,因为开发人员将无法在指定的位置找到注释;他们可以使用修订控制历史记录来查找注释发生了什么,然后更新引用。相反,如果文档是重复的,并且一些副本没有得到更新,那么开发人员就不会知道他们使用的是陈旧的信息。 +如果没有一个“明显的”地方来将特定的文档放在开发人员可以找到的地方,那么创建一个 designNotes 文件,如第 13.7 节所述。或者,在现有的地方中选择一个最好的地方,把文档放在那里。此外,在引用中心位置的其他地方添加简短的注释:“查看 xyz 中的注释以了解下面代码的解释。“如果引用因为主注释被移动或删除而使引用变得过时,这种不一致性将是不言而喻的,因为开发人员将无法在指定的位置找到注释;他们可以使用修订控制历史记录来查找注释发生了什么,然后更新引用。相反,如果文档是重复的,而一些副本没有得到更新,那么开发人员就不会知道他们使用的是陈旧的信息。 > Don’t redocument one module’s design decisions in another module. For example, don’t put comments before a method call that explain what happens in the called method. If readers want to know, they should look at the interface comments for the method. Good development tools will usually provide this information automatically, for example, by displaying the interface comments for a method if you select the method’s name or hover the mouse over it. Try to make it easy for developers to find appropriate documentation, but don’t do it by repeating the documentation. -不要在另一个模块中记录一个模块的设计决策。例如,不要在方法调用前添加注释,以解释被调用方法中发生的情况。如果读者想知道,他们应该查看该方法的接口注释。好的开发工具通常会自动提供此信息,例如,如果您选择了方法的名称或将鼠标悬停在该方法的名称上,则将显示该方法的接口注释。尝试使开发人员容易找到合适的文档,但是不要重复文档。 +不要在另一个模块中记录一个模块的设计决策。例如,不要在方法调用前添加注释,以解释被调用方法中发生的情况。如果读者想知道,他们应该查看该方法的接口注释。好的开发工具通常会自动提供此信息,例如,如果您选择了方法的名称或将鼠标悬停在该方法的名称上,则将显示该方法的接口注释。尽量让开发人员容易找到合适的文档,但是不要通过重复文档来做到这一点。 > If information is already documented someplace outside your program, don’t repeat the documentation inside the program; just reference the external documentation. For example, if you write a class that implements the HTTP protocol, there’s no need for you to describe the HTTP protocol inside your code. There are already numerous sources for this documentation on the Web; just add a short comment to your code with a URL for one of these sources. Another example is features that are already documented in a user manual. Suppose you are writing a program that implements a collection of commands, with one method responsible for implementing each command. If there is a user manual that describes those commands, there’s no need to duplicate this information in the code. Instead, include a short note like the following in the interface comment for each command method: @@ -105,7 +105,7 @@ > One good way to make sure documentation stays up to date is to take a few minutes before committing a change to your revision control system to scan over all the changes for that commit; make sure that each change is properly reflected in the documentation. These pre-commit scans will also detect several other problems, such as accidentally leaving debugging code in the system or failing to fix TODO items. -确保文档保持最新状态的一种好方法是,在将更改提交到修订控制系统之前需要花费几分钟,以扫描该提交的所有更改。确保文档中正确反映了每个更改。这些预先提交的扫描还将检测其他一些问题,例如意外地将调试代码留在系统中或无法修复 TODO 项目。 +确保文档保持最新状态的一种好方法是,在将更改提交到修订控制系统之前需要花费几分钟,以扫描该提交的所有更改。确保文档中正确反映了每个更改。这些预先提交的扫描还将检测其他一些问题,例如意外地将调试代码留在系统中或未完成的 TODO 项目。 ## 16.6 Higher-level comments are easier to maintain 更高级的注释更易于维护 diff --git a/docs/ch17.md b/docs/ch17.md index a428c0f0..8bce8752 100644 --- a/docs/ch17.md +++ b/docs/ch17.md @@ -4,17 +4,17 @@ > Consistency is a powerful tool for reducing the complexity of a system and making its behavior more obvious. If a system is consistent, it means that similar things are done in similar ways, and dissimilar things are done in different ways. Consistency creates cognitive leverage: once you have learned how something is done in one place, you can use that knowledge to immediately understand other places that use the same approach. If a system is not implemented in a consistent fashion, developers must learn about each situation separately. This will take more time. -一致性是降低系统复杂性并使其行为更明显的强大工具。如果系统是一致的,则意味着相似的事情以相似的方式完成,而不同的事情则以不同的方式完成。一致性会产生认知影响力:一旦您了解了某个地方的工作方式,就可以使用该知识立即了解其他使用相同方法的地方。如果系统的实施方式不一致,则开发人员必须分别了解每种情况。这将花费更多时间。 +一致性是一个强大的工具,可以降低系统复杂性并使其行为更明显。如果系统是一致的,则意味着相似的事情以相似的方式完成,而不同的事情则以不同的方式完成。一致性创造了认知杠杆:一旦您了解了某个地方的工作方式,就可以使用该知识立即了解其他使用相同方法的地方。如果一个系统的没有以一致的方式实施,则开发人员必须分别了解每种情况。这将花费更多时间。 > Consistency reduces mistakes. If a system is not consistent, two situations may appear the same when in fact they are different. A developer may see a pattern that looks familiar and make incorrect assumptions based on previous encounters with that pattern. On the other hand, if the system is consistent, assumptions made based on familiar-looking situations will be safe. Consistency allows developers to work more quickly with fewer mistakes. -一致性减少了错误。如果系统不一致,则实际上两种情况可能不同,但两种情况可能看起来相同。开发人员可能会看到一个看起来很熟悉的模式,并根据以前对该模式的遭遇做出错误的假设。另一方面,如果系统是一致的,则基于熟悉情况的假设将是安全的。一致性允许开发人员以更少的错误来更快地工作。 +一致性减少了错误。如果系统不一致,两种情况可能看起来是一样的,但实际上它们是不同的。开发人员可能会看到一个看起来很熟悉的模式,并根据以前遇到的模式做出错误的假设。另一方面,如果系统是一致的,则基于看起来很熟悉的情况所做的假设就会很安全。一致性允许开发人员更快速的工作,并减少错误。 ## 17.1 Examples of consistency 一致性示例 > Consistency can be applied at many levels in a system; here are a few examples. -一致性可以应用于系统中的许多级别。这里有一些例子。 +一致性可以应用于系统中的许多层面。这里有一些例子。 > Names. Chapter 14 has already discussed the benefits of using names in a consistent way. @@ -22,7 +22,7 @@ > Coding style. It is common nowadays for development organizations to have style guides that restrict program structure beyond the rules enforced by compilers. Modern style guides address a range of issues, such as indentation, curly-brace placement, order of declarations, naming, commenting, and restrictions on language features considered dangerous. Style guidelines make code easier to read and can reduce some kinds of errors. -编码样式。如今,开发组织通常拥有样式指南,这些样式指南将程序结构限制在编译器所强制执行的规则之外。现代风格指南解决了一系列问题,例如缩进,大括号放置,声明顺序,命名,注释以及对认为危险的语言功能的限制。样式指南使代码更易于阅读,并且可以减少某些类型的错误。 +编码风格。如今,开发组织通常会制定风格指南,将程序结构限制在编译器所强制执行的规则之外。现代风格指南解决了一系列问题,例如缩进,大括号放置,声明顺序,命名,注释以及对认为危险的语言功能的限制。风格指南使代码更易于阅读,并且可以减少某些类型的错误。 > Interfaces. An interface with multiple implementations is another example of consistency. Once you understand one implementation of the interface, any other implementation becomes easier to understand because you already know the features it will have to provide. @@ -30,58 +30,58 @@ > Design patterns. Design patterns are generally-accepted solutions to certain common problems, such as the model-view-controller approach to user interface design. If you can use an existing design pattern to solve the problem, the implementation will proceed more quickly, it is more likely to work, and your code will be more obvious to readers. Design patterns are discussed in more detail in Section 19.5. -> 设计模式。设计模式是某些常见问题的普遍接受的解决方案,例如用于用户界面设计的模型视图控制器方法。如果您可以使用现有的设计模式来解决问题,则实现会更快地进行,更有可能起作用,并且您的代码对读者来说也会更明显。设计模式将在 19.5 节中详细讨论。 +设计模式。设计模式是某些常见问题的普遍接受的解决方案,例如用于用户界面设计的模型-视图-控制器方法。如果您可以使用现有的设计模式来解决问题,则实现会更快地进行,更有可能奏效,并且您的代码对读者来说也会更明显。设计模式将在 19.5 节中详细讨论。 > Invariants. An invariant is a property of a variable or structure that is always true. For example, a data structure storing lines of text might enforce an invariant that each line is terminated by a newline character. Invariants reduce the number of special cases that must be considered in code and make it easier to reason about the code’s behavior. -> 不变量。不变式是始终为真的变量或结构的属性。例如,存储文本行的数据结构可能会强制要求每行以换行符终止。不变式减少了代码中必须考虑的特殊情况的数量,并使推理行为的方式变得更加容易。 +不变量。不变量是一个变量或结构的属性,它总是为真的。例如,存储文本行的数据结构可能会强制要求每行以换行符终止。不变量减少了代码中必须考虑的特殊情况的数量,并且更容易推断代码的行为。 ## 17.2 Ensuring consistency 确保一致性 > Consistency is hard to maintain, especially when many people work on a project over a long time. People in one group may not know about conventions established in another group. Newcomers don’t know the rules, so they unintentionally violate the conventions and create new conventions that conflict with existing ones. Here are a few tips for establishing and maintaining consistency: -一致性很难维护,尤其是当许多人长时间从事一个项目时。一组人可能不了解另一组中建立的约定。新来者不了解规则,因此他们无意间违反了约定并创建了与现有约定冲突的新约定。以下是建立和保持一致性的一些技巧: +一致性很难保持,尤其是当许多人长时间从事一个项目时。一个小组的人可能不了解另一小组中建立的约定。新来的人不了解约定,因此他们无意间违反了约定,并创建了与现有约定冲突的新约定。以下是建立和保持一致性的一些技巧: > Document. Create a document that lists the most important overall conventions, such as coding style guidelines. Place the document in a spot where developers are likely to see it, such as a conspicuous place on the project Wiki. Encourage new people joining the group to read the document, and encourage existing people to review it every once in a while. Several style guides from various organizations have been published on the Web; consider starting with one of these. -文献。创建一个列出最重要的总体约定的文档,例如编码样式准则。将文档放置在开发人员可能会看到的位置,例如项目 Wiki 上的显眼位置。鼓励新成员加入小组阅读文档,并鼓励现有人员不时审阅该文档。Web 上已经发布了来自各个组织的一些样式指南;考虑从其中之一开始。 +文档。创建一个文档,列出最重要的总体约定,例如编码风格指南。将文档放置在开发人员可能会看到的位置,例如项目 Wiki 上的显眼位置。鼓励新成员加入小组阅读文档,并鼓励现有人员不时的回顾一下。一些来自不同组织的风格指南已经在网上发布;考虑从其中之一开始。 For conventions that are more localized, such as invariants, find an appropriate spot in the code to document them. If you don’t write the conventions down, it’s unlikely that other people will follow them. -对于局部性更强的约定,例如不变式,请在代码中找到合适的位置进行记录。如果您不写下约定,那么其他人不太可能会遵循它们。 +对于那些更加本地化的约定,例如不变量,请在代码中找到合适的位置进行记录。如果您不把这些约定写下来,那么其他人不太可能会遵循它们。 > Enforce. Even with good documentation, it’s hard for developers to remember all of the conventions. The best way to enforce conventions is to write a tool that checks for violations, and make sure that code cannot be committed to the repository unless it passes the checker. Automated checkers work particularly well for low-level syntactic conventions. -执行。即使有好的文档,开发人员也很难记住所有约定。实施约定的最佳方法是编写一个检查违规的工具,并确保除非通过检查程序,否则代码无法提交到存储库。自动检查器对于底层语法约定特别有用。 +执行。即使有好的文档,开发人员也很难记住所有约定。执行约定的最佳方法是编写一个检查违规的工具,并确保代码在通过检查器之前不能提交到存储库。自动检查器对于低级别的语法约定特别有用。 > One of my recent projects had problems with line termination characters. Some developers worked on Unix, where lines are terminated by newlines; others worked on Windows, where lines are normally terminated by a carriage-return followed by a newline. If a developer on one system made a small edit to a file previously edited on the other system, the editor would sometimes replace all of the line terminators with ones appropriate for that system. This gave the appearance that every line of the file had been modified, which made it hard to track the meaningful changes. We established a convention that files should contain newlines only, but it was hard to ensure that every tool used by every developer followed the convention. Every time a new developer joined the project, we would experience a rash of line termination problems while that developer adjusted to the convention. -我最近的一个项目有行终止字符的问题。一些开发人员在 Unix 上工作,行被换行终止;其他的工作在 Windows 上,行通常由一个 carriage-return 后跟一个换行符来结束。如果一个系统上的开发人员对先前在另一个系统上编辑过的文件进行了小的编辑,那么编辑器有时会将所有行终止符替换为适合该系统的行终止符。这给人的感觉是文件的每一行都被修改了,这使得跟踪有意义的更改变得很困难。我们建立了一个约定,即文件应该只包含换行,但是很难确保每个开发人员使用的每个工具都遵循这个约定。每当一个新的开发人员加入这个项目,我们就会经历一连串的线路终止问题,而那个开发人员就会适应这个约定。 +我最近的一个项目有行终止字符的问题。一些开发人员在 Unix 上工作,行被换行终止;其他的工作在 Windows 上,行通常由一个 carriage-return 后跟一个换行符来结束。如果一个系统上的开发人员对先前在另一个系统上编辑过的文件进行了小的编辑,那么编辑器有时会将所有行终止符替换为适合该系统的行终止符。这给人的感觉是文件的每一行都被修改了,这使人很难追踪有意义的变化。我们建立了一个约定,即文件应该只包含换行,但是很难确保每个开发人员使用的每个工具都遵循这个约定。每当一个新的开发人员加入这个项目,我们就会遇到大量的行终止问题,而该开发者才会适应这个约定。 > We eventually solved this problem by writing a short script that was executed automatically before changes are committed to the source code repository. The script checks all of the files that have been modified and aborts the commit if any of them contain carriage returns. The script can also be run manually to repair damaged files by replacing carriage-return/newline sequences with newlines. This instantly eliminated the problems, and it also helped train new developers. -我们最终通过编写一个简短的脚本解决了这个问题,该脚本在更改提交到源代码存储库之前自动执行。该脚本检查所有已修改的文件,如果其中任何一个包含回车符,则中止提交。还可以通过用换行符替换回车/换行符序列来手动运行脚本以修复损坏的文件。这立即消除了问题,并且还帮助培训了新开发人员。 +我们最终解决了这个问题,通过编写了一个简短的脚本,该脚本在更改提交到源代码存储库之前自动执行。该脚本检查所有已修改的文件,如果其中任何一个包含回车符,则中止提交。该脚本也可以手动运行,通过用换行符替换回车/换行符序来修复损坏的文件。这一下子就消除了问题,并且还有助于培训新的开发人员。 > Code reviews provide another opportunity for enforcing conventions and for educating new developers about the conventions. The more nit-picky that code reviewers are, the more quickly everyone on the team will learn the conventions, and the cleaner the code will be. -代码审查为实施约定和向新开发者提供有关约定的教育提供了另一个机会。代码审阅者越挑剔,团队中的每个人都将更快地学习约定,并且代码越干净。 +代码审查为实施约定和向新开发者提供有关约定的教育提供了另一个机会。代码审阅者越挑剔,团队中的每个人学习约定的速度就越快,并且代码越清晰。 > When in Rome ... The most important convention of all is that every developer should follow the old adage “When in Rome, do as the Romans do.” When working in a new file, look around to see how the existing code is structured. Are all public variables and methods declared before private ones? Are the methods in alphabetical order? Do variables use “camel case,” as in firstServerName, or “snake case,” as in first_server_name? When you see anything that looks like it might possibly be a convention, follow it. When making a design decision, ask yourself if it’s likely that a similar decision was made elsewhere in the project; if so, find an existing example and use the same approach in your new code. -在罗马时……最重要的约定是每个开发人员都应遵循古老的格言“在罗马时,就像罗马人一样。” 在处理新文件时,请环顾四周以了解现有代码的结构。是否在私有变量和方法之前声明了所有公共变量和方法?方法是否按字母顺序排列?变量是否使用 firstServerName 中的“ camel case”或使用 first_server_name 中的“ snake case”?当您看到任何看起来可能是约定的内容时,请遵循该约定。在做出设计决策时,请问自己是否有可能在项目的其他地方做出了类似的决策;如果是这样,请找到一个现有示例,并在新代码中使用相同的方法。 +在罗马时……最重要的约定是每个开发人员都应遵循古老的格言“在罗马时,就像罗马人一样。” 在处理新文件时,请环顾四周以了解现有代码的结构。是否在私有变量和方法之前声明了所有公共变量和方法?方法是否按字母顺序排列?变量是像 firstServerName 中那样使用“camel case”,还是像 first_server_name 中那样使用“snake case”?当您看到任何看起来可能是约定的内容时,请遵循该约定。在做出设计决策时,请问自己是否有可能在项目的其他地方做出了类似的决策;如果是这样,请找到一个现有示例,并在新代码中使用相同的方法。 > Don’t change existing conventions. Resist the urge to “improve” on existing conventions. Having a “better idea” is not a sufficient excuse to introduce inconsistencies. Your new idea may indeed be better, but the value of consistency over inconsistency is almost always greater than the value of one approach over another. Before introducing inconsistent behavior, ask yourself two questions. First, do you have significant new information justifying your approach that wasn’t available when the old convention was established? Second, is the new approach so much better that it is worth taking the time to update all of the old uses? If your organization agrees that the answers to both questions are “yes,” then go ahead and make the upgrade; when you are done, there should be no sign of the old convention. However, you still run the risk that other developers will not know about the new convention, so they may reintroduce the old approach in the future. Overall, reconsidering established conventions is rarely a good use of developer time. -不要更改现有约定。抵制“改善”现有公约的冲动。拥有一个“更好的主意”不足以引起矛盾。您的新想法可能确实更好,但是一致性胜于不一致的价值几乎总是大于一种方法胜过另一种方法的价值。在引入不一致的行为之前,请问自己两个问题。首先,您是否拥有大量的新信息来证明您的方法在建立旧约定时是不可用的?其次,新方法是否好得多,值得花时间更新所有旧用法?如果您的组织同意对两个问题的回答均为“是”,则继续进行升级;否则,请进行升级。完成后,应该没有旧约定的迹象。然而,您仍然冒着其他开发人员不了解新约定的风险,因此他们将来可能会重新引入旧方法。总体而言,重新考虑已建立的约定很少会很好地利用开发人员时间。 +不要改变现有约定。抵制“改善”现有约定的冲动。拥有一个“更好的主意”并不是引入不一致的充分借口。您的新想法可能确实更好,但是一致性胜于不一致的价值几乎总是大于一种方法胜过另一种方法的价值。在引入不一致的行为之前,请问自己两个问题。首先,您是否拥有大量的新信息来证明您的方法在建立旧约定时是不可用的?其次,新方法是否值得花时间更新所有旧用法?如果您的组织同意对两个问题的回答均为“是”,那么就去进行更新;当您完成后,应该没有旧约定的迹象。然而,您仍然面临着其他开发人员不了解新约定的风险,因此他们将来可能会重新引入旧方法。总体而言,重新考虑已建立的约定很少会很好能很好的利用开发人员时间。 ## 17.3 Taking it too far 走得太远 > Consistency means not only that similar things should be done in similar ways, but that dissimilar things should be done in different ways. If you become overzealous about consistency and try to force dissimilar things into the same approach, such as by using the same variable name for things that are really different or using an existing design pattern for a task that doesn’t fit the pattern, you’ll create complexity and confusion. Consistency only provides benefits when developers have confidence that “if it looks like an x, it really is an x.” -一致性不仅意味着相似的事情应该以相似的方式完成,而且不同的事情也应该以不同的方式完成。如果您对一致性过于热衷,并试图将不同的事物强制采用相同的方法,例如对确实不同的事物使用相同的变量名,或者对不适合该模式的任务使用现有的设计模式,那么会造成复杂性和混乱。一致性只有在开发人员确信“如果看起来像 x 时,它确实是 x”时才有好处。 +一致性不仅意味着相似的事情应该以相似的方式完成,而且不同的事情也应该以不同的方式完成。如果您对一致性过于热衷,并试图将不同的事物强制采用相同的方法,例如对确实不同的事物使用相同的变量名,或者对不适合该模式的任务使用现有的设计模式,那么会造成复杂性和混乱。一致性只有在开发人员确信“如果看起来像 x 时,它确实是 x”时才会带来好处。 ## 17.4 Conclusion 结论 > Consistency is another example of the investment mindset. It will take a bit of extra work to ensure consistency: work to decide on conventions, work to create automated checkers, work to look for similar situations to mimic in new code, and work in code reviews to educate the team. The return on this investment is that your code will be more obvious. Developers will be able to understand the code’s behavior more quickly and accurately, and this will allow them to work faster, with fewer bugs. -一致性是投资心态的另一个例子。确保一致性的工作将需要一些额外的工作:确定约定,创建自动检查程序,寻找类似情况以模仿新代码,以及进行代码审查以教育团队。这项投资的回报是您的代码将更加明显。开发人员将能够更快,更准确地了解代码的行为,这将使他们能够以更少的错误来更快地工作。 +一致性是投资心态的另一个例子。确保一致性的工作将需要一些额外的工作:确定约定,创建自动检查程序,寻找类似情况以在新代码中模仿,并在代码审查中教育团队成员。这项投资的回报是您的代码将更加明显。开发人员将能够更快,更准确地了解代码的行为,这将使他们能够更快地工作,并减少错误。 diff --git a/docs/ch18.md b/docs/ch18.md index bd2cc99a..556dbd75 100644 --- a/docs/ch18.md +++ b/docs/ch18.md @@ -4,21 +4,21 @@ > Obscurity is one of the two main causes of complexity described in Section 2.3. Obscurity occurs when important information about a system is not obvious to new developers. The solution to the obscurity problem is to write code in a way that makes it obvious; this chapter discusses some of the factors that make code more or less obvious. -晦涩难懂是第 2.3 节中描述的导致复杂性的两个主要原因之一。当有关系统的重要信息对于新开发人员而言并不明显时,就会发生模糊(当和一个系统相关的一些重要信息对于新的开发人员不那么容易理解,那就是模糊性)。解决晦涩问题的方法是以显而易见的方式编写代码。本章讨论使代码或多或少变得显而易见的一些因素。 +晦涩难懂是第 2.3 节中描述的造成复杂性的两个主要原因之一。当有关系统的重要信息对于新开发人员而言并不明显时,就会发生模糊(当和一个系统相关的一些重要信息对于新的开发人员不那么容易理解,那就是模糊性)。解决晦涩问题的方法是以显而易见的方式编写代码。本章讨论了一些使代码更明显或不明显的因素。 > If code is obvious, it means that someone can read the code quickly, without much thought, and their first guesses about the behavior or meaning of the code will be correct. If code is obvious, a reader doesn’t need to spend much time or effort to gather all the information they need to work with the code. If code is not obvious, then a reader must expend a lot of time and energy to understand it. Not only does this reduce their efficiency, but it also increases the likelihood of misunderstanding and bugs. Obvious code needs fewer comments than nonobvious code. -如果代码很明显,则意味着某人可以不加思索地快速阅读该代码,并且他们对代码的行为或含义的最初猜测将是正确的。如果代码很明显,那么读者就不需要花费很多时间或精力来收集他们使用代码所需的所有信息。如果代码不明显,那么读者必须花费大量时间和精力来理解它。这不仅降低了它们的效率,而且还增加了误解和错误的可能性。明显的代码比不明显的代码需要更少的注释。 +如果代码是显而易见的,则意味着某人可以不加思索地快速阅读该代码,无需多想,他们对代码的行为或含义的初步猜测将是正确的。如果代码是显而易见的,那么读者就不需要花费太多时间或精力来收集他们使用代码所需的所有信息。如果代码不明显,那么读者必须花费大量时间和精力来理解它。这不仅会降低他们的效率,而且还增加了误解和错误的可能性。显而易见的代码比不明显的代码需要更少的注释。 > “Obvious” is in the mind of the reader: it’s easier to notice that someone else’s code is nonobvious than to see problems with your own code. Thus, the best way to determine the obviousness of code is through code reviews. If someone reading your code says it’s not obvious, then it’s not obvious, no matter how clear it may seem to you. By trying to understand what made the code nonobvious, you will learn how to write better code in the future. -读者的想法是“显而易见”(易读性是由读者来判断的):注意到别人的代码不明显比发现自己的代码有问题要容易得多(相对来说关注到别人代码中的难理解比注意到自己的代码要容易的多)。因此,确定代码是否显而易见的最佳方法是通过代码审查。如果有人在阅读您的代码时说它并不明显,那么无论您看起来多么清晰,它也不是显而易见。通过尝试理解什么使代码变得不明显,您将学习如何在将来编写更好的代码。 +读者的想法是“显而易见”(易读性是由读者来判断的):注意到别人的代码不明显比发现自己的代码有问题要容易得多(相对来说关注到别人代码中的难理解比注意到自己的代码要容易的多)。因此,确定代码是否显而易见的最佳方法是通过代码审查。如果阅读您代码的人说它并不明显,那么它就不明显,无论它对您来说是多么清晰。通过尝试理解什么使代码变得不明显,您将学习如何在未来写出更好的代码。 -## 18.1 Things that make code more obvious +## 18.1 Things that make code more obvious 使代码更明显的事情 > Two of the most important techniques for making code obvious have already been discussed in previous chapters. The first is choosing good names (Chapter 14). Precise and meaningful names clarify the behavior of the code and reduce the need for documentation. If a name is vague or ambiguous, then readers will have read through the code in order to deduce the meaning of the named entity; this is time-consuming and error-prone. The second technique is consistency (Chapter 17). If similar things are always done in similar ways, then readers can recognize patterns they have seen before and immediately draw (safe) conclusions without analyzing the code in detail. -在前面的章节中已经讨论了使代码显而易见的两种最重要的技术。首先是选择好名字(第 14 章)。精确而有意义的名称可以阐明代码的行为,并减少对文档的需求。如果名称含糊不清或含糊不清,那么读者将通读代码以推论命名实体的含义;这既费时又容易出错。第二种技术是一致性(第 17 章)。如果总是以相似的方式完成相似的事情,那么读者可以识别出他们以前所见过的模式,并立即得出(安全)结论,而无需详细分析代码。 +在前面的章节中已经讨论了使代码显而易见的两种最重要的技术。首先是选择好名字(第 14 章)。精确而有意义的名称可以阐明代码的行为,并减少对文档的需求。如果名字含糊不清,那么读者将不得不通读代码,以推论命名实体的含义;这既费时又容易出错。第二种技术是一致性(第 17 章)。如果总是以相似的方式完成相似的事情,那么读者可以识别出他们以前所见过的模式,并立即得出(安全)结论,而无需详细分析代码。 > Here are a few other general-purpose techniques for making code more obvious: @@ -26,7 +26,7 @@ > Judicious use of white space. The way code is formatted can impact how easy it is to understand. Consider the following parameter documentation, in which whitespace has been squeezed out: -明智地使用空白。代码格式化的方式会影响其理解的容易程度。考虑以下参数文档,其中空格已被压缩: +明智地使用空白。代码的格式化方式会影响其理解的容易程度。考虑以下参数文档,其中空格已被压缩: ```java /** @@ -112,25 +112,25 @@ for (int pass = 1; pass >= 0 && !empty; pass--) { > Comments. Sometimes it isn’t possible to avoid code that is nonobvious. When this happens, it’s important to use comments to compensate by providing the missing information. To do this well, you must put yourself in the position of the reader and figure out what is likely to confuse them, and what information will clear up that confusion. The next section shows a few examples. -注释。有时无法避免非显而易见的代码。发生这种情况时,重要的是使用注释来提供缺少的信息以进行补偿。要做到这一点,您必须使自己处于读者的位置,弄清楚什么可能会使他们感到困惑,以及哪些信息可以消除这种混乱。下一部分显示了一些示例。 +注释。有时无法避免非显而易见的代码。发生这种情况时,重要的是使用注释来提供缺少的信息以进行弥补。要做好这一点,您必须放自己放在读者的位置上,弄清楚什么可能会使他们感到困惑,以及哪些信息可以消除这种困惑。下一节将介绍几个示例。 ## 18.2 Things that make code less obvious 使代码不那么明显的事情 > There are many things that can make code nonobvious; this section provides a few examples. Some of these, such as event-driven programming, are useful in some situations, so you may end up using them anyway. When this happens, extra documentation can help to minimize reader confusion. -有很多事情可以使代码变得不明显。本节提供了一些示例。其中某些功能(例如事件驱动的编程)在某些情况下很有用,因此您可能最终还是要使用它们。发生这种情况时,额外的文档可以帮助最大程度地减少读者的困惑。 +有很多事情可以使代码变得不明显。本节提供了一些示例。其中一些,例如事件驱动编程,在某些情况下很有用,所以您可能最终会使用它们。发生这种情况时,额外的文档有助于最大程度地减少读者的困惑。 > Event-driven programming. In event-driven programming, an application responds to external occurrences, such as the arrival of a network packet or the press of a mouse button. One module is responsible for reporting incoming events. Other parts of the application register interest in certain events by asking the event module to invoke a given function or method when those events occur. -事件驱动的编程。在事件驱动的编程中,应用程序对外部事件做出响应,例如网络数据包的到来或按下鼠标按钮。一个模块负责报告传入事件。应用程序的其他部分通过在事件发生时要求事件模块调用给定的函数或方法来注册对某些事件的兴趣。 +事件驱动编程。在事件驱动编程中,应用程序对外部事件做出响应,例如网络数据包的到来或按下鼠标按钮。一个模块负责报告传入事件。应用程序的其他部分通过要求事件模块调用给定的函数或方法在事件发生时来注册对某些事件的兴趣。 > Event-driven programming makes it hard to follow the flow of control. The event handler functions are never invoked directly; they are invoked indirectly by the event module, typically using a function pointer or interface. Even if you find the point of invocation in the event module, it still isn’t possible to tell which specific function will be invoked: this will depend on which handlers were registered at runtime. Because of this, it’s hard to reason about event-driven code or convince yourself that it works. -事件驱动的编程使其很难遵循控制流程。永远不要直接调用事件处理函数。它们是由事件模块间接调用的,通常使用函数指针或接口。即使您在事件模块中找到了调用点,也仍然无法确定将调用哪个特定功能:这将取决于在运行时注册了哪些处理程序。因此,很难推理事件驱动的代码或说服自己相信它是可行的。 +事件驱动编程使得控制流程很难被跟踪。事件处理函数从未被直接调用;它们是由事件模块间接调用的,通常使用函数指针或接口。即使您在事件模块中找到了调用点,也仍然无法确定哪个具体的函数会被调用:这将取决于在运行时注册了哪些处理程序。正因为如此,很难对事件驱动的代码进行推理,也很难说服自己相信它是有效的。 > To compensate for this obscurity, use the interface comment for each handler function to indicate when it is invoked, as in this example: -为了弥补这种模糊性,请为每个处理程序函数使用接口注释,以指示何时调用该函数,如以下示例所示: +为了弥补这种模糊性,使用每个处理函数的接口注释来表明它何时被调用,如以下示例所示: ```java /** @@ -146,11 +146,11 @@ void Transport::RpcNotifier::failed() { > If the meaning and behavior of code cannot be understood with a quick reading, it is a red flag. Often this means that there is important information that is not immediately clear to someone reading the code. -如果无法通过快速阅读来理解代码的含义和行为,则它是一个危险标记。通常,这意味着有些重要的信息对于阅读代码的人来说并不能立即清除。 +如果无法通过快速阅读来理解代码的含义和行为,则它是一个危险标记。通常,这意味阅读代码的人并不能立即清楚某些重要的信息。 > Generic containers. Many languages provide generic classes for grouping two or more items into a single object, such as Pair in Java or std::pair in C++. These classes are tempting because they make it easy to pass around several objects with a single variable. One of the most common uses is to return multiple values from a method, as in this Java example: -通用容器。许多语言提供了用于将两个或多个项目组合到一个对象中的通用类,例如 Java 中的 Pair 或 C++ 中的 std :: pair。这些类很诱人,因为它们使使用单个变量轻松传递多个对象变得容易。最常见的用途之一是从一个方法返回多个值,如以下 Java 示例所示: +通用容器。许多语言提供了用于将两个或多个项目组合到一个对象中的通用类,例如 Java 中的 Pair 或 C++ 中的 std::pair。这些类很诱人,因为它们使使用单个变量轻松传递多个对象变得容易。最常见的用途之一是从一个方法返回多个值,如以下 Java 示例所示: ```java return new Pair(currentTerm, false); @@ -158,15 +158,15 @@ return new Pair(currentTerm, false); > Unfortunately, generic containers result in nonobvious code because the grouped elements have generic names that obscure their meaning. In the example above, the caller must reference the two returned values with result.getKey() and result.getValue(), which give no clue about the actual meaning of the values. -不幸的是,通用容器导致代码不清晰,因为分组后的元素的通用名称模糊了它们的含义。在上面的示例中,调用者必须使用 result.getKey()和 result.getValue()引用两个返回的值,而这两个值都不提供这些值的实际含义。 +不幸的是,通用容器导致代码不清晰,因为分组后的元素具有模糊其含义的通用名称。在上面的示例中,调用者必须使用 result.getKey() 和 result.getValue() 引用两个返回的值,这并没有给出关于这些值的实际含义的任何线索。 > Thus, it’s better not to use generic containers. If you need a container, define a new class or structure that is specialized for the particular use. You can then use meaningful names for the elements, and you can provide additional documentation in the declaration, which is not possible with the generic container. -因此,最好不要使用通用容器。如果需要容器,请定义专门用于特定用途的新类或结构。然后,您可以为元素使用有意义的名称,并且可以在声明中提供其他文档,而对于常规容器而言,这是不可能的。 +因此,最好不要使用通用容器。如果需要容器,请定义专门用于特定用途的新类或结构。然后,您可以为元素使用有意义的名称,并且可以在声明中提供额外文档,而对于常规容器而言,这是不可能的。 > This example illustrates a general rule: software should be designed for ease of reading, not ease of writing. Generic containers are expedient for the person writing the code, but they create confusion for all the readers that follow. It’s better for the person writing the code to spend a few extra minutes to define a specific container structure, so that the resulting code is more obvious. -此示例说明了一条通用规则:**软件的设计应易于阅读而不是易于编写**。通用容器对于编写代码的人来说是很方便的,但是它们会使随后的所有读者感到困惑。对于编写代码的人来说,花一些额外的时间来定义特定的容器结构是更好的选择,以便使生成的代码更加明显。 +此示例说明了一条通用规则:**软件的设计应易于阅读而不是易于编写**。通用容器对于编写代码的人来说是很方便的,但是它们会给所有的后续读者带来困惑。对于编写代码的人来说,花一些额外的时间来定义特定的容器结构是更好的选择,这样写出来的代码更加明显。 > Different types for declaration and allocation. Consider the following Java example: @@ -195,7 +195,7 @@ public static void main(String[] args) { > Most applications exit when their main programs return, so readers are likely to assume that will happen here. However, that is not the case. The constructor for RaftClient creates additional threads, which continue to operate even though the application’s main thread finishes. This behavior should be documented in the interface comment for the RaftClient constructor, but the behavior is nonobvious enough that it’s worth putting a short comment at the end of main as well. The comment should indicate that the application will continue executing in other threads. Code is most obvious if it conforms to the conventions that readers will be expecting; if it doesn’t, then it’s important to document the behavior so readers aren’t confused. -大多数应用程序在其主程序返回时退出,因此读者可能会认为这将在此处发生。但是,事实并非如此。RaftClient 的构造函数创建其他线程,即使应用程序的主线程完成,该线程仍可继续运行。应该在 RaftClient 构造函数的接口注释中记录此行为,但是该行为不够明显,因此值得在 main 末尾添加简短注释。该注释应指示该应用程序将继续在其他线程中执行。如果代码符合读者期望的惯例,那么它是最明显的。如果没有,那么记录该行为很重要,以免使读者感到困惑。 +大多数应用程序在其主程序返回时退出,因此读者可能会认为这将在此处发生。然而,事实并非如此。RaftClient 的构造函数创建了额外的线程,即使应用程序的主线程结束了,该线程仍在继续运行。应该在 RaftClient 构造函数的接口注释中记录此行为,但是该行为不够明显,因此值得在 main 末尾添加简短注释。该注释应指示该应用程序将继续在其他线程中执行。如果代码符合读者期望的惯例,那么它是最明显的。如果没有,那么记录该行为很重要,这样读者就不会感到困惑。 ## 18.3 Conclusion 结论 @@ -205,4 +205,5 @@ public static void main(String[] args) { > To make code obvious, you must ensure that readers always have the information they need to understand it. You can do this in three ways. The best way is to reduce the amount of information that is needed, using design techniques such as abstraction and eliminating special cases. Second, you can take advantage of information that readers have already acquired in other contexts (for example, by following conventions and conforming to expectations) so readers don’t have to learn new information for your code. Third, you can present the important information to them in the code, using techniques such as good names and strategic comments. -为了使代码清晰可见,您必须确保读者始终拥有理解它们所需的信息。您可以通过三种方式执行此操作。最好的方法是使用抽象等设计技术并消除特殊情况,以减少所需的信息量。其次,您可以利用读者在其他情况下已经获得的信息(例如,通过遵循约定并符合期望),从而使读者不必为代码学习新的信息。第三,您可以使用诸如好名和战略注释之类的技术在代码中向他们提供重要信息。 +为了使代码清晰可见,您必须确保读者始终拥有理解它们所需的信息。您可以通过三种方式执行此操作。最好的方法是使用抽象等设计技术并消除特殊情况,以减少所需的信息量。其次,您可以利用读者在其他情况下已经获得的信息(例如,通过遵循约定并符合期望),这样读者不必为您的代码学习新的信息。第三,您可以使用诸如好名和战略注释之类的技术在代码中向他们提供重要信息。 + diff --git a/docs/ch19.md b/docs/ch19.md index 94a7f4e7..7130e783 100644 --- a/docs/ch19.md +++ b/docs/ch19.md @@ -10,49 +10,49 @@ > Object-oriented programming is one of the most important new ideas in software development over the last 30–40 years. It introduced notions such as classes, inheritance, private methods, and instance variables. If used carefully, these mechanisms can help to produce better software designs. For example, private methods and variables can be used to ensure information hiding: no code outside a class can invoke private methods or access private variables, so there can’t be any external dependencies on them. -在过去的 30-40 年中,面向对象编程是软件开发中最重要的新思想之一。它引入了诸如类,继承,私有方法和实例变量之类的概念。如果仔细使用,这些机制可以帮助产生更好的软件设计。例如,私有方法和变量可用于确保信息隐藏:类外的任何代码都不能调用私有方法或访问私有变量,因此它们上没有任何外部依赖关系。 +在过去的 30-40 年中,面向对象编程是软件开发中最重要的新思想之一。它引入了诸如类,继承,私有方法和实例变量之类的概念。如果谨慎使用,这些机制可以帮助产生更好的软件设计。例如,私有方法和变量可用于确保信息隐藏:类外部的任何代码都不能调用私有方法或访问私有变量,所以没有任何外部依赖。 > One of the key elements of object-oriented programming is inheritance. Inheritance comes in two forms, which have different implications for software complexity. The first form of inheritance is interface inheritance, in which a parent class defines the signatures for one or more methods, but does not implement the methods. Each subclass must implement the signatures, but different subclasses can implement the same methods in different ways. For example, the interface might define methods for performing I/O; one subclass might implement the I/O operations for disk files, and another subclass might implement the same operations for network sockets. -面向对象编程的关键要素之一是继承。继承有两种形式,它们对软件复杂性有不同的含义。继承的第一种形式是接口继承,其中父类定义一个或多个方法的签名,但不实现这些方法。每个子类都必须实现签名,但是不同的子类可以以不同的方式实现相同的方法。例如,该接口可能定义用于执行 I/O 的方法。一个子类可能对磁盘文件实现 I/O 操作,而另一个子类可能对网络套接字实现相同的操作。 +面向对象编程的关键要素之一是继承。继承有两种形式,它们对软件复杂性有不同的影响。继承的第一种形式是接口继承,其中父类定义一个或多个方法的签名,但不实现这些方法。每个子类都必须实现签名,但是不同的子类可以以不同的方式实现相同的方法。例如,该接口可能定义用于执行 I/O 的方法。一个子类可能对磁盘文件实现 I/O 操作,而另一个子类可能对网络套接字实现相同的操作。 > Interface inheritance provides leverage against complexity by reusing the same interface for multiple purposes. It allows knowledge acquired in solving one problem (such as how to use an I/O interface to read and write disk files) to be used to solve other problems (such as communicating over a network socket). Another way of thinking about this is in terms of depth: the more different implementations there are of an interface, the deeper the interface becomes. In order for an interface to have many implementations, it must capture the essential features of all the underlying implementations while steering clear of the details that differ between the implementations; this notion is at the heart of abstraction. -接口继承通过出于多种目的重用同一接口,从而提供了针对复杂性的杠杆作用。它使解决一个问题(例如如何使用 I/O 接口读取和写入磁盘文件)中获得的知识可以用于解决其他问题(例如通过网络套接字进行通信)。关于深度的另一种思考方式是:接口的实现越不同,接口就越深入。为了使接口具有许多实现,它必须捕获所有基础实现的基本功能,同时避免实现之间的差异。这个概念是抽象的核心。 +接口继承通过将同一接口用于多种用途,从而提供了对抗复杂性的杠杆作用。它使解决一个问题(例如如何使用 I/O 接口读取和写入磁盘文件)中获得的知识可以用于解决其他问题(例如通过网络套接字进行通信)。关于深度的另一种思考方式是:接口的不同实现越多,接口的深度就越大。为了让一个接口有很多实现,它必须拥有所有底层实现的基本特征,同时避免不同实现之间的差异。这个概念是抽象的核心所在。 > The second form of inheritance is implementation inheritance. In this form, a parent class defines not only signatures for one or more methods, but also default implementations. Subclasses can choose to inherit the parent’s implementation of a method or override it by defining a new method with the same signature. Without implementation inheritance, the same method implementation might need to be duplicated in several subclasses, which would create dependencies between those subclasses (modifications would need to be duplicated in all copies of the method). Thus, implementation inheritance reduces the amount of code that needs to be modified as the system evolves; in other words, it reduces the change amplification problem described in Chapter 2. -继承的第二种形式是实现继承。以这种形式,父类不仅定义了一个或多个方法的签名,而且还定义了默认实现。子类可以选择继承方法的父类实现,也可以通过定义具有相同签名的新方法来覆盖它。如果没有实现继承,则可能需要在几个子类中复制相同的方法实现,这将在这些子类之间创建依赖关系(修改需要在方法的所有副本中复制(方法的任何修改变动将会同样的被子类复制))。因此,实现继承减少了随着系统的发展而需要修改的代码量。换句话说,它减少了第 2 章中描述的变化放大问题。 +继承的第二种形式是实现继承。以这种形式,父类不仅定义了一个或多个方法的签名,而且还定义了默认实现。子类可以选择继承方法的父类实现,也可以通过定义具有相同签名的新方法来覆盖它。如果没有实现继承,则可能需要在几个子类中复制相同的方法实现,这将在这些子类之间创建依赖关系(需要在方法的所有副本中复制修改)。因此,实现继承减少了随着系统的发展而需要修改的代码量。换句话说,它减少了第 2 章中描述的变化放大问题。 > However, implementation inheritance creates dependencies between the parent class and each of its subclasses. Class instance variables in the parent class are often accessed by both the parent and child classes; this results in information leakage between the classes in the inheritance hierarchy and makes it hard to modify one class in the hierarchy without looking at the others. For example, a developer making changes to the parent class may need to examine all of the subclasses to ensure that the changes don’t break anything. Similarly, if a subclass overrides a method in the parent class, the developer of the subclass may need to examine the implementation in the parent. In the worst case, programmers will need complete knowledge of the entire class hierarchy underneath the parent class in order to make changes to any of the classes. Class hierarchies that use implementation inheritance extensively tend to have high complexity. -但是,实现继承会在父类及其每个子类之间创建依赖关系。父类和子类通常都访问父类中的类实例变量。这会导致继承层次结构中的类之间的信息泄漏,并且使得在不查看其他类的情况下很难修改层次结构中的一个类。例如,对父类进行更改的开发人员可能需要检查所有子类,以确保所做的更改不会破坏任何内容。同样,如果子类覆盖父类中的方法,则子类的开发人员可能需要检查父类中的实现。在最坏的情况下,程序员将需要完全了解父类下的整个类层次结构,以便对任何类进行更改。 +但是,实现继承会在父类及其每个子类之间创建依赖关系。父类中的类实例变量经常被父类和子类访问。这导致了继承层次中的类之间的信息泄漏,并且使修改层次中的一个类时很难不考虑其他类。例如,对父类进行修改的开发人员可能需要检查所有子类,以确保所做的修改不会破坏任何内容。同样,如果子类覆盖了父类中的方法,则子类的开发人员可能需要检查父类中的实现。在最坏的情况下,程序员将需要完全了解父类下的整个类层次结构,以便对任何类进行更改。广泛使用实现继承的类层次结构往往具有很高的复杂性。 > Thus, implementation inheritance should be used with caution. Before using implementation inheritance, consider whether an approach based on composition can provide the same benefits. For instance, it may be possible to use small helper classes to implement the shared functionality. Rather than inheriting functions from a parent, the original classes can each build upon the features of the helper classes. -因此,应谨慎使用实现继承。在使用实现继承之前,请考虑基于组合的方法是否可以提供相同的好处。例如,可以使用小型帮助程序类来实现共享功能。原始类可以从辅助类的功能构建,而不是从父类继承函数。 +因此,应谨慎使用实现继承。在使用实现继承之前,请考虑基于组合的方法是否可以提供相同的好处。例如,可以使用小型辅助类来实现共享功能。与其从父类中继承功能,原始类可以各自建立在辅助类的功能之上。 > If there is no viable alternative to implementation inheritance, try to separate the state managed by the parent class from that managed by subclasses. One way to do this is for certain instance variables to be managed entirely by methods in the parent class, with subclasses using them only in a read-only fashion or through other methods in the parent class. This applies the notion of information hiding within the class hierarchy to reduce dependencies. -如果没有实现继承的可行选择,请尝试将父类管理的状态与子类管理的状态分开。一种方法是,某些实例变量完全由父类中的方法管理,子类仅以只读方式或通过父类中的其他方法使用它们。这适用于隐藏在类层次结构中的信息的概念,以减少依赖性。 +如果没有实现继承的可行的替代方案,请尝试将父类管理的状态与子类管理的状态分开。一种方法是让某些实例变量完全由父类中的方法管理,子类仅以只读方式或通过父类中的其他方法使用它们。这适用于隐藏在类层次结构中的信息的概念,以减少依赖性。 > Although the mechanisms provided by object-oriented programming can assist in implementing clean designs, they do not, by themselves, guarantee good design. For example, if classes are shallow, or have complex interfaces, or permit external access to their internal state, then they will still result in high complexity. -尽管面向对象编程提供的机制可以帮助实现干净的设计,但是它们本身不能保证良好的设计。例如,如果类很浅,或者具有复杂的接口,或者允许外部访问其内部状态,那么它们仍将导致很高的复杂性。 +尽管面向对象编程提供的机制可有助于实现干净的设计,但是它们本身不能保证良好的设计。例如,如果类很浅,或者具有复杂的接口,或者允许外部访问其内部状态,那么它们仍将导致很高的复杂性。 ## 19.2 Agile development 敏捷开发 > Agile development is an approach to software development that emerged in the late 1990’s from a collection of ideas about how to make software development more lightweight, flexible, and incremental; it was formally defined during a meeting of practitioners in 2001. Agile development is mostly about the process of software development (organizing teams, managing schedules, the role of unit testing, interacting with customers, etc.) as opposed to software design. Nonetheless, it relates to some of the design principles in this book. -敏捷开发是一种软件开发方法,它是在 1990 年代末期出现的,其思想涉及如何使软件开发更加轻量,灵活和增量。它是在 2001 年的一次从业者会议上正式定义的。敏捷开发主要是关于软件开发的过程(组织团队,管理进度表,单元测试的角色,与客户交互等),而不是软件设计。但是,它与本书中的某些设计原则有关。 +敏捷开发是 20 世纪 90 年代末出现的一种软件开发方法,是关于如何使软件开发更加轻量,灵活和渐进的一系列想法。它是在 2001 年的一次从业者会议上正式定义的。敏捷开发主要是关于软件开发的过程(组织团队,管理进度表,单元测试的角色,与客户交互等),而不是软件设计。但是,它与本书中的一些设计原则有关。 > One of the most important elements of agile development is the notion that development should be incremental and iterative. In the agile approach, a software system is developed in a series of iterations, each of which adds and evaluates a few new features; each iteration includes design, test, and customer input. In general, this is similar to the incremental approach advocated here. As mentioned in Chapter 1, it isn’t possible to visualize a complex system well enough at the outset of a project to determine the best design. The best way to end up with a good design is to develop a system in increments, where each increment adds a few new abstractions and refactors existing abstractions based on experience. This is similar to the agile development approach. -敏捷开发中最重要的元素之一是开发应该是渐进的和迭代的概念。在敏捷方法中,软件系统是通过一系列迭代开发的,每个迭代都添加并评估了一些新功能。每个迭代都包括设计,测试和客户输入。通常,这类似于此处提倡的增量方法。如第 1 章所述,在项目开始时就不可能对复杂的系统进行充分的可视化以决定最佳设计。最终获得良好设计的最佳方法是逐步开发一个系统,其中每个增量都会添加一些新的抽象,并根据经验重构现有的抽象。这类似于敏捷开发方法。 +敏捷开发中最重要的元素之一是开发应该是渐进的和迭代的概念。在敏捷方法中,软件系统是通过一系列迭代开发的,每个迭代都添加并评估了一些新功能。每个迭代都包括设计,测试和客户的意见。通常,这类似于这里提倡的增量方法。如第 1 章所述,在项目开始时就不可能对复杂的系统进行充分的可视化以决定最佳设计。最终获得良好设计的最佳方法是逐步开发一个系统,其中每个增量都会添加一些新的抽象,并根据经验重构现有的抽象。这类似于敏捷开发方法。 > One of the risks of agile development is that it can lead to tactical programming. Agile development tends to focus developers on features, not abstractions, and it encourages developers to put off design decisions in order to produce working software as soon as possible. For example, some agile practitioners argue that you shouldn’t implement general-purpose mechanisms right away; implement a minimal special-purpose mechanism to start with, and refactor into something more generic later, once you know that it’s needed. Although these arguments make sense to a degree, they argue against an investment approach, and they encourage a more tactical style of programming. This can result in a rapid accumulation of complexity. -敏捷开发的风险之一是它可能导致战术编程。敏捷开发倾向于使开发人员专注于功能,而不是抽象,它鼓励开发人员推迟设计决策,以便尽快生产可运行的软件。例如,一些敏捷的从业者认为,您不应该立即实施通用机制。实现一个最小的特殊用途机制,从此开始,并在以后知道需要时重构为更通用的东西。尽管这些论点在一定程度上是合理的,但它们反对投资方法,并鼓励采用更具战术性的编程风格。这可以导致复杂性的快速累积。 +敏捷开发的风险之一是它可能导致战术编程。敏捷开发倾向于将开发人员的注意力集中在功能上,而不是抽象上,它鼓励开发人员推迟设计决策,以便尽快生产可以使用的软件。例如,一些敏捷的实践者认为,您不应该马上实现通用机制;应该先实现一个最小的特殊用途机制,然后在知道需要它时重构为更通用的东西。尽管这些论点在一定程度上是合理的,但它们反对投资方法,并鼓励采用更具战术性的编程风格。这会导致复杂性的快速累积。 > Developing incrementally is generally a good idea, but the increments of development should be abstractions, not features. It’s fine to put off all thoughts about a particular abstraction until it’s needed by a feature. Once you need the abstraction, invest the time to design it cleanly; follow the advice of Chapter 6 and make it somewhat general-purpose. @@ -62,7 +62,7 @@ > It used to be that developers rarely wrote tests. If tests were written at all, they were written by a separate QA team. However, one of the tenets of agile development is that testing should be tightly integrated with development, and programmers should write tests for their own code. This practice has now become widespread. Tests are typically divided into two kinds: unit tests and system tests. Unit tests are the ones most often written by developers. They are small and focused: each test usually validates a small section of code in a single method. Unit tests can be run in isolation, without setting up a production environment for the system. Unit tests are often run in conjunction with a test coverage tool to ensure that every line of code in the application is tested. Whenever developers write new code or modify existing code, they are responsible for updating the unit tests to maintain proper test coverage. -过去,开发人员很少编写测试。如果测试是由一个独立的 QA 团队编写的,那么它们就是由一个独立的 QA 团队编写的。然而,敏捷开发的原则之一是测试应该与开发紧密集成,程序员应该为他们自己的代码编写测试。这种做法现在已经很普遍了。测试通常分为两类:单元测试和系统测试。单元测试是开发人员最常编写的测试。它们很小,而且重点突出:每个测试通常在单个方法中验证一小段代码。单元测试可以独立运行,而不需要为系统设置生产环境。单元测试通常与测试覆盖工具一起运行,以确保应用程序中的每一行代码都经过了测试。每当开发人员编写新代码或修改现有代码时,他们都要负责更新单元测试以保持适当的测试覆盖率。 +过去,开发人员很少编写测试。如果测试是由一个独立的 QA 团队编写的,那么它们就是由一个独立的 QA 团队编写的。然而,敏捷开发的原则之一是测试应该与开发紧密集成,程序员应该为他们自己的代码编写测试。这种做法现在已经很普遍了。测试通常分为两类:单元测试和系统测试。单元测试是开发人员最常编写的测试。它们很小,而且重点突出:每个测试通常在单个方法中验证一小段代码。单元测试可以独立运行,而不需要为系统设置生产环境。单元测试通常与测试覆盖工具一起运行,以确保应用程序中的每一行代码都经过了测试。每当开发人员编写新代码或修改现有代码时,他们都要负责更新单元测试以保持适当的测试覆盖率。 > The second kind of test consists of system tests (sometimes called integration tests), which ensure that the different parts of an application all work together properly. They typically involve running the entire application in a production environment. System tests are more likely to be written by a separate QA or testing team. @@ -70,11 +70,11 @@ > Tests, particularly unit tests, play an important role in software design because they facilitate refactoring. Without a test suite, it’s dangerous to make major structural changes to a system. There’s no easy way to find bugs, so it’s likely that bugs will go undetected until the new code is deployed, where they are much more expensive to find and fix. As a result, developers avoid refactoring in systems without good test suites; they try to minimize the number of code changes for each new feature or bug fix, which means that complexity accumulates and design mistakes don’t get corrected. -测试,尤其是单元测试,在软件设计中起着重要作用,因为它们有助于重构。没有测试套件,对系统进行重大结构更改很危险。没有容易找到错误的方法,因此在部署新代码之前,很可能将无法检测到错误,因为在新代码中查找和修复它们的成本要高得多。结果,开发人员避免在没有良好测试套件的系统中进行重构。他们尝试将每个新功能或错误修复的代码更改次数减至最少,这意味着复杂性会累积,而设计错误不会得到纠正。 +测试,尤其是单元测试,在软件设计中起着重要作用,因为它们有助于重构。没有测试套件,对系统进行重大结构更改很危险。没有简单的方法可以找到错误,因此在部署新代码之前,很可能将无法检测到错误,因为在新代码中查找和修复它们的成本要高得多。结果,开发人员避免在没有良好测试套件的系统中进行重构。他们尝试将每个新功能或错误修复的代码更改次数降至最低,这意味着复杂性会累积,而设计错误不会得到纠正。 > With a good set of tests, developers can be more confident when refactoring because the test suite will find most bugs that are introduced. This encourages developers to make structural improvements to a system, which results in a better design. Unit tests are particularly valuable: they provide a higher degree of code coverage than system tests, so they are more likely to uncover any bugs. -有了一套很好的测试,开发人员可以在重构时更有信心,因为测试套件将发现大多数引入的错误。这鼓励开发人员对系统进行结构改进,从而获得更好的设计。单元测试特别有价值:与系统测试相比,它们提供更高的代码覆盖率,因此它们更有可能发现任何错误。 +有了一套很好的测试,开发人员可以在重构时更有信心,因为测试套件将发现大多数引入的 bug。这鼓励开发人员对系统进行结构改进,从而获得更好的设计。单元测试特别有价值:与系统测试相比,它们提供更高的代码覆盖率,因此它们更有可能发现任何 bug。 > For example, during the development of the Tcl scripting language, we decided to improve performance by replacing Tcl’s interpreter with a byte-code compiler. This was a huge change that affected almost every part of the core Tcl engine. Fortunately, Tcl had an excellent unit test suite, which we ran on the new byte-code engine. The existing tests were so effective in uncovering bugs in the new engine that only a single bug turned up after the alpha release of the byte-code compiler. @@ -84,33 +84,33 @@ > Test-driven development is an approach to software development where programmers write unit tests before they write code. When creating a new class, the developer first writes unit tests for the class, based on its expected behavior. None of the tests pass, since there is no code for the class. Then the developer works through the tests one at a time, writing enough code for that test to pass. When all of the tests pass, the class is finished. -测试驱动开发是一种软件开发方法,程序员可以在编写代码之前先编写单元测试。创建新类时,开发人员首先根据其预期行为为该类编写单元测试。没有测试通过,因为该类没有代码。然后,开发人员一次完成一个测试,编写足够的代码以使该测试通过。所有测试通过后,该类结束。 +测试驱动开发是一种软件开发方法,程序员可以在编写代码之前先编写单元测试。创建新类时,开发人员首先根据其预期行为为该类编写单元测试。没有一个测试通过,因为该类没有代码。然后,开发人员一次完成一个测试,编写足够的代码以使该测试通过。所有测试通过后,这个类的功能就完成了。 > Although I am a strong advocate of unit testing, I am not a fan of test-driven development. The problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design. This is tactical programming pure and simple, with all of its disadvantages. Test-driven development is too incremental: at any point in time, it’s tempting to just hack in the next feature to make the next test pass. There’s no obvious time to do design, so it’s easy to end up with a mess. -尽管我是单元测试的坚决拥护者,但我不喜欢测试驱动的开发。测试驱动开发的问题在于,它将注意力集中在使特定功能起作用,而不是寻找最佳设计上。这是一种纯净而简单的战术编程,具有所有缺点。测试驱动的开发过于增量:在任何时间点,很容易破解下一个功能以进行下一个测试通过。没有明显的时间进行设计,因此很容易陷入混乱。 +尽管我是单元测试的坚决拥护者,但我并不热衷测试驱动的开发。测试驱动开发的问题在于,它将注意力集中在使特定功能正常工作上,而不是寻找最佳设计。这是纯粹的战术性编程,有其所有的弊端。测试驱动的开发过于增量:在任何时间点,很容易完成一个功能然后让测试通过。没有明显的时间来做设计,因此很容易搞的一团糟。 > As mentioned in Section 19.2, the units of development should be abstractions, not features. Once you discover the need for an abstraction, don’t create the abstraction in pieces over time; design it all at once (or at least enough to provide a reasonably comprehensive set of core functions). This is more likely to produce a clean design whose pieces fit together well. -如第 19.2 节所述,开发单位应该是抽象的,而不是功能。一旦发现需要抽象,就不要随着时间的流逝而逐步创建抽象。一次设计所有功能(或至少足以提供一组合理全面的核心功能)。这样更有可能产生干净的设计,使各个部分很好地契合在一起。 +如第 19.2 节所述,开发单位应该是抽象的,而不是功能。一旦发现了对抽象的需求,就不要在一段时间内零散的创建抽象,而应该一次性的设计(或至少足以提供一组合理全面的核心功能)。这样更有可能产生干净的设计,使各个部分很好地契合在一起。 > One place where it makes sense to write the tests first is when fixing bugs. Before fixing a bug, write a unit test that fails because of the bug. Then fix the bug and make sure that the unit test now passes. This is the best way to make sure you really have fixed the bug. If you fix the bug before writing the test, it’s possible that the new unit test doesn’t actually trigger the bug, in which case it won’t tell you whether you really fixed the problem. -首先编写测试的地方是修复错误。修复错误之前,请编写由于该错误而失败的单元测试。然后修复该错误,并确保现在可以通过单元测试。这是确保您已真正修复该错误的最佳方法。如果您在编写测试之前就已修复了该错误,则新的单元测试很可能实际上不会触发该错误,在这种情况下,它不会告诉您是否确实修复了该问题。 +有一个地方先编写测试是有意义的,那就是修复 bug 的时候。在修复错误之前,请编写由于该错误而失败的单元测试。然后修复该错误,并确保现在可以通过单元测试。这是确保您已真正修复该错误的最佳方法。如果您在编写测试之前就已修复了该错误,则新的单元测试很可能实际上不会触发该错误,在这种情况下,它不会告诉您是否真的修复了该问题。 ## 19.5 Design patterns 设计模式 > A design pattern is a commonly used approach for solving a particular kind of problem, such as an iterator or an observer. The notion of design patterns was popularized by the book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides, and design patterns are now widely used in object-oriented software development. -设计模式是解决特定类型问题(例如迭代器或观察器)的常用方法。设计模式的概念在 Gamma,Helm,Johnson 和 Vlissides 的《设计模式:可重用的面向对象软件的元素》一书中得到了普及,现在设计模式已广泛用于面向对象的软件开发中。 +设计模式是解决特定类型问题(例如迭代器或观察器)的常用方法。设计模式的概念在 Gamma,Helm,Johnson 和 Vlissides 的《设计模式:可复用的面向对象软件的基础》一书中而普及,现在设计模式已广泛用于面向对象的软件开发中。 > Design patterns represent an alternative to design: rather than designing a new mechanism from scratch, just apply a well-known design pattern. For the most part, this is good: design patterns arose because they solve common problems, and because they are generally agreed to provide clean solutions. If a design pattern works well in a particular situation, it will probably be hard for you to come up with a different approach that is better. -设计模式代表了设计的替代方法:与其从头设计新的机制,不如应用一种众所周知的设计模式。在大多数情况下,这是件好事:出现设计模式是因为它们解决了常见的问题,并且因为它们被普遍同意提供干净的解决方案。如果设计模式在特定情况下运作良好,那么您可能很难想出另一种更好的方法。 +设计模式代表了设计的另一种选择:与其从头设计新机制,不如应用一种众所周知的设计模式。在大多数情况下,这是很好的:设计模式的出现是因为它们解决了常见的问题,并且因为它们被普遍认为提供干净的解决方案。如果设计模式在特定情况下运作良好,那么您可能很难想出另一种更好的方法。 > The greatest risk with design patterns is over-application. Not every problem can be solved cleanly with an existing design pattern; don’t try to force a problem into a design pattern when a custom approach will be cleaner. Using design patterns doesn’t automatically improve a software system; it only does so if the design patterns fit. As with many ideas in software design, the notion that design patterns are good doesn’t necessarily mean that more design patterns are better. -设计模式的最大风险是过度使用。使用现有的设计模式并不能完全解决所有问题。当自定义方法更加简洁时,请勿尝试将问题强加到设计模式中。使用设计模式并不能自动改善软件系统。只有在设计模式合适的情况下才这样做。与软件设计中的许多想法一样,设计模式良好的概念并不一定意味着更多的设计模式会更好。 +设计模式的最大风险是过度使用。不是每个问题都可以用现有的设计模式来解决。当自定义方法更加简洁时,请勿尝试将问题强加到设计模式中。使用设计模式并不能自动改善软件系统。只有在设计模式合适的情况下才会如此。与软件设计中的许多想法一样,设计模式良好的概念并不一定意味着更多的设计模式会更好。 ## 19.6 Getters and setters Getter 和 Setters @@ -120,18 +120,18 @@ > Getters and setters aren’t strictly necessary, since instance variables can be made public. The argument for getters and setters is that they allow additional functions to be performed while getting and setting, such as updating related values when a variable changes, notifying listeners of changes, or enforcing constraints on values. Even if these features aren’t needed initially, they can be added later without changing the interface. -由于实例变量可以公开,因此不一定必须使用 getter 和 setter 方法。getter 和 setter 的论点是,它们允许在获取和设置时执行其他功能,例如在变量更改时更新相关值,通知更改的侦听器或对值实施约束。即使最初不需要这些功能,也可以稍后添加它们而无需更改界面。 +由于实例变量可以公开,因此不一定必须使用 getter 和 setter 方法。getter 和 setter 的论点是,它们允许在获取和设置时执行额外功能,例如在变量更改时更新相关值,通知监听器的变化或对值实施约束。即使最初不需要这些功能,以后也可以在不更改界面的情况下添加它们。 > Although it may make sense to use getters and setters if you must expose instance variables, it’s better not to expose instance variables in the first place. Exposed instance variables mean that part of the class’s implementation is visible externally, which violates the idea of information hiding and increases the complexity of the class’s interface. Getters and setters are shallow methods (typically only a single line), so they add clutter to the class’s interface without providing much functionality. It’s better to avoid getters and setters (or any exposure of implementation data) as much as possible. -尽管如果必须公开实例变量,则可以使用 getter 和 setter 方法,但最好不要首先公开实例变量。暴露的实例变量意味着类的实现的一部分在外部是可见的,这违反了信息隐藏的思想,并增加了类接口的复杂性。Getter 和 Setter 是浅层方法(通常只有一行),因此它们在不提供太多功能的情况下为类的接口增加了混乱。最好避免使用 getter 和 setter(或任何暴露的实现数据)。 +如果必须公开实例变量,则可以使用 getter 和 setter 方法,但最好不要首先公开实例变量。暴露的实例变量意味着类的实现的一部分在外部是可见的,这违反了信息隐藏的思想,并增加了类接口的复杂性。Getter 和 Setter 是浅层方法(通常只有一行),因此它们在不提供太多功能的情况下使类的接口变得混乱。最好避免使用 getter 和 setter(或任何公开的实现数据)。 > One of the risks of establishing a design pattern is that developers assume the pattern is good and try to use it as much as possible. This has led to overusage of getters and setters in Java. -建立设计模式的风险之一是,开发人员认为该模式是好的,并尝试尽可能多地使用它。这导致 Java 中的 getter 和 setter 的过度使用。 +建立设计模式的风险之一是开发人员认为该模式是好的,并试图尽可能多地使用它。这导致 Java 中的 getter 和 setter 的过度使用。 ## 19.7 Conclusion 结论 > Whenever you encounter a proposal for a new software development paradigm, challenge it from the standpoint of complexity: does the proposal really help to minimize complexity in large software systems? Many proposals sound good on the surface, but if you look more deeply you will see that some of them make complexity worse, not better. -每当您遇到有关新软件开发范例的提案时,就必须从复杂性的角度对其进行挑战:该提案确实有助于最大程度地降低大型软件系统的复杂性吗?从表面上看,许多建议听起来不错,但是如果您深入研究,您会发现其中一些会使复杂性恶化,而不是更好。 +每当您遇到有关新软件开发范例的提案时,就必须从复杂性的角度对其进行挑战:该提案确实有助于最大程度地降低大型软件系统的复杂性吗?许多建议表面上听起来不错,但是如果您深入研究,您会发现其中一些会使复杂性恶化,而不是更好。 diff --git a/docs/ch20.md b/docs/ch20.md index 613545a9..753cea3a 100644 --- a/docs/ch20.md +++ b/docs/ch20.md @@ -4,17 +4,17 @@ > Up until this point, the discussion of software design has focused on complexity; the goal has been to make software as simple and understandable as possible. But what if you are working on a system that needs to be fast? How should performance considerations affect the design process? This chapter discusses how to achieve high performance without sacrificing clean design. The most important idea is still simplicity: not only does simplicity improve a system’s design, but it usually makes systems faster. -到目前为止,关于软件设计的讨论都集中在复杂性上。目标是使软件尽可能简单易懂。但是,如果您在需要快速的系统上工作,该怎么办?性能方面的考虑应如何影响设计过程?本章讨论如何在不牺牲简洁设计的情况下实现高性能。最重要的想法仍然是简单性:简单性不仅可以改善系统的设计,而且通常可以使系统更快。 +到目前为止,关于软件设计的讨论都集中在复杂性上。目标是使软件尽可能简单易懂。但是,如果您需要让一个系统运行的更加高效,该怎么办?性能方面的考虑应如何影响设计过程?本章讨论如何在不牺牲简洁设计的情况下实现高性能。最重要的想法仍然是简单性:简单性不仅可以改善系统的设计,而且通常可以使系统更快。 ## 20.1 How to think about performance 如何考虑性能 > The first question to address is “how much should you worry about performance during the normal development process?” If you try to optimize every statement for maximum speed, it will slow down development and create a lot of unnecessary complexity. Furthermore, many of the “optimizations” won’t actually help performance. On the other hand, if you completely ignore performance issues, it’s easy to end up with a large number of significant inefficiencies spread throughout the code; the resulting system can easily be 5–10x slower than it needs to be. In this “death by a thousand cuts” scenario it’s hard to come back later and improve the performance, because there is no single improvement that will have much impact. -要解决的第一个问题是“您在正常的开发过程中应该为性能多少担心?” 如果您尝试优化每条语句以获得最大速度,则它将减慢开发速度并产生许多不必要的复杂性。此外,许多“优化”实际上对性能没有帮助。另一方面,如果您完全忽略了性能问题,则很容易导致遍及整个代码的大量效率低下。结果系统很容易比所需的速度慢 5–10 倍。在这种“千刀砍死”的情况下,以后很难再回来提高性能了,因为没有单一的改进会产生很大的影响。 +要解决的第一个问题是“在正常的开发过程中,您应该在多大程序上担心性能?” 如果您尝试优化每条语句以获得最大速度,则它将减慢开发速度并产生很多不必要的复杂性。此外,许多“优化”实际上对性能没有帮助。另一方面,如果您完全忽略了性能问题,则很容易导致遍及整个代码中出现大量严重的低效问题。结果系统很容易比所需的速度慢 5–10 倍。在这种“千刀万剐”的情况下,以后很难再回来改进性能了,因为没有单一的改进会产生很大的影响。 > The best approach is something between these extremes, where you use basic knowledge of performance to choose design alternatives that are “naturally efficient” yet also clean and simple. The key is to develop an awareness of which operations are fundamentally expensive. Here are a few examples of operations that are relatively expensive today: -最好的方法是介于这两种极端之间,在这种极端情况下,您可以使用性能的基本知识来选择“自然高效”但又干净又简单的设计替代方案。关键是要了解哪些操作根本是昂贵的。以下是一些今天相对昂贵的操作示例: +最好的方法是介于这两种极端之间,在这种极端情况下,您可以利用性能的基本知识来选择“自然高效”但又干净简单的设计方案。关键是要意识到哪些操作从根本上来说是昂贵的。以下是一些今天相对昂贵的操作示例: > - Network communication: even within a datacenter, a round-trip message exchange can take 10–50 µs, which is tens of thousands of instruction times. Wide-area round-trips can take 10–100 ms. > - I/O to secondary storage: disk I/O operations typically take 5–10 ms, which is millions of instruction times. Flash storage takes 10–100 µs. New emerging nonvolatile memories may be as fast as 1 µs, but this is still around 2000 instruction times. @@ -23,98 +23,98 @@ --- -- 网络通信:即使在数据中心内,往返消息交换也可能花费 10–50 µs,这是数以万计的指令时间。广域往返可能需要 10 到 100 毫秒。 -- I/O 到辅助存储:磁盘 I/O 操作通常需要 5 到 10 毫秒,这是数百万条指令时间。闪存存储需要 10–100 µs。新出现的非易失性存储器的速度可能高达 1 µs,但这仍约为 2000 条指令时间。 +- 网络通信:即使在数据中心内,往返消息交换也可能花费 10–50 µs,这是数以万计的指令时间。广域网往返可能需要 10 到 100 毫秒。 +- 二级存储的 I/O:磁盘 I/O 操作通常需要 5 到 10 毫秒,这是数百万条指令时间。闪存存储需要 10–100 µs。新出现的非易失性存储器的速度可能高达 1 µs,但这仍约为 2000 条指令时间。 - 动态内存分配(C 语言中的 malloc, C++ 或 Java 中的新增功能)通常涉及分配,释放和垃圾回收的大量开销。 -- 高速缓存未命中:将数据从 DRAM 提取到片上处理器高速缓存中需要数百条指令时间;在许多程序中,整体性能取决于缓存未命中和计算成本。 +- 缓存缺失:将数据从 DRAM 提取到片上处理器高速缓存中需要数百条指令时间;在许多程序中,整体性能取决于缓存未命中和计算成本。 > The best way to learn which things are expensive is to run micro-benchmarks (small programs that measure the cost of a single operation in isolation). In the RAMCloud project, we created a simple program that provides a framework for microbenchmarks. It took a few days to create the framework, but the framework makes it possible to add new micro-benchmarks in five or ten minutes. This has allowed us to accumulate dozens of micro-benchmarks. We use these both to understand the performance of existing libraries used in RAMCloud, and also to measure the performance of new classes written for RAMCloud. -了解哪些东西最昂贵的最好方法是运行微基准测试(小型程序,这些程序单独测量单个操作的成本)。在 RAMCloud 项目中,我们创建了一个简单的程序,该程序提供了微基准测试的框架。创建该框架花了几天时间,但是该框架使在五到十分钟内添加新的微基准成为可能。这使我们积累了几十个微基准。我们既可以使用它们来了解 RAMCloud 中使用的现有库的性能,也可以衡量为 RAMCloud 编写的新类的性能。 +了解哪些东西最昂贵的最好方法是运行微基准测试(单独衡量单个操作成本的小程序)。在 RAMCloud 项目中,我们创建了一个提供微基准测试的框架简单的程序。创建该框架花了几天时间,但是该框架使在五到十分钟内添加新的微基准成为可能。这使我们积累了几十个微基准。我们既可以使用它们来了解 RAMCloud 中使用的现有库的性能,也可以衡量为 RAMCloud 编写的新类的性能。 > Once you have a general sense for what is expensive and what is cheap, you can use that information to choose cheap operations whenever possible. In many cases, a more efficient approach will be just as simple as a slower approach. For example, when storing a large collection of objects that will be looked up using a key value, you could use either a hash table or an ordered map. Both are commonly available in library packages, and both are simple and clean to use. However, hash tables can easily be 5–10x faster. Thus, you should always use a hash table unless you need the ordering properties provided by the map. -一旦对什么是昂贵和什么便宜有了一般的认识,就可以使用该信息尽可能地选择便宜的业务。在许多情况下,更有效的方法将与较慢的方法一样简单。例如,当存储将使用键值查找的大量对象时,可以使用哈希表或有序映射。两者都通常在库包中提供,并且都简单易用。但是,哈希表可以轻松地快 5-10 倍。因此,除非需要映射提供的排序属性,否则应始终使用哈希表。 +一旦您对什么是昂贵和什么便宜有了大致的了解,就可以使用该信息尽可能地选择便宜的操作。在许多情况下,更有效的方法将与较慢的方法一样简单。例如,当存储将使用键值查找的大量对象时,可以使用哈希表或有序映射。两者都通常在库包中提供,并且都简单易用。但是,哈希表可以轻松地快 5-10 倍。因此,除非需要映射提供的排序属性,否则您应始终使用哈希表。 > As another example, consider allocating an array of structures in a language such as C or C++. There are two ways you can do this. One way is for the array to hold pointers to structures, in which case you must first allocate space for the array, then allocate space for each individual structure. It is much more efficient to store the structures in the array itself, so you only allocate one large block for everything. -作为另一个示例,请考虑使用诸如 C 或 C++ 之类的语言分配结构数组。有两种方法可以执行此操作。一种方法是让数组保留指向结构的指针,在这种情况下,您必须首先为数组分配空间,然后为每个单独的结构分配空间。将结构存储在数组本身中效率要高得多,因此您只为所有内容分配一个大块。 +作为另一个示例,请考虑使用诸如 C 或 C++ 之类的语言分配结构数组。有两种方法可以执行此操作。一种方法是让数组保留指向结构的指针,在这种情况下,您必须首先为数组分配空间,然后为每个单独的结构分配空间。将结构存储在数组本身中效率要高得多,因此您只为所有内容分配一大块内存。 > If the only way to improve efficiency is by adding complexity, then the choice is more difficult. If the more efficient design adds only a small amount of complexity, and if the complexity is hidden, so it doesn’t affect any interfaces, then it may be worthwhile (but beware: complexity is incremental). If the faster design adds a lot of implementation complexity, or if it results in more complicated interfaces, then it may be better to start off with the simpler approach and optimize later if performance turns out to be a problem. However, if you have clear evidence that performance will be important in a particular situation, then you might as well implement the faster approach immediately. -如果提高效率的唯一方法是增加复杂性,那么选择就更加困难。如果更高效的设计仅增加了少量复杂性,并且复杂性是隐藏的,因此它不影响任何接口,那么它可能是值得的(但要注意:复杂性是递增的)。如果更快的设计增加了很多实现复杂性,或者导致更复杂的接口,那么最好是从更简单的方法开始,然后在性能出现问题时进行优化。但是,如果您有明确的证据表明性能在特定情况下很重要,那么您最好立即实施更快的方法。 +如果提高效率的唯一方法是增加复杂性,那么选择就更困难了。如果更高效的设计仅增加了少量复杂性,并且复杂性是隐藏的,那么它不影响任何接口,那么它可能是值得的(但要注意:复杂性是递增的)。如果更快的设计增加了很多实现复杂性,或者导致更复杂的接口,那么最好是从更简单的方法开始,然后在性能出现问题时进行优化。但是,如果您有明确的证据表明性能在特定情况下很重要,那么您不妨立即实施更高效的方法。 > In the RAMCloud project one of our overall goals was to provide the lowest possible latency for client machines accessing the storage system over a datacenter network. As a result, we decided to use special hardware for networking, which allowed RAMCloud to bypass the kernel and communicate directly with the network interface controller to send and receive packets. We made this decision even though it added complexity, because we knew from prior measurements that kernel-based networking would be too slow to meet our needs. In most of the rest of the RAMCloud system we were able to design for simplicity; getting this one big issue “right” made many other things easier. -在 RAMCloud 项目中,我们的总体目标之一是为客户端计算机通过数据中心网络访问存储系统提供尽可能低的延迟。结果,我们决定使用特殊的硬件进行联网,从而使 RAMCloud 绕过内核并直接与网络接口控制器进行通信以发送和接收数据包。即使增加了复杂性,我们还是做出了这个决定,因为我们从先前的测量中知道,基于内核的网络太慢了,无法满足我们的需求。在其余的 RAMCloud 系统中,我们能够进行简单设计。解决这个大问题“对”使其他事情变得更加容易。 +在 RAMCloud 项目中,我们的总体目标之一是为通过数据中心网络访问存储系统的客户端机器提供尽可能低的延迟。结果,我们决定使用特殊的硬件进行联网,从而使 RAMCloud 绕过内核并直接与网络接口控制器进行通信以发送和接收数据包。尽管增加了复杂性,但我们还是做出了这个决定,因为我们从先前的测量中知道,基于内核的网络太慢了,无法满足我们的需求。在其余的 RAMCloud 系统中,我们能够进行简单设计。把这个大问题“解决”会让其他事情变得更加容易。 > In general, simpler code tends to run faster than complex code. If you have defined away special cases and exceptions, then no code is needed to check for those cases and the system runs faster. Deep classes are more efficient than shallow ones, because they get more work done for each method call. Shallow classes result in more layer crossings, and each layer crossing adds overhead. -通常,较简单的代码往往比复杂的代码运行更快。如果您定义了特殊情况和例外,则无需代码即可检查这些情况,并且系统运行速度更快。深层类比浅层类更有效,因为它们为每个方法调用完成了更多工作。浅类会导致更多的层交叉,并且每个层交叉都会增加开销。 +通常,简单的代码往往比复杂的代码运行得更快。如果您已经定义好了特殊情况和异常情况,那么就不需要代码来检查这些情况,系统就会运行速度更快。深层类比浅层类更有效,因为它们为每个方法调用完成了更多工作。浅类会导致更多的层交叉,并且每个层交叉都会增加开销。 ## 20.2 Measure before modifying 修改前的度量 > But suppose that your system is still too slow, even though you have designed it as described above. It’s tempting to rush off and start making performance tweaks, based on your intuitions about what is slow. Don’t do this! Programmers’ intuitions about performance are unreliable. This is true even for experienced developers. If you start making changes based on intuition, you’ll waste time on things that don’t actually improve performance, and you’ll probably make the system more complicated in the process. -但是,即使您如上所述进行设计,也请假设您的系统仍然太慢。根据您对慢速运动的直觉,急于着手开始进行性能调整。不要这样!程序员对性能的直觉是不可靠的。即使对于有经验的开发人员也是如此。如果您开始根据直觉进行更改,则会浪费时间在实际上无法提高性能的事情上,并且可能会使系统变得更加复杂。 +但是假设您的系统仍然太慢,即使您已经按照上面描述的方式设计了它。根据您对什么是缓慢的直觉,很容易匆忙进行性能调整。不要这样做!程序员对性能的直觉是不可靠的。即使对于有经验的开发人员也是如此。如果您开始根据直觉进行修改,你会把时间浪费在实际上无法提高性能的事情上,并且在这个过程中可能会使系统变得更加复杂。 > Before making any changes, measure the system’s existing behavior. This serves two purposes. First, the measurements will identify the places where performance tuning will have the biggest impact. It isn’t sufficient just to measure the top-level system performance. This may tell you that the system is too slow, but it won’t tell you why. You’ll need to measure deeper to identify in detail the factors that contribute to overall performance; the goal is to identify a small number of very specific places where the system is currently spending a lot of time, and where you have ideas for improvement. The second purpose of the measurements is to provide a baseline, so that you can re-measure performance after making your changes to ensure that performance actually improved. If the changes didn’t make a measurable difference in performance, then back them out (unless they made the system simpler). There’s no point in retaining complexity unless it provides a significant speedup. -进行任何更改之前,请测量系统的现有行为。这有两个目的。首先,这些测量将确定性能调整将产生最大影响的地方。仅仅测量顶级系统性能是不够的。这可能会告诉您系统速度太慢,但不会告诉您原因。您需要进行更深入的衡量,以详细确定影响整体绩效的因素;目标是确定系统当前花费大量时间的少量非常具体的地方,以及您有改进想法的地方。测量的第二个目的是提供基线,以便您可以在进行更改后重新测量性能,以确保性能得到实际改善。如果这些更改并未在效果上带来可衡量的变化,然后将其退出(除非它们使系统更简单)。除非能够显着提高速度,否则保持复杂性毫无意义。 +进行任何更改之前,请测量系统的现有行为。这有两个目的。首先,这些测量将确定性能调整将产生最大影响的地方。仅仅测量顶级系统性能是不够的。这可能会告诉您系统速度太慢,但不会告诉您原因。您需要进行更深入的衡量,以详细确定影响整体绩效的因素;目标是确定系统当前花费大量时间的少量非常具体的地方,以及您有改进想法的地方。测量的第二个目的是提供基线,以便您可以在进行更改后重新测量性能,以确保性能确实得到改善。如果更改并未在性能上产生可衡量的差异,则将它们撤销(除非它们使系统更简单)。保留复杂性是没有意义的,除非它提供了显著的速度提升。 ## 20.3 Design around the critical path 围绕关键路径进行设计 > At this point, let’s assume that you have carefully analyzed performance and have identified a piece of code that is slow enough to affect the overall system performance. The best way to improve its performance is with a “fundamental” change, such as introducing a cache, or using a different algorithmic approach (balanced tree vs. list, for instance). Our decision to bypass the kernel for network communication in RAMCloud is an example of a fundamental fix. If you can identify a fundamental fix, then you can implement it using the design techniques discussed in previous chapters. -在这一点上,我们假设您已经仔细分析了性能,并确定了一段缓慢的代码来影响整个系统的性能。改善其性能的最佳方法是进行“根本”更改,例如引入缓存,或使用其他算法方法(例如,平衡树与列表)。我们决定绕过内核进行 RAMCloud 中的网络通信的决定是一个基本修补程序的示例。如果您可以确定基本修复程序,则可以使用前面各章中讨论的设计技术来实施它。 +在这一点上,假设您已经仔细分析了性能并确定了一段速度缓慢到足以影响整个系统的性能的代码。改善其性能的最佳方法是进行“根本性”更改,例如引入缓存,或使用其他算法方法(例如,平衡树与列表)。我们决定绕过内核进行 RAMCloud 中的网络通信的决定是一个根本性修正的示例。如果您能确定一个根本性的修正,则可以使用前面各章中讨论的设计技术来实施它。 > Unfortunately, situations will sometimes arise where there isn’t a fundamental fix. This brings us to the core issue for this chapter, which is how to redesign an existing piece of code so that it runs faster. This should be your last resort, and it shouldn’t happen often, but there are cases where it can make a big difference. The key idea is to design the code around the critical path. -不幸的是,有时会出现一些根本无法解决的情况。这将我们带到本章的核心问题,即如何重新设计现有代码,使其运行更快。这应该是您的不得已的方法,并且不应该经常发生,但是在某些情况下它可能会带来很大的不同。关键思想是围绕关键路径设计代码。 +不幸的是,有时会出现一些没有根本解决办法的情况。这就把我们带到本章的核心问题,即如何重新设计现有代码,使其运行更快。这应该是您的不得已的方法,并且不应该经常发生,但是在某些情况下它可能会带来很大的不同。关键思想是围绕关键路径设计代码。 > Start off by asking yourself what is the smallest amount of code that must be executed to carry out the desired task in the common case. Disregard any existing code structure. Imagine instead that you are writing a new method that implements just the critical path, which is the minimum amount of code that must be executed in the the most common case. The current code is probably cluttered with special cases; ignore them in this exercise. The current code might pass through several method calls on the critical path; imagine instead that you could put all the relevant code in a single method. The current code may also use a variety of variables and data structures; consider only the data needed for the critical path, and assume whatever data structure is most convenient for the critical path. For example, it may make sense to combine multiple variables into a single value. Assume that you could completely redesign the system in order to minimize the code that must be executed for the critical path. Let’s call this code “the ideal.” -首先,问自己在通常情况下执行所需任务必须执行的最少代码量是多少。忽略任何现有的代码结构。相反,想象一下您正在编写一个仅实现关键路径的新方法,这是在最常见的情况下必须执行的最少代码量。当前的代码可能充满特殊情况。在此练习中,请忽略它们。当前的代码可能会在关键路径上通过多个方法调用。想象一下,您可以将所有相关代码放在一个方法中。当前代码还可以使用各种变量和数据结构。仅考虑关键路径所需的数据,并假定最适合关键路径的任何数据结构。例如,将多个变量合并为一个值可能很有意义。假设您可以完全重新设计系统,以最大程度地减少必须为关键路径执行的代码。我们将此代码称为“理想”。 +首先,问自己在通常情况下执行所需任务必须执行的最少代码量是多少。忽略任何现有的代码结构。想象一下您正在编写一个仅实现关键路径的新方法,这是在最常见的情况下必须执行的最少代码量。当前的代码可能充满特殊情况。在此练习中,请忽略它们。当前的代码可能会在关键路径上通过多个方法调用。想象一下,您可以将所有相关代码放在一个方法中。当前代码还可以使用各种变量和数据结构。仅考虑关键路径所需的数据,并假定最适合关键路径的任何数据结构。例如,将多个变量合并为一个值可能很有意义。假设您可以完全重新设计系统,以最大程度地减少必须为关键路径执行的代码。我们把这段代码称为“理想”。 > The ideal code probably clashes with your existing class structure, and it may not be practical, but it provides a good target: this represents the simplest and fastest that the code can ever be. The next step is to look for a new design that comes as close as possible to the ideal while still having a clean structure. You can apply all of the design ideas from previous chapters of this book, but with the additional constraint of keeping the ideal code (mostly) intact. You may have to add a bit of extra code to the ideal in order to allow clean abstractions; for example, if the code involves a hash table lookup, it’s OK to introduce an extra method call to a general-purpose hash table class. In my experience it’s almost always possible to find a design that is clean and simple, yet comes very close to the ideal. -理想的代码可能会与您现有的类结构冲突,并且可能不切实际,但它提供了一个很好的目标:这代表了代码可能是最简单,最快的。下一步是寻找一种新设计,使其尽可能接近理想状态,同时又要保持干净的结构。您可以应用本书前面各章中的所有设计思想,但要保持(最好)保持理想代码的附加约束。您可能需要在理想情况下添加一些额外的代码,以允许使用简洁的抽象。例如,如果代码涉及哈希表查找,则可以向通用哈希表类引入额外的方法调用。以我的经验,几乎总是可以找到干净简洁的设计,但非常接近理想。 +理想的代码可能会与您现有的类结构冲突,并且可能不切实际,但它提供了一个很好的目标:这代表了代码可能是最简单和最快的。下一步是寻找一种新设计,使其尽可能接近理想状态,同时又要保持干净的结构。您可以应用本书前面各章中的所有设计思想,但要保持(大部分)理想代码的完整性。您可能需要在理想情况下添加一些额外的代码,以便实现干净的抽象。例如,如果代码涉及哈希表查找,引入一个额外的方法调用到一个通用哈希表类是可以的。根据我的经验,几乎总是能找到一种简洁明了但是但非常接近理想的设计。 > One of the most important things that happens in this process is to remove special cases from the critical path. When code is slow, it’s often because it must handle a variety of situations, and the code gets structured to simplify the handling of all the different cases. Each special case adds a little bit of code to the critical path, in the form of extra conditional statements and/or method calls. Each of these additions makes the code a bit slower. When redesigning for performance, try to minimize the number of special cases you must check. Ideally, there will be a single if statement at the beginning, which detects all special cases with one test. In the normal case, only this one test will need to be made, after which the the critical path can be executed with no additional tests for special cases. If the initial test fails (which means a special case has occurred) the code can branch to a separate place off the critical path to handle it. Performance isn’t as important for special cases, so you can structure the special-case code for simplicity rather than performance. -在此过程中发生的最重要的事情之一是从关键路径中除去特殊情况。当代码运行缓慢时,通常是因为它必须处理各种情况,并且代码经过结构化以简化所有不同情况的处理。每个特殊情况都以额外的条件语句和/或方法调用的形式向关键路径添加了一些代码。这些添加中的每一个都会使代码变慢。重新设计性能时,请尝试减少必须检查的特殊情况的数量。理想情况下,开头应该有一个 if 语句,该语句可以通过一个测试检测所有特殊情况。在正常情况下,只需要进行一项测试,之后就可以执行关键路径,而对于特殊情况则无需进行其他测试。如果初始测试失败(这意味着发生了特殊情况),则代码可以分支到关键路径之外的单独位置以进行处理。对于特殊情况,性能并不是那么重要,因此您可以为简化而不是性能来构造特殊情况的代码。 +在此过程中发生的最重要的事情之一是从关键路径中除去特殊情况。当代码运行缓慢时,通常是因为它必须处理各种情况,并且代码经过结构化以简化所有不同情况的处理。每个特殊情况都以额外的条件语句和/或方法调用的形式向关键路径添加了一些代码。这些添加中的每一个都会使代码变慢。重新设计性能时,请尝试减少必须检查的特殊情况的数量。理想情况下,开头应该有一个 if 语句,该语句可以通过一个测试检测所有特殊情况。在正常情况下,只需要进行一项测试,之后就可以执行关键路径,而对于特殊情况则无需进行其他测试。如果初始测试失败(这意味着发生了特殊情况),则代码可以分支到关键路径之外的单独位置以进行处理。对于特殊情况,性能并不是那么重要,因此您可以将特殊情况下的代码结构化,使之简单化而不是性能化。 ## 20.4 An example: RAMCloud Buffers 示例:RAMCloud 缓冲区 > Let’s consider an example, in which the Buffer class of the RAMCloud storage system was optimized to achieve a speedup of about 2x for the most common operations. -让我们考虑一个示例,其中 RAMCloud 存储系统的 Buffer 类经过优化,以使大多数常见操作的速度提高约 2 倍。 +让我们考虑一个例子,在这个例子中,对 RAMCloud 存储系统的 Buffer 类进行了优化,在最常见的操作中实现了约 2 倍的速度提升。 > RAMCloud uses Buffer objects to manage variable-length arrays of memory, such as request and response messages for remote procedure calls. Buffers are designed to reduce overheads from memory copying and dynamic storage allocation. A Buffer stores what appears to be a linear array of bytes, but for efficiency it allows the underlying storage to be divided into multiple discontiguous chunks of memory, as shown in Figure 20.1. A Buffer is created by appending chunks of data. Each chunk is either external or internal. If a chunk is external, its storage is owned by the caller; the Buffer keeps a reference to this storage. External chunks are typically used for large chunks in order to avoid memory copies. If a chunk is internal, the Buffer owns the storage for the chunk; data supplied by the caller is copied into the Buffer’s internal storage. Each Buffer contains a small built-in allocation, which is a block of memory available for storing internal chunks. If this space is exhausted, then the Buffer creates additional allocations, which must be freed when the Buffer is destroyed. Internal chunks are convenient for small chunks where the memory copying costs are negligible. Figure 20.1 shows a Buffer with 5 chunks: the first chunk is internal, the next two are external, and the final two chunks are internal. -RAMCloud 使用 Buffer 对象管理可变长度的内存数组,例如远程过程调用的请求和响应消息。缓冲区旨在减少内存复制和动态存储分配的开销。缓冲区存储看似线性的字节数组,但是为了提高效率,它允许将底层存储划分为多个不连续的内存块,如图 20.1 所示。通过附加数据块来创建缓冲区。每个块都是外部的或内部的。如果块在外部,则其存储由调用方拥有;缓冲区保留对此存储的引用。外部块通常用于大型块,以避免内存复制。如果内部有块,则 Buffer 拥有该块的存储;调用者提供的数据将被复制到缓冲区的内部存储器中。每个缓冲区包含一个小的内置分配,这是一个内存块,可用于存储内部块。如果此空间已用完,则缓冲区将创建其他分配,销毁缓冲区时必须释放这些分配。内部块对于内存复制成本可忽略不计的小块很方便。图 20.1 显示了具有 5 个块的 Buffer:第一个块是内部的,接下来的两个块是外部的,最后两个块是内部的。 +RAMCloud 使用 Buffer 对象管理可变长度的内存数组,例如远程过程调用的请求和响应消息。缓冲区旨在减少内存复制和动态存储分配的开销。缓冲区存储的似乎是一个线性的字节数组,但为了提高效率,它允许将底层存储划分为多个不连续的内存块,如图 20.1 所示。缓冲区是通过附加数据块创建的。每个块要么是外部的,要么是内部的。如果块是外部的,则其存储空间由调用方拥有;Buffer 保留对此存储的引用。外部块通常用于大型块,以避免内存复制。如果块是内部的,则 Buffer 拥有该块的存储;调用者提供的数据将被复制到缓冲区的内部存储器中。每个 Buffer 包含一个小的内置分配,这是一个可用于存储内部块的内存块。如果此空间已用完,则 Buffer 需要额外分配内存,这些分配的内存必须在 Buffer 销毁时进行释放。内部块对于小块来说是很方便的,因为内存复制的成本可以忽略不计。图 20.1 显示了具有 5 个块的 Buffer:第一个块是内部的,接下来的两个块是外部的,最后两个块是内部的。 ![](./figures/00022.jpeg) > Figure 20.1: A Buffer object uses a collection of memory chunks to store what appears to be a linear array of bytes. Internal chunks are owned by the Buffer and freed when the Buffer is destroyed; external chunks are not owned by the Buffer. -图 20.1:Buffer 对象使用内存块的集合来存储看似线性字节数组。内部块由 Buffer 拥有,并在 Buffer 销毁时释放;外部块不属于缓冲区。 +图 20.1:Buffer 对象使用内存块的集合来存储看似线性字节数组。内部块为 Buffer 拥有,并在 Buffer 销毁时释放;外部块不为 Buffer 所有。 > The Buffer class itself represents a “fundamental fix,” in that it eliminates expensive memory copies that would have been required without it. For example, when assembling a response message containing a short header and the contents of a large object in the RAMCloud storage system, RAMCloud uses a Buffer with two chunks. The first chunk is an internal one that contains the header; the second chunk is an external one that refers to the object contents in the RAMCloud storage system. The response can be collected in the Buffer without copying the large object. -Buffer 类本身代表“根本性的修补程序”,因为它消除了没有它就需要的昂贵的内存副本。例如,在 RAMCloud 存储系统中组装包含短标头和大对象内容的响应消息时,RAMCloud 使用带有两个块的 Buffer。第一个块是包含头的内部块;第二个块是一个外部块,它引用 RAMCloud 存储系统中的对象内容。可以在不复制大对象的情况下将响应收集到缓冲区中。 +Buffer 类本身代表“根本性的修补程序”,因为它消除了昂贵的内存拷贝,而如果没有它的话,就需要进行拷贝。例如,在 RAMCloud 存储系统中组装包含短标头和大对象内容的响应消息时,RAMCloud 使用带有两个块的 Buffer。第一个块是包含头的内部块;第二个块是一个外部块,它引用 RAMCloud 存储系统中的对象内容。可以在不复制大对象的情况下将响应收集到 Buffer 中。 > Aside from the fundamental approach of allowing discontiguous chunks, we did not attempt to optimize the code of the Buffer class in the original implementation. Over time, however, we noticed Buffers being used in more and more situations; for example, at least four Buffers are created during the execution of each remote procedure call. Eventually, it became clear that speeding up the implementation of Buffer could have a noticeable impact on overall system performance. We decided to see if we could improve the performance of the Buffer class. -除了允许不连续块的基本方法外,我们没有尝试在原始实现中优化 Buffer 类的代码。但是,随着时间的流逝,我们注意到缓冲区越来越多地被使用。例如,在每个远程过程调用的执行期间,至少创建四个缓冲区。最终,很明显,加速 Buffer 的实现可能会对整体系统性能产生显着影响。我们决定看看是否可以提高 Buffer 类的性能。 +除了允许不连续块的基本方法外,在最初的实现中,我们并没有尝试优化 Buffer 类的代码。然而,随着时间的流逝,我们注意到 Buffer 越来越多地被使用。例如,在执行每个远程过程调用期间,至少会创建四个缓冲区。最终,我们发现,加速 Buffer 的实现可能会对整体系统性能产生显著影响。我们决定看看是否可以提高 Buffer 类的性能。 > The most common operation for Buffer is to allocate space for a small amount of new data using an internal chunk. This happens, for example, when creating headers for request and response messages. We decided to use this operation as the critical path for optimization. In the simplest possible case, the space can be allocated by enlarging the last existing chunk in the Buffer. However, this is only possible if the last existing chunk is internal, and if there is enough space in its allocation to accommodate the new data. The ideal code would perform a single check to confirm that the simple approach is possible, then it would adjust the size of the existing chunk. -Buffer 最常见的操作是使用内部块为少量新数据分配空间。例如,在为请求和响应消息创建标题时,就会发生这种情况。我们决定将此操作用作优化的关键路径。在最简单的情况下,可以通过扩大 Buffer 中最后存在的块来分配空间。但是,只有在最后一个现有块位于内部,并且其分配中有足够的空间来容纳新数据时,才有可能这样做。理想的代码将执行一次检查以确认简单方法是否可行,然后将调整现有块的大小。 +Buffer 最常见的操作是使用内部块为少量新数据分配空间。例如,在为请求和响应消息创建标头时,就会发生这种情况。我们决定使用将此操作作为优化的关键路径。在最简单的情况下,可以通过扩大 Buffer 中最后一个现有块来分配空间。但是,只有在最后一个现有块是内部块,并且在其分配中有足够的空间来容纳新数据时才有可能。理想的代码将执行一次检查,以确认简单方法是否可行的,然后将调整现有块的大小。 > Figure 20.2 shows the original code for the critical path, which starts with the method Buffer::alloc. In the fastest possible case, Buffer::alloc calls Buffer:: allocateAppend, which calls Buffer::Allocation::allocateAppend. From a performance standpoint, this code has two problems. The first problem is that numerous special cases are checked individually: -图 20.2 显示了关键路径的原始代码,该代码以 Buffer :: alloc 方法开头。在最快的情况下,Buffer :: alloc 调用 Buffer :: allocateAppend,后者调用 Buffer :: Allocation :: allocateAppend。从性能的角度来看,此代码有两个问题。第一个问题是要单独检查许多特殊情况: +图 20.2 显示了关键路径的原始代码,该代码以 Buffer::alloc 方法开头。在最快的情况下,Buffer::alloc 调用 Buffer::allocateAppend,后者调用 Buffer::Allocation::allocateAppend。从性能的角度来看,此代码有两个问题。第一个问题是要单独检查许多特殊情况: > - Buffer::allocateAppend checks to see if the Buffer currently has any allocations. > - The code checks twice to see if the current allocation has enough room for the new data: once in Buffer::Allocation::allocateAppend, and again when its return value is tested by Buffer::allocateAppend. @@ -122,21 +122,21 @@ Buffer 最常见的操作是使用内部块为少量新数据分配空间。例 --- -- Buffer::allocateAppend 检查缓冲区当前是否有任何分配。 +- Buffer::allocateAppend 检查当前 Buffer 是否有足够的空间。 - 代码检查两次以查看当前分配是否有足够的空间容纳新数据:一次在 Buffer::Allocation::allocateAppend 中,一次在其返回值由 Buffer::allocateAppend 测试时。 -- Buffer::alloc 测试 Buffer::allocAppend 的返回值,以再次确认分配成功。 +- Buffer::alloc 测试 Buffer::allocAppend 的返回值,再次确认分配成功。 > Furthermore, rather than trying to expand the last chunk directly, the code allocates new space without any consideration of the last chunk. Then Buffer::alloc checks to see if that space happens to be adjacent to the last chunk, in which case it merges the new space with the existing chunk. This results in additional checks. Overall, this code tests 6 distinct conditions in the critical path. -此外,该代码没有尝试直接扩展最后一个块,而是在不考虑最后一个块的情况下分配了新空间。然后,Buffer::alloc 检查该空间是否恰好与最后一块相邻,在这种情况下,它将新空间与现有块合并。这导致其他检查。总体而言,此代码测试关键路径中的 6 种不同条件。 +此外,该代码没有尝试直接扩展最后一个块,而是在不考虑最后一个块的情况下分配了新空间。然后,Buffer::alloc 检查该空间是否恰好与最后一块相邻,在这种情况下,它将新空间与现有块合并。这导致额外的检查。总体而言,此代码测试了关键路径中的 6 种不同条件。 > The second problem with the original code is that it has too many layers, all of which are shallow. This is both a performance problem and a design problem. The critical path makes two additional method calls in addition to the original invocation of Buffer::alloc. Each method call takes additional time, and the result of each call must be checked by its caller, which results in more special cases to consider. Chapter 7 discussed how abstractions should normally change as you pass from one layer to another, but all three of the methods in Figure 20.2 have identical signatures and they provide essentially the same abstraction; this is a red flag. Buffer::allocateAppend is nearly a pass-though method; its only contribution is to create a new allocation if needed. The extra layers make the code both slower and more complicated. -原始代码的第二个问题是它具有太多的层,所有层都很浅。这既是性能问题,也是设计问题。关键路径除了对 Buffer::alloc 的原始调用之外,还进行了另外两个方法调用。每个方法调用花费额外的时间,并且每个调用的结果必须由其调用者检查,这导致需要考虑更多特殊情况。第 7 章讨论了当您从一层传递到另一层时,抽象通常应该如何变化,但是图 20.2 中的所有三种方法都具有相同的签名,并且它们提供了基本相同的抽象。这是一个危险信号。Buffer::allocateAppend 几乎是一个传递方法;它的唯一作用是在需要时创建新的分配。额外的层使代码既慢又复杂。 +原始代码的第二个问题是它的层数太多,而且都很浅。这既是性能问题,也是设计问题。除了对 Buffer::alloc 的原始调用之外,关键路径还进行了两个额外的方法调用。每个方法调用都需要额外的时间,并且每个调用的结果必须由其调用者检查,这导致需要考虑更多的特殊情况。第 7 章讨论了当您从一层传递到另一层时,抽象通常应该如何变化,但是图 20.2 中的所有三种方法都具有相同的签名,并且它们提供了基本相同的抽象。这是一个危险信号。Buffer::allocateAppend 几乎是一个传递方法;它的唯一作用是在需要时创建新的分配。额外的层使代码更慢,更复杂。 > To fix these problems, we refactored the Buffer class so that its design is centered around the most performance-critical paths. We considered not just the allocation code above but several other commonly executed paths, such as retrieving the total number of bytes of data currently stored in a Buffer. For each of these critical paths, we tried to identify the smallest amount of code that must be executed in the common case. Then we designed the rest of the class around these critical paths. We also applied the design principles from this book to simplify the class in general. For example, we eliminated shallow layers and created deeper internal abstractions. The refactored class is 20% smaller than the original version (1476 lines of code, versus 1886 lines in the original). -为了解决这些问题,我们重构了 Buffer 类,使其设计围绕最关键性能的路径进行。我们不仅考虑了上面的分配代码,还考虑了其 ​​ 他几种常用的执行路径,例如检索当前存储在 Buffer 中的数据的字节总数。对于这些关键路径中的每一个,我们试图确定在通常情况下必须执行的最少代码量。然后,我们围绕这些关键路径设计了课程的其余部分。我们还应用了本书中的设计原则来简化整个类。例如,我们消除了浅层并创建了更深的内部抽象。重构的类比原始版本小 20%(1476 行代码,而原始版本为 1886 行)。 +为了解决这些问题,我们重构了 Buffer 类,使其设计围绕最关键性能的路径进行。我们不仅考虑了上面的分配代码,还考虑了其他几种通用的执行路径,例如检索当前存储在 Buffer 中的数据的字节总数。对于这些关键路径中的每一个,我们试图确定在通常情况下必须执行的最少代码量。然后,我们围绕这些关键路径设计了课程的其余部分。我们还应用了本书中的设计原则来简化整个类。例如,我们消除了浅层并创建了更深的内部抽象。重构后的类比原始版本小 20%(1476 行代码,而原始版本为 1886 行)。 ![](./figures/00023.gif) @@ -152,18 +152,18 @@ Buffer 最常见的操作是使用内部块为少量新数据分配空间。例 > Figure 20.3 shows the new critical path for allocating internal space in a Buffer. The new code is not only faster, but it is also easier to read, since it avoids shallow abstractions. The entire path is handled in a single method, and it uses a single test to rule out all of the special cases. The new code introduces a new instance variable, extraAppendBytes, in order to simplify the critical path. This variable keeps track of how much unused space is available immediately after the last chunk in the Buffer. If there is no space available, or if the last chunk in the Buffer isn’t an internal chunk, or if the Buffer contains no chunks at all, then extraAppendBytes is zero. The code in Figure 20.3 represents the least possible amount of code to handle this common case. -图 20.3 显示了用于在 Buffer 中分配内部空间的新关键路径。新代码不仅速度更快,而且更容易阅读,因为它避免了浅层抽象。整个路径使用单一方法处理,并且使用单一测试排除所有特殊情况。新代码引入了新的实例变量 extraAppendBytes,以简化关键路径。此变量跟踪缓冲区中最后一个块之后立即有多少未使用空间可用。如果没有可用空间,或者 Buffer 中的最后一个块不是内部块,或者 Buffer 根本不包含任何块,则 extraAppendBytes 为零。图 20.3 中的代码表示处理这种常见情况的最少代码量。 +图 20.3 显示了用于在 Buffer 中分配内部空间的新关键路径。新代码不仅更快,而且更容易阅读,因为它避免了浅层抽象。整个路径使用单一方法处理,它使用单一测试来排除所有特殊情况。新代码引入了新的实例变量 extraAppendBytes,以简化关键路径。该变量跟踪缓冲区中最后一个块之后立即有多少未使用空间可用。如果没有可用空间,或者 Buffer 中的最后一个块不是内部块,或者 Buffer 根本不包含任何块,则 extraAppendBytes 为零。图 20.3 中的代码表示处理这种常见情况的最少代码量。 > Note: the update to totalLength could have been eliminated by recomputing the total Buffer length from the individual chunks whenever it is needed. However, this approach would be expensive for a large Buffer with many chunks, and fetching the total Buffer length is another common operation. Thus, we chose to add a small amount of extra overhead to alloc in order to ensure that the Buffer length is always immediately available. -注意:只要需要,就可以通过重新计算各个块的总缓冲区长度来消除对 totalLength 的更新。但是,这种方法对于具有许多块的大型 Buffer 而言将是昂贵的,并且获取 Buffer 的总长度是另一种常见的操作。因此,我们选择添加少量额外的开销来分配,以确保 Buffer 长度始终立即可用。 +注意:可以通过在需要重新计算各个块的总缓冲区长度来消除对 totalLength 的更新。但是,这种方法对于具有许多块的大型 Buffer 而言将是昂贵的,并且获取 Buffer 的总长度是另一种常见的操作。因此,我们选择向 alloc 添加少量额外开销,以确保 Buffer 长度始终立即可用。 > The new code is about twice as fast as the old code: the total time to append a 1-byte string to a Buffer using internal storage dropped from 8.8 ns to 4.75 ns. Many other Buffer operations also speeded up because of the revisions. For example, the time to construct a new Buffer, append a small chunk in internal storage, and destroy the Buffer dropped from 24 ns to 12 ns. -新代码的速度约为旧代码的两倍:使用内部存储将 1 字节字符串附加到缓冲区的总时间从 8.8 ns 降低到 4.75 ns。由于修订,许多其他缓冲区操作也加快了速度。例如,构建新缓冲区,在内部存储中附加一小块并销毁缓冲区所需的时间从 24 ns 降至 12 ns。 +新代码的速度约为旧代码的两倍:使用内部存储将 1 字节字符串附加到缓冲区的总时间从 8.8ns 降低到 4.75ns。许多其他 Buffer 操作也因为修改而加快了速度。例如,构建一个新的 Buffer,在内部存储中附加一小块,销毁 Buffer 的时间从 24ns 降至 12ns。 ## 20.5 Conclusion 结论 > The most important overall lesson from this chapter is that clean design and high performance are compatible. The Buffer class rewrite improved its performance by a factor of 2 while simplifying its design and reducing code size by 20%. Complicated code tends to be slow because it does extraneous or redundant work. On the other hand, if you write clean, simple code, your system will probably be fast enough that you don’t have to worry much about performance in the first place. In the few cases where you do need to optimize performance, the key is simplicity again: find the critical paths that are most important for performance and make them as simple as possible. -本章最重要的总体教训是,干净的设计和高性能是兼容的。重写 Buffer 类可将其性能提高 2 倍,同时简化其设计并将代码大小减少 20%。复杂的代码通常会很慢,因为它会执行多余或多余的工作。另一方面,如果您编写干净,简单的代码,则系统可能会足够快,因此您一开始就不必担心性能。在少数需要优化性能的情况下,关键再次是简单性:找到对性能最重要的关键路径并使它们尽可能简单。 +本章最重要的总体经验是,简洁的设计和高性能是兼容的。重写 Buffer 类可将其性能提高 2 倍,同时简化其设计并将代码大小减少 20%。复杂的代码通常会很慢,因为它会执行无关或冗余的工作。另一方面,如果您编写干净,简单的代码,则系统可能会足够快,您一开始就不必担心性能。在少数需要优化性能的情况下,关键还是简化:找到对性能最重要的关键路径,并使它们尽可能简单。 diff --git a/docs/ch21.md b/docs/ch21.md index abf6cfdc..ecd6997e 100644 --- a/docs/ch21.md +++ b/docs/ch21.md @@ -4,20 +4,20 @@ > This book is about one thing: complexity. Dealing with complexity is the most important challenge in software design. It is what makes systems hard to build and maintain, and it often makes them slow as well. Over the course of the book I have tried to describe the root causes that lead to complexity, such as dependencies and obscurity. I have discussed red flags that can help you identify unnecessary complexity, such as information leakage, unneeded error conditions, or names that are too generic. I have presented some general ideas you can use to create simpler software systems, such as striving for classes that are deep and generic, defining errors out of existence, and separating interface documentation from implementation documentation. And, finally, I have discussed the investment mindset needed to produce simple designs. -这本书是关于一件事的:复杂性。处理复杂性是软件设计中最重要的挑战。这是使系统难以构建和维护的原因,并且通常也使它们变慢。在本书的整个过程中,我试图描述导致复杂性的根本原因,例如依赖性和模糊性。我已经讨论了可以帮助您识别不必要的复杂性的危险标记,例如信息泄漏,不必要的错误情况或名称过于笼统。我已经提出了一些通用的思想,可以用来创建更简单的软件系统,例如,努力研究更深和更通用的类,定义不存在的错误以及将接口文档与实现文档分离。最后,我讨论了产生简单设计所需的投资思路。 +这本书只针对一件事:复杂性。处理复杂性是软件设计中最重要的挑战。这也是为什么系统难以构建和维护的原因,而且复杂的系统通常运行也很缓慢。在本书中,我试图描述导致复杂性的根本原因,例如依赖性和模糊性。我已经讨论了可以帮助您识别不必要的复杂性的危险信号,例如信息泄漏,不必要的错误情况或名称过于笼统。我提出了一些通用的思想,可以用来创建更简单的软件系统,例如,努力创建更深和更通用的类,定义不存在的错误以及将接口文档与实现文档分离。最后,我讨论了产生简单设计所需的投资思路。 > The downside of all these suggestions is that they create extra work in the early stages of a project. Furthermore, if you aren’t used to thinking about design issues, then you will slow down even more while you learn good design techniques. If the only thing that matters to you is making your current code work as soon as possible, then thinking about design will seem like drudge work that is getting in the way of your real goal. -所有这些建议的缺点是它们会在项目的早期阶段创建额外的工作。此外,如果您不习惯于思考设计问题,那么当您学习良好的设计技巧时,您甚至会放慢脚步。如果对您而言唯一重要的事情就是尽快使当前代码工作,那么思考设计就好像是在费劲工作,而这实际上妨碍了您实现真正的目标。 +所有这些建议的缺点是它们会在项目的早期阶段需要额外的工作量。此外,如果您不习惯于思考设计问题,那么当您学习良好的设计技巧时,您的速度会比较慢。如果对您而言唯一重要的事情让您当前的代码尽快运行,那么思考设计就好像是件苦差事,而这实际上妨碍了您实现真正的目标。 > On the other hand, if good design is an important goal for you, then the ideas in this book should make programming more fun. Design is a fascinating puzzle: how can a particular problem be solved with the simplest possible structure? It’s fun to explore different approaches, and it’s a great feeling to discover a solution that is both simple and powerful. A clean, simple, and obvious design is a beautiful thing. -另一方面,如果良好的设计对您来说是重要的目标,那么本书中的思想应使编程更有趣。设计是一个令人着迷的难题:如何用最简单的结构解决特定问题?探索不同的方法很有趣,找到一种既简单又强大的解决方案是一种很好的感觉。干净,简单和明显的设计是一件美丽的事情。 +另一方面,如果良好的设计对您来说是重要的目标,那么本书中的思想会让编程变得更有趣。设计是一个令人着迷的难题:如何用最简单的结构解决特定问题?探索不同的方法很有趣,找到一种既简单又强大的解决方案是一种很棒的感觉。干净,简单和明显的设计是一件美丽的事情。 > Furthermore, the investments you make in good design will pay off quickly. The modules you defined carefully at the beginning of a project will save you time later as you reuse them over and over. The clear documentation that you wrote six months ago will save you time when you return to the code to add a new feature. The time you spent honing your design skills will also pay for itself: as your skills and experience grow, you will find that you can produce good designs more and more quickly. Good design doesn’t really take much longer than quick-and-dirty design, once you know how. -此外,您对优质设计的投资将很快获得回报。在项目开始时仔细定义的模块将为您节省时间,因为您一遍又一遍地重复使用它们。您六个月前编写的清晰文档将为您节省返回代码添加新功能的时间。花在磨练设计技能上的时间也将有所回报:随着技能和经验的增长,您会发现可以越来越快地制作出好的设计。一旦知道了什么,一个好的设计实际上并不会比一个简单的设计花费更多的时间。 +此外,您对优质设计的投资将很快获得回报。在项目开始时仔细定义的模块,在您一遍又一遍地重复使用它们时,会节省您的时间。您六个月前编写的清晰文档将为您节省返回代码添加新功能的时间。花在磨练设计技能上的时间也将有所回报:随着技能和经验的增长,您会发现您可以越来越快地做出好的设计。一旦您掌握了方法,好的设计实际上并不会比草率的设计花费更多的时间。 > The reward for being a good designer is that you get to spend a larger fraction of your time in the design phase, which is fun. Poor designers spend most of their time chasing bugs in complicated and brittle code. If you improve your design skills, not only will you produce higher quality software more quickly, but the software development process will be more enjoyable. -成为优秀设计师的好处是,您可以在设计阶段花费大部分时间,这很有趣。可怜的设计师花费大量时间在复杂而脆弱的代码中寻找错误。如果提高设计技能,不仅可以更快地生产出更高质量的软件,而且软件开发过程也将变得更加愉快。 +成为一名优秀设计师的回报是,您可以将大部分时间花在设计阶段,这很有趣。糟糕的设计师花费大量时间在复杂而脆弱的代码中寻找错误。如果您提高了设计技能,不仅可以更快地生产出更高质量的软件,而且软件开发过程也将也会更加愉快。