update
This commit is contained in:
@@ -7,3 +7,5 @@
|
|||||||
|
|
||||||
|
|
||||||
在线阅读地址:https://go7hic.github.io/A-Philosophy-of-Software-Design/#/
|
在线阅读地址:https://go7hic.github.io/A-Philosophy-of-Software-Design/#/
|
||||||
|
|
||||||
|
仅仅是为了方便学习,如果你想看双语版的,可以看 [A-Philosophy-of-Software-Design-zh](https://github.com/gdut-yy/A-Philosophy-of-Software-Design-zh)。本仓库很多内容是借鉴那边的。
|
||||||
|
|||||||
@@ -21,3 +21,33 @@
|
|||||||
#### [第六章:通用模块更深入](./ch6.md)
|
#### [第六章:通用模块更深入](./ch6.md)
|
||||||
|
|
||||||
#### [第七章: 不同的层,不同的抽象](./ch7.md)
|
#### [第七章: 不同的层,不同的抽象](./ch7.md)
|
||||||
|
|
||||||
|
#### [第八章: 降低复杂性](./ch8.md)
|
||||||
|
|
||||||
|
#### [第九章: 在一起更好还是分开更好?](./ch9.md)
|
||||||
|
|
||||||
|
#### [第十章: 定义不存在的错误](./ch10.md)
|
||||||
|
|
||||||
|
#### [第十一章: 设计它两次](./ch11.md)
|
||||||
|
|
||||||
|
#### [第十二章: 为什么写注释?四个理由](./ch12.md)
|
||||||
|
|
||||||
|
#### [第十三章: 注释应该描述代码中不明显的内容](./ch13.md)
|
||||||
|
|
||||||
|
#### [第十四章: 选择的名字](./ch14.md)
|
||||||
|
|
||||||
|
#### [第十五章: 先写注释](./ch15.md)
|
||||||
|
|
||||||
|
#### [第十六章: 修改现有的代码](./ch16.md)
|
||||||
|
|
||||||
|
#### [第十七章: 一致性](./ch17.md)
|
||||||
|
|
||||||
|
#### [第十八章: 代码应该是显而易见的](./ch18.md)
|
||||||
|
|
||||||
|
#### [第十九章: 软件发展趋势](./ch19.md)
|
||||||
|
|
||||||
|
#### [第二十章: 设计性能](./ch20.md)
|
||||||
|
|
||||||
|
#### [第二十一章: 结论](./ch21.md)
|
||||||
|
|
||||||
|
#### [总结](./summary.md)
|
||||||
|
|||||||
@@ -1,11 +1,49 @@
|
|||||||
<!-- docs/_sidebar.md -->
|
<!-- docs/_sidebar.md -->
|
||||||
|
|
||||||
* [首页](/)
|
* [首页](/)
|
||||||
* [前言](./Preface.md)
|
|
||||||
|
* [前言](./preface)
|
||||||
|
|
||||||
* [第一章:介绍](./ch1.md)
|
* [第一章:介绍](./ch1.md)
|
||||||
|
|
||||||
* [第二章:复杂性的本质](./ch2.md)
|
* [第二章:复杂性的本质](./ch2.md)
|
||||||
|
|
||||||
* [第三章:工作代码是不够的(战略与战术编程)](./ch3.md)
|
* [第三章:工作代码是不够的(战略与战术编程)](./ch3.md)
|
||||||
|
|
||||||
* [第四章:模块应该是深的](./ch4.md)
|
* [第四章:模块应该是深的](./ch4.md)
|
||||||
|
|
||||||
* [第五章:信息隐藏(和泄露)](./ch5.md)
|
* [第五章:信息隐藏(和泄露)](./ch5.md)
|
||||||
|
|
||||||
* [第六章:通用模块更深入](./ch6.md)
|
* [第六章:通用模块更深入](./ch6.md)
|
||||||
|
|
||||||
* [第七章: 不同的层,不同的抽象](./ch7.md)
|
* [第七章: 不同的层,不同的抽象](./ch7.md)
|
||||||
|
|
||||||
|
* [第八章: 降低复杂性](./ch8.md)
|
||||||
|
|
||||||
|
* [第九章: 在一起更好还是分开更好?](./ch9.md)
|
||||||
|
|
||||||
|
* [第十章: 定义不存在的错误](./ch10.md)
|
||||||
|
|
||||||
|
* [第十一章: 设计它两次](./ch11.md)
|
||||||
|
|
||||||
|
* [第十二章: 为什么写注释?四个理由](./ch12.md)
|
||||||
|
|
||||||
|
* [第十三章: 注释应该描述代码中不明显的内容](./ch13.md)
|
||||||
|
|
||||||
|
* [第十四章: 选择的名字](./ch14.md)
|
||||||
|
|
||||||
|
* [第十五章: 先写注释](./ch15.md)
|
||||||
|
|
||||||
|
* [第十六章: 修改现有的代码](./ch16.md)
|
||||||
|
|
||||||
|
* [第十七章: 一致性](./ch17.md)
|
||||||
|
|
||||||
|
* [第十八章: 代码应该是显而易见的](./ch18.md)
|
||||||
|
|
||||||
|
* [第十九章: 软件发展趋势](./ch19.md)
|
||||||
|
|
||||||
|
* [第二十章: 设计性能](./ch20.md)
|
||||||
|
|
||||||
|
* [第二十一章: 结论](./ch21.md)
|
||||||
|
|
||||||
|
* [总结](./summary.md)
|
||||||
301
docs/ch10.md
Normal file
301
docs/ch10.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# 第 10 章 定义不存在的错误
|
||||||
|
|
||||||
|
> Chapter 10 Define Errors Out Of Existence
|
||||||
|
|
||||||
|
Exception handling is one of the worst sources of complexity in software systems. Code that deals with special conditions is inherently harder to write than code that deals with normal cases, and developers often define exceptions without considering how they will be handled. This chapter discusses why exceptions contribute disproportionately to complexity, then it shows how to simplify exception handling. The key overall lesson from this chapter is to reduce the number of places where exceptions must be handled; in many cases the semantics of operations can be modified so that the normal behavior handles all situations and there is no exceptional condition to report (hence the title of this chapter).
|
||||||
|
|
||||||
|
> 异常处理是软件系统中最糟糕的复杂性来源之一。处理特殊情况的代码在本质上比处理正常情况的代码更难编写,并且开发人员经常在定义异常时不考虑异常的处理方式。本章讨论了为什么异常对复杂性的贡献不成比例,然后说明了如何简化异常处理。本章总的主要教训是减少必须处理异常的地方的数量。在许多情况下,可以修改操作的语义,以便正常行为可以处理所有情况,并且没有要报告的特殊条件(因此,本章标题)。
|
||||||
|
|
||||||
|
## 10.1 Why exceptions add complexity 为什么异常会增加复杂性
|
||||||
|
|
||||||
|
I use the term exception to refer to any uncommon condition that alters the normal flow of control in a program. Many programming languages include a formal exception mechanism that allows exceptions to be thrown by lower-level code and caught by enclosing code. However, exceptions can occur even without using a formal exception reporting mechanism, such as when a method returns a special value indicating that it didn’t complete its normal behavior. All of these forms of exceptions contribute to complexity.
|
||||||
|
|
||||||
|
> 我使用“异常”一词来指代任何会改变程序中正常控制流程的不常见条件。许多编程语言都包含一种正式的异常机制,该机制允许异常由低级代码引发并由封闭代码捕获。但是,即使不使用正式的异常报告机制,异常也可能发生,例如,当某个方法返回一个特殊值指示其未完成其正常行为时。所有这些形式的异常都会增加复杂性。
|
||||||
|
|
||||||
|
A particular piece of code may encounter exceptions in several different ways:
|
||||||
|
|
||||||
|
> 一段特定的代码可能会以几种不同的方式遇到异常:
|
||||||
|
|
||||||
|
- A caller may provide bad arguments or configuration information.
|
||||||
|
- An invoked method may not be able to complete a requested operation. For example, an I/O operation may fail, or a required resource may not be available.
|
||||||
|
- In a distributed system, network packets may be lost or delayed, servers may not respond in a timely fashion, or peers may communicate in unexpected ways.
|
||||||
|
- The code may detect bugs, internal inconsistencies, or situations it is not prepared to handle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> - 调用方可能会提供错误的参数或配置信息。
|
||||||
|
> - 调用的方法可能无法完成请求的操作。例如,I/O 操作可能失败,或者所需的资源可能不可用。
|
||||||
|
> - 在分布式系统中,网络数据包可能会丢失或延迟,服务器可能无法及时响应,或者对等方可能会以意想不到的方式进行通信。
|
||||||
|
> - 该代码可能会检测到错误,内部不一致或未准备处理的情况。
|
||||||
|
|
||||||
|
Large systems have to deal with many exceptional conditions, particularly if they are distributed or need to be fault-tolerant. Exception handling can account for a significant fraction of all the code in a system.
|
||||||
|
|
||||||
|
> 大型系统必须应对许多特殊情况,特别是在它们是分布式的或需要容错的情况下。异常处理可以占系统中所有代码的很大一部分。
|
||||||
|
|
||||||
|
Exception handling code is inherently more difficult to write than normal-case code. An exception disrupts the normal flow of the code; it usually means that something didn’t work as expected. When an exception occurs, the programmer can deal with it in two ways, each of which can be complicated. The first approach is to move forward and complete the work in progress in spite of the exception. For example, if a network packet is lost, it can be resent; if data is corrupted, perhaps it can be recovered from a redundant copy. The second approach is to abort the operation in progress and report the exception upwards. However, aborting can be complicated because the exception may have occurred at a point where system state is inconsistent (a data structure might have been partially initialized); the exception handling code must restore consistency, such as by unwinding any changes made before the exception occurred.
|
||||||
|
|
||||||
|
> 异常处理代码天生就比正常情况下的代码更难写。异常中断了正常的代码流;它通常意味着某事没有像预期的那样工作。当异常发生时,程序员可以用两种方法处理它,每种方法都很复杂。第一种方法是向前推进并完成正在进行的工作,尽管存在例外。例如,如果一个网络数据包丢失,它可以被重发;如果数据损坏了,也许可以从冗余副本中恢复数据。第二种方法是中止正在进行的操作,向上报告异常。但是,中止可能很复杂,因为异常可能发生在系统状态不一致的地方(数据结构可能已经部分初始化);异常处理代码必须恢复一致性,例如通过撤销发生异常之前所做的任何更改。
|
||||||
|
|
||||||
|
Furthermore, exception handling code creates opportunities for more exceptions. Consider the case of resending a lost network packet. Perhaps the packet wasn’t actually lost, but was simply delayed. In this case, resending the packet will result in duplicate packets arriving at the peer; this introduces a new exceptional condition that the peer must handle. Or, consider the case of recovering lost data from a redundant copy: what if the redundant copy has also been lost? Secondary exceptions occurring during recovery are often more subtle and complex than the primary exceptions. If an exception is handled by aborting the operation in progress, then this must be reported to the caller as another exception. To prevent an unending cascade of exceptions, the developer must eventually find a way to handle exceptions without introducing more exceptions.
|
||||||
|
|
||||||
|
> 此外,异常处理代码为更多异常创造了机会。考虑重新发送丢失的网络数据包的情况。也许该数据包实际上并没有丢失,但是只是被延迟了。在这种情况下,重新发送数据包将导致重复的数据包到达对等方;这引入了对等方必须处理的新的例外条件。或者,考虑从冗余副本恢复丢失的数据的情况:如果冗余副本也丢失了怎么办?在恢复期间发生的次要异常通常比主要异常更加微妙和复杂。如果通过中止正在进行的操作来处理异常,则必须将此异常作为另一个异常报告给调用方。为了防止无休止的异常级联,开发人员最终必须找到一种在不引入更多异常的情况下处理异常的方法。
|
||||||
|
|
||||||
|
Language support for exceptions tends to be verbose and clunky, which makes exception handling code hard to read. For example, consider the following code, which reads a collection of tweets from a file using Java’s support for object serialization and deserialization:
|
||||||
|
|
||||||
|
> 语言对异常的支持往往是冗长而笨拙的,这使得异常处理代码难以阅读。例如,考虑以下代码,该代码使用 Java 对对象序列化和反序列化的支持从文件中读取 tweet 的集合:
|
||||||
|
|
||||||
|
```java
|
||||||
|
try (
|
||||||
|
FileInputStream fileStream = new FileInputStream(fileName);
|
||||||
|
BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);
|
||||||
|
ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
|
||||||
|
) {
|
||||||
|
for (int i = 0; i < tweetsPerFile; i++) {
|
||||||
|
tweets.add((Tweet) objectStream.readObject());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException e) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException e) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
catch (EOFException e) {
|
||||||
|
// Not a problem: not all tweet files have full
|
||||||
|
// set of tweets.
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
catch (ClassCastException e) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Just the basic try-catch boilerplate accounts for more lines of code than the code for normal-case operation, without even considering the code that actually handles the exceptions. It is hard to relate the exception handling code to the normal-case code: for example, it’s not obvious where each exception is generated. An alternative approach is to break up the code into many distinct try blocks; in the extreme case there could be a try for each line of code that can generate an exception. This would make it clear where exceptions occur, but the try blocks themselves break up the flow of the code and make it harder to read; in addition, some exception handling code might end up duplicated in multiple try blocks.
|
||||||
|
|
||||||
|
> 只是基本的 try-catch 样板代码比正常情况下的操作代码所占的代码行更多,甚至没有考虑实际处理异常的代码。很难将异常处理代码与普通情况代码相关联:例如,每个异常的生成位置都不明显。另一种方法是将代码分解为许多不同的 try 块。在极端情况下,可能会尝试尝试每行可能产生异常的代码。这样可以清楚地说明异常发生的位置,但是 try 块本身会破坏代码流,并使代码难以阅读。此外,某些异常处理代码可能最终会在多个 try 块中重复。
|
||||||
|
|
||||||
|
It’s difficult to ensure that exception handling code really works. Some exceptions, such as I/O errors, can’t easily be generated in a test environment, so it’s hard to test the code that handles them. Exceptions don’t occur very often in running systems, so exception handling code rarely executes. Bugs can go undetected for a long time, and when the exception handling code is finally needed, there’s a good chance that it won’t work (one of my favorite sayings: “code that hasn’t been executed doesn’t work”). A recent study found that more than 90% of catastrophic failures in distributed data-intensive systems were caused by incorrect error handling1. When exception handling code fails, it’s difficult to debug the problem, since it occurs so infrequently.
|
||||||
|
|
||||||
|
> 确保异常处理代码真正起作用是困难的。某些异常(例如 I/O 错误)在测试环境中不易生成,因此很难测试处理它们的代码。异常在运行的系统中很少发生,因此异常处理代码很少执行。错误可能会长时间未被发现,并且当最终需要异常处理代码时,它很有可能无法正常工作(我最喜欢的一句话是:“未执行的代码无效”) 。最近的一项研究发现,分布式数据密集型系统中超过 90%的灾难性故障是由错误的错误处理引起的 1。当异常处理代码失败时,很难调试该问题,因为它很少发生。
|
||||||
|
|
||||||
|
## 10.2 Too many exceptions 异常过多
|
||||||
|
|
||||||
|
Programmers exacerbate the problems related to exception handling by defining unnecessary exceptions. Most programmers are taught that it’s important to detect and report errors; they often interpret this to mean “the more errors detected, the better.” This leads to an over-defensive style where anything that looks even a bit suspicious is rejected with an exception, which results in a proliferation of unnecessary exceptions that increase the complexity of the system.
|
||||||
|
|
||||||
|
> 程序员通过定义不必要的异常加剧了与异常处理有关的问题。告诉大多数程序员,检测和报告错误很重要。他们通常将其解释为“检测到的错误越多越好”。这导致了一种过分防御的风格,其中任何看起来甚至有点可疑的东西都被拒绝,并带有异常,这导致了不必要的异常的泛滥,从而增加了系统的复杂性。
|
||||||
|
|
||||||
|
I made this mistake myself in the design of the Tcl scripting language. Tcl contains an unset command that can be used to remove a variable. I defined unset so that it throws an error if the variable doesn’t exist. At the time I thought that it must be a bug if someone tries to delete a variable that doesn’t exist, so Tcl should report it. However, one of the most common uses of unset is to clean up temporary state created by some previous operation. It’s often hard to predict exactly what state was created, particularly if the operation aborted partway through. Thus, the simplest thing is to delete all of the variables that might possibly have been created. The definition of unset makes this awkward: developers end up enclosing calls to unset in catch statements to catch and ignore errors thrown by unset. In retrospect, the definition of the unset command is one of the biggest mistakes I made in the design of Tcl.
|
||||||
|
|
||||||
|
> 在设计 Tcl 脚本语言时,我自己就犯了这个错误。Tcl 包含一个未设置的命令,可用于删除变量。我定义了 unset 以便在变量不存在时抛出错误。当时我认为,如果有人试图删除一个不存在的变量,那么它一定是一个 bug,所以 Tcl 应该报告它。然而,unset 最常见的用途之一是清理以前操作创建的临时状态。通常很难准确地预测创建了什么状态,特别是在操作中途中止的情况下。因此,最简单的方法是删除可能已经创建的所有变量。unset 的定义使得这种情况很尴尬:开发人员最终会在 catch 语句中封装对 unset 的调用,以捕获并忽略 unset 抛出的错误。回顾过去,unset 命令的定义是我在 Tcl 设计中犯下的最大错误之一。
|
||||||
|
|
||||||
|
It’s tempting to use exceptions to avoid dealing with difficult situations: rather than figuring out a clean way to handle it, just throw an exception and punt the problem to the caller. Some might argue that this approach empowers callers, since it allows each caller to handle the exception in a different way. However, if you are having trouble figuring out what to do for the particular situation, there’s a good chance that the caller won’t know what to do either. Generating an exception in a situation like this just passes the problem to someone else and adds to the system’s complexity.
|
||||||
|
|
||||||
|
> 试图使用异常来避免处理困难的情况很诱人:与其想出一种干净的方法来处理它,不如抛出一个异常并将问题平移给调用者。有人可能会争辩说,这种方法可以赋予调用者权力,因为它允许每个调用者以不同的方式处理异常。但是,如果您在确定特定情况下该怎么做时遇到困难,则呼叫者很可能都不知道该怎么办。在这种情况下生成异常只会将问题传递给其他人,并增加系统的复杂性。
|
||||||
|
|
||||||
|
The exceptions thrown by a class are part of its interface; classes with lots of exceptions have complex interfaces, and they are shallower than classes with fewer exceptions. An exception is a particularly complex element of an interface. It can propagate up through several stack levels before being caught, so it affects not just the method’s caller, but potentially also higher-level callers (and their interfaces).
|
||||||
|
|
||||||
|
> 类抛出的异常是其接口的一部分;具有大量异常的类具有复杂的接口,并且比具有较少异常的类浅。异常是接口中特别复杂的元素。它可以在被捕获之前通过多个堆栈级别向上传播,因此它不仅影响方法的调用者,而且还可能影响更高级别的调用者(及其接口)。
|
||||||
|
|
||||||
|
Throwing exceptions is easy; handling them is hard. Thus, the complexity of exceptions comes from the exception handling code. The best way to reduce the complexity damage caused by exception handling is to reduce the number of places where exceptions have to be handled. The rest of this chapter will discuss four techniques for reducing the number of exception handlers.
|
||||||
|
|
||||||
|
> 抛出异常很容易;处理它们很困难。因此,异常的复杂性来自异常处理代码。减少由异常处理引起的复杂性破坏的最佳方法是减少必须处理异常的位置的数量。本章的其余部分将讨论减少异常处理程序数量的四种技术。
|
||||||
|
|
||||||
|
## 10.3 Define errors out of existence 定义错误不存在
|
||||||
|
|
||||||
|
The best way to eliminate exception handling complexity is to define your APIs so that there are no exceptions to handle: define errors out of existence. This may seem sacrilegious, but it is very effective in practice. Consider the Tcl unset command discussed above. Rather than throwing an error when unset is asked to delete an unknown variable, it should have simply returned without doing anything. I should have changed the definition of unset slightly: rather than deleting a variable, unset should ensure that a variable no longer exists. With the first definition, unset can’t do its job if the variable doesn’t exist, so generating an exception makes sense. With the second definition, it is perfectly natural for unset to be invoked with the name of a variable that doesn’t exist. In this case, its work is already done, so it can simply return. There is no longer an error case to report.
|
||||||
|
|
||||||
|
> 消除异常处理复杂性的最好方法是定义您的 API,以便没有异常要处理:定义错误而已。这似乎是牺牲品,但在实践中非常有效。考虑上面讨论的 Tcl unset 命令。而不是在要求 unset 删除未知变量时引发错误,它应该只是返回而无需执行任何操作。我应该稍微修改一下 unset 的定义:与其删除一个变量,不应该删除 unset 来确保一个变量不再存在。根据第一个定义,如果变量不存在,则 unset 不能执行其工作,因此生成异常是有意义的。使用第二个定义,使用不存在的变量名调用 unset 是很自然的。在这种情况下,它的工作已经完成,因此可以简单地返回。
|
||||||
|
|
||||||
|
## 10.4 Example: file deletion in Windows 示例:Windows 中的文件删除
|
||||||
|
|
||||||
|
File deletion provides another example of how errors can be defined away. The Windows operating system does not permit a file to be deleted if it is open in a process. This is a continual source of frustration for developers and users. In order to delete a file that is in use, the user must search through the system to find the process that has the file open, and then kill that process. Sometimes users give up and reboot their system, just so they can delete a file.
|
||||||
|
|
||||||
|
> 文件删除提供了如何定义错误的另一个示例。Windows 操作系统不允许删除文件(如果已在进程中打开文件)。对于开发人员和用户来说,这是不断沮丧的根源。为了删除正在使用的文件,用户必须在系统中搜索以找到已打开文件的进程,然后终止该进程。有时用户放弃并重新启动系统,只是为了删除文件。
|
||||||
|
|
||||||
|
The Unix operating system defines file deletion more elegantly. In Unix, if a file is open when it is deleted, Unix does not delete the file immediately. Instead, it marks the file for deletion, then the delete operation returns successfully. The file name has been removed from its directory, so no other processes can open the old file and a new file with the same name can be created, but the existing file data persists. Processes that already have the file open can continue to read it and write it normally. Once the file has been closed by all of the accessing processes, its data is freed.
|
||||||
|
|
||||||
|
> Unix 操作系统更优雅地定义了文件删除。在 Unix 中,如果在删除文件时打开了文件,则 Unix 不会立即删除该文件。而是将文件标记为删除,然后删除操作成功返回。该文件名已从其目录中删除,因此其他进程无法打开该旧文件,并且可以创建具有相同名称的新文件,但现有文件数据将保留。已经打开文件的进程可以继续读取和正常写入文件。一旦所有访问进程都关闭了文件,便释放其数据。
|
||||||
|
|
||||||
|
The Unix approach defines away two different kinds of errors. First, the delete operation no longer returns an error if the file is currently in use; the delete succeeds, and the file will eventually be deleted. Second, deleting a file that’s in use does not create exceptions for the processes using the file. One possible approach to this problem would have been to delete the file immediately and mark all of the opens of the file to disable them; any attempts by other processes to read or write the deleted file would fail. However, this approach would create new errors for those processes to handle. Instead, Unix allows them to keep accessing the file normally; delaying the file deletion defines errors out of existence.
|
||||||
|
|
||||||
|
> Unix 方法定义了两种不同的错误。首先,如果文件当前正在使用中,则删除操作不再返回错误;删除成功,该文件最终将被删除。其次,删除正在使用的文件不会为使用该文件的进程创建例外。解决此问题的一种可能方法是立即删除文件并标记文件的所有打开以禁用它们。其他进程读取或写入已删除文件的任何尝试均将失败。但是,此方法将为那些要处理的过程创建新的错误。相反,Unix 允许他们继续正常访问文件。延迟文件删除将定义错误不存在。
|
||||||
|
|
||||||
|
It may seem strange that Unix allows a process to continue to read and write a doomed file, but I have never encountered a situation where this caused significant problems. The Unix definition of file deletion is much simpler to work with, both for developers and users, than the Windows definition.
|
||||||
|
|
||||||
|
> Unix 允许进程继续读取和写入已损坏的文件可能看起来很奇怪,但是我从未遇到过引起严重问题的情况。对于开发人员和用户,Unix 删除文件的定义比 Windows 定义要容易得多。
|
||||||
|
|
||||||
|
## 10.5 Example: Java substring method 示例:Java 子字符串方法
|
||||||
|
|
||||||
|
As a final example, consider the Java String class and its substring method. Given two indexes into a string, substring returns the substring starting at the character given by the first index and ending with the character just before the second index. However, if either index is outside the range of the string, then substring throws IndexOutOfBoundsException. This exception is unnecessary and complicates the use of this method. I often find myself in a situation where one or both of the indices may be outside the range of the string, and I would like to extract all of the characters in the string that overlap the specified range. Unfortunately, this requires me to check each of the indices and round them up to zero or down to the end of the string; a one-line method call now becomes 5–10 lines of code.
|
||||||
|
|
||||||
|
> 作为最后一个示例,请考虑 Java String 类及其子字符串方法。给定一个字符串中的两个索引,substring 返回该子字符串,该字符串从第一个索引给定的字符开始,以第二个索引之前的字符结束。但是,如果两个索引中的任何一个都超出字符串的范围,则子字符串将引发 IndexOutOfBoundsException。此异常是不必要的,并且会使此方法的使用复杂化。我经常发现自己处于一个或两个索引可能不在字符串范围内的情况,并且我想提取字符串中与指定范围重叠的所有字符。不幸的是,这要求我检查每个索引并将它们向上舍入为零或向下舍入到字符串的末尾。现在,单行方法调用变成 5-10 行代码。
|
||||||
|
|
||||||
|
The Java substring method would be easier to use if it performed this adjustment automatically, so that it implemented the following API: “returns the characters of the string (if any) with index greater than or equal to beginIndex and less than endIndex.” This is a simple and natural API, and it defines the IndexOutOfBoundsException exception out of existence. The method’s behavior is now well-defined even if one or both of the indexes are negative, or if beginIndex is greater than endIndex. This approach simplifies the API for the method while increasing its functionality, so it makes the method deeper. Many other languages have taken the error-free approach; for example, Python returns an empty result for out-of-range list slices.
|
||||||
|
|
||||||
|
> 如果 Java 子字符串方法自动执行此调整,则将更易于使用,因此它实现了以下 API:“返回索引大于或等于 beginIndex 且小于 endIndex 的字符串的字符(如果有)。” 这是一个简单自然的 API,它定义了 IndexOutOfBoundsException 异常。现在,即使一个或两个索引均为负,或者 beginIndex 大于 endIndex,该方法的行为也已明确定义。这种方法简化了方法的 API,同时增加了其功能,因此使方法更深。许多其他语言都采用了无错误的方法。例如,Python 对于超出范围的列表切片返回空结果。
|
||||||
|
|
||||||
|
When I argue for defining errors out of existence, people sometimes counter that throwing errors will catch bugs; if errors are defined out of existence, won’t that result in buggier software? Perhaps this is why the Java developers decided that substring should throw exceptions. The error-ful approach may catch some bugs, but it also increases complexity, which results in other bugs. In the error-ful approach, developers must write additional code to avoid or ignore the errors, and this increases the likelihood of bugs; or, they may forget to write the additional code, in which case unexpected errors may be thrown at runtime. In contrast, defining errors out of existence simplifies APIs and it reduces the amount of code that must be written.
|
||||||
|
|
||||||
|
> 当我主张定义错误而不再存在时,人们有时会反驳说抛出错误会捕获错误。如果错误定义不存在,那会不会导致 Buggier 软件出现?也许这就是 Java 开发人员决定子字符串应引发异常的原因。错误的方法可能会捕获一些错误,但也会增加复杂性,从而导致其他错误。在错误有效的方法中,开发人员必须编写额外的代码来避免或忽略错误,这增加了发生错误的可能性。或者,他们可能会忘记编写其他代码,在这种情况下,运行时可能会引发意外错误。相反,定义错误而不存在将简化 API,并减少必须编写的代码量。
|
||||||
|
|
||||||
|
Overall, the best way to reduce bugs is to make software simpler.
|
||||||
|
|
||||||
|
> 总体而言,减少错误的最好方法是简化软件。
|
||||||
|
|
||||||
|
## 10.6 Mask exceptions 掩码异常
|
||||||
|
|
||||||
|
The second technique for reducing the number of places where exceptions must be handled is exception masking. With this approach, an exceptional condition is detected and handled at a low level in the system, so that higher levels of software need not be aware of the condition. Exception masking is particularly common in distributed systems. For instance, in a network transport protocol such as TCP, packets can be dropped for various reasons such as corruption and congestion. TCP masks packet loss by resending lost packets within its implementation, so all data eventually gets through and clients are unaware of the dropped packets.
|
||||||
|
|
||||||
|
> 减少必须处理异常的地方数量的第二种技术是异常屏蔽。使用这种方法,可以在系统的较低级别上检测和处理异常情况,因此,更高级别的软件无需知道该情况。异常屏蔽在分布式系统中尤其常见。例如,在诸如 TCP 的网络传输协议中,由于各种原因(例如损坏和拥塞),可能会丢弃数据包。TCP 通过在其实现中重新发送丢失的数据包来掩盖数据包的丢失,因此所有数据最终都将通过,并且客户端不知道丢失的数据包。
|
||||||
|
|
||||||
|
A more controversial example of masking occurs in the NFS network file system. If an NFS file server crashes or fails to respond for any reason, clients reissue their requests to the server over and over again until the problem is eventually resolved. The low-level file system code on the client does not report any exceptions to the invoking application. The operation in progress (and hence the application) just hangs until the operation can complete successfully. If the hang lasts more than a short time, the NFS client prints messages on the user’s console of the form “NFS server xyzzy not responding still trying.”
|
||||||
|
|
||||||
|
> NFS 网络文件系统中出现了一个更具争议性的屏蔽示例。如果 NFS 文件服务器由于任何原因崩溃或无法响应,客户端将一遍又一遍地向服务器发出请求,直到问题最终得到解决。客户端上的低级文件系统代码不会向调用应用程序报告任何异常。正在进行的操作(以及因此的应用程序)只是挂起,直到操作可以成功完成。如果挂起持续的时间不超过一小段时间,则 NFS 客户端将在用户控制台上以“ NFS 服务器 xyzzy 无法响应仍在尝试响应”的形式打印消息。
|
||||||
|
|
||||||
|
NFS users often complain about the fact that their applications hang while waiting for an NFS server to resume normal operation. Many people have suggested that NFS should abort operations with an exception rather than hanging. However, reporting exceptions would make things worse, not better. There’s not much an application can do if it loses access to its files. One possibility would be for the application to retry the file operation, but this would still hang the application, and it’s easier to perform the retry in one place in the NFS layer, rather than at every file system call in every application (a compiler shouldn’t have to worry about this!). The other alternative is for applications to abort and return errors to their callers. It’s unlikely that the callers would know what to do either, so they would abort as well, resulting in a collapse of the user’s working environment. Users still wouldn’t be able to get any work done while the file server was down, and they would have to restart all of their applications once the file server came back to life.
|
||||||
|
|
||||||
|
> NFS 用户经常抱怨这样的事实,即他们的应用程序在等待 NFS 服务器恢复正常运行时挂起。许多人建议 NFS 应该异常终止操作而不是挂起。但是,报告异常会使情况更糟,而不是更好。如果应用程序无法访问其文件,则无能为力。一种可能性是应用程序重试文件操作,但这仍然会使应用程序挂起,并且在 NFS 层中的一个位置执行重试会比在每个应用程序中的每个文件系统调用处执行重试更容易(编译器应不必为此担心!)。另一种选择是让应用程序中止并将错误返回给调用者。呼叫者不太可能知道该怎么做,因此他们也将中止,导致用户工作环境崩溃。用户在文件服务器关闭时仍然无法完成任何工作,并且一旦文件服务器恢复工作,他们将不得不重新启动所有应用程序。
|
||||||
|
|
||||||
|
Thus, the best alternative is for NFS to mask the errors and hang applications. With this approach, applications don’t need any code to deal with server problems, and they can resume seamlessly once the server comes back to life. If users get tired of waiting, they can always abort applications manually.
|
||||||
|
|
||||||
|
> 因此,最好的替代方法是让 NFS 掩盖错误并挂起应用程序。通过这种方法,应用程序不需要任何代码来处理服务器问题,并且一旦服务器恢复运行,它们就可以无缝恢复。如果用户厌倦了等待,他们总是可以手动中止应用程序。
|
||||||
|
|
||||||
|
Exception masking doesn’t work in all situations, but it is a powerful tool in the situations where it works. It results in deeper classes, since it reduces the class’s interface (fewer exceptions for users to be aware of) and adds functionality in the form of the code that masks the exception. Exception masking is an example of pulling complexity downward.
|
||||||
|
|
||||||
|
> 异常屏蔽并非在所有情况下都有效,但是在它起作用的情况下它是一个强大的工具。它导致了更深的类,因为它减少了类的界面(用户需要注意的异常更少)并以掩盖异常的代码形式添加了功能。异常屏蔽是降低复杂性的一个例子。
|
||||||
|
|
||||||
|
## 10.7 Exception aggregation 异常聚集
|
||||||
|
|
||||||
|
The third technique for reducing complexity related to exceptions is exception aggregation. The idea behind exception aggregation is to handle many exceptions with a single piece of code; rather than writing distinct handlers for many individual exceptions, handle them all in one place with a single handler.
|
||||||
|
|
||||||
|
> 减少与异常相关的复杂性的第三种技术是异常聚合。异常聚合的思想是用一个代码段处理许多异常。与其为多个单独的异常编写不同的处理程序,不如用一个处理程序将它们全部处理在一个地方。
|
||||||
|
|
||||||
|
Consider how to handle missing parameters in a Web server. A Web server implements a collection of URLs. When the server receives an incoming URL, it dispatches to a URL-specific service method to process that URL and generate a response. The URL contains various parameters that are used to generate the response. Each service method will call a lower-level method (let’s call it getParameter) to extract the parameters that it needs from the URL. If the URL does not contain the desired parameter, getParameter throws an exception.
|
||||||
|
|
||||||
|
> 考虑如何处理 Web 服务器中缺少的参数。Web 服务器实现 URL 的集合。服务器收到传入的 URL 时,将分派到特定于 URL 的服务方法来处理该 URL 并生成响应。该 URL 包含用于生成响应的各种参数。每个服务方法都将调用一个较低层的方法(将其称为 getParameter)以从 URL 中提取所需的参数。如果 URL 不包含所需的参数,则 getParameter 会引发异常。
|
||||||
|
|
||||||
|
When students in a software design class implemented such a server, many of them wrapped each distinct call to getParameter in a separate exception handler to catch NoSuchParameter exceptions, as in Figure 10.1. This resulted in a large number of handlers, all of which did essentially the same thing (generate an error response).
|
||||||
|
|
||||||
|
> 当参加软件设计课程的学生实现这样的服务器时,他们中的许多人将对 getParameter 的每个不同调用包装在单独的异常处理程序中以捕获 NoSuchParameter 异常,如图 10.1 所示。这导致大量的处理程序,所有这些处理程序基本上都执行相同的操作(生成错误响应)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Figure 10.1: The code at the top dispatches to one of several methods in a Web server, each of which handles a particular URL. Each of those methods (bottom) uses parameters from the incoming HTTP request. In this figure, there is a separate exception handler for each call to getParameter; this results in duplicated code.
|
||||||
|
|
||||||
|
> 图 10.1:顶部的代码将分派给 Web 服务器中的几种方法之一,每种方法都处理一个特定的 URL。每个方法(底部)都使用传入 HTTP 请求中的参数。在此图中,每个对 getParameter 的调用都有一个单独的异常处理程序。这导致重复的代码。
|
||||||
|
|
||||||
|
A better approach is to aggregate the exceptions. Instead of catching the exceptions in the individual service methods, let them propagate up to the top-level dispatch method for the Web server, as in Figure 10.2. A single handler in this method can catch all of the exceptions and generate an appropriate error response for missing parameters.
|
||||||
|
|
||||||
|
> 更好的方法是汇总异常。让它们传播到 Web 服务器的顶级调度方法,而不是在单个服务方法中捕获异常,如图 10.2 所示。此方法中的单个处理程序可以捕获所有异常,并为丢失的参数生成适当的错误响应。
|
||||||
|
|
||||||
|
The aggregation approach can be taken even further in the Web example. There are many other errors besides missing parameters that can occur while processing a Web page; for example, a parameter might not have the right syntax (the service method expected an integer, but the value was “xyz”), or the user might not have permission for the requested operation. In each case, the error should result in an error response; the errors differ only in the error message to include in the response (“parameter 'quantity' not present in URL” or “bad value 'xyz' for 'quantity' parameter; must be positive integer”). Thus, all conditions resulting in an error response can be handled with a single top-level exception handler. The error message can be generated at the time the exception is thrown and included as a variable in the exception record; for example, getParameter will generate the “parameter 'quantity' not present in URL” message. The top-level handler extracts the message from the exception and incorporates it into the error response.
|
||||||
|
|
||||||
|
> 在 Web 示例中甚至可以采用聚合方法。处理网页时,除了缺少参数外,还有许多其他错误;例如,参数可能没有正确的语法(服务方法应为整数,但值为“ xyz”),或者用户可能无权执行所请求的操作。在每种情况下,错误都应导致错误响应。错误仅在响应中包含的错误消息中有所不同(“ URL 中不存在参数'数量'”或“'数量'参数的错误值'xyz';必须为正整数”)。因此,所有导致错误响应的条件都可以使用单个顶级异常处理程序进行处理。错误消息可以在引发异常时生成,并作为变量包含在异常记录中。例如,getParameter 将生成“ URL 中不存在的参数'数量'”消息。顶级处理程序从异常中提取消息,并将其合并到错误响应中。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Figure 10.2: This code is functionally equivalent to Figure 10.1, but exception handling has been aggregated: a single exception handler in the dispatcher catches all of the NoSuchParameter exceptions from all of the URL-specific methods.
|
||||||
|
|
||||||
|
> 图 10.2:此代码在功能上等效于图 10.1,但是异常处理已聚合:分派器中的单个异常处理程序从所有特定于 URL 的方法中捕获所有 NoSuchParameter 异常。
|
||||||
|
|
||||||
|
The aggregation described in the preceding paragraph has good properties from the standpoint of encapsulation and information hiding. The top-level exception handler encapsulates knowledge about how to generate error responses, but it knows nothing about specific errors; it just uses the error message provided in the exception. The getParameter method encapsulates knowledge about how to extract a parameter from a URL, and it also knows how to describe extraction errors in a human-readable form. These two pieces of information are closely related, so it makes sense for them to be in the same place. However, getParameter knows nothing about the syntax of an HTTP error response. As new functionality is added to the Web server, new methods like getParameter may be created with their own errors. If the new methods throw exceptions in the same way as getParameter (by generating exceptions that inherit from the same superclass and including an error message in each exception), they can plug into the existing system with no other changes: the top-level handler will automatically generate error responses for them.
|
||||||
|
|
||||||
|
> 从封装和信息隐藏的角度来看,上一段中描述的聚合具有良好的属性。顶级异常处理程序封装了有关如何生成错误响应的知识,但对特定错误一无所知。它仅使用异常中提供的错误消息。getParameter 方法封装了有关如何从 URL 提取参数的知识,并且还知道如何以人类可读的形式描述提取错误。这两个信息密切相关,因此将它们放在同一位置是很有意义的。但是,getParameter 对 HTTP 错误响应的语法一无所知。随着向 Web 服务器中添加了新功能,可能会创建具有类似自身错误的新方法,如 getParameter。
|
||||||
|
|
||||||
|
This example illustrates a generally-useful design pattern for exception handling. If a system processes a series of requests, it’s useful to define an exception that aborts the current request, cleans up the system’s state, and continues with the next request. The exception is caught in a single place near the top of the system’s request-handling loop. This exception can be thrown at any point in the processing of a request to abort the request; different subclasses of the exception can be defined for different conditions. Exceptions of this type should be clearly distinguished from exceptions that are fatal to the entire system.
|
||||||
|
|
||||||
|
> 此示例说明了用于异常处理的通用设计模式。如果系统处理一系列请求,则定义一个异常以中止当前请求,清除系统状态并继续下一个请求非常有用。异常被捕获在系统请求处理循环顶部附近的单个位置。在处理中止请求的任何时候都可以抛出该异常。可以为不同的条件定义异常的不同子类。应该将这种类型的异常与对整个系统致命的异常区分开来。
|
||||||
|
|
||||||
|
Exception aggregation works best if an exception propagates several levels up the stack before it is handled; this allows more exceptions from more methods to be handled in the same place. This is the opposite of exception masking: masking usually works best if an exception is handled in a low-level method. For masking, the low-level method is typically a library method used by many other methods, so allowing the exception to propagate would increase the number of places where it is handled. Masking and aggregation are similar in that both approaches position an exception handler where it can catch the most exceptions, eliminating many handlers that would otherwise need to be created.
|
||||||
|
|
||||||
|
> 如果异常在处理之前在堆栈中传播了多个级别,则异常聚集最有效。这样可以在同一位置处理更多方法的更多异常。这与异常屏蔽相反:如果使用低级方法处理异常,则屏蔽通常效果最好。对于屏蔽,低级方法通常是许多其他方法使用的库方法,因此,允许传播异常会增加处理该异常的位置数。掩码和聚合的相似之处在于,这两种方法都将异常处理程序置于可以捕获最多异常的位置,从而消除了许多本来需要创建的处理程序。
|
||||||
|
|
||||||
|
Another example of exception aggregation occurs in the RAMCloud storage system for crash recovery. A RAMCloud system consists of a collection of storage servers that keep multiple copies of each object, so the system can recover from a variety of failures. For example, if a server crashes and loses all of its data, RAMCloud reconstructs the lost data using copies stored on other servers. Errors can also happen on a smaller scale; for example, a server may discover that an individual object is corrupted.
|
||||||
|
|
||||||
|
> RAMCloud 存储系统中发生异常聚集的另一个示例是崩溃恢复。RAMCloud 系统由一组存储服务器组成,这些存储服务器保留每个对象的多个副本,因此系统可以从各种故障中恢复。例如,如果服务器崩溃并丢失其所有数据,RAMCloud 会使用存储在其他服务器上的副本来重建丢失的数据。错误也可能在较小的范围内发生。例如,服务器可能发现单个对象已损坏。
|
||||||
|
|
||||||
|
RAMCloud does not have separate recovery mechanisms for each different kind of error. Instead, RAMCloud “promotes” many smaller errors into larger ones. RAMCloud could, in principle, handle a corrupted object by restoring that one object from a backup copy. However, it doesn’t do this. Instead, if it discovers a corrupted object it crashes the server containing the object. RAMCloud uses this approach because crash recovery is quite complex and this approach minimized the number of different recovery mechanisms that had to be created. Creating a recovery mechanism for crashed servers was unavoidable, so RAMCloud uses the same mechanism for other kinds of recovery as well. This reduced the amount of code that had to be written, and it also meant that server crash recovery gets invoked more often. As a result, bugs in recovery are more likely to be discovered and fixed.
|
||||||
|
|
||||||
|
> 对于每种不同类型的错误,RAMCloud 没有单独的恢复机制。相反,RAMCloud 将许多较小的错误“提升”为较大的错误。原则上,RAMCloud 可以通过从备份副本中恢复一个损坏的对象来处理这个损坏的对象。然而,它并不这样做。相反,如果它发现一个损坏的对象,它会使包含该对象的服务器崩溃。RAMCloud 使用这种方法是因为崩溃恢复非常复杂,而且这种方法最小化了必须创建的不同恢复机制的数量。为崩溃的服务器创建恢复机制是不可避免的,因此 RAMCloud 对其他类型的恢复也使用相同的机制。这减少了必须编写的代码量,而且这还意味着服务器崩溃恢复将更频繁地被调用。因此,恢复中的 bug 更有可能被发现和修复。
|
||||||
|
|
||||||
|
One disadvantage of promoting a corrupted object into a server crash is that it increases the cost of recovery considerably. This is not a problem in RAMCloud, since object corruption is quite rare. However, error promotion may not make sense for errors that happen frequently. As one example, it would not be practical to crash a server anytime one of its network packets is lost.
|
||||||
|
|
||||||
|
> 将损坏的对象升级为服务器崩溃的一个缺点是,它大大增加了恢复成本。这在 RAMCloud 中不是问题,因为对象损坏非常罕见。但是,错误升级对于经常发生的错误可能没有意义。举一个例子,在服务器的任何网络数据包丢失时使服务器崩溃是不切实际的。
|
||||||
|
|
||||||
|
One way of thinking about exception aggregation is that it replaces several special-purpose mechanisms, each tailored for a particular situation, with a single general-purpose mechanism that can handle multiple situations. This provides another illustration of the benefits of general-purpose mechanisms.
|
||||||
|
|
||||||
|
> 考虑异常聚合的一种方法是,它用可以处理多种情况的单个通用机制替换了几种针对特定情况而量身定制的特殊用途的机制。这再次说明了通用机制的好处。
|
||||||
|
|
||||||
|
## 10.8 Just crash? 崩溃了吗?
|
||||||
|
|
||||||
|
The fourth technique for reducing complexity related to exception handling is to crash the application. In most applications there will be certain errors that it’s not worth trying to handle. Typically, these errors are difficult or impossible to handle and don’t occur very often. The simplest thing to do in response to these errors is to print diagnostic information and then abort the application.
|
||||||
|
|
||||||
|
> 减少与异常处理相关的复杂性的第四种技术是使应用程序崩溃。在大多数应用程序中,有些错误是不值得尝试的。通常,这些错误很难或不可能处理,而且很少发生。针对这些错误的最简单的操作是打印诊断信息,然后中止应用程序。
|
||||||
|
|
||||||
|
One example is “out of memory” errors that occur during storage allocation. Consider the malloc function in C, which returns NULL if it cannot allocate the desired block of memory. This is an unfortunate behavior, because it assumes that every single caller of malloc will check the return value and take appropriate action if there is no memory. Applications contain numerous calls to malloc, so checking the result after each call would add significant complexity. If a programmer forgets the check (which is fairly likely), then the application will dereference a null pointer if memory runs out, resulting in a crash that camouflages the real problem.
|
||||||
|
|
||||||
|
> 一个示例是在存储分配期间发生的“内存不足”错误。考虑一下 C 语言中的 malloc 函数,如果它无法分配所需的内存块,则该函数将返回 NULL。这是一个不幸的行为,因为它假定 malloc 的每个调用者都将检查返回值并在没有内存的情况下采取适当的措施。应用程序包含许多对 malloc 的调用,因此在每次调用后检查结果将增加相当大的复杂性。如果程序员忘记了检查(这很有可能),那么如果内存用完,应用程序将取消引用空指针,从而导致崩溃,从而掩盖了实际问题。
|
||||||
|
|
||||||
|
Furthermore, there isn’t much an application can do when it discovers that memory is exhausted. In principle the application could look for unneeded memory to free, but if the application had unneeded memory it could already have freed it, which would have prevented the out-of-memory error in the first place. Today’s systems have so much memory that memory almost never runs out; if it does, it usually indicates a bug in the application. Thus, it rarely make sense to try to handle out-of-memory errors; this creates too much complexity for too little benefit.
|
||||||
|
|
||||||
|
> 此外,当应用程序发现内存已用完时,它无能为力。原则上,应用程序可以寻找不需要的内存以释放它,但是如果应用程序有不需要的内存,它可以已经释放它,这首先可以防止内存不足错误。当今的系统具有如此大的内存,以至于内存几乎永远不会耗尽。如果是这样,通常表明应用程序中存在错误。因此,尝试处理内存不足错误几乎没有道理。这会带来太多的复杂性,而带来的收益却太少。
|
||||||
|
|
||||||
|
A better approach is to define a new method ckalloc, which calls malloc, checks the result, and aborts the application with an error message if memory is exhausted. The application never invokes malloc directly; it always invokes ckalloc.
|
||||||
|
|
||||||
|
> 更好的方法是定义一个新的 ckalloc 方法,该方法调用 malloc,检查结果,并在内存耗尽时通过错误消息中止应用程序。该应用程序从不直接调用 malloc。它总是调用 ckalloc。
|
||||||
|
|
||||||
|
In newer languages such as C++ and Java, the new operator throws an exception if memory is exhausted. There’s not much point in catching this exception, since there’s a good chance that the exception handler will also try to allocate memory, which will also fail. Dynamically allocated memory is such a fundamental element of any modern application that it doesn’t make sense for the application to continue if memory is exhausted; it’s better to crash as soon as the error is detected.
|
||||||
|
|
||||||
|
> 在较新的语言(例如 C ++和 Java)中,如果内存耗尽,则 new 运算符将引发异常。捕获此异常没有什么意义,因为异常处理程序很有可能还会尝试分配内存,这也会失败。动态分配的内存是任何现代应用程序中的基本元素,如果内存耗尽,则继续应用程序是没有意义的。最好在检测到错误后立即崩溃。
|
||||||
|
|
||||||
|
There are many other examples of errors where crashing the application makes sense. For most programs, if an I/O error occurs while reading or writing an open file (such as a disk hard error), or if a network socket cannot be opened, there’s not much the application can do to recover, so aborting with a clear error message is a sensible approach. These errors are infrequent, so they are unlikely to affect the overall usability of the application. Aborting with an error message is also appropriate if an application encounters an internal error such as an inconsistent data structure. Conditions like this probably indicate bugs in the program.
|
||||||
|
|
||||||
|
> 还有许多其他错误示例,这些错误会使应用程序崩溃很有意义。对于大多数程序,如果在读取或写入打开的文件时发生 I/O 错误(例如磁盘硬错误),或者无法打开网络套接字,则应用程序无济于事,因此中止了操作。清除错误消息是一种明智的方法。这些错误很少发生,因此它们不太可能影响应用程序的整体可用性。如果应用程序遇到内部错误(如数据结构不一致),则错误消息中止也是合适的。这样的条件可能表明程序中存在错误。
|
||||||
|
|
||||||
|
Whether or not it is acceptable to crash on a particular error depends on the application. For a replicated storage system, it isn’t appropriate to abort on an I/O error. Instead, the system must use replicated data to recover any information that was lost. The recovery mechanisms will add considerable complexity to the program, but recovering lost data is an essential part of the value the system provides to its users.
|
||||||
|
|
||||||
|
> 在特定错误上崩溃是否可以接受取决于应用程序。对于复制的存储系统,不适合因 I/O 错误而中止。相反,系统必须使用复制的数据来恢复丢失的任何信息。恢复机制将给程序增加相当大的复杂性,但是恢复丢失的数据是系统为用户提供的价值的重要组成部分。
|
||||||
|
|
||||||
|
## 10.9 Design special cases out of existence 设计特殊情况不存在
|
||||||
|
|
||||||
|
For the same reason that it makes sense to define errors out of existence, it also makes sense to define other special cases out of existence. Special cases can result in code that is riddled with if statements, which make the code hard to understand and lead to bugs. Thus, special cases should be eliminated wherever possible. The best way to do this is by designing the normal case in a way that automatically handles the special cases without any extra code.
|
||||||
|
|
||||||
|
> 出于同样的原因,定义不存在的错误是有意义的,而定义其他不存在的特殊情况也是有意义的。特殊情况可能导致代码中混入 if 语句,这使代码难以理解并导致错误。因此,应尽可能消除特殊情况。做到这一点的最佳方法是设计一种普通情况,这种方式可以自动处理特殊情况而无需任何额外的代码。
|
||||||
|
|
||||||
|
In the text editor project described in Chapter 6, students had to implement a mechanism for selecting text and copying or deleting the selection. Most students introduced a state variable in their selection implementation to indicate whether or not the selection exists. They probably chose this approach because there are times when no selection is visible on the screen, so it seemed natural to represent this notion in the implementation. However, this approach resulted in numerous checks to detect the “no selection” condition and handle it specially.
|
||||||
|
|
||||||
|
> 在第 6 章中描述的文本编辑器项目中,学生必须实现一种选择文本以及复制或删除所选内容的机制。大多数学生在他们的选择实现中引入了状态变量,以表明选择是否存在。他们之所以选择这种方法,是因为有时屏幕上看不到任何选择,因此在实现中似乎很自然地代表了这一概念。但是,这种方法导致大量检查以检测“无选择”条件并进行特殊处理。
|
||||||
|
|
||||||
|
The selection handling code can be simplified by eliminating the “no selection” special case, so that the selection always exists. When there is no selection visible on the screen, it can be represented internally with an empty selection, whose starting and ending positions are the same. With this approach, the selection management code can be written without any checks for “no selection”. When copying the selection, if the selection is empty then 0 bytes will be inserted at the new location (if implemented correctly, there will be no need to check for 0 bytes as a special case). Similarly, it should be possible to design the code for deleting the selection so that the empty case is handled without any special-case checks. Consider a selection all on a single line. To delete the selection, extract the portion of the line preceding the selection and concatenate it with the portion of the line following the selection to form the new line. If the selection is empty, this approach will regenerate the original line.
|
||||||
|
|
||||||
|
> 通过消除“不选择”的特殊情况,可以简化选择处理代码,从而使选择始终存在。当屏幕上没有可见的选择时,可以在内部用空的选择表示,其开始和结束位置相同。使用这种方法,可以编写选择管理代码,而无需对“不选择”进行任何检查。复制所选内容时,如果所选内容为空,则将在新位置插入 0 字节(如果正确实现,则在特殊情况下无需检查 0 字节)。同样,应该有可能设计用于删除选择的代码,以便无需任何特殊情况检查就可以处理空情况。在一行上考虑所有选择。要删除选择,提取选择之前的行的一部分,并将其与选择之后的行的部分连接起来以形成新行。如果选择为空,则此方法将重新生成原始行。
|
||||||
|
|
||||||
|
This example also illustrates the “different layer, different abstraction” idea from Chapter 7. The notion of “no selection” makes sense in terms of how the user thinks about the application’s interface, but that doesn’t mean it has to be represented explicitly inside the application. Having a selection that always exists, but is sometimes empty and thus invisible, results in a simpler implementation.
|
||||||
|
|
||||||
|
> 此示例还说明了第 7 章中的“不同的层,不同的抽象”概念。“无选择”的概念在用户对应用程序界面的看法方面很有意义,但这并不意味着必须明确表示它在应用程序内部。选择总是存在的,但有时是空的,因此是不可见的,这样可以简化实现。
|
||||||
|
|
||||||
|
## 10.10 Taking it too far 走得太远
|
||||||
|
|
||||||
|
Defining away exceptions, or masking them inside a module, only makes sense if the exception information isn’t needed outside the module. This was true for the examples in this chapter, such the Tcl unset command and the Java substring method; in the rare situations where a caller cares about the special cases detected by the exceptions, there are other ways for it to get this information.
|
||||||
|
|
||||||
|
> 定义异常或将其屏蔽在模块内部,仅在模块外部不需要异常信息时才有意义。对于本章中的示例,例如 Tcl unset 命令和 Java 子字符串方法,都是如此。在极少数情况下,呼叫者关心异常检测到的特殊情况,还有其他方法可以获取此信息。
|
||||||
|
|
||||||
|
However, it is possible to take this idea too far. In a module for network communication, a student team masked all network exceptions: if a network error occurred, the module caught it, discarded it, and continued as if there were no problem. This meant that applications using the module had no way to find out if messages were lost or a peer server failed; without this information, it was impossible to build robust applications. In this case, it is essential for the module to expose the exceptions, even though they add complexity to the module’s interface.
|
||||||
|
|
||||||
|
> 但是,有可能使这个想法太过分。在用于网络通信的模块中,一个学生团队掩盖了所有网络异常:如果发生网络错误,则模块将其捕获,丢弃并继续进行,就好像没有问题一样。这意味着使用该模块的应用程序无法确定消息是否丢失或对等服务器是否发生故障;没有这些信息,就不可能构建健壮的应用程序。在这种情况下,模块必须公开异常,即使它们增加了模块接口的复杂性。
|
||||||
|
|
||||||
|
With exceptions, as with many other areas in software design, you must determine what is important and what is not important. Things that are not important should be hidden, and the more of them the better. But when something is important, it must be exposed.
|
||||||
|
|
||||||
|
> 与软件设计中的许多其他领域一样,您必须确定哪些是重要的,哪些是不重要的。不重要的事物应该被隐藏起来,它们越多越好。但是,当某件事很重要时,必须将其暴露出来。
|
||||||
|
|
||||||
|
## 10.11 Conclusion 结论
|
||||||
|
|
||||||
|
Special cases of any form make code harder to understand and increase the likelihood of bugs. This chapter focused on exceptions, which are one of the most significant sources of special-case code, and discussed how to reduce the number of places where exceptions must be handled. The best way to do this is by redefining semantics to eliminate error conditions. For exceptions that can’t be defined away, you should look for opportunities to mask them at a low level, so their impact is limited, or aggregate several special-case handlers into a single more generic handler. Together, these techniques can have a significant impact on overall system complexity.
|
||||||
|
|
||||||
|
> 任何形式的特殊情况都使代码更难以理解,并增加了发生错误的可能性。本章重点讨论异常,异常是特殊情况代码的最重要来源之一,并讨论了如何减少必须处理异常的地方的数量。做到这一点的最佳方法是重新定义语义以消除错误条件。对于无法定义的异常,您应该寻找机会将它们掩盖到较低的水平,以免影响有限,或者将多个特殊情况的处理程序聚合到一个更通用的处理程序中。总之,这些技术可能会对整体系统复杂性产生重大影响。
|
||||||
|
|
||||||
|
1Ding Yuan et. al., “Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems,” 2014 USENIX Conference on Operating System Design and Implementation.
|
||||||
|
|
||||||
|
> 1 丁元等 等人,“简单的测试可以防止最关键的故障:对分布式数据密集型系统中的生产故障的分析”,2014 USENIX 操作系统设计和实施大会。
|
||||||
57
docs/ch11.md
Normal file
57
docs/ch11.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# 第 11 章 设计它两次
|
||||||
|
|
||||||
|
> Chapter 11 Design it Twice
|
||||||
|
|
||||||
|
Designing software is hard, so it’s unlikely that your first thoughts about how to structure a module or system will produce the best design. You’ll end up with a much better result if you consider multiple options for each major design decision: design it twice.
|
||||||
|
|
||||||
|
> 设计软件非常困难,因此您对如何构造模块或系统的初步思考不太可能会产生最佳的设计。如果为每个主要设计决策考虑多个选项,最终将获得更好的结果:设计两次。
|
||||||
|
|
||||||
|
Suppose you are designing the class that will manage the text of a file for a GUI text editor. The first step is to define the interface that the class will present to the rest of the editor; rather than picking the first idea that comes to mind, consider several possibilities. One choice is a line-oriented interface, with operations to insert, modify, and delete whole lines of text. Another option is an interface based on individual character insertions and deletions. A third choice is a string-oriented interface, which operates on arbitrary ranges of characters that may cross line boundaries. You don’t need to pin down every feature of each alternative; it’s sufficient at this point to sketch out a few of the most important methods.
|
||||||
|
|
||||||
|
> 假设您正在设计用于管理 GUI 文本编辑器文件文本的类。第一步是定义该类将呈现给编辑器其余部分的接口。与其选择想到的第一个想法,不如考虑几种可能性。一种选择是面向行的界面,该界面具有插入,修改和删除整行文本的操作。另一个选择是基于单个字符插入和删除的接口。第三种选择是面向字符串的接口,该接口可对可能跨越线边界的任意范围的字符进行操作。您无需确定每个替代方案的每个功能;在这一点上,勾勒出一些最重要的方法就足够了。
|
||||||
|
|
||||||
|
Try to pick approaches that are radically different from each other; you’ll learn more that way. Even if you are certain that there is only one reasonable approach, consider a second design anyway, no matter how bad you think it will be. It will be instructive to think about the weaknesses of that design and contrast them with the features of other designs.
|
||||||
|
|
||||||
|
> 尝试选择彼此根本不同的方法;这样您将学到更多。即使您确定只有一种合理的方法,无论您认为有多糟糕,都应该考虑第二种设计。考虑该设计的弱点并将它们与其他设计的特征进行对比将很有启发性。
|
||||||
|
|
||||||
|
After you have roughed out the designs for the alternatives, make a list of the pros and cons of each one. The most important consideration for an interface is ease of use for higher level software. In the example above, both the line-oriented interface and the character-oriented interface will require extra work in software that uses the text class. The line-oriented interface will require higher level software to split and join lines during partial-line and multi-line operations such as cutting and pasting the selection. The character-oriented interface will require loops to implement operations that modify more than a single character. It is also worth considering other factors:
|
||||||
|
|
||||||
|
> 在对备选方案进行粗略设计之后,列出每个方案的优缺点。接口最重要的考虑因素是高级软件的易用性。在上面的示例中,面向行的界面和面向字符的界面都需要使用文本类的软件中的额外工作。面向行的界面将需要更高级别的软件来在部分行和多行操作(例如剪切和粘贴所选内容)期间拆分和合并行。面向字符的接口将需要循环来实现修改多个字符的操作。还值得考虑其他因素:
|
||||||
|
|
||||||
|
- Does one alternative have a simpler interface than another? In the text example, all of the text interfaces are relatively simple.
|
||||||
|
- Is one interface more general-purpose than another?
|
||||||
|
- Does one interface enable a more efficient implementation than another? In the text example, the character-oriented approach is likely to be significantly slower than the others, because it requires a separate call into the text module for each character.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> - 一种选择是否具有比另一种更简单的界面?在文本示例中,所有文本界面都相对简单。
|
||||||
|
> - 一个接口比另一个接口更通用吗?
|
||||||
|
> - 一个接口是否比另一个接口更有效地实现?在文本示例中,面向字符的方法可能比其他方法慢得多,因为它需要为每个字符单独调用文本模块。
|
||||||
|
|
||||||
|
Once you have compared alternative designs, you will be in a better position to identify the best design. The best choice may be one of the alternatives, or you may discover that you can combine features of multiple alternatives into a new design that is better than any of the original choices.
|
||||||
|
|
||||||
|
> 比较了备选设计之后,您将可以更好地确定最佳设计。最佳选择可能是这些选择之一,或者您可能发现可以将多个选择的功能组合到一个比任何原始选择都要好的新设计中。
|
||||||
|
|
||||||
|
Sometimes none of the alternatives is particularly attractive; when this happens, see if you can come up with additional schemes. Use the problems you identified with the original alternatives to drive the new design(s). If you were designing the text class and considered only the line-oriented and character-oriented approaches, you might notice that each of the alternatives is awkward because it requires higher level software to perform additional text manipulations. That’s a red flag: if there’s going to be a text class, it should handle all of the text manipulation. In order to eliminate the additional text manipulations, the text interface needs to match more closely the operations happening in higher level software. These operations don’t always correspond to single characters or single lines. This line of reasoning should lead you to a range-oriented API for text, which eliminates the problem with the earlier designs.
|
||||||
|
|
||||||
|
> 有时,没有其他选择特别有吸引力。发生这种情况时,请查看是否可以提出其他方案。使用您在原始替代方案中发现的问题来推动新设计。如果您在设计文本类并且仅考虑面向行和面向字符的方法,则可能会注意到每个替代方案都比较笨拙,因为它需要更高级别的软件来执行其他文本操作。那是一个危险信号:如果要有一个文本类,它应该处理所有文本操作。为了消除其他文本操作,文本界面需要更紧密地匹配高级软件中发生的操作。这些操作并不总是对应于单个字符或一行。
|
||||||
|
|
||||||
|
The design-it-twice principle can be applied at many levels in a system. For a module, you can use this approach first to pick the interface, as described above. Then you can apply it again when you are designing the implementation: for the text class, you might consider implementations such as a linked list of lines, fixed-size blocks of characters, or a “gap buffer.” The goals will be different for the implementation than for the interface: for the implementation, the most important things are simplicity and performance. It’s also useful to explore multiple designs at higher levels in the system, such as when choosing features for a user interface, or when decomposing a system into major modules. In each case, it’s easier to identify the best approach if you can compare a few alternatives.
|
||||||
|
|
||||||
|
> 两次设计原则可以在系统的许多级别上应用。对于模块,您可以首先使用此方法来选择接口,如上所述。然后,您可以在设计实现时再次应用它:对于文本类,您可以考虑实现这些实现,例如行的链接列表,固定大小的字符块或“间隙缓冲区”。实现的目标与接口的目标是不同的:对于实现,最重要的是简单性和性能。在系统的更高层次上探索多种设计也很有用,例如在为用户界面选择功能或将系统分解为主要模块时。在每种情况下,如果您可以比较几种选择,则更容易确定最佳方法。
|
||||||
|
|
||||||
|
Designing it twice does not need to take a lot of extra time. For a smaller module such as a class, you may not need more than an hour or two to consider alternatives. This is a small amount of time compared to the days or weeks you will spend implementing the class. The initial design experiments will probably result in a significantly better design, which will more than pay for the time spent designing it twice. For larger modules you’ll spend more time in the initial design explorations, but the implementation will also take longer, and the benefits of a better design will also be higher.
|
||||||
|
|
||||||
|
> 对其进行两次设计不需要花费很多额外的时间。对于较小的模块(如课程),您可能不需要一两个小时就能考虑替代方法。与您将花费数天或数周时间来实施该课程相比,这是很少的时间。最初的设计实验可能会导致明显更好的设计,这将比花两次设计时间所花的时间多。对于较大的模块,您将花费更多的时间进行初始设计探索,但是实现也将花费更长的时间,并且更好的设计所带来的好处也会更高。
|
||||||
|
|
||||||
|
I have noticed that the design-it-twice principle is sometimes hard for really smart people to embrace. When they are growing up, smart people discover that their first quick idea about any problem is sufficient for a good grade; there is no need to consider a second or third possibility. This makes it easy to develop bad work habits. However, as these people get older, they get promoted into environments with harder and harder problems. Eventually, everyone reaches a point where your first ideas are no longer good enough; if you want to get really great results, you have to consider a second possibility, or perhaps a third, no matter how smart you are. The design of large software systems falls in this category: no-one is good enough to get it right with their first try.
|
||||||
|
|
||||||
|
> 我已经注意到,真正聪明的人有时很难接受两次设计原则。当他们长大后,聪明的人会发现,他们对任何问题的第一个快速构想就足以取得良好的成绩。无需考虑第二种或第三种可能性。这使得容易养成不良的工作习惯。但是,随着这些人变老,他们将被提升到越来越困难的环境中。最终,每个人 都达到了您的第一个想法不再足够好的地步。如果您想获得非常好的结果,那么无论您多么聪明,都必须考虑第二种可能性,或者第三种可能性。大型软件系统的设计属于此类:没有人能很好地在首次尝试时就将其正确。
|
||||||
|
|
||||||
|
Unfortunately, I often see smart people who insist on implementing the first idea that comes to mind, and this causes them to underperform their true potential (it also makes them frustrating to work with). Perhaps they subconsciously believe that “smart people get it right the first time,” so if they try multiple designs it would mean they are not smart after all. This is not the case. It isn’t that you aren’t smart; it’s that the problems are really hard! Furthermore, that’s a good thing: it’s much more fun to work on a difficult problem where you have to think carefully, rather than an easy problem where you don’t have to think at all.
|
||||||
|
|
||||||
|
> 不幸的是,我经常看到聪明的人坚持要实现第一个想到的想法,这会使他们无法发挥其真正的潜力(这也使他们沮丧地工作)。也许他们下意识地相信“聪明的人第一次就能做到”,因此,如果他们尝试多种设计,那将意味着他们毕竟并不聪明。不是这种情况。不是说你不聪明;问题真的很难解决!此外,这是一件好事:处理一个必须认真思考的难题比处理一个根本不需要思考的难题更有趣。
|
||||||
|
|
||||||
|
The design-it-twice approach not only improves your designs, but it also improves your design skills. The process of devising and comparing multiple approaches will teach you about the factors that make designs better or worse. Over time, this will make it easier for you to rule out bad designs and hone in on really great ones.
|
||||||
|
|
||||||
|
> “两次设计”方法不仅可以改善您的设计,而且可以提高您的设计技能。设计和比较多种方法的过程将教您使设计更好或更坏的因素。随着时间的流逝,这将使您更容易排除不良的设计并磨练真正的出色设计。
|
||||||
103
docs/ch12.md
Normal file
103
docs/ch12.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# 第 12 章 为什么写注释?四个理由
|
||||||
|
|
||||||
|
> Chapter 12 Why Write Comments? The Four Excuses
|
||||||
|
|
||||||
|
In-code documentation plays a crucial role in software design. Comments are essential to help developers understand a system and work efficiently, but the role of comments goes beyond this. Documentation also plays an important role in abstraction; without comments, you can’t hide complexity. Finally, the process of writing comments, if done correctly, will actually improve a system’s design. Conversely, a good software design loses much of its value if it is poorly documented.
|
||||||
|
|
||||||
|
> 代码内文档在软件设计中起着至关重要的作用。注释对于帮助开发人员理解系统和有效工作至关重要,但是注释的作用不止于此。文档在抽象中也起着重要作用。没有注释,您就无法隐藏复杂性。最后,编写注释的过程(如果正确完成)将实际上改善系统的设计。相反,如果没有很好的文档记录,那么好的软件设计会失去很多价值。
|
||||||
|
|
||||||
|
Unfortunately, this view is not universally shared. A significant fraction of production code contains essentially no comments. Many developers think that comments are a waste of time; others see the value in comments, but somehow never get around to writing them. Fortunately, many development teams recognize the value of documentation, and it feels like the prevalence of these teams is gradually increasing. However, even in teams that encourage documentation, comments are often viewed as drudge work and many developers don’t understand how to write them, so the resulting documentation is often mediocre. Inadequate documentation creates a huge and unnecessary drag on software development.
|
||||||
|
|
||||||
|
> 不幸的是,这种观点并未得到普遍认同。生产代码的很大一部分基本上不包含任何注释。许多开发人员认为注释是浪费时间。其他人则看到了注释中的价值,但不知何故从不动手编写它们。幸运的是,许多开发团队认识到了文档的价值,并且感觉这些团队的普及率正在逐渐提高。但是,即使在鼓励文档的团队中,注释也经常被视为繁琐的工作,而且许多开发人员也不了解如何编写注释,因此生成的文档通常是平庸的。文档不足会给软件开发带来巨大且不必要的拖累。
|
||||||
|
|
||||||
|
In this chapter I will discuss the excuses developers use to avoid writing comments, and the reasons why comments really do matter. Chapter 13 will then describe how to write good comments and the next few chapters after that will discuss related issues such as choosing variable names and how to use documentation to improve a system’s design. I hope these chapters will convince you of three things: good comments can make a big difference in the overall quality of software; it isn’t hard to write good comments; and (this may be hard to believe) writing comments can actually be fun.
|
||||||
|
|
||||||
|
> 在本章中,我将讨论开发人员避免写注释的借口,以及注释真正重要的原因。然后,第 13 章将描述如何编写好的注释,其后的几章将讨论相关问题,例如选择变量名以及如何使用文档来改进系统的设计。我希望这些章节能使您相信三件事:好的注释可以对软件的整体质量产生很大的影响;写好注释并不难;并且(可能很难相信)写注释实际上很有趣。
|
||||||
|
|
||||||
|
When developers don’t write comments, they usually justify their behavior with one or more of the following excuses:
|
||||||
|
|
||||||
|
> 当开发人员不写注释时,他们通常会以以下一种或多种借口为自己的行为辩护:
|
||||||
|
|
||||||
|
- “Good code is self-documenting.”
|
||||||
|
- “I don’t have time to write comments.”
|
||||||
|
- “Comments get out of date and become misleading.”
|
||||||
|
- “The comments I have seen are all worthless; why bother?” In the sections below I will address each of these excuses in turn.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> - “好的代码可以自我记录。”
|
||||||
|
> - “我没有时间写注释。”
|
||||||
|
> - “注释过时,并会产生误导。”
|
||||||
|
> - “我所看到的注释都是毫无价值的;何必?” 在以下各节中,我将依次讨论这些借口。
|
||||||
|
|
||||||
|
## 12.1 Good code is self-documenting 好的代码可以自我记录
|
||||||
|
|
||||||
|
Some people believe that if code is written well, it is so obvious that no comments are needed. This is a delicious myth, like a rumor that ice cream is good for your health: we’d really like to believe it! Unfortunately, it’s simply not true. To be sure, there are things you can do when writing code to reduce the need for comments, such as choosing good variable names (see Chapter 14). Nonetheless, there is still a significant amount of design information that can’t be represented in code. For example, only a small part of a class’s interface, such as the signatures of its methods, can be specified formally in the code. The informal aspects of an interface, such as a high-level description of what each method does or the meaning of its result, can only be described in comments. There are many other examples of things that can’t be described in the code, such as the rationale for a particular design decision, or the conditions under which it makes sense to call a particular method.
|
||||||
|
|
||||||
|
> 有人认为,如果代码编写得当,那么显而易见,不需要注释。这是一个美味的神话,就像谣言说冰淇淋对您的健康有益:我们真的很想相信!不幸的是,事实并非如此。可以肯定的是,在编写代码时可以做一些事情来减少对注释的需求,例如选择好的变量名(请参阅第 14 章)。尽管如此,仍有大量设计信息无法用代码表示。例如,只能在代码中正式指定类接口的一小部分,例如其方法的签名。接口的非正式方面,例如对每种方法的作用或其结果含义的高级描述,只能在注释中描述。
|
||||||
|
|
||||||
|
Some developers argue that if others want to know what a method does, they should just read the code of the method: this will be more accurate than any comment. It’s possible that a reader could deduce the abstract interface of the method by reading its code, but it would be time-consuming and painful. In addition, if you write code with the expectation that users will read method implementations, you will try to make each method as short as possible, so that it’s easy to read. If the method does anything nontrivial, you will break it up into several smaller methods. This will result in a large number of shallow methods. Furthermore, it doesn’t really make the code easier to read: in order to understand the behavior of the top-level method, readers will probably need to understand the behaviors of the nested methods. For large systems it isn’t practical for users to read the code to learn the behavior.
|
||||||
|
|
||||||
|
> 一些开发人员认为,如果其他人想知道某个方法的作用,那么他们应该只阅读该方法的代码:这将比任何注释都更准确。读者可能会通过阅读其代码来推断该方法的抽象接口,但这既费时又痛苦。另外,如果在编写代码时期望用户会阅读方法实现,则将尝试使每个方法尽可能短,以便于阅读。如果该方法执行了一些重要操作,则将其分解为几个较小的方法。这将导致大量浅层方法。此外,它并没有真正使代码更易于阅读:为了理解顶层方法的行为,读者可能需要了解嵌套方法的行为。
|
||||||
|
|
||||||
|
Moreover, comments are fundamental to abstractions. Recall from Chapter 4 that the goal of abstractions is to hide complexity: an abstraction is a simplified view of an entity, which preserves essential information but omits details that can safely be ignored. If users must read the code of a method in order to use it, then there is no abstraction: all of the complexity of the method is exposed. Without comments, the only abstraction of a method is its declaration, which specifies its name and the names and types of its arguments and results. The declaration is missing too much essential information to provide a useful abstraction by itself. For example, a method to extract a substring might have two arguments, start and end, indicating the range of characters to extract. From the declaration alone, it isn’t possible to tell whether the extracted substring will include the character indicated by end, or what happens if start > end. Comments allow us to capture the additional information that callers need, thereby completing the simplified view while hiding implementation details. It’s also important that comments are written in a human language such as English; this makes them less precise than code, but it provides more expressive power, so we can create simple, intuitive descriptions. If you want to use abstractions to hide complexity, comments are essential.
|
||||||
|
|
||||||
|
> 此外,注释是抽象的基础。回顾第四章,抽象的目的是隐藏复杂性:抽象是实体的简化视图,该实体保留必要的信息,但忽略了可以安全忽略的细节。如果用户必须阅读方法的代码才能使用它,则没有任何抽象:方法的所有复杂性都将暴露出来。没有注释,方法的唯一抽象就是其声明,该声明指定其名称以及其参数和结果的名称和类型。该声明缺少太多基本信息,无法单独提供有用的抽象。例如,提取子字符串的方法可能有两个参数,开始和结束,表示要提取的字符范围。仅凭宣言,无法确定提取的子字符串是否将包含 end 指示的字符,或者如果 start> end 会发生什么。注释使我们能够捕获调用者所需的其他信息,从而在隐藏实现细节的同时完成简化的视图。用人类语言(例如英语)写注释也很重要;这使它们不如代码精确,但提供了更多的表达能力,因此我们可以创建简单直观的描述。如果要使用抽象来隐藏复杂性,则注释必不可少。
|
||||||
|
|
||||||
|
## 12.2 I don’t have time to write comments 我没有时间写注释
|
||||||
|
|
||||||
|
It’s tempting to prioritize comments lower than other development tasks. Given a choice between adding a new feature and documenting an existing feature, it seems logical to choose the new feature. However, software projects are almost always under time pressure, and there will always be things that seem higher priority than writing comments. Thus, if you allow documentation to be de-prioritized, you’ll end up with no documentation.
|
||||||
|
|
||||||
|
> 优先考虑低于其他开发任务的注释是很诱人的。在添加新功能和记录现有功能之间做出选择之后,选择新功能似乎合乎逻辑。但是,软件项目几乎总是处于时间压力之下,并且总会有比编写注释优先级更高的事情。因此,如果您允许取消对文档的优先级,则最终将没有文档。
|
||||||
|
|
||||||
|
The counter-argument to this excuse is the investment mindset discussed on page 15. If you want a clean software structure, which will allow you to work efficiently over the long-term, then you must take some extra time up front in order to create that structure. Good comments make a huge difference in the maintainability of software, so the effort spent on them will pay for itself quickly. Furthermore, writing comments needn’t take a lot of time. Ask yourself how much of your development time you spend typing in code (as opposed to designing, compiling, testing, etc.), assuming you don’t include any comments; I doubt that the answer is more than 10%. Now suppose that you spend as much time typing comments as typing code; this should be a safe upper bound. With these assumptions, writing good comments won’t add more than about 10% to your development time. The benefits of having good documentation will quickly offset this cost.
|
||||||
|
|
||||||
|
> 与该借口相反的是第 15 页上讨论的投资思路。如果您想要一个干净的软件结构,可以长期有效地工作,那么您必须花一些额外的时间才能创建该结构。好的注释对软件的可维护性有很大的影响,因此花费在它们上面的精力将很快收回成本。此外,撰写注释不需要花费很多时间。询问自己,假设您不包含任何注释,那么您花费了多少开发时间来键入代码(与设计,编译,测试等相对)。我怀疑答案是否超过 10%。现在假设您花在输入注释上的时间与输入代码所花费的时间一样多。这应该是一个安全的上限。基于这些假设,撰写好的注释不会增加您的开发时间约 10%。拥有良好文档的好处将迅速抵消这一成本。
|
||||||
|
|
||||||
|
Furthermore, many of the most important comments are those related to abstractions, such as the top-level documentation for classes and methods. Chapter 15 will argue that these comments should be written as part of the design process, and that the act of writing the documentation serves as an important design tool that improves the overall design. These comments pay for themselves immediately.
|
||||||
|
|
||||||
|
> 此外,许多最重要的注释是与抽象有关的注释,例如类和方法的顶级文档。第 15 章认为,这些注释应作为设计过程的一部分编写,并且编写文档的行为是改善整体设计的重要设计工具。这些注释立即付诸行动。
|
||||||
|
|
||||||
|
## 12.3 Comments get out of date and become misleading 注释过时并产生误导
|
||||||
|
|
||||||
|
Comments do sometimes get out of date, but this need not be a major problem in practice. Keeping documentation up-to-date does not require an enormous effort. Large changes to the documentation are only required if there have been large changes to the code, and the code changes will take more time than the documentation changes. Chapter 16 discusses how to organize documentation so that it is as easy as possible to keep it updated after code modifications (the key ideas are to avoid duplicated documentation and keep the documentation close to the corresponding code). Code reviews provide a great mechanism for detecting and fixing stale comments.
|
||||||
|
|
||||||
|
> 注释有时确实会过时,但这实际上并不是主要问题。使文档保持最新状态并不需要付出巨大的努力。仅当对代码进行了较大的更改时才需要对文档进行大的更改,并且代码更改将比文档的更改花费更多的时间。第 16 章讨论了如何组织文档,以便在修改代码后尽可能容易地对其进行更新(主要思想是避免重复的文档并使文档与相应的代码保持一致)。代码审查提供了一种检测和修复陈旧注释的强大机制。
|
||||||
|
|
||||||
|
## 12.4 All the comments I have seen are worthless 我所看到的所有注释都是毫无价值的
|
||||||
|
|
||||||
|
Of the four excuses, this is probably the one with the most merit. Every software developer has seen comments that provide no useful information, and most existing documentation is so-so at best. Fortunately, this problem is solvable; writing solid documentation is not hard, once you know how. The next chapters will lay out a framework for how to write good documentation and maintain it over time.
|
||||||
|
|
||||||
|
> 在这四个借口中,这可能是最有价值的借口。每个软件开发人员都看到没有提供有用信息的注释,并且大多数现有文档充其量都是这样。幸运的是,这个问题是可以解决的。一旦知道了如何编写可靠的文档并不难。下一章将为如何编写良好的文档并随时间进行维护提供一个框架。
|
||||||
|
|
||||||
|
## 12.5 Benefits of well-written comments
|
||||||
|
|
||||||
|
Now that I have discussed (and, hopefully, debunked) the arguments against writing comments, let’s consider the benefits that you will get from good comments. The overall idea behind comments is to capture information that was in the mind of the designer but couldn’t be represented in the code. This information ranges from low-level details, such as a hardware quirk that motivates a particularly tricky piece of code, up to high-level concepts such as the rationale for a class. When other developers come along later to make modifications, the comments will allow them to work more quickly and accurately. Without documentation, future developers will have to rederive or guess at the developer’s original knowledge; this will take additional time, and there is a risk of bugs if the new developer misunderstands the original designer’s intentions. Comments are valuable even when the original designer is the one making the changes: if it has been more than a few weeks since you last worked in a piece of code, you will have forgotten many of the details of the original design.
|
||||||
|
|
||||||
|
> 既然我已经讨论了(并希望揭穿了这些)反对撰写注释的论点,让我们考虑一下从良好注释中将获得的好处。注释背后的总体思想是捕获设计者所想但不能在代码中表示的信息。这些信息从低级详细信息(例如,激发特殊代码的硬件怪癖)到高级概念(例如,类的基本原理)。当其他开发人员稍后进行修改时,这些注释将使他们能够更快,更准确地工作。没有文档,未来的开发人员将不得不重新编写或猜测开发人员的原始知识。这将花费额外的时间,并且如果新开发者误解了原始设计者的意图,则存在错误的风险。
|
||||||
|
|
||||||
|
Chapter 2 described three ways in which complexity manifests itself in software systems:
|
||||||
|
|
||||||
|
> 第 2 章介绍了在软件系统中表现出复杂性的三种方式:
|
||||||
|
|
||||||
|
- Change amplification: a seemingly simple change requires code modifications in many places.
|
||||||
|
- Cognitive load: in order to make a change, the developer must accumulate a large amount of information.
|
||||||
|
- Unknown unknowns: it is unclear what code needs to be modified, or what information must be considered in order to make those modifications.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> - 变更放大:看似简单的变更需要在许多地方进行代码修改。
|
||||||
|
> - 认知负荷:为了进行更改,开发人员必须积累大量信息。
|
||||||
|
> - 未知未知数:尚不清楚需要修改哪些代码,或必须考虑哪些信息才能进行这些修改。
|
||||||
|
|
||||||
|
Good documentation helps with the last two of these issues. Documentation can reduce cognitive load by providing developers with the information they need to make changes and by making it easy for developers to ignore information that is irrelevant. Without adequate documentation, developers may have to read large amounts of code to reconstruct what was in the designer’s mind. Documentation can also reduce the unknown unknowns by clarifying the structure of the system, so that it is clear what information and code is relevant for any given change.
|
||||||
|
|
||||||
|
> 好的文档可以帮助解决最后两个问题。通过为开发人员提供他们进行更改所需的信息,并使开发人员容易忽略不相关的信息,文档可以减轻认知负担。没有足够的文档,开发人员可能必须阅读大量代码才能重构设计人员的想法。文档还可以通过阐明系统的结构来减少未知的未知数,从而可以清楚地了解与任何给定更改相关的信息和代码。
|
||||||
|
|
||||||
|
Chapter 2 pointed out that the primary causes of complexity are dependencies and obscurity. Good documentation can clarify dependencies, and it fills in gaps to eliminate obscurity.
|
||||||
|
|
||||||
|
> 第 2 章指出,导致复杂性的主要原因是依赖性和模糊性。好的文档可以阐明依赖关系,并且可以填补空白以消除模糊性。
|
||||||
|
|
||||||
|
The next few chapters will show you how to write good documentation. They will also discuss how to integrate documentation-writing into the design process so that it improves the design of your software.
|
||||||
|
|
||||||
|
> 接下来的几章将向您展示如何编写好的文档。他们还将讨论如何将文档编写集成到设计过程中,从而改善软件设计。
|
||||||
708
docs/ch13.md
Normal file
708
docs/ch13.md
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
# 第 13 章 注释应该描述代码中不明显的内容
|
||||||
|
|
||||||
|
> Chapter 13 Comments Should Describe Things that Aren’t Obvious from the Code
|
||||||
|
|
||||||
|
The reason for writing comments is that statements in a programming language can’t capture all of the important information that was in the mind of the developer when the code was written. Comments record this information so that developers who come along later can easily understand and modify the code. The guiding principle for comments is that comments should describe things that aren’t obvious from the code.
|
||||||
|
|
||||||
|
> 编写注释的原因是,使用编程语言编写的语句无法捕获编写代码时开发人员想到的所有重要信息。注释记录了这些信息,以便后来的开发人员可以轻松地理解和修改代码。注释的指导原则是,注释应描述代码中不明显的内容。
|
||||||
|
|
||||||
|
There are many things that aren’t obvious from the code. Sometimes it’s low-level details that aren’t obvious. For example, when a pair of indices describe a range, it isn’t obvious whether the elements given by the indices are inside the range or out. Sometimes it’s not clear why code is needed, or why it was implemented in a particular way. Sometimes there are rules the developer followed, such as “always invoke a before b.” You might be able to guess at a rule by looking at all of the code, but this is painful and error-prone; a comment can make the rule explicit and clear.
|
||||||
|
|
||||||
|
> 从代码中看不到很多事情。有时,底层细节并不明显。例如,当一对索引描述一个范围时,由索引给出的元素是在范围之内还是之外并不明显。有时不清楚为什么需要代码,或者为什么要以特定方式实现代码。有时,开发人员遵循一些规则,例如“总是在 b 之前调用 a”。您可能可以通过查看所有代码来猜测规则,但这很痛苦且容易出错。注释可以使规则清晰明了。
|
||||||
|
|
||||||
|
One of the most important reasons for comments is abstractions, which include a lot of information that isn’t obvious from the code. The idea of an abstraction is to provide a simple way of thinking about something, but code is so detailed that it can be hard to see the abstraction just from reading the code. Comments can provide a simpler, higher-level view (“after this method is invoked, network traffic will be limited to maxBandwidth bytes per second”). Even if this information can be deduced by reading the code, we don’t want to force users of a module to do that: reading the code is time-consuming and forces them to consider a lot of information that isn’t needed to use the module. Developers should be able to understand the abstraction provided by a module without reading any code other than its externally visible declarations. The only way to do this is by supplementing the declarations with comments.
|
||||||
|
|
||||||
|
> 注释的最重要原因之一是抽象,其中包括许多从代码中看不到的信息。抽象的思想是提供一种思考问题的简单方法,但是代码是如此详细,以至于仅通过阅读代码就很难看到抽象。注释可以提供一个更简单,更高级的视图(“调用此方法后,网络流量将被限制为每秒 maxBandwidth 字节”)。即使可以通过阅读代码推断出此信息,我们也不想强迫模块用户这样做:阅读代码很耗时,并且迫使他们考虑很多不需要使用的信息模块。开发人员应该能够理解模块提供的抽象,而无需阅读其外部可见声明以外的任何代码。
|
||||||
|
|
||||||
|
This chapter discusses what information needs to be described in comments and how to write good comments. As you will see, good comments typically explain things at a different level of detail than the code, which is more detailed in some situations and less detailed (more abstract) in others.
|
||||||
|
|
||||||
|
> 本章讨论需要在注释中描述哪些信息以及如何编写良好的注释。就像您将看到的那样,好的注释通常以与代码不同的详细程度来解释事物,在某些情况下,注释会更详细,而在某些情况下,代码则较不抽象(更抽象)。
|
||||||
|
|
||||||
|
## 13.1 Pick conventions
|
||||||
|
|
||||||
|
The first step in writing comments is to decide on conventions for commenting, such as what you will comment and the format you will use for comments. If you are programming in a language for which there exists a document compilation tool, such as Javadoc for Java, Doxygen for C++, or godoc for Go!, follow the conventions of the tools. None of these conventions is perfect, but the tools provide enough benefits to make up for that. If you are programming in an environment where there are no existing conventions to follow, try to adopt the conventions from some other language or project that is similar; this will make it easier for other developers to understand and adhere to your conventions.
|
||||||
|
|
||||||
|
> 编写注释的第一步是确定注释的约定,例如您要注释的内容和注释的格式。如果您正在使用存在文档编译工具的语言进行编程,例如 Java 的 Javadoc,C ++的 Doxygen 或 Go!的 godoc,请遵循工具的约定。这些约定都不是完美的,但是这些工具可提供足够的好处来弥补这一缺点。如果在没有现有约定可遵循的环境中进行编程,请尝试从其他类似的语言或项目中采用这些约定;这将使其他开发人员更容易理解和遵守您的约定。
|
||||||
|
|
||||||
|
Conventions serve two purposes. First, they ensure consistency, which makes comments easier to read and understand. Second, they help to ensure that you actually write comments. If you don’t have a clear idea what you are going to comment and how, it’s easy to end up writing no comments at all.
|
||||||
|
|
||||||
|
> 约定有两个目的。首先,它们确保一致性,这使得注释更易于阅读和理解。其次,它们有助于确保您实际编写评论。如果您不清楚要发表的评论以及发表评论的方式,那么很容易最终根本不发表评论。
|
||||||
|
|
||||||
|
Most comments fall into one of the following categories:
|
||||||
|
|
||||||
|
> 大多数评论属于以下类别之一:
|
||||||
|
|
||||||
|
Interface: a comment block that immediately precedes the declaration of a module such as a class, data structure, function, or method. The comment describe’s the module’s interface. For a class, the comment describes the overall abstraction provided by the class. For a method or function, the comment describes its overall behavior, its arguments and return value, if any, any side effects or exceptions that it generates, and any other requirements the caller must satisfy before invoking the method.
|
||||||
|
|
||||||
|
> 接口:在模块声明(例如类,数据结构,函数或方法)之前的注释块。注释描述模块的接口。对于一个类,注释描述了该类提供的整体抽象。对于方法或函数,注释描述其整体行为,其参数和返回值(如果有),其生成的任何副作用或异常,以及调用者在调用该方法之前必须满足的任何其他要求。
|
||||||
|
|
||||||
|
Data structure member: a comment next to the declaration of a field in a data structure, such as an instance variable or static variable for a class.
|
||||||
|
|
||||||
|
> 数据结构成员:数据结构中字段声明旁边的注释,例如类的实例变量或静态变量。
|
||||||
|
|
||||||
|
Implementation comment: a comment inside the code of a method or function, which describes how the code works internally.
|
||||||
|
|
||||||
|
> 实现注释:方法或函数代码内部的注释,它描述代码在内部的工作方式。
|
||||||
|
|
||||||
|
Cross-module comment: a comment describing dependencies that cross module boundaries.
|
||||||
|
|
||||||
|
> 跨模块注释:描述跨模块边界的依赖项的注释。
|
||||||
|
|
||||||
|
The most important comments are those in the first two categories. Every class should have an interface comment, every class variable should have a comment, and every method should have an interface comment. Occasionally, the declaration for a variable or method is so obvious that there is nothing useful to add in a comment (getters and setters sometimes fall in this category), but this is rare; it is easier to comment everything rather than spend energy worrying about whether a comment is needed. Implementation comments are often unnecessary (see Section 13.6 below). Cross-module comments are the most rare of all and they are problematic to write, but when they are needed they are quite important; Section 13.7 discusses them in more detail.
|
||||||
|
|
||||||
|
> 最重要的评论是前两个类别中的评论。每个类都应有一个接口注释,每个类变量应有一个注释,每个方法都应有一个接口注释。有时,变量或方法的声明是如此明显,以至于在注释中没有添加任何有用的东西(getter 和 setter 有时都属于此类),但这很少见。评论所有内容要比花精力担心是否需要评论要容易得多。实施注释通常是不必要的(请参阅下面的 13.6 节)。跨模块注释是最罕见的,而且编写起来很成问题,但是当需要它们时,它们就很重要。第 13.7 节将更详细地讨论它们。
|
||||||
|
|
||||||
|
## 13.2 Don’t repeat the code 不要重复代码
|
||||||
|
|
||||||
|
Unfortunately, many comments are not particularly helpful. The most common reason is that the comments repeat the code: all of the information in the comment can easily be deduced from the code next to the comment. Here is a code sample that appeared in a recent research paper:
|
||||||
|
|
||||||
|
> 不幸的是,许多评论并不是特别有用。最常见的原因是注释重复了代码:可以轻松地从注释旁边的代码中推断出注释中的所有信息。这是最近研究论文中出现的代码示例:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ptr_copy = get_copy(obj) # Get pointer copy
|
||||||
|
if is_unlocked(ptr_copy): # Is obj free?
|
||||||
|
return obj # return current obj
|
||||||
|
if is_copy(ptr_copy): # Already a copy?
|
||||||
|
return obj # return obj
|
||||||
|
thread_id = get_thread_id(ptr_copy)
|
||||||
|
if thread_id == ctx.thread_id: # Locked by current ctx
|
||||||
|
return ptr_copy # Return copy
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no useful information in any of these comments except for the “Locked by” comment, which suggests something about the thread that might not be obvious from the code. Notice that these comments are at roughly the same level of detail as the code: there is one comment per line of code, which describes that line. Comments like this are rarely useful.
|
||||||
|
|
||||||
|
> 这些注释中没有任何有用的信息,但“ Locked by”注释除外,该注释暗示了有关线程的某些信息可能在代码中并不明显。请注意,这些注释的详细程度与代码大致相同:每行代码有一个注释,用于描述该行。这样的注释很少有用。
|
||||||
|
|
||||||
|
Here are more examples of comments that repeat the code:
|
||||||
|
|
||||||
|
> 以下是重复代码的注释的更多示例:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Add a horizontal scroll bar
|
||||||
|
hScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
|
||||||
|
add(hScrollBar, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
// Add a vertical scroll bar
|
||||||
|
vScrollBar = new JScrollBar(JScrollBar.VERTICAL);
|
||||||
|
add(vScrollBar, BorderLayout.EAST);
|
||||||
|
|
||||||
|
// Initialize the caret-position related values
|
||||||
|
caretX = 0;
|
||||||
|
caretY = 0;
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 编写评论后,请问自己以下问题:从未看过代码的人能否仅通过查看评论旁边的代码来编写评论?如果答案是肯定的(如上述示例所示),则注释不会使代码更易于理解。像这样的评论是为什么有些人认为评论毫无价值的原因。
|
||||||
|
|
||||||
|
Another common mistake is to use the same words in the comment that appear in the name of the entity being documented:
|
||||||
|
|
||||||
|
> 另一个常见的错误是在注释中使用与要记录的实体名称相同的词:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/*
|
||||||
|
* Obtain a normalized resource name from REQ.
|
||||||
|
*/
|
||||||
|
private static String[] getNormalizedResourceNames(
|
||||||
|
HTTPRequest req) ...
|
||||||
|
/*
|
||||||
|
* Downcast PARAMETER to TYPE.
|
||||||
|
*/
|
||||||
|
private static Object downCastParameter(String parameter, String type) ...
|
||||||
|
/*
|
||||||
|
* The horizontal padding of each line in the text.
|
||||||
|
*/
|
||||||
|
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”!再说一次,这些注释可以仅通过查看声明来编写,而无需任何了解变量的方法。结果,它们没有价值。
|
||||||
|
|
||||||
|
img Red Flag: Comment Repeats Code img
|
||||||
|
|
||||||
|
If the information in a comment is already obvious from the code next to the comment, then the comment isn’t helpful. One example of this is when the comment uses the same words that make up the name of the thing it is describing.
|
||||||
|
|
||||||
|
> 如果注释旁边的代码中的注释信息已经很明显,则注释无济于事。这样的一个例子是,当注释使用与所描述事物名称相同的单词时。
|
||||||
|
|
||||||
|
At the same time, there is important information that is missing from the comments: for example, what is a “normalized resource name”, and what are the elements of the array returned by getNormalizedResourceNames? What does “downcast” mean? What are the units of padding, and is the padding on one side of each line or both? Describing these things in comments would be helpful.
|
||||||
|
|
||||||
|
> 同时,注释中缺少一些重要信息:例如,什么是“标准化资源名称”,以及 getNormalizedResourceNames 返回的数组的元素是什么?“贬低”是什么意思?填充的单位是什么,填充是在每行的一侧还是在两者的两侧?在注释中描述这些内容将很有帮助。
|
||||||
|
|
||||||
|
A first step towards writing good comments is to use different words in the comment from those in the name of the entity being described. Pick words for the comment that provide additional information about the meaning of the entity, rather than just repeating its name. For example, here is a better comment for textHorizontalPadding:
|
||||||
|
|
||||||
|
> 编写良好评论的第一步是在评论中使用与所描述实体名称不同的词。为注释选择单词,以提供有关实体含义的更多信息,而不仅仅是重复其名称。例如,以下是针对 textHorizontalPadding 的更好注释:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/*
|
||||||
|
* The amount of blank space to leave on the left and
|
||||||
|
* right sides of each line of text, in pixels.
|
||||||
|
*/
|
||||||
|
private static final int textHorizontalPadding = 4;
|
||||||
|
```
|
||||||
|
|
||||||
|
This comment provides additional information that is not obvious from the declaration itself, such as the units (pixels) and the fact that padding applies to both sides of each line. Instead of using the term “padding”, the comment explains what padding is, in case the reader isn’t already familiar with the term.
|
||||||
|
|
||||||
|
> 该注释提供了从声明本身不明显的其他信息,例如单位(像素)以及填充适用于每行两边的事实。如果读者不熟悉该术语,则注释将解释什么是填充,而不是使用术语“填充”。
|
||||||
|
|
||||||
|
## 13.3 Lower-level comments add precision 低级注释可提高精度
|
||||||
|
|
||||||
|
Now that you know what not to do, let’s discuss what information you should put in comments. Comments augment the code by providing information at a different level of detail. Some comments provide information at a lower, more detailed, level than the code; these comments add precision by clarifying the exact meaning of the code. Other comments provide information at a higher, more abstract, level than the code; these comments offer intuition, such as the reasoning behind the code, or a simpler and more abstract way of thinking about the code. Comments at the same level as the code are likely to repeat the code. This section discusses the lower-level approach in more detail, and the next section discusses the higher-level approach.
|
||||||
|
|
||||||
|
> 现在您知道了不应该做的事情,让我们讨论应该在注释中添加哪些信息。注释通过提供不同详细程度的信息来增强代码。一些注释提供了比代码更低,更详细的信息。这些注释通过阐明代码的确切含 义来增加精度。其他注释提供了比代码更高,更抽象的信息。这些注释提供了直觉,例如代码背后的推理,或者更简单,更抽象的代码思考方式。与代码处于同一级别的注释可能会重复该代码。本节将更详细地讨论下层方法,而下一节将讨论上层方法。
|
||||||
|
|
||||||
|
Precision is most useful when commenting variable declarations such as class instance variables, method arguments, and return values. The name and type in a variable declaration are typically not very precise. Comments can fill in missing details such as:
|
||||||
|
|
||||||
|
> 在注释变量声明(例如类实例变量,方法参数和返回值)时,精度最有用。变量声明中的名称和类型通常不是很精确。注释可以填写缺少的详细信息,例如:
|
||||||
|
|
||||||
|
- What are the units for this variable?
|
||||||
|
- Are the boundary conditions inclusive or exclusive?
|
||||||
|
- If a null value is permitted, what does it imply?
|
||||||
|
- If a variable refers to a resource that must eventually be freed or closed, who is responsible for freeing or closing it?
|
||||||
|
- Are there certain properties that are always true for the variable (invariants), such as “this list always contains at least one entry”?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> - 此变量的单位是什么?
|
||||||
|
> - 边界条件是包容性还是排他性?
|
||||||
|
> - 如果允许使用空值,则意味着什么?
|
||||||
|
> - 如果变量引用了最终必须释放或关闭的资源,那么谁负责释放或关闭该资源?
|
||||||
|
> - 是否存在某些对于变量始终不变的属性(不变量),例如“此列表始终包含至少一个条目”?
|
||||||
|
|
||||||
|
Some of this information could potentially be figured out by examining all of the code where the variable is used. However, this is time-consuming and error-prone; the declaration’s comment should be clear and complete enough to make this unnecessary. When I say that the comment for a declaration should describe things that aren’t obvious from the code, “the code” refers to the code next to the comment (the declaration), not “all of the code in the application.”
|
||||||
|
|
||||||
|
> 通过检查使用该变量的所有代码,可以潜在地了解某些信息。但是,这很耗时且容易出错。宣言的评论应清晰,完整,以免不必要。当我说声明的注释应描述代码中不明显的内容时,“代码”是指注释(声明)旁边的代码,而不是“应用程序中的所有代码”。
|
||||||
|
|
||||||
|
The most common problem with comments for variables is that the comments are too vague. Here are two examples of comments that aren’t precise enough:
|
||||||
|
|
||||||
|
> 变量注释最常见的问题是注释太模糊。这是两个不够精确的注释示例:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Current offset in resp Buffer
|
||||||
|
uint32_t offset;
|
||||||
|
|
||||||
|
// Contains all line-widths inside the document and
|
||||||
|
// number of appearances.
|
||||||
|
private TreeMap<Integer, Integer> lineWidths;
|
||||||
|
```
|
||||||
|
|
||||||
|
In the first example, it’s not clear what “current” means. In the second example, it’s not clear that the keys in the TreeMap are line widths and values are occurrence counts. Also, are widths measured in pixels or characters? The revised comments below provide additional details:
|
||||||
|
|
||||||
|
> 在第一个示例中,尚不清楚“当前”的含义。在第二个示例中,尚不清楚 TreeMap 中的键是线宽,值是出现次数。另外,宽度是以像素或字符为单位测量的吗?以下修订后的注释提供了更多详细信息:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Position in this buffer of the first object that hasn't
|
||||||
|
// been returned to the client.
|
||||||
|
uint32_t offset;
|
||||||
|
|
||||||
|
// Holds statistics about line lengths of the form <length, count>
|
||||||
|
// where length is the number of characters in a line (including
|
||||||
|
// the newline), and count is the number of lines with
|
||||||
|
// exactly that many characters. If there are no lines with
|
||||||
|
// a particular length, then there is no entry for that length.
|
||||||
|
private TreeMap<Integer, Integer> numLinesWithLength;
|
||||||
|
```
|
||||||
|
|
||||||
|
The second declaration uses a longer name that conveys more information. It also changes “width” to “length”, because this term is more likely to make people think that the units are characters rather than pixels. Notice that the second comment documents not only the details of each entry, but also what it means if an entry is missing.
|
||||||
|
|
||||||
|
> 第二个声明使用一个较长的名称来传达更多信息。它还将“宽度”更改为“长度”,因为该术语更可能使人们认为单位是字符而不是像素。请注意,第二条注释不仅记录了每个条目的详细信息,还记录了缺少条目的含义。
|
||||||
|
|
||||||
|
When documenting a variable, think nouns, not verbs. In other words, focus on what the variable represents, not how it is manipulated. Consider the following comment:
|
||||||
|
|
||||||
|
> 在记录变量时,请考虑名词而不是动词。换句话说,关注变量代表什么,而不是如何操纵变量。考虑以下评论:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/* FOLLOWER VARIABLE: indicator variable that allows the Receiver and the
|
||||||
|
* PeriodicTasks thread to communicate about whether a heartbeat has been
|
||||||
|
* received within the follower's election timeout window.
|
||||||
|
* Toggled to TRUE when a valid heartbeat is received.
|
||||||
|
* Toggled to FALSE when the election timeout window is reset. */
|
||||||
|
private boolean receivedValidHeartbeat;
|
||||||
|
```
|
||||||
|
|
||||||
|
This documentation describes how the variable is modified by several pieces of code in the class. The comment will be both shorter and more useful if it describes what the variable represents rather than mirroring the code structure:
|
||||||
|
|
||||||
|
> 本文档描述了如何通过类中的几段代码来修改变量。如果注释描述变量代表什么而不是镜像代码结构,则注释将更短且更有用:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/* True means that a heartbeat has been received since the last time
|
||||||
|
* the election timer was reset. Used for communication between the
|
||||||
|
* Receiver and PeriodicTasks threads. */
|
||||||
|
private boolean receivedValidHeartbeat;
|
||||||
|
```
|
||||||
|
|
||||||
|
Given this documentation, it’s easy to infer that the variable must be set to true when a heartbeat is received and false when the election timer is reset.
|
||||||
|
|
||||||
|
> 根据本文档,很容易推断出,当接收到心跳信号时,变量必须设置为 true;而当重置选举计时器时,则必须将变量设置为 false。
|
||||||
|
|
||||||
|
## 13.4 Higher-level comments enhance intuition 高级注释可增强直觉
|
||||||
|
|
||||||
|
The second way in which comments can augment code is by providing intuition. These comments are written at a higher level than the code. They omit details and help the reader to understand the overall intent and structure of the code. This approach is commonly used for comments inside methods, and for interface comments. For example, consider the following code:
|
||||||
|
|
||||||
|
> 注释可以增加代码的第二种方法是提供直觉。这些注释是在比代码更高的级别上编写的。它们忽略了细节,并帮助读者理解了代码的整体意图和结构。此方法通常用于方法内部的注释以及接口注释。例如,考虑以下代码:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// If there is a LOADING readRpc using the same session
|
||||||
|
// as PKHash pointed to by assignPos, and the last PKHash
|
||||||
|
// in that readRPC is smaller than current assigning
|
||||||
|
// PKHash, then we put assigning PKHash into that readRPC.
|
||||||
|
int readActiveRpcId = RPC_ID_NOT_ASSIGNED;
|
||||||
|
for (int i = 0; i < NUM_READ_RPC; i++) {
|
||||||
|
if (session == readRpc[i].session
|
||||||
|
&& readRpc[i].status == LOADING
|
||||||
|
&& readRpc[i].maxPos < assignPos
|
||||||
|
&& readRpc[i].numHashes < MAX_PKHASHES_PERRPC) {
|
||||||
|
readActiveRpcId = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The comment is too low-level and detailed. On the one hand, it partially repeats the code: “if there is a LOADING readRPC” just duplicates the test `readRpc[i].status == LOADING`. On the other hand, the comment doesn’t explain the overall purpose of this code, or how it fits into the method that contains it. As a result, the comment doesn’t help the reader to understand the code.
|
||||||
|
|
||||||
|
> 该评论太底层和太详细。一方面,它部分重复了代码:“如果有 LOADING readRPC”仅重复测试 `readRpc[i].status == LOADING`。另一方面,注释不能解释此代码的总体目的,也不能解释其如何适合包含此代码的方法。如此一来注释不能帮助读者理解代码。
|
||||||
|
|
||||||
|
Here is a better comment:
|
||||||
|
|
||||||
|
> 这是一个更好的评论:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Try to append the current key hash onto an existing
|
||||||
|
// RPC to the desired server that hasn't been sent yet.
|
||||||
|
```
|
||||||
|
|
||||||
|
This comment doesn’t contain any details; instead, it describes the code’s overall function at a higher level. With this high-level information, a reader can explain almost everything that happens in the code: the loop must be iterating over all the existing remote procedure calls (RPCs); the session test is probably used to see if a particular RPC is destined for the right server; the LOADING test suggests that RPCs can have multiple states, and in some states it isn’t safe to add more hashes; the MAX - PKHASHES_PERRPC test suggests that there is a limit to how many hashes can be sent in a single RPC. The only thing not explained by the comment is the maxPos test. Furthermore, the new comment provides a basis for readers to judge the code: does it do everything that is needed to add the key hash to an existing RPC? The original comment didn’t describe the overall intent of the code, so it’s hard for a reader to decide whether the code is behaving correctly.
|
||||||
|
|
||||||
|
> 此评论不包含任何详细信息。相反,它在更高级别上描述了代码的整体功能。有了这些高级信息,读者就可以解释代码中几乎发生的所有事情:循环必须遍历所有现有的远程过程调用(RPC);会话测试可能用于查看特定的 RPC 是否发往正确的服务器;LOADING 测试表明 RPC 可以具有多个状态,在某些状态下添加更多的哈希值是不安全的;MAX-PKHASHES_PERRPC 测试表明在单个 RPC 中可以发送多少个哈希值是有限制的。注释中唯一没有解释的是 maxPos 测试。此外,新注释为读者判断代码提供了基础:它可以完成将密钥哈希添加到现有 RPC 所需的一切吗?原始注释并未描述代码的整体意图,因此,读者很难确定代码是否行为正确。
|
||||||
|
|
||||||
|
Higher-level comments are more difficult to write than lower-level comments because you must think about the code in a different way. Ask yourself: What is this code trying to do? What is the simplest thing you can say that explains everything in the code? What is the most important thing about this code?
|
||||||
|
|
||||||
|
> 高级别的注释比低级别的注释更难编写,因为您必须以不同的方式考虑代码。问问自己:这段代码要做什么?您能说什么最简单的东西来解释代码中的所有内容?这段代码最重要的是什么?
|
||||||
|
|
||||||
|
Engineers tend to be very detail-oriented. We love details and are good at managing lots of them; this is essential for being a good engineer. But, great software designers can also step back from the details and think about a system at a higher level. This means deciding which aspects of the system are most important, and being able to ignore the low-level details and think about the system only in terms of its most fundamental characteristics. This is the essence of abstraction (finding a simple way to think about a complex entity), and it’s also what you must do when writing higher-level comments. A good higher-level comment expresses one or a few simple ideas that provide a conceptual framework, such as “append to an existing RPC.” Given the framework, it becomes easy to see how specific code statements relate to the overall goal.
|
||||||
|
|
||||||
|
> 工程师往往非常注重细节。我们喜欢细节,善于管理其中的许多细节;这对于成为一名优秀的工程师至关重要。但是,优秀的软件设计师也可以从细节退后一步,从更高层次考虑系统。这意味着要确定系统的哪些方面最重要,并且能够忽略底层细节,仅根据系统的最基本特征来考虑系统。这是抽象的本质(找到一种思考复杂实体的简单方法),这也是编写高级注释时必须执行的操作。一个好的高层注释表达了一个或几个简单的想法,这些想法提供了一个概念框架,例如“附加到现有的 RPC”。使用该框架,可以很容易地看到特定的代码语句与总体目标之间的关系。
|
||||||
|
|
||||||
|
Here is another code sample, which has a good higher-level comment:
|
||||||
|
|
||||||
|
> 这是另一个代码示例,具有较高层次的注释:
|
||||||
|
|
||||||
|
```java
|
||||||
|
if (numProcessedPKHashes < readRpc[i].numHashes) {
|
||||||
|
// Some of the key hashes couldn't be looked up in
|
||||||
|
// this request (either because they aren't stored
|
||||||
|
// on the server, the server crashed, or there
|
||||||
|
// wasn't enough space in the response message).
|
||||||
|
// Mark the unprocessed hashes so they will get
|
||||||
|
// reassigned to new RPCs.
|
||||||
|
for (size_t p = removePos; p < insertPos; p++) {
|
||||||
|
if (activeRpcId[p] == i) {
|
||||||
|
if (numProcessedPKHashes > 0) {
|
||||||
|
numProcessedPKHashes--;
|
||||||
|
} else {
|
||||||
|
if (p < assignPos)
|
||||||
|
assignPos = p;
|
||||||
|
activeRpcId[p] = RPC_ID_NOT_ASSIGNED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This comment does two things. The second sentence provides an abstract description of what the code does. The first sentence is different: it explains (in high level terms) why the code is executed. Comments of the form “how we get here” are very useful for helping people to understand code. For example, when documenting a method, it can be very helpful to describe the conditions under which the method is most likely to be invoked (especially if the method is only invoked in unusual situations).
|
||||||
|
|
||||||
|
> 此评论有两件事。第二句话提供了代码功能的抽象描述。第一句话是不同的:它以高级的方式解释了为什么执行代码。“如何到达这里”形式的注释对于帮助人们理解代码非常有用。例如,在记录方法时,描述最有可能在什么情况下调用该方法的条件(特别是仅在异常情况下调用该方法的情况)会非常有帮助。
|
||||||
|
|
||||||
|
## 13.5 Interface documentation 接口文档
|
||||||
|
|
||||||
|
One of the most important roles for comments is to define abstractions. Recall from Chapter 4 that an abstraction is a simplified view of an entity, which preserves essential information but omits details that can safely be ignored. Code isn’t suitable for describing abstractions; it’s too low level and it includes implementation details that shouldn’t be visible in the abstraction. The only way to describe an abstraction is with comments. If you want code that presents good abstractions, you must document those abstractions with comments.
|
||||||
|
|
||||||
|
> 注释最重要的作用之一就是定义抽象。回想一下第 4 章,抽象是实体的简化视图,它保留了基本信息,但省略了可以安全忽略的细节。代码不适合描述抽象;它的级别太低,它包含实现细节,这些细节在抽象中不应该看到。描述抽象的唯一方法是使用注释。如果您想要呈现良好抽象的代码,则必须用注释记录这些抽象。
|
||||||
|
|
||||||
|
The first step in documenting abstractions is to separate interface comments from implementation comments. Interface comments provide information that someone needs to know in order to use a class or method; they define the abstraction. Implementation comments describe how a class or method works internally in order to implement the abstraction. It’s important to separate these two kinds of comments, so that users of an interface are not exposed to implementation details. Furthermore, these two forms had better be different. If interface comments must also describe the implementation, then the class or method is shallow. This means that the act of writing comments can provide clues about the quality of a design; Chapter 15 will return to this idea.
|
||||||
|
|
||||||
|
> 记录抽象的第一步是将接口注释与实现注释分开。接口注释提供了使用类或方法时需要知道的信息。他们定义了抽象。实现注释描述了类或方法如何在内部工作以实现抽象。区分这两种注释很重要,这样界面的用户就不会暴露于实现细节。此外,这两种形式最好有所不同。如果接口注释也必须描述实现,则该类或方法很浅。这意味着撰写评论的行为可以提供有关设计质量的线索;第 15 章将回到这个想法。
|
||||||
|
|
||||||
|
The interface comment for a class provides a high-level description of the abstraction provided by the class, such as the following:
|
||||||
|
|
||||||
|
> 类的接口注释提供了该类提供的抽象的高级描述,例如:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* This class implements a simple server-side interface to the HTTP
|
||||||
|
* protocol: by using this class, an application can receive HTTP
|
||||||
|
* requests, process them, and return responses. Each instance of
|
||||||
|
* this class corresponds to a particular socket used to receive
|
||||||
|
* requests. The current implementation is single-threaded and
|
||||||
|
* processes one request at a time.
|
||||||
|
*/
|
||||||
|
public class Http {...}
|
||||||
|
```
|
||||||
|
|
||||||
|
This comment describes the overall capabilities of the class, without any implementation details or even the specifics of particular methods. It also describes what each instance of the class represents. Finally, the comments describe the limitations of the class (it does not support concurrent access from multiple threads), which may be important to developers contemplating whether to use it.
|
||||||
|
|
||||||
|
> 该注释描述了类的整体功能,没有任何实现细节,甚至没有特定方法的细节。它还描述了该类的每个实例代表什么。最后,注释描述了该类的限制(它不支持从多个线程的并发访问),这对于考虑是否使用它的开发人员可能很重要。
|
||||||
|
|
||||||
|
The interface comment for a method includes both higher-level information for abstraction and lower-level details for precision:
|
||||||
|
|
||||||
|
> 方法的接口注释既包括用于抽象的高层信息,又包括用于精度的低层细节:
|
||||||
|
|
||||||
|
- The comment usually starts with a sentence or two describing the behavior of the method as perceived by callers; this is the higher-level abstraction.
|
||||||
|
- The comment must describe each argument and the return value (if any). These comments must be very precise, and must describe any constraints on argument values as well as dependencies between arguments.
|
||||||
|
- If the method has any side effects, these must be documented in the interface comment. A side effect is any consequence of the method that affects the future behavior of the system but is not part of the result. For example, if the method adds a value to an internal data structure, which can be retrieved by future method calls, this is a side effect; writing to the file system is also a side effect.
|
||||||
|
- A method’s interface comment must describe any exceptions that can emanate from the method.
|
||||||
|
- If there are any preconditions that must be satisfied before a method is invoked, these must be described (perhaps some other method must be invoked first; for a binary search method, the list being searched must be sorted). It is a good idea to minimize preconditions, but any that remain must be documented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> - 注释通常以一两个句子开头,描述调用者感知到的方法的行为。这是更高层次的抽象。
|
||||||
|
> - 注释必须描述每个参数和返回值(如果有)。这些注释必须非常精确,并且必须描述对参数值的任何约束以及参数之间的依赖关系。
|
||||||
|
> - 如果该方法有任何副作用,则必须在界面注释中记录这些副作用。副作用是该方法的任何结果都会影响系统的未来行为,但不属于结果的一部分。例如,如果该方法将一个值添加到内部数据结构中,可以通过将来的方法调用来检索该值,则这是副作用。写入文件系统也是一个副作用。
|
||||||
|
> - 方法的界面注释必须描述该方法可能产生的任何异常。
|
||||||
|
> - 如果在调用某个方法之前必须满足任何前提条件,则必须对其进行描述(也许必须先调用其他方法;对于二进制搜索方法,必须对要搜索的列表进行排序)。尽量减少前提条件是一个好主意,但是任何保留的条件都必须记录在案。
|
||||||
|
|
||||||
|
Here is the interface comment for a method that copies data out of a Buffer object:
|
||||||
|
|
||||||
|
> 这是从 Buffer 对象复制数据的方法的接口注释:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* Copy a range of bytes from a buffer to an external location.
|
||||||
|
*
|
||||||
|
* \param offset
|
||||||
|
* Index within the buffer of the first byte to copy.
|
||||||
|
* \param length
|
||||||
|
* Number of bytes to copy.
|
||||||
|
* \param dest
|
||||||
|
* Where to copy the bytes: must have room for at least
|
||||||
|
* length bytes.
|
||||||
|
*
|
||||||
|
* \return
|
||||||
|
* The return value is the actual number of bytes copied,
|
||||||
|
* which may be less than length if the requested range of
|
||||||
|
* bytes extends past the end of the buffer. 0 is returned
|
||||||
|
* if there is no overlap between the requested range and
|
||||||
|
* the actual buffer.
|
||||||
|
*/
|
||||||
|
|
||||||
|
uint32_t
|
||||||
|
Buffer::copy(uint32_t offset, uint32_t length, void* dest)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The syntax of this comment (e.g., \return) follows the conventions of Doxygen, a program that extracts comments from C/C++ code and compiles them into Web pages. The goal of the comment is to provide all the information a developer needs in order to invoke the method, including how special cases are handled (note how this method follows the advice of Chapter 10 and defines out of existence any errors associated with the range specification). The developer should not need to read the body of the method in order to invoke it, and the interface comment provides no information about how the method is implemented, such as how it scans its internal data structures to find the desired data.
|
||||||
|
|
||||||
|
> 此注释的语法(例如\ return)遵循 Doxygen 的约定,该程序从 C / C ++代码中提取注释并将其编译为 Web 页。注释的目的是提供开发人员调用该方法所需的所有信息,包括特殊情况的处理方式(请注意,此方法如何遵循第 10 章的建议并定义与范围规范相关的任何错误。 )。开发人员不必为了调用它而阅读方法的主体,并且接口注释不提供有关如何实现该方法的信息,例如它如何扫描其内部数据结构以查找所需的数据。
|
||||||
|
|
||||||
|
For a more extended example, let’s consider a class called IndexLookup, which is part of a distributed storage system. The storage system holds a collection of tables, each of which contains many objects. In addition, each table can have one or more indexes; each index provides efficient access to objects in the table based on a particular field of the object. For example, one index might be used to look up objects based on their name field, and another index might be used to look up objects based on their age field. With these indexes, applications can quickly extract all of the objects with a particular name, or all of those with an age in a given range.
|
||||||
|
|
||||||
|
> 对于更扩展的示例,让我们考虑一个称为 IndexLookup 的类,该类是分布式存储系统的一部分。存储系统拥有一个表集合,每个表包含许多对象。另外,每个表可以具有一个或多个索引;每个索引都基于对象的特定字段提供对表中对象的有效访问。例如,一个索引可以用于根据对象的名称字段查找对象,而另一个索引可以用于根据对象的年龄字段查找对象。使用这些索引,应用程序可以快速提取具有特定名称的所有对象,或具有给定范围内的年龄的所有对象。
|
||||||
|
|
||||||
|
The IndexLookup class provides a convenient interface for performing indexed lookups. Here is an example of how it might be used in an application:
|
||||||
|
|
||||||
|
> IndexLookup 类为执行索引查找提供了一个方便的接口。这是一个如何在应用程序中使用的示例:
|
||||||
|
|
||||||
|
```java
|
||||||
|
query = new IndexLookup(table, index, key1, key2);
|
||||||
|
while (true) {
|
||||||
|
object = query.getNext();
|
||||||
|
if (object == NULL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
... process object ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The application first constructs an object of type IndexLookup, providing arguments that select a table, an index, and a range within the index (for example, if the index is based on an age field, key1 and key2 might be specified as 21 and 65 to select all objects with ages between those values). Then the application calls the getNext method repeatedly. Each invocation returns one object that falls within the desired range; once all of the matching objects have been returned, getNext returns NULL. Because the storage system is distributed, the implementation of this class is somewhat complex. The objects in a table may be spread across multiple servers, and each index may also be distributed across a different set of servers; the code in the IndexLookup class must first communicate with all of the relevant index servers to collect information about the objects in the range, then it must communicate with the servers that actually store the objects in order to retrieve their values.
|
||||||
|
|
||||||
|
> 应用程序首先构造一个类型为 IndexLookup 的对象,并提供用于选择表,索引和索引内范围的参数(例如,如果索引基于年龄字段,则 key1 和 key2 可以指定为 21 和 65 选择年龄介于这些值之间的所有对象)。然后,应用程序重复调用 getNext 方法。每次调用都返回一个位于所需范围内的对象。一旦返回所有匹配的对象,getNext 将返回 NULL。因为存储系统是分布式的,所以此类的实现有些复杂。表中的对象可以分布在多个服务器上,每个索引也可以分布在一组不同的服务器上。
|
||||||
|
|
||||||
|
Now let’s consider what information needs to be included in the interface comment for this class. For each piece of information given below, ask yourself whether a developer needs to know that information in order to use the class (my answers to the questions are at the end of the chapter):
|
||||||
|
|
||||||
|
> 现在,让我们考虑该类的接口注释中需要包含哪些信息。对于下面给出的每条信息,问自己一个开发人员是否需要知道该信息才能使用该类(我对问题的回答在本章的结尾):
|
||||||
|
|
||||||
|
1. The format of messages that the IndexLookup class sends to the servers holding indexes and objects.
|
||||||
|
2. The comparison function used to determine whether a particular object falls in the desired range (is comparison done using integers, floating-point numbers, or strings?).
|
||||||
|
3. The data structure used to store indexes on servers.
|
||||||
|
4. Whether or not IndexLookup issues multiple requests to different servers concurrently.
|
||||||
|
5. The mechanism for handling server crashes.
|
||||||
|
|
||||||
|
> 1. IndexLookup 类发送给包含索引和对象的服务器的消息格式。
|
||||||
|
> 2. 用于确定特定对象是否在所需范围内的比较功能(使用整数,浮点数或字符串进行比较吗?)。
|
||||||
|
> 3. 用于在服务器上存储索引的数据结构。
|
||||||
|
> 4. IndexLookup 是否同时向多个服务器发出多个请求。
|
||||||
|
> 5. 处理服务器崩溃的机制。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Here is the original version of the interface comment for the IndexLookup class; the excerpt also includes a few lines from the class’s definition, which are referred to in the comment:
|
||||||
|
|
||||||
|
> 这是 IndexLookup 类的接口注释的原始版本;摘录还包括类定义的几行内容,在注释中进行了引用:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
/*
|
||||||
|
* This class implements the client side framework for index range
|
||||||
|
* lookups. It manages a single LookupIndexKeys RPC and multiple
|
||||||
|
* IndexedRead RPCs. Client side just includes "IndexLookup.h" in
|
||||||
|
* its header to use IndexLookup class. Several parameters can be set
|
||||||
|
* in the config below:
|
||||||
|
* - The number of concurrent indexedRead RPCs
|
||||||
|
* - The max number of PKHashes a indexedRead RPC can hold at a time
|
||||||
|
* - The size of the active PKHashes
|
||||||
|
*
|
||||||
|
* To use IndexLookup, the client creates an object of this class by
|
||||||
|
* providing all necessary information. After construction of
|
||||||
|
* IndexLookup, client can call getNext() function to move to next
|
||||||
|
* available object. If getNext() returns NULL, it means we reached
|
||||||
|
* the last object. Client can use getKey, getKeyLength, getValue,
|
||||||
|
* and getValueLength to get object data of current object.
|
||||||
|
*/
|
||||||
|
class IndexLookup {
|
||||||
|
...
|
||||||
|
private:
|
||||||
|
/// Max number of concurrent indexedRead RPCs
|
||||||
|
static const uint8_t NUM_READ_RPC = 10;
|
||||||
|
/// Max number of PKHashes that can be sent in one
|
||||||
|
/// indexedRead RPC
|
||||||
|
static const uint32_t MAX_PKHASHES_PERRPC = 256;
|
||||||
|
/// Max number of PKHashes that activeHashes can
|
||||||
|
/// hold at once.
|
||||||
|
static const size_t MAX_NUM_PK = (1 << LG_BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Before reading further, see if you can identify the problems with this comment. Here are the problems that I found:
|
||||||
|
|
||||||
|
> 在进一步阅读之前,请先查看您是否可以使用此注释确定问题所在。这是我发现的问题:
|
||||||
|
|
||||||
|
- Most of the first paragraph concerns the implementation, not the interface. As one example, users don’t need to know the names of the particular remote procedure calls used to communicate with the servers. The configuration parameters referred to in the second half of the first paragraph are all private variables that are relevant only to the maintainer of the class, not to its users. All of this implementation information should be omitted from the comment.
|
||||||
|
- The comment also includes several things that are obvious. For example, there’s no need to tell users to include IndexLookup.h: anyone who writes C++ code will be able to guess that this is necessary. In addition, the text “by providing all necessary information” says nothing, so it can be omitted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> - 第一段的大部分与实现有关,而不是接口。举一个例子,用户不需要知道用于与服务器通信的特定远程过程调用的名称。在第一段的后半部分中提到的配置参数都是所有私有变量,它们仅与类的维护者相关,而与类的用户无关。所有这些实现信息都应从注释中省略。
|
||||||
|
> - 该评论还包括一些显而易见的事情。例如,不需要告诉用户包括 IndexLookup.h:任何编写 C ++代码的人都可以猜测这是必要的。另外,“通过提供所有必要的信息”一词无语,因此可以省略。
|
||||||
|
|
||||||
|
A shorter comment for this class is sufficient (and preferable):
|
||||||
|
|
||||||
|
> 对此类的简短评论就足够了(并且更可取):
|
||||||
|
|
||||||
|
```java
|
||||||
|
/*
|
||||||
|
* This class is used by client applications to make range queries
|
||||||
|
* using indexes. Each instance represents a single range query.
|
||||||
|
*
|
||||||
|
* To start a range query, a client creates an instance of this
|
||||||
|
* class. The client can then call getNext() to retrieve the objects
|
||||||
|
* in the desired range. For each object returned by getNext(), the
|
||||||
|
* caller can invoke getKey(), getKeyLength(), getValue(), and
|
||||||
|
* getValueLength() to get information about that object.
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
The last paragraph of this comment is not strictly necessary, since it mostly duplicates information in the comments for individual methods. However, it can be helpful to have examples in the class documentation that illustrate how its methods work together, particularly for deep classes with usage patterns that are nonobvious. Note that the new comment does not mention NULL return values from getNext. This comment is not intended to document every detail of each method; it just provides high level information to help readers understand how the methods work together and when each method might be invoked. For details, readers can refer to the interface comments for individual methods. This comment also does not mention server crashes; that is because server crashes are invisible to users of this class (the system automatically recovers from them).
|
||||||
|
|
||||||
|
> 此注释的最后一段不是严格必需的,因为它主要针对单个方法复制了注释中的信息。但是,在类文档中提供示例来说明其方法如何协同工作可能会有所帮助,特别是对于使用模式不明显的深层类尤其如此。注意,新注释未提及 getNext 的 NULL 返回值。此注释无意记录每种方法的每个细节;它只是提供高级信息,以帮助读者了解这些方法如何协同工作以及何时可以调用每种方法。有关详细信息,读者可以参考接口注释中的各个方法。此评论也没有提到服务器崩溃;这是因为此类服务器的用户看不到服务器崩溃(系统会自动从中恢复)。
|
||||||
|
|
||||||
|
img Red Flag: Implementation Documentation Contaminates Interface img
|
||||||
|
|
||||||
|
This red flag occurs when interface documentation, such as that for a method, describes implementation details that aren’t needed in order to use the thing being documented.
|
||||||
|
|
||||||
|
> 当接口文档(例如方法的文档)描述了不需要使用要记录的事物的实现详细信息时,就会出现此红色标记。
|
||||||
|
|
||||||
|
Now consider the following code, which shows the first version of the documentation for the isReady method in IndexLookup:
|
||||||
|
|
||||||
|
> 现在考虑以下代码,该代码显示 IndexLookup 中 isReady 方法的文档的第一版:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
/**
|
||||||
|
* Check if the next object is RESULT_READY. This function is
|
||||||
|
* implemented in a DCFT module, each execution of isReady() tries
|
||||||
|
* to make small progress, and getNext() invokes isReady() in a
|
||||||
|
* while loop, until isReady() returns true.
|
||||||
|
*
|
||||||
|
* isReady() is implemented in a rule-based approach. We check
|
||||||
|
* different rules by following a particular order, and perform
|
||||||
|
* certain actions if some rule is satisfied.
|
||||||
|
*
|
||||||
|
* \return
|
||||||
|
* True means the next Object is available. Otherwise, return
|
||||||
|
* false.
|
||||||
|
*/
|
||||||
|
bool IndexLookup::isReady() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Once again, most of this documentation, such as the reference to DCFT and the entire second paragraph, concerns the implementation, so it doesn’t belong here; this is one of the most common errors in interface comments. Some of the implementation documentation is useful, but it should go inside the method, where it will be clearly separated from interface documentation. In addition, the first sentence of the documentation is cryptic (what does RESULT_READY mean?) and some important information is missing. Finally, it isn’t necessary to describe the implementation of getNext here. Here is a better version of the comment:
|
||||||
|
|
||||||
|
> 再一次,本文档中的大多数内容,例如对 DCFT 的引用以及整个第二段,都与实现有关,因此不属于此处。这是界面注释中最常见的错误之一。某些实现文档很有用,但应放在方法内部,在该方法中应将其与接口文档明确分开。此外,文档的第一句话是含糊的(RESULT_READY 是什么意思?),并且缺少一些重要信息。最后,无需在此处描述 getNext 的实现。这是评论的更好版本:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/*
|
||||||
|
* Indicates whether an indexed read has made enough progress for
|
||||||
|
* getNext to return immediately without blocking. In addition, this
|
||||||
|
* method does most of the real work for indexed reads, so it must
|
||||||
|
* be invoked (either directly, or indirectly by calling getNext) in
|
||||||
|
* order for the indexed read to make progress.
|
||||||
|
*
|
||||||
|
* \return
|
||||||
|
* True means that the next invocation of getNext will not block
|
||||||
|
* (at least one object is available to return, or the end of the
|
||||||
|
* lookup has been reached); false means getNext may block.
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
This version of the comment provides more precise information about what “ready” means, and it provides the important information that this method must eventually be invoked if the indexed retrieval is to move forward.
|
||||||
|
|
||||||
|
> 此注释版本提供了有关“就绪”含义的更精确信息,并且提供了重要信息,如果要继续进行索引检索,则必须最终调用此方法。
|
||||||
|
|
||||||
|
## 13.6 Implementation comments: what and why, not how 实施注释:什么以及为什么,而不是如何
|
||||||
|
|
||||||
|
Implementation comments are the comments that appear inside methods to help readers understand how they work internally. Most methods are so short and simple that they don’t need any implementation comments: given the code and the interface comments, it’s easy to figure out how a method works.
|
||||||
|
|
||||||
|
> 实现注释是出现在方法内部的注释,以帮助读者了解它们在内部的工作方式。大多数方法是如此简短,简单,以至于它们不需要任何实现注释:有了代码和接口注释,就很容易弄清楚方法的工作原理。
|
||||||
|
|
||||||
|
The main goal of implementation comments is to help readers understand what the code is doing (not how it does it). Once readers know what the code is trying to do, it’s usually easy to understand how the code works. For short methods, the code only does one thing, which is already described in its interface comment, so no implementation comments are needed. Longer methods have several blocks of code that do different things as part of the method’s overall task. Add a comment before each of the major blocks to provide a high-level (more abstract) description of what that block does. Here is an example:
|
||||||
|
|
||||||
|
> 实现注释的主要目的是帮助读者理解代码在做什么(而不是代码如何工作)。一旦读者知道了代码要做什么,通常就很容易理解代码的工作原理。对于简短的方法,代码只做一件事,该问题已在其接口注释中进行了描述,因此不需要实现注释。较长的方法具有多个代码块,这些代码块作为方法的整体任务的一部分执行不同的操作。在每个主要块之前添加注释,以提供对该块的作用的高级(更抽象)描述。这是一个例子:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Phase 1: Scan active RPCs to see if any have completed.
|
||||||
|
```
|
||||||
|
|
||||||
|
For loops, it’s helpful to have a comment before the loop that describes what happens in each iteration:
|
||||||
|
|
||||||
|
> 对于循环,在循环前加一个注释来描述每次迭代中发生的事情是有帮助的:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Each iteration of the following loop extracts one request from
|
||||||
|
// the request message, increments the corresponding object, and
|
||||||
|
// appends a response to the response message.
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice how this comment describes the loop at a more abstract and intuitive level; it doesn’t go into any details about how a request is extracted from the request message or how the object is incremented. Loop comments are only needed for longer or more complex loops, where it may not be obvious what the loop is doing; many loops are short and simple enough that their behavior is already obvious.
|
||||||
|
|
||||||
|
> 请注意,此注释如何更抽象和直观地描述循环。它没有详细介绍如何从请求消息中提取请求或对象如何递增。仅对于更长或更复杂的循环才需要循环注释,在这种情况下,循环的作用可能并不明显。许多循环足够短且简单,以至于其行为已经很明显。
|
||||||
|
|
||||||
|
In addition to describing what the code is doing, implementation comments are also useful to explain why. If there are tricky aspects to the code that won’t be obvious from reading it, you should document them. For example, if a bug fix requires the addition of code whose purpose isn’t totally obvious, add a comment describing why the code is needed. For bug fixes where there is a well-written bug report describing the problem, the comment can refer to the issue in the bug tracking database rather than repeating all its details (“Fixes RAM-436, related to device driver crashes in Linux 2.4.x”). Developers can look in the bug database for more details (this is an example of avoiding duplication in comments, which will be discussed in Chapter 16).
|
||||||
|
|
||||||
|
> 除了描述代码在做什么之外,实现注释还有助于解释原因。如果代码中有些棘手的方面从阅读中看不出来,则应将它们记录下来。例如,如果一个错误修复程序需要添加目的不是很明显的代码,请添加注释以说明为什么需要该代码。对于错误修复,其中有写得很好的错误报告来描述问题,该注释可以引用错误跟踪数据库中的问题,而不是重复其所有详细信息(“修复 RAM-436,与 Linux 2.4 中的设备驱动程序崩溃有关。” X”)。开发人员可以在 bug 数据库中查找更多详细信息(这是一个避免注释重复的示例,这将在第 16 章中进行讨论)。
|
||||||
|
|
||||||
|
For longer methods, it can be helpful to write comments for a few of the most important local variables. However, most local variables don’t need documentation if they have good names. If all of the uses of a variable are visible within a few lines of each other, it’s usually easy to understand the variable’s purpose without a comment. In this case it’s OK to let readers read the code to figure out the meaning of the variable. However, if the variable is used over a large span of code, then you should consider adding a comment to describe the variable. When documenting variables, focus on what the variable represents, not how it is manipulated in the code.
|
||||||
|
|
||||||
|
> 对于更长的方法,为一些最重要的局部变量写注释会很有帮助。但是,如果大多数局部变量具有好名字,则不需要文档。如果变量的所有用法在几行之内都是可见的,则通常无需注释即可轻松理解变量的用途。在这种情况下,可以让读者阅读代码来弄清楚变量的含义。但是,如果在大量代码中使用了该变量,则应考虑添加注释以描述该变量。在记录变量时,应关注变量表示的内容,而不是代码中如何对其进行操作。
|
||||||
|
|
||||||
|
## 13.7 Cross-module design decisions 跨模块设计决策
|
||||||
|
|
||||||
|
In a perfect world, every important design decision would be encapsulated within a single class. Unfortunately, real systems inevitably end up with design decisions that affect multiple classes. For example, the design of a network protocol will affect both the sender and the receiver, and these may be implemented in different places. Cross-module decisions are often complex and subtle, and they account for many bugs, so good documentation for them is crucial.
|
||||||
|
|
||||||
|
> 在理想环境中,每个重要的设计决策都将封装在一个类中。不幸的是,真实的系统不可避免地最终会影响到多个类的设计决策。例如,网络协议的设计将影响发送方和接收方,并且它们可以在不同的地方实现。跨模块决策通常是复杂而微妙的,并且会导致许多错误,因此,为它们提供良好的文档至关重要。
|
||||||
|
|
||||||
|
The biggest challenge with cross-module documentation is finding a place to put it where it will naturally be discovered by developers. Sometimes there is an obvious central place to put such documentation. For example, the RAMCloud storage system defines a Status value, which is returned by each request to indicate success or failure. Adding a Status for a new error condition requires modifying many different files (one file maps Status values to exceptions, another provides a human-readable message for each Status, and so on). Fortunately, there is one obvious place where developers will have to go when adding a new status value, which is the declaration of the Status enum. We took advantage of this by adding comments in that enum to identify all of the other places that must also be modified:
|
||||||
|
|
||||||
|
> 跨模块文档的最大挑战是找到一个放置它的位置,以便开发人员自然地发现它。有时,放置此类文档的中心位置很明显。例如,RAMCloud 存储系统定义一个状态值,每个请求均返回该状态值以指示成功或失败。为新的错误状况添加状态需要修改许多不同的文件(一个文件将状态值映射到异常,另一个文件为每个状态提供人类可读的消息,依此类推)。幸运的是,添加新的状态值(即 Status 枚举的声明)时,开发人员必须去一个明显的地方。我们通过在该枚举中添加注释来标识所有其他必须修改的地方,从而利用了这一点:在理想环境中,每个重要的设计决策都将封装在一个类中。不幸的是,真实的系统不可避免地最终会影响到多个类的设计决策。例如,网络协议的设计将影响发送方和接收方,并且它们可以在不同的地方实现。跨模块决策通常是复杂而微妙的,并且会导致许多错误,因此,为它们提供良好的文档至关重要。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef enum Status {
|
||||||
|
STATUS_OK = 0,
|
||||||
|
STATUS_UNKNOWN_TABLET = 1,
|
||||||
|
STATUS_WRONG_VERSION = 2,
|
||||||
|
...
|
||||||
|
STATUS_INDEX_DOESNT_EXIST = 29,
|
||||||
|
STATUS_INVALID_PARAMETER = 30,
|
||||||
|
STATUS_MAX_VALUE = 30,
|
||||||
|
// Note: if you add a new status value you must make the following
|
||||||
|
// additional updates:
|
||||||
|
// (1) Modify STATUS_MAX_VALUE to have a value equal to the
|
||||||
|
// largest defined status value, and make sure its definition
|
||||||
|
// is the last one in the list. STATUS_MAX_VALUE is used
|
||||||
|
// primarily for testing.
|
||||||
|
// (2) Add new entries in the tables "messages" and "symbols" in
|
||||||
|
// Status.cc.
|
||||||
|
// (3) Add a new exception class to ClientException.h
|
||||||
|
// (4) Add a new "case" to ClientException::throwException to map
|
||||||
|
// from the status value to a status-specific ClientException
|
||||||
|
// subclass.
|
||||||
|
// (5) In the Java bindings, add a static class for the exception
|
||||||
|
// to ClientException.java
|
||||||
|
// (6) Add a case for the status of the exception to throw the
|
||||||
|
// exception in ClientException.java
|
||||||
|
// (7) Add the exception to the Status enum in Status.java, making
|
||||||
|
// sure the status is in the correct position corresponding to
|
||||||
|
// its status code.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
New status values will be added at the end of the existing list, so the comments are also placed at the end, where they are most likely to be seen.
|
||||||
|
|
||||||
|
> 新状态值将添加到现有列表的末尾,因此注释也将放置在最有可能出现的末尾。
|
||||||
|
|
||||||
|
Unfortunately, in many cases there is not an obvious central place to put cross-module documentation. One example from the RAMCloud storage system was the code for dealing with zombie servers, which are servers that the system believes have crashed, but in fact are still running. Neutralizing zombie servers required code in several different modules, and these pieces of code all depend on each other. None of the pieces of code is an obvious central place to put documentation. One possibility is to duplicate parts of the documentation in each location that depends on it. However, this is awkward, and it is difficult to keep such documentation up to date as the system evolves. Alternatively, the documentation can be located in one of the places where it is needed, but in this case it’s unlikely that developers will see the documentation or know where to look for it.
|
||||||
|
|
||||||
|
> 不幸的是,在许多情况下,并没有一个明显的中心位置来放置跨模块文档。RAMCloud 存储系统中的一个例子是处理僵尸服务器的代码,僵尸服务器是系统认为已经崩溃但实际上仍在运行的服务器。中和 zombie server 需要几个不同模块中的代码,这些代码都相互依赖。没有一段代码明显是放置文档的中心位置。一种可能性是在每个依赖文档的位置复制文档的部分。然而,这是令人尴尬的,并且随着系统的发展,很难使这样的文档保持最新。或者,文档可以位于需要它的位置之一,但是在这种情况下,开发人员不太可能看到文档或者知道在哪里查找它。
|
||||||
|
|
||||||
|
I have recently been experimenting with an approach where cross-module issues are documented in a central file called designNotes. The file is divided up into clearly labeled sections, one for each major topic. For example, here is an excerpt from the file:
|
||||||
|
|
||||||
|
> 我最近一直在尝试一种方法,该方法将跨模块问题记录在一个名为 designNotes 的中央文件中。该文件分为清楚标记的部分,每个主要主题一个。例如,以下是该文件的摘录:
|
||||||
|
|
||||||
|
```
|
||||||
|
...
|
||||||
|
Zombies
|
||||||
|
-------
|
||||||
|
A zombie is a server that is considered dead by the rest of the
|
||||||
|
cluster; any data stored on the server has been recovered and will
|
||||||
|
be managed by other servers. However, if a zombie is not actually
|
||||||
|
dead (e.g., it was just disconnected from the other servers for a
|
||||||
|
while) two forms of inconsistency can arise:
|
||||||
|
* A zombie server must not serve read requests once replacement servers have taken over; otherwise it may return stale data that does not reflect writes accepted by the replacement servers.
|
||||||
|
* The zombie server must not accept write requests once replacement servers have begun replaying its log during recovery; if it does, these writes may be lost (the new values may not be stored on the replacement servers and thus will not be returned by reads).
|
||||||
|
|
||||||
|
RAMCloud uses two techniques to neutralize zombies. First,
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in any piece of code that relates to one of these issues there is a short comment referring to the designNotes file:
|
||||||
|
|
||||||
|
> 然后,在与这些问题之一相关的任何代码段中,都有一条简短的注释引用了 designNotes 文件:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// See "Zombies" in designNotes.
|
||||||
|
```
|
||||||
|
|
||||||
|
With this approach, there is only a single copy of the documentation and it is relatively easy for developers to find it when they need it. However, this has the disadvantage that the documentation is not near any of the pieces of code that depend on it, so it may be difficult to keep up-to-date as the system evolves.
|
||||||
|
|
||||||
|
> 使用这种方法,文档只有一个副本,因此开发人员在需要时可以相对容易地找到它。但是,这样做的缺点是,文档离它依赖的任何代码段都不近,因此随着系统的发展,可能难以保持最新。
|
||||||
|
|
||||||
|
## 13.8 Conclusion 结论
|
||||||
|
|
||||||
|
The goal of comments is to ensure that the structure and behavior of the system is obvious to readers, so they can quickly find the information they need and make modifications to the system with confidence that they will work. Some of this information can be represented in the code in a way that will already be obvious to readers, but there is a significant amount of information that can’t easily be deduced from the code. Comments fill in this information.
|
||||||
|
|
||||||
|
> 评论的目的是确保系统的结构和行为对读者来说是显而易见的,因此他们可以快速找到所需的信息,并有信心对其进行修改,以对系统进行修改。这些信息中的某些信息可以以对读者来说显而易见的方式表示在代码中,但是有大量信息无法从代码中轻易推导出。注释将填写此信息。
|
||||||
|
|
||||||
|
When following the rule that comments should describe things that aren’t obvious from the code, “obvious” is from the perspective of someone reading your code for the first time (not you). When writing comments, try to put yourself in the mindset of the reader and ask yourself what are the key things he or she will need to know. If your code is undergoing review and a reviewer tells you that something is not obvious, don’t argue with them; if a reader thinks it’s not obvious, then it’s not obvious. Instead of arguing, try to understand what they found confusing and see if you can clarify that, either with better comments or better code.
|
||||||
|
|
||||||
|
> 当遵循注释应描述代码中不明显的内容的规则时,“明显”是从第一次读取您的代码的人(不是您)的角度出发。在撰写评论时,请尝试使自己进入读者的心态,并问自己他或她需要知道哪些关键事项。如果您的代码正在接受审核,并且审核者告诉您某些不明显的内容,请不要与他们争论。如果读者认为它不明显,那么它就不明显。不用争论,而是尝试了解他们发现的令人困惑的地方,并查看是否可以通过更好的注释或更好的代码来澄清它们。
|
||||||
|
|
||||||
|
## 13.9 Answers to questions from Section 13.5 回答第 13.5 节中的问题
|
||||||
|
|
||||||
|
Does a developer need to know each of the following pieces of information in order to use the IndexLookup class?
|
||||||
|
|
||||||
|
> 开发人员是否需要了解以下每条信息才能使用 IndexLookup 类?
|
||||||
|
|
||||||
|
1. The format of messages that the IndexLookup class sends to the servers holding indexes and objects. No: this is an implementation detail that should be hidden within the class.
|
||||||
|
2. The comparison function used to determine whether a particular object falls in the desired range (is comparison done using integers, floating-point numbers, or strings?). Yes: users of the class need to know this information.
|
||||||
|
3. The data structure used to store indexes on servers. No: this information should be encapsulated on the servers; not even the implementation of IndexLookup should need to know this.
|
||||||
|
4. Whether or not IndexLookup issues multiple requests to different servers concurrently. Possibly: if IndexLookup uses special techniques to improve performance, then the documentation should provide some high-level information about this, since users may care about performance.
|
||||||
|
5. The mechanism for handling server crashes. No: RAMCloud recovers automatically from server crashes, so crashes are not visible to application-level software; thus, there is no need to mention crashes in the interface documentation for IndexLookup. If crashes were reflected up to applications, then the interface documentation would need to describe how they manifest themselves (but not the details of how crash recovery works).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 1. IndexLookup 类发送给包含索引和对象的服务器的消息格式。否:这是应隐藏在类中的实现细节。
|
||||||
|
> 2. 用于确定特定对象是否在所需范围内的比较功能(使用整数,浮点数或字符串进行比较吗?)。是:该课程的用户需要了解此信息。
|
||||||
|
> 3. 用于在服务器上存储索引的数据结构。否:此信息应封装在服务器上;甚至 IndexLookup 的实现都不需要知道这一点。
|
||||||
|
> 4. IndexLookup 是否同时向多个服务器发出多个请求。可能:如果 IndexLookup 使用特殊技术来提高性能,则文档应提供有关此问题的一些高级信息,因为用户可能会在意性能。
|
||||||
|
> 5. 处理服务器崩溃的机制。否:RAMCloud 可从服务器崩溃中自动恢复,因此崩溃对于应用程序级软件不可见;因此,在 IndexLookup 的接口文档中无需提及崩溃。如果崩溃反映到应用程序中,则接口文档将需要描述它们如何表现出来(而不是崩溃恢复如何工作的详细信息)。
|
||||||
225
docs/ch14.md
Normal file
225
docs/ch14.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# 第 14 章 选择的名字
|
||||||
|
|
||||||
|
> Chapter 14 Choosing Names
|
||||||
|
|
||||||
|
Selecting names for variables, methods, and other entities is one of the most underrated aspects of software design. Good names are a form of documentation: they make code easier to understand. They reduce the need for other documentation and make it easier to detect errors. Conversely, poor name choices increase the complexity of code and create ambiguities and misunderstandings that can result in bugs. Name choice is an example of the principle that complexity is incremental. Choosing a mediocre name for a particular variable, as opposed to the best possible name, probably won’t have much impact on the overall complexity of a system. However, software systems have thousands of variables; choosing good names for all of these will have a significant impact on complexity and manageability.
|
||||||
|
|
||||||
|
> 为变量,方法和其他实体选择名称是软件设计中被低估的方面之一。良好的名字是一种文档形式:它们使代码更易于理解。它们减少了对其他文档的需求,并使检测错误更加容易。相反,名称选择不当会增加代码的复杂性,并造成可能导致错误的歧义和误解。名称选择是复杂度是递增的原理的一个示例。为特定变量选择一个平庸的名称,而不是最好的名称,这可能不会对系统的整体复杂性产生太大影响。但是,软件系统具有数千个变量。为所有这些选择好名字将对复杂性和可管理性产生重大影响。
|
||||||
|
|
||||||
|
## 14.1 Example: bad names cause bugs 示例:名称错误会导致错误
|
||||||
|
|
||||||
|
Sometimes even a single poorly named variable can have severe consequences. The most challenging bug I ever fixed came about because of a poor name choice. In the late 1980’s and early 1990’s my graduate students and I created a distributed operating system called Sprite. At some point we noticed that files would occasionally lose data: one of the data blocks suddenly became all zeroes, even though the file had not been modified by a user. The problem didn’t happen very often, so it was exceptionally difficult to track down. A few of the graduate students tried to find the bug, but they were unable to make progress and eventually gave up. However, I consider any unsolved bug to be an intolerable personal insult, so I decided to track it down.
|
||||||
|
|
||||||
|
> 有时,即使是一个名称不正确的变量也会产生严重的后果。我曾经修复过的最具挑战性的错误是由于名称选择不当造成的。在 1980 年代末和 1990 年代初,我的研究生和我创建了一个名为 Sprite 的分布式操作系统。在某个时候,我们注意到文件偶尔会丢失数据:即使用户未修改文件,数据块之一突然变为全零。该问题并不经常发生,因此很难追踪。一些研究生试图找到该错误,但他们未能取得进展,最终放弃了。但是,我认为任何未解决的错误都是无法忍受的个人侮辱,因此我决定对其进行跟踪。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 花了六个月的时间,但我最终找到并修复了该错误。这个问题实际上很简单(就像大多数错误一样,一旦找出它们)。文件系统代码将变量名块用于两个不同的目的。在某些情况下,块是指磁盘上的物理块号。在其他情况下,块是指文件中的逻辑块号。不幸的是,在代码的某一点上有一个包含逻辑块号的块变量,但是在需要物理块号的情况下意外地使用了它。结果,磁盘上无关的块被零覆盖。
|
||||||
|
|
||||||
|
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。
|
||||||
|
|
||||||
|
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 创建图像
|
||||||
|
|
||||||
|
When choosing a name, the goal is to create an image in the mind of the reader about the nature of the thing being named. A good name conveys a lot of information about what the underlying entity is, and, just as important, what it is not. When considering a particular name, ask yourself: “If someone sees this name in isolation, without seeing its declaration, its documentation, or any code that uses the name, how closely will they be able to guess what the name refers to? Is there some other name that will paint a clearer picture?” Of course, there is a limit to how much information you can put in a single name; names become unwieldy if they contain more than two or three words. Thus, the challenge is to find just a few words that capture the most important aspects of the entity.
|
||||||
|
|
||||||
|
> 选择名称时,目标是在读者的脑海中创建一幅关于被命名事物的性质的图像。一个好名字传达了很多有关底层实体是什么,以及同样重要的是,不是什么的信息。在考虑特定名称时,请问自己:“如果有人孤立地看到该名称,而没有看到其声明,文档或使用该名称的任何代码,他们将能够猜到该名称指的是什么?还有其他名称可以使画面更清晰吗?” 当然,一个名字可以输入多少信息是有限制的。如果名称包含两个或三个以上的单词,则会变得笨拙。因此,面临的挑战是仅找到捕获实体最重要方面的几个单词。
|
||||||
|
|
||||||
|
Names are a form of abstraction: they provide a simplified way of thinking about a more complex underlying entity. Like other forms of abstraction, the best names are those that focus attention on what is most important about the underlying entity while omitting details that are less important.
|
||||||
|
|
||||||
|
> 名称是一种抽象形式:名称提供了一种简化的方式来考虑更复杂的基础实体。像其他形式的抽象一样,最好的名字是那些将注意力集中在对底层实体最重要的东西上,而忽略那些次要的细节。
|
||||||
|
|
||||||
|
## 14.3 Names should be precise 名称应准确
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
> 良好名称具有两个属性:精度和一致性。让我们从精度开始。名称最常见的问题是名称太笼统或含糊不清。结果,读者很难说出这个名字指的是什么。读者可能会认为该名称所指的是与现实不符的事物,如上面的代码错误所示。考虑以下方法声明:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* Returns the total number of indexlets this object is managing.
|
||||||
|
*/
|
||||||
|
int IndexletManager::getCount() {...}
|
||||||
|
```
|
||||||
|
|
||||||
|
The term “count” is too generic: count of what? If someone sees an invocation of this method, they are unlikely to know what it does unless they read its documentation. A more precise name like getActiveIndexlets or numIndexlets would be better: with one of these names, readers will probably be able to guess what the method returns without having to look at its documentation.
|
||||||
|
|
||||||
|
> 术语“计数”太笼统了:计数什么?如果有人看到此方法的调用,除非他们阅读了它的文档,否则他们不太可能知道它的作用。像 getActiveIndexlets 或 numIndexlets 这样的更精确的名称会更好:使用这些名称之一,读者可能无需查看其文档就能猜测该方法返回的内容。
|
||||||
|
|
||||||
|
Here are some other examples of names that aren’t precise enough, taken from various student projects:
|
||||||
|
|
||||||
|
> 以下是来自其他学生项目的一些名称不够精确的示例:
|
||||||
|
|
||||||
|
- A project building a GUI text editor used the names x and y to refer to the position of a character in the file. These names are too generic. They could mean many things; for example, they might also represent the coordinates (in pixels) of a character on the screen. Someone seeing the name x in isolation is unlikely to think that it refers to the position of a character within a line of text. The code would be clearer if it used names such as charIndex and lineIndex, which reflect the specific abstractions that the code implements.
|
||||||
|
> 建立 GUI 文本编辑器的项目使用名称 x 和 y 来引用字符在文件中的位置。这些名称太笼统了。他们可能意味着很多事情;例如,它们也可能代表屏幕上字符的坐标(以像素为单位)。单独看到名称 x 的人不太可能会认为它是指字符在一行文本中的位置。如果使用诸如 charIndex 和 lineIndex 之类的名称来反映代码实现的特定抽象,该代码将更加清晰。
|
||||||
|
- Another editor project contained the following code:
|
||||||
|
|
||||||
|
> 另一个编辑器项目包含以下代码:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Blink state: true when cursor visible.
|
||||||
|
private boolean blinkStatus = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
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 无法传达足够的信息。“状态”一词对于布尔值来说太含糊了:它不提供关于真值或假值含义的任何线索。“闪烁”一词也含糊不清,因为它并不表示闪烁的内容。以下替代方法更好:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Controls cursor blinking: true means the cursor is visible,
|
||||||
|
// false means the cursor is not displayed.
|
||||||
|
private boolean cursorVisible = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
The name cursorVisible conveys more information; for example, it allows readers to guess what a true value means (as a general rule, names of boolean variables should always be predicates). The word “blink” is no longer in the name, so readers will have to consult the documentation if they want to know why the cursor isn’t always visible; this information is less important.
|
||||||
|
|
||||||
|
> 名称 cursorVisible 传达了更多信息;例如,它允许读者猜测一个真值的含义(通常,布尔变量的名称应始终为谓词)。名称中不再包含“ blink”一词,因此,如果读者想知道为什么光标不总是可见,则必须查阅文档。此信息不太重要。
|
||||||
|
|
||||||
|
- A project implementing a consensus protocol contained the following code:
|
||||||
|
|
||||||
|
> 一个实施共识协议的项目包含以下代码:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Value representing that the server has not voted (yet) for
|
||||||
|
// anyone for the current election term.
|
||||||
|
private static final String VOTED_FOR_SENTINEL_VALUE = "null";
|
||||||
|
```
|
||||||
|
|
||||||
|
The name for this value indicates that it’s special but it doesn’t say what the special meaning is. A more specific name such as NOT_YET_VOTED would be better.
|
||||||
|
|
||||||
|
> 此值的名称表示它是特殊的,但没有说明特殊含义是什么。使用更具体的名称(例如 NOT_YET_VOTED)会更好。
|
||||||
|
|
||||||
|
- A variable named result was used in a method with no return value. This name has multiple problems. First, it creates the misleading impression that it will be the return value of the method. Second, it provides essentially no information about what it actually holds, except that it is some computed value. The name should provide information about what the result actually is, such as mergedLine or totalChars. In methods that do actually have return values, then using the name result is reasonable. This name is still a bit generic, but readers can look at the method documentation to see its meaning, and it’s helpful to know that the value will eventually become the return value.
|
||||||
|
|
||||||
|
> 在没有返回值的方法中使用了名为 result 的变量。这个名字有多个问题。首先,它会产生误导性的印象,即它将作为方法的返回值。其次,除了它是一些计算值外,它实际上不提供有关其实际持有内容的任何信息。该名称应提供有关实际结果是什么的信息,例如 mergedLine 或 totalChars。在实际上确实具有返回值的方法中,使用名称结果是合理的。该名称仍然有点通用,但是读者可以查看方法文档以了解其含义,这有助于知道该值最终将成为返回值。
|
||||||
|
|
||||||
|
img Red Flag: Vague Name img
|
||||||
|
|
||||||
|
If a variable or method name is broad enough to refer to many different things, then it doesn’t convey much information to the developer and the underlying entity is more likely to be misused.
|
||||||
|
|
||||||
|
> 如果变量或方法的名称足够广泛,可以引用许多不同的事物,那么它不会向开发人员传达太多信息,因此底层实体很可能会被滥用。
|
||||||
|
|
||||||
|
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)用作循环迭代变量。如果您可以看到一个变量的整个用法范围,那么该变量的含义在代码中就很明显了,因此您不需要长名称。例如,考虑以下代码:
|
||||||
|
|
||||||
|
```java
|
||||||
|
for (i = 0; i < numLines; i++) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It’s clear from this code that i is being used to iterate over each of the lines in some entity. If the loop gets so long that you can’t see it all at once, or if the meaning of the iteration variable is harder to figure out from the code, then a more descriptive name is in order.
|
||||||
|
|
||||||
|
> 从这段代码中很明显,我正被用来迭代某个实体中的每一行。如果循环时间太长,以至于您无法一次看到全部内容,或者如果很难从代码中找出迭代变量的含义,那么应该使用更具描述性的名称。
|
||||||
|
|
||||||
|
It’s also possible for a name to be too specific, such as in this declaration for a method that deletes a range of text:
|
||||||
|
|
||||||
|
> 名称也可能太具体,例如在此声明中删除一个文本范围的方法:
|
||||||
|
|
||||||
|
```java
|
||||||
|
void delete(Range selection) {...}
|
||||||
|
```
|
||||||
|
|
||||||
|
The argument name selection is too specific, since it suggests that the text being deleted is always selected in the user interface. However, this method can be invoked on any range of text, selected or not. Thus, the argument name should be more generic, such as range.
|
||||||
|
|
||||||
|
> 参数名称的选择过于具体,因为它建议始终在用户界面中选择要删除的文本。但是,可以在任意范围的文本(无论是否选中)上调用此方法。因此,参数名称应更通用,例如范围。
|
||||||
|
|
||||||
|
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 一致使用名称
|
||||||
|
|
||||||
|
The second important property of good names is consistency. In any program there are certain variables that are used over and over again. For example, a file system manipulates block numbers repeatedly. For each of these common usages, pick a name to use for that purpose, and use the same name everywhere. For example, a file system might always use fileBlock to hold the index of a block within a file. Consistent naming reduces cognitive load in much the same way as reusing a common class: once the reader has seen the name in one context, they can reuse their knowledge and instantly make assumptions when they see the name in a different context.
|
||||||
|
|
||||||
|
> 名誉的第二个重要属性是一致性。在任何程序中,都会反复使用某些变量。例如,文件系统反复操作块号。对于每种常见用法,请选择一个用于该目的的名称,并在各处使用相同的名称。例如,文件系统可能总是使用 fileBlock 来保存文件中块的索引。一致的命名方式与重用普通类的方式一样,可以减轻认知负担:一旦读者在一个上下文中看到了该名称,他们就可以重用其知识并在不同上下文中看到该名称时立即做出假设。
|
||||||
|
|
||||||
|
Consistency has three requirements: first, always use the common name for the given purpose; second, never use the common name for anything other than the given purpose; third, make sure that the purpose is narrow enough that all variables with the name have the same behavior. This third requirement was violated in the file system bug at the beginning of the chapter. The file system used block for variables with two different behaviors (file blocks and disk blocks); this led to a false assumption about the meaning of a variable, which in turn resulted in a bug.
|
||||||
|
|
||||||
|
> 一致性具有三个要求:首先,始终将通用名称用于给定目的;第二,除了给定目的外,切勿使用通用名称;第三,确保目的足够狭窄,以使所有具有名称的变量都具有相同的行为。在本章开头的文件系统错误中违反了此第三项要求。文件系统使用块来表示具有两种不同行为的变量(文件块和磁盘块);这导致对变量含义的错误假设,进而导致错误。
|
||||||
|
|
||||||
|
Sometimes you will need multiple variables that refer to the same general sort of thing. For example, a method that copies file data will need two block numbers, one for the source and one for the destination. When this happens, use the common name for each variable but add a distinguishing prefix, such as srcFileBlock and dstFileBlock.
|
||||||
|
|
||||||
|
> 有时您将需要多个变量来引用相同的一般事物。例如,一种复制文件数据的方法将需要两个块号,一个为源,一个为目标。发生这种情况时,请对每个变量使用通用名称,但要添加一个可区分的前缀,例如 srcFileBlock 和 dstFileBlock。
|
||||||
|
|
||||||
|
Loops are another area where consistent naming can help. If you use names such as i and j for loop variables, always use i in outermost loops and j for nested loops. This allows readers to make instant (safe) assumptions about what’s happening in the code when they see a given name.
|
||||||
|
|
||||||
|
> 循环是一致性命名可以提供帮助的另一个领域。如果将诸如 i 和 j 之类的名称用于循环变量,则始终在最外层循环中使用 i,而在嵌套循环中始终使用 j。这使读者可以在看到给定名称时对代码中发生的事情做出即时(安全)假设。
|
||||||
|
|
||||||
|
## 14.5 A different opinion: Go style guide 不同的意见:Go 样式指南
|
||||||
|
|
||||||
|
Not everyone shares my views about naming. Some of the developers of the Go language argue that names should be very short, often only a single character. In a presentation on name choice for Go, Andrew Gerrand states that “long names obscure what the code does.”1 He presents this code sample, which uses single-letter variable names:
|
||||||
|
|
||||||
|
> 并非所有人都同意我对命名的看法。一些使用 Go 语言的开发人员认为,名称应该非常简短,通常只能是一个字符。在关于 Go 的名称选择的演示中,Andrew Gerrand 指出“长名称模糊了代码的作用。” 1 他介绍了此代码示例,该示例使用单字母变量名:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func RuneCount(b []byte) int {
|
||||||
|
i, n := 0, 0
|
||||||
|
for i < len(b) {
|
||||||
|
if b[i] < RuneSelf {
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
_, size := DecodeRune(b[i:])
|
||||||
|
i += size
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and argues that it is more readable than the following version, which uses longer names:
|
||||||
|
|
||||||
|
> 并认为它比以下使用更长名称的版本更具可读性:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func RuneCount(buffer []byte) int {
|
||||||
|
index, count := 0, 0
|
||||||
|
for index < len(buffer) {
|
||||||
|
if buffer[index] < RuneSelf {
|
||||||
|
index++
|
||||||
|
} else {
|
||||||
|
_, size := DecodeRune(buffer[index:])
|
||||||
|
index += size
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Personally, I don’t find the second version any more difficult to read than the first. If anything, the name count gives a slightly better clue to the behavior of the variable than n. With the first version I ended up reading through the code trying to figure out what n means, whereas I didn’t feel that need with the second version. But, if n is used consistently throughout the system to refer to counts (and nothing else), then the short name will probably be clear to other developers.
|
||||||
|
|
||||||
|
> 就个人而言,我不觉得第二版比第一版更难读。如果有的话,与 n 相比,名称计数为变量的行为提供了更好的线索。在第一个版本中,我最终通读了代码,试图弄清楚 n 的含义,而第二个版本中我并没有这种需要。但是,如果在整个系统中一致地使用 n 来引用计数(而没有其他内容),那么其他开发人员可能会清楚知道该短名称。
|
||||||
|
|
||||||
|
The Go culture encourages the use of the same short name for multiple different things: ch for character or channel, d for data, difference, or distance, and so on. To me, ambiguous names like these are likely to result in confusion and error, just as in the block example.
|
||||||
|
|
||||||
|
> Go 文化鼓励在多个不同的事物上使用相同的短名称:ch 用于字符或通道,d 用于数据,差异或距离,等等。对我来说,像这样的模棱两可的名称很可能导致混乱和错误,就像在示例中一样。
|
||||||
|
|
||||||
|
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”(使用语言简称)会识别出几种此类抱怨)。同样,如果我开始抱怨长变量名使我的代码难以阅读,那么我会考虑使用较短的变量名。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> Gerrand 发表一个我同意的评论:“名称声明与使用之间的距离越大,名称就应该越长。” 前面有关使用名为 i 和 j 的循环变量的讨论是此规则的示例。
|
||||||
|
|
||||||
|
## 14.6 Conclusion 结论
|
||||||
|
|
||||||
|
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 章讨论的投资思维方式的一个示例:如果您花一些额外的时间来选择好名字,那么将来您将更容易处理代码。此外,您不太可能引入错误。培养命名技巧也是一项投资。当您第一次决定停止为平庸的名字定居时,您会发现想出好名字的过程既令人沮丧又耗时。但是,随着您获得更多的经验,您会发现它变得更加容易。最终,您将几乎不需要花费额外的时间来选择好名字,因此您几乎可以免费获得好处。
|
||||||
|
|
||||||
|
1https://talks.golang.org/2014/names.slide#1
|
||||||
93
docs/ch15.md
Normal file
93
docs/ch15.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 第 15 章 先写注释
|
||||||
|
|
||||||
|
> Chapter 15 Write The Comments First(Use Comments As Part Of The Design Process)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 即使你有自律性回去写注释(不要欺骗你自己:你可能没有),注释也不会很好。在这个过程的这个时候,你已经在精神上离开了。在你的脑海中,这段代码已经完成了;你急于开始下一个项目。你知道写注释是正确的事情,但它没有乐趣。你只想尽快度过难关。因此,您可以快速地浏览代码,添加足够的注释以使其看起来令人满意。到目前为止,您已经有一段时间没有设计代码了,所以您对设计过程的记忆变得模糊了。您在编写注释时查看代码,因此注释重复了代码。即使您试图重构代码中不明显的设计思想,也会有您不记得的事情。因此,这些注释忽略了他们应该描述的一些最重要的事情。
|
||||||
|
|
||||||
|
## 15.2 Write the comments first 首先写注释
|
||||||
|
|
||||||
|
I use a different approach to writing comments, where I write the comments at the very beginning:
|
||||||
|
|
||||||
|
> 我使用一种不同的方法来编写注释,在开始时就写注释:
|
||||||
|
|
||||||
|
- For a new class, I start by writing the class interface comment.
|
||||||
|
- Next, I write interface comments and signatures for the most important public methods, but I leave the method bodies empty.
|
||||||
|
- I iterate a bit over these comments until the basic structure feels about right.
|
||||||
|
- At this point I write declarations and comments for the most important class instance variables in the class.
|
||||||
|
- Finally, I fill in the bodies of the methods, adding implementation comments as needed.
|
||||||
|
- While writing method bodies, I usually discover the need for additional methods and instance variables. For each new method I write the interface comment before the body of the method; for instance variables I fill in the comment at the same time that I write the variable declaration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> - 对于新类,我首先编写类接口注释。
|
||||||
|
> - 接下来,我为最重要的公共方法编写接口注释和签名,但将方法主体保留为空。
|
||||||
|
> - 我对这些注释进行了迭代,直到基本结构感觉正确为止。
|
||||||
|
> - 在这一点上,我为类中最重要的类实例变量编写了声明和注释。
|
||||||
|
> - 最后,我填写方法的主体,并根据需要添加实现注释。
|
||||||
|
> - 在编写方法主体时,我通常会发现需要其他方法和实例变量。对于每个新方法,我在方法主体之前编写接口注释。例如变量,我在编写变量声明的同时填写了注释。
|
||||||
|
|
||||||
|
When the code is done, the comments are also done. There is never a backlog of unwritten comments.
|
||||||
|
|
||||||
|
> 代码完成后,注释也将完成。从来没有积压的书面注释。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 在开始时编写注释的第二个也是最重要的好处是可以改善系统设计。注释提供了完全捕获抽象的唯一方法,好的抽象是好的系统设计的基础。如果您在一开始就写了描述抽象的注释,则可以在编写实现代码之前对其进行检查和调整。要写一个好的注释,您必须确定一个变量或一段代码的本质:这件事最重要的方面是什么?在设计过程的早期进行此操作很重要;否则,您只是在破解代码。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 注释是复杂煤矿中的金丝雀。如果方法或变量需要较长的注释,则它是一个危险信号,表明您没有很好的抽象。请记住,在第 4 章中,类应该很深:最好的类具有非常简单的接口,但可以实现强大的功能。判断接口复杂性的最佳方法是从描述接口的注释中进行。如果某个方法的接口注释提供了使用该方法所需的所有信息,并且又简短又简单,则表明该方法具有简单的接口。相反,如果没有冗长而复杂的注释无法完全描述一个方法,则该方法具有复杂的接口。您可以将方法的接口注释与实现进行比较,以了解该方法的深度:如果接口注释必须描述实现的所有主要功能,则该方法很浅。同样的想法也适用于变量:如果要花很长的时间来完整描述一个变量,那是一个危险信号,表明您可能没有选择正确的变量分解。总体而言,编写注释的行为使您可以及早评估设计决策,以便发现并解决问题。
|
||||||
|
|
||||||
|
img Red Flag: Hard to Describe img
|
||||||
|
|
||||||
|
The comment that describes a method or variable should be simple and yet complete. If you find it difficult to write such a comment, that’s an indicator that there may be a problem with the design of the thing you are describing.
|
||||||
|
|
||||||
|
> 描述方法或变量的注释应该简单而完整。如果您发现很难写这样的注释,则表明您所描述的内容的设计可能存在问题。
|
||||||
|
|
||||||
|
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%以上。将注释延迟到最后只会节省其中的一小部分,这不是很多。
|
||||||
|
|
||||||
|
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 结论
|
||||||
|
|
||||||
|
If you haven’t ever tried writing the comments first, give it a try. Stick with it long enough to get used to it. Then think about how it affects the quality of your comments, the quality of your design, and your overall enjoyment of software development. After you have tried this for a while, let me know whether your experience matches mine, and why or why not.
|
||||||
|
|
||||||
|
> 如果您从未尝试过先编写注释,请尝试一下。坚持足够长的时间来习惯它。然后考虑它如何影响您的注释质量,设计质量以及软件开发的整体乐趣。在尝试了一段时间之后,让我知道您的经历是否与我的相符,以及为什么或为什么不这样。
|
||||||
114
docs/ch16.md
Normal file
114
docs/ch16.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# 第 16 章 修改现有的代码
|
||||||
|
|
||||||
|
> Chapter 16 Modifying Existing Code
|
||||||
|
|
||||||
|
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 章介绍了软件开发是如何迭代和增量的。大型软件系统是通过一系列演化阶段开发的,其中每个阶段都添加了新功能并修改了现有模块。这意味着系统的设计在不断发展。一开始就不可能为系统设计正确的设计。一个成熟的系统的设计更多地取决于系统演化过程中所做的更改,而不是任何初始概念。前面的章节描述了如何在初始设计和实现过程中降低复杂性。本章讨论如何防止随着系统的发展而增加复杂性。
|
||||||
|
|
||||||
|
## 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 章介绍了战术编程和战略编程之间的区别:在战术编程中,主要目标是使某些事物快速工作,即使这会导致额外的复杂性;在战略编程中,最重要的目标是进行出色的系统设计。战术方法很快导致系统设计混乱。如果您想要一个易于维护和增强的系统,那么“工作”还不够高。您必须优先考虑设计并进行战略思考。当您修改现有代码时,此想法也适用。
|
||||||
|
|
||||||
|
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 页介绍的投资心态的一个示例:如果您花费一些额外的时间来重构和改善系统设计,您将得到一个更干净的系统。这将加快开发速度,您将收回在重构方面投入的精力。即使您的特定更改不需要重构,您仍然应该注意在代码中可以修复的设计缺陷。每当您修改任何代码时,都尝试在该过程中至少找到一点方法来改进系统设计。如果您没有使设计更好,则可能会使它变得更糟。
|
||||||
|
|
||||||
|
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 章所述,投资心态有时与商业软件开发的现实相冲突。如果“正确的方式”重构系统需要三个月,而快速且肮脏的修复仅需两个小时,则您可能必须采取快速而肮脏的方法,尤其是在紧迫的期限内工作时。或者,如果重构系统会造成影响许多其他人员和团队的不兼容性,则重构可能不切实际。
|
||||||
|
|
||||||
|
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 个月的重构一样干净,但是可以在几天内完成?或者,如果您现在负担不起大型重构,请让您的老板为您分配时间,让您在当前截止日期之后恢复到原来的水平。每个开发组织都应计划将其全部工作的一小部分用于清理和重构;从长远来看,这项工作将收回成本。
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
> 确保注释更新的最佳方法是将注释放置在它们描述的代码附近,以便开发人员在更改代码时可以看到它们。注释离其关联的代码越远,正确更新的可能性就越小。例如,方法界面注释的最佳位置是在代码文件中,紧靠该方法主体的位置。对方法的任何更改都将涉及此代码,因此开发人员很可能会看到界面注释,并在需要时进行更新。
|
||||||
|
|
||||||
|
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 都会提取文档并将其呈现给用户,例如在键入方法名称时显示方法的文档。给定诸如此类的工具,文档应位于对开发人员进行代码开发最方便的位置。
|
||||||
|
|
||||||
|
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:
|
||||||
|
// Phase 1: Find feasible candidates
|
||||||
|
// Phase 2: Assign each candidate a score
|
||||||
|
// Phase 3: Choose the best, and remove it
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional details can be documented just above the code for each phase.
|
||||||
|
|
||||||
|
> 每个阶段的代码上方都可以记录其他详细信息。
|
||||||
|
|
||||||
|
In general, the farther a comment is from the code it describes, the more abstract it should be (this reduces the likelihood that the comment will be invalidated by code changes).
|
||||||
|
|
||||||
|
> 通常,注释离描述的代码越远,注释应该越抽象(这减少了注释因代码更改而无效的可能性)。
|
||||||
|
|
||||||
|
## 16.3 Comments belong in the code, not the commit log 注释属于代码,而不是提交日志
|
||||||
|
|
||||||
|
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 中的注释以了解下面代码的解释。“如果引用因为主注释被移动或删除而变得过时,这种不一致性将是不言而喻的,因为开发人员将无法在指定的位置找到注释;他们可以使用修订控制历史记录来查找注释发生了什么,然后更新引用。相反,如果文档是重复的,并且一些副本没有得到更新,那么开发人员就不会知道他们使用的是陈旧的信息。
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
> 如果信息已经在程序之外的某个地方记录了,不要在程序内部重复记录;只需参考外部文档。例如,如果您编写一个实现 HTTP 协议的类,那么就不需要在代码中描述 HTTP 协议。在网上已经有很多关于这个文档的来源;只需在您的代码中添加一个简短的注释,并为其中一个源添加一个 URL。另一个例子是已经在用户手册中记录的特性。假设您正在编写一个实现命令集合的程序,其中有一个负责实现每个命令的方法。如果有描述这些命令的用户手册,就不需要在代码中重复这些信息。相反,在每个命令方法的接口注释中包含如下简短说明:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Implements the Foo command; see the user manual for details.
|
||||||
|
```
|
||||||
|
|
||||||
|
It’s important that readers can easily find all the documentation needed to understand your code, but that doesn’t mean you have to write all of that documentation.
|
||||||
|
|
||||||
|
> 读者可以轻松找到理解代码所需的所有文档,这一点很重要,但这并不意味着您必须编写所有这些文档。
|
||||||
|
|
||||||
|
## 16.5 Maintaining comments: check the diffs 维护注释:检查差异
|
||||||
|
|
||||||
|
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 项目。
|
||||||
|
|
||||||
|
## 16.6 Higher-level comments are easier to maintain 更高级的注释更易于维护
|
||||||
|
|
||||||
|
One final thought on maintaining documentation: comments are easier to maintain if they are higher-level and more abstract than the code. These comments do not reflect the details of the code, so they will not be affected by minor code changes; only changes in overall behavior will affect these comments. Of course, as discussed in Chapter 13, some comments do need to be detailed and precise. But in general, the comments that are most useful (they don’t simply repeat the code) are also easiest to maintain.
|
||||||
|
|
||||||
|
> 关于维护文档的最后一个想法:如果注释比代码更高级,更抽象,则注释更易于维护。这些注释不反映代码的详细信息,因此它们不会受到代码更改的影响;只有整体行为的变化才会影响这些评论。当然,正如第 13 章所讨论的那样,某些注释的确需要详细和精确。但总的来说,最有用的注释(它们不只是重复代码)也最容易维护。
|
||||||
87
docs/ch17.md
Normal file
87
docs/ch17.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# 第 17 章 一致性
|
||||||
|
|
||||||
|
> Chapter 17 Consistency
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 名字。第 14 章已经讨论了以一致的方式使用名称的好处。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 接口。具有多个实现的接口是一致性的另一个示例。一旦了解了接口的一种实现,其他任何实现都将变得更易于理解,因为您已经知道它将必须提供的功能。
|
||||||
|
|
||||||
|
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 节中详细讨论。
|
||||||
|
|
||||||
|
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 上已经发布了来自各个组织的一些样式指南;考虑从其中之一开始。
|
||||||
|
|
||||||
|
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 后跟一个换行符来结束。如果一个系统上的开发人员对先前在另一个系统上编辑过的文件进行了小的编辑,那么编辑器有时会将所有行终止符替换为适合该系统的行终止符。这给人的感觉是文件的每一行都被修改了,这使得跟踪有意义的更改变得很困难。我们建立了一个约定,即文件应该只包含换行,但是很难确保每个开发人员使用的每个工具都遵循这个约定。每当一个新的开发人员加入这个项目,我们就会经历一连串的线路终止问题,而那个开发人员就会适应这个约定。
|
||||||
|
|
||||||
|
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”?当您看到任何看起来可能是约定的内容时,请遵循该约定。在做出设计决策时,请问自己是否有可能在项目的其他地方做出了类似的决策;如果是这样,请找到一个现有示例,并在新代码中使用相同的方法。
|
||||||
|
|
||||||
|
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”时才有好处。
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
> 一致性是投资心态的另一个例子。确保一致性的工作将需要一些额外的工作:确定约定,创建自动检查程序,寻找类似情况以模仿新代码,以及进行代码审查以教育团队。这项投资的回报是您的代码将更加明显。开发人员将能够更快,更准确地了解代码的行为,这将使他们能够以更少的错误来更快地工作。
|
||||||
208
docs/ch18.md
Normal file
208
docs/ch18.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# 第 18 章 代码应该是显而易见的
|
||||||
|
|
||||||
|
> Chapter 18 Code Should be Obvious
|
||||||
|
|
||||||
|
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 节中描述的导致复杂性的两个主要原因之一。当有关系统的重要信息对于新开发人员而言并不明显时,就会发生模糊。解决晦涩问题的方法是以显而易见的方式编写代码。本章讨论使代码或多或少变得显而易见的一些因素。
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 章)。如果总是以相似的方式完成相似的事情,那么读者可以识别出他们以前所见过的模式,并立即得出(安全)结论,而无需详细分析代码。
|
||||||
|
|
||||||
|
Here are a few other general-purpose techniques for making code more obvious:
|
||||||
|
|
||||||
|
> 以下是使代码更明显的其他一些通用技术:
|
||||||
|
|
||||||
|
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
|
||||||
|
/**
|
||||||
|
* ...
|
||||||
|
* @param numThreads The number of threads that this manager should
|
||||||
|
* spin up in order to manage ongoing connections. The MessageManager
|
||||||
|
* spins up at least one thread for every open connection, so this
|
||||||
|
* should be at least equal to the number of connections you expect
|
||||||
|
* to be open at once. This should be a multiple of that number if
|
||||||
|
* you expect to send a lot of messages in a short amount of time.
|
||||||
|
* @param handler Used as a callback in order to handle incoming
|
||||||
|
* messages on this MessageManager's open connections. See
|
||||||
|
* {@code MessageHandler} and {@code handleMessage} for details.
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
It’s hard to see where the documentation for one parameter ends and the next begins. It’s not even obvious how many parameters there are, or what their names are. If a little whitespace is added, the structure suddenly becomes clear and the documentation is easier to scan:
|
||||||
|
|
||||||
|
> 很难看到一个参数的文档在哪里结束而下一个参数的文档在哪里开始。甚至不知道有多少个参数或它们的名称是什么。如果添加了一些空白,结构会突然变得清晰,文档也更容易扫描:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* @param numThreads
|
||||||
|
* The number of threads that this manager should spin up in
|
||||||
|
* order to manage ongoing connections. The MessageManager spins
|
||||||
|
* up at least one thread for every open connection, so this
|
||||||
|
* should be at least equal to the number of connections you
|
||||||
|
* expect to be open at once. This should be a multiple of that
|
||||||
|
* number if you expect to send a lot of messages in a short
|
||||||
|
* amount of time.
|
||||||
|
* @param handler
|
||||||
|
* Used as a callback in order to handle incoming messages on
|
||||||
|
* this MessageManager's open connections. See
|
||||||
|
* {@code MessageHandler} and {@code handleMessage} for details.
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Blank lines are also useful to separate major blocks of code within a method, such as in the following example:
|
||||||
|
|
||||||
|
> 空行也可用于分隔方法中的主要代码块,例如以下示例:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void* Buffer::allocAux(size_t numBytes) {
|
||||||
|
// Round up the length to a multiple of 8 bytes, to ensure alignment.
|
||||||
|
uint32_t numBytes32 = (downCast<uint32_t>(numBytes) + 7) & ~0x7;
|
||||||
|
assert(numBytes32 != 0);
|
||||||
|
|
||||||
|
// If there is enough memory at firstAvailable, use that. Work down
|
||||||
|
// from the top, because this memory is guaranteed to be aligned
|
||||||
|
// (memory at the bottom may have been used for variable-size chunks).
|
||||||
|
if (availableLength >= numBytes32) {
|
||||||
|
availableLength -= numBytes32;
|
||||||
|
return firstAvailable + availableLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, see if there is extra space at the end of the last chunk.
|
||||||
|
if (extraAppendBytes >= numBytes32) {
|
||||||
|
extraAppendBytes -= numBytes32;
|
||||||
|
return lastChunk->data + lastChunk->length + extraAppendBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must create a new space allocation; allocate space within it.
|
||||||
|
uint32_t allocatedLength;
|
||||||
|
firstAvailable = getNewAllocation(numBytes32, &allocatedLength);
|
||||||
|
availableLength = allocatedLength numBytes32;
|
||||||
|
return firstAvailable + availableLength;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach works particularly well if the first line after each blank line is a comment describing the next block of code: the blank lines make the comments more visible.
|
||||||
|
|
||||||
|
> 如果每个空白行之后的第一行是描述下一个代码块的注释,则此方法特别有效:空白行使注释更可见。
|
||||||
|
|
||||||
|
White space within a statement helps to clarify the structure of the statement. Compare the following two statements, one of which has whitespace and one of which doesn’t:
|
||||||
|
|
||||||
|
> 语句中的空白有助于阐明语句的结构。比较以下两个语句,其中之一具有空格,而其中一个没有空格:
|
||||||
|
|
||||||
|
```java
|
||||||
|
for(int pass=1;pass>=0&&!empty;pass--) {
|
||||||
|
|
||||||
|
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
|
||||||
|
/**
|
||||||
|
* This method is invoked in the dispatch thread by a transport if a
|
||||||
|
* transport-level error prevents an RPC from completing.
|
||||||
|
*/
|
||||||
|
void Transport::RpcNotifier::failed() {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
img Red Flag: Nonobvious Code img
|
||||||
|
|
||||||
|
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
|
||||||
|
return new Pair<Integer, Boolean>(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()引用两个返回的值,而这两个值都不提供这些值的实际含义。
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
> 不同类型的声明和分配。考虑以下 Java 示例:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private List<Message> incomingMessageList;
|
||||||
|
...
|
||||||
|
incomingMessageList = new ArrayList<Message>();
|
||||||
|
```
|
||||||
|
|
||||||
|
The variable is declared as a List, but the actual value is an ArrayList. This code is legal, since List is a superclass of ArrayList, but it can mislead a reader who sees the declaration but not the actual allocation. The actual type may impact how the variable is used (ArrayLists have different performance and thread-safety properties than other subclasses of List), so it is better to match the declaration with the allocation.
|
||||||
|
|
||||||
|
> 将该变量声明为 List,但实际值为 ArrayList。这段代码是合法的,因为 List 是 ArrayList 的超类,但是它会误导看到声明但不是实际分配的读者。实际类型可能会影响变量的使用方式(ArrayList 与 List 的其他子类相比,具有不同的性能和线程安全属性),因此最好将声明与分配匹配。
|
||||||
|
|
||||||
|
Code that violates reader expectations. Consider the following code, which is the main program for a Java application
|
||||||
|
|
||||||
|
> 违反读者期望的代码。考虑以下代码,这是 Java 应用程序的主程序
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void main(String[] args) {
|
||||||
|
...
|
||||||
|
new RaftClient(myAddress, serverAddresses);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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 末尾添加简短注释。该注释应指示该应用程序将继续在其他线程中执行。如果代码符合读者期望的惯例,那么它是最明显的。如果没有,那么记录该行为很重要,以免使读者感到困惑。
|
||||||
|
|
||||||
|
## 18.3 Conclusion 结论
|
||||||
|
|
||||||
|
Another way of thinking about obviousness is in terms of information. If code is nonobvious, that usually means there is important information about the code that the reader does not have: in the RaftClient example, the reader might not know that the RaftClient constructor created new threads; in the Pair example, the reader might not know that result.getKey() returns the number of the current term.
|
||||||
|
|
||||||
|
> 关于显而易见性的另一种思考方式是信息。如果代码不是显而易见的,则通常意味着存在有关读者所不具备的代码的重要信息:在 RaftClient 示例中,读者可能不知道 RaftClient 构造函数创建了新线程;在“配对”示例中,读者可能不知道 result.getKey()返回当前项的编号。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 为了使代码清晰可见,您必须确保读者始终拥有理解它们所需的信息。您可以通过三种方式执行此操作。最好的方法是使用抽象等设计技术并消除特殊情况,以减少所需的信息量。其次,您可以利用读者在其他情况下已经获得的信息(例如,通过遵循约定并符合期望),从而使读者不必为代码学习新的信息。第三,您可以使用诸如好名和战略注释之类的技术在代码中向他们提供重要信息。
|
||||||
137
docs/ch19.md
Normal file
137
docs/ch19.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# 第 19 章 软件发展趋势
|
||||||
|
|
||||||
|
> Chapter 19 Software Trends
|
||||||
|
|
||||||
|
As a way of illustrating the principles discussed in this book, this chapter considers several trends and patterns that have become popular in software development over the last few decades. For each trend, I will describe how that trend relates to the principles in this book and use the principles to evaluate whether that trend provides leverage against software complexity.
|
||||||
|
|
||||||
|
> 为了说明本书中讨论的原理,本章考虑了过去几十年来在软件开发中流行的几种趋势和模式。对于每种趋势,我将描述该趋势与本书中的原理之间的关系,并使用这些原理来评估该趋势是否提供了针对软件复杂性的杠杆作用。
|
||||||
|
|
||||||
|
## 19.1 Object-oriented programming and inheritance 面向对象的编程和继承
|
||||||
|
|
||||||
|
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 年中,面向对象编程是软件开发中最重要的新思想之一。它引入了诸如类,继承,私有方法和实例变量之类的概念。如果仔细使用,这些机制可以帮助产生更好的软件设计。例如,私有方法和变量可用于确保信息隐藏:类外的任何代码都不能调用私有方法或访问私有变量,因此它们上没有任何外部依赖关系。
|
||||||
|
|
||||||
|
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 操作,而另一个子类可能对网络套接字实现相同的操作。
|
||||||
|
|
||||||
|
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 接口读取和写入磁盘文件)中获得的知识可以用于解决其他问题(例如通过网络套接字进行通信)。关于深度的另一种思考方式是:接口的实现越不同,接口就越深入。为了使接口具有许多实现,它必须捕获所有基础实现的基本功能,同时避免实现之间的差异。这个概念是抽象的核心。
|
||||||
|
|
||||||
|
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 章中描述的变化放大问题。
|
||||||
|
|
||||||
|
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 年的一次从业者会议上正式定义的。敏捷开发主要是关于软件开发的过程(组织团队,管理进度表,单元测试的角色,与客户交互等),而不是软件设计。但是,它与本书中的某些设计原则有关。
|
||||||
|
|
||||||
|
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 章所述,在项目开始时就不可能对复杂的系统进行充分的可视化以决定最佳设计。最终获得良好设计的最佳方法是逐步开发一个系统,其中每个增量都会添加一些新的抽象,并根据经验重构现有的抽象。这类似于敏捷开发方法。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 渐进式开发通常是一个好主意,但是渐进式开发应该是抽象的,而不是功能。可以推迟对特定抽象的所有想法,直到功能需要它为止。一旦需要抽象,就要花一些时间进行简洁的设计。遵循第 6 章的建议并使其具有通用性。
|
||||||
|
|
||||||
|
## 19.3 Unit tests 单元测试
|
||||||
|
|
||||||
|
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 团队编写的。然而,敏捷开发的原则之一是测试应该与开发紧密集成,程序员应该为他们自己的代码编写测试。这种做法现在已经很普遍了。测试通常分为两类:单元测试和系统测试。单元测试是开发人员最常编写的测试。它们很小,而且重点突出:每个测试通常在单个方法中验证一小段代码。单元测试可以独立运行,而不需要为系统设置生产环境。单元测试通常与测试覆盖工具一起运行,以确保应用程序中的每一行代码都经过了测试。每当开发人员编写新代码或修改现有代码时,他们都要负责更新单元测试以保持适当的测试覆盖率。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 第二种测试包括系统测试(有时称为集成测试),这些测试可确保应用程序的不同部分都能正常协同工作。它们通常涉及在生产环境中运行整个应用程序。系统测试更有可能由独立的质量检查或测试小组编写。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 有了一套很好的测试,开发人员可以在重构时更有信心,因为测试套件将发现大多数引入的错误。这鼓励开发人员对系统进行结构改进,从而获得更好的设计。单元测试特别有价值:与系统测试相比,它们提供更高的代码覆盖率,因此它们更有可能发现任何错误。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 例如,在开发 Tcl 脚本语言期间,我们决定通过用字节码编译器替换 Tcl 的解释器来提高性能。这是一个巨大的变化,几乎影响了核心 Tcl 引擎的每个部分。幸运的是,Tcl 有一个出色的单元测试套件,我们在新的字节码引擎上运行了该套件。现有测试在发现新引擎中的错误方面是如此有效,以至于在字节码编译器的 alpha 版本发布之后仅出现了一个错误。
|
||||||
|
|
||||||
|
## 19.4 Test-driven development 测试驱动的开发
|
||||||
|
|
||||||
|
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 节所述,开发单位应该是抽象的,而不是功能。一旦发现需要抽象,就不要随着时间的流逝而逐步创建抽象。一次设计所有功能(或至少足以提供一组合理全面的核心功能)。这样更有可能产生干净的设计,使各个部分很好地契合在一起。
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
> 首先编写测试的地方是修复错误。修复错误之前,请编写由于该错误而失败的单元测试。然后修复该错误,并确保现在可以通过单元测试。这是确保您已真正修复该错误的最佳方法。如果您在编写测试之前就已修复了该错误,则新的单元测试很可能实际上不会触发该错误,在这种情况下,它不会告诉您是否确实修复了该问题。
|
||||||
|
|
||||||
|
## 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 的《设计模式:可重用的面向对象软件的元素》一书中得到了普及,现在设计模式已广泛用于面向对象的软件开发中。
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
In the Java programming community, getter and setter methods are a popular design pattern. A getter and a setter are associated with an instance variable for a class. They have names like getFoo and setFoo, where Foo is the name of the variable. The getter method returns the current value of the variable, and the setter method modifies the value.
|
||||||
|
|
||||||
|
> 在 Java 编程社区中,getter 和 setter 方法是一种流行的设计模式。一个 getter 和一个 setter 与一个类的实例变量相关联。它们具有类似 getFoo 和 setFoo 的名称,其中 Foo 是变量的名称。getter 方法返回变量的当前值,setter 方法修改该值。
|
||||||
|
|
||||||
|
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 的论点是,它们允许在获取和设置时执行其他功能,例如在变量更改时更新相关值,通知更改的侦听器或对值实施约束。即使最初不需要这些功能,也可以稍后添加它们而无需更改界面。
|
||||||
|
|
||||||
|
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(或任何暴露的实现数据)。
|
||||||
|
|
||||||
|
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 的过度使用。
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
> 每当您遇到有关新软件开发范例的提案时,就必须从复杂性的角度对其进行挑战:该提案确实有助于最大程度地降低大型软件系统的复杂性吗?从表面上看,许多建议听起来不错,但是如果您深入研究,您会发现其中一些会使复杂性恶化,而不是更好。
|
||||||
96
docs/ch20.md
Normal file
96
docs/ch20.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# 第 20 章 设计性能
|
||||||
|
|
||||||
|
到目前为止,关于软件设计的讨论都集中在复杂性上。目标是使软件尽可能简单易懂。但是,如果您在需要快速的系统上工作,该怎么办?性能方面的考虑应如何影响设计过程?本章讨论如何在不牺牲简洁设计的情况下实现高性能。最重要的想法仍然是简单性:简单性不仅可以改善系统的设计,而且通常可以使系统更快。
|
||||||
|
|
||||||
|
## 20.1 如何考虑性能
|
||||||
|
|
||||||
|
要解决的第一个问题是“您在正常的开发过程中应该为性能多少担心?” 如果您尝试优化每条语句以获得最大速度,则它将减慢开发速度并产生许多不必要的复杂性。此外,许多“优化”实际上对性能没有帮助。另一方面,如果您完全忽略了性能问题,则很容易导致遍及整个代码的大量效率低下。结果系统很容易比所需的速度慢 5–10 倍。在这种“千刀砍死”的情况下,以后很难再回来提高性能了,因为没有单一的改进会产生很大的影响。
|
||||||
|
|
||||||
|
最好的方法是介于这两种极端之间,在这种极端情况下,您可以使用性能的基本知识来选择“自然高效”但又干净又简单的设计替代方案。关键是要了解哪些操作根本是昂贵的。以下是一些今天相对昂贵的操作示例:
|
||||||
|
|
||||||
|
- 网络通信:即使在数据中心内,往返消息交换也可能花费 10–50 µs,这是数以万计的指令时间。广域往返可能需要 10 到 100 毫秒。
|
||||||
|
- I/O 到辅助存储:磁盘 I/O 操作通常需要 5 到 10 毫秒,这是数百万条指令时间。闪存存储需要 10–100 µs。新出现的非易失性存储器的速度可能高达 1 µs,但这仍约为 2000 条指令时间。
|
||||||
|
- 动态内存分配(C 语言中的 malloc,C ++或 Java 中的新增功能)通常涉及分配,释放和垃圾回收的大量开销。
|
||||||
|
- 高速缓存未命中:将数据从 DRAM 提取到片上处理器高速缓存中需要数百条指令时间;在许多程序中,整体性能取决于缓存未命中和计算成本。
|
||||||
|
|
||||||
|
了解哪些东西最昂贵的最好方法是运行微基准测试(小型程序,这些程序单独测量单个操作的成本)。在 RAMCloud 项目中,我们创建了一个简单的程序,该程序提供了微基准测试的框架。创建该框架花了几天时间,但是该框架使在五到十分钟内添加新的微基准成为可能。这使我们积累了几十个微基准。我们既可以使用它们来了解 RAMCloud 中使用的现有库的性能,也可以衡量为 RAMCloud 编写的新类的性能。
|
||||||
|
|
||||||
|
一旦对什么是昂贵和什么便宜有了一般的认识,就可以使用该信息尽可能地选择便宜的业务。在许多情况下,更有效的方法将与较慢的方法一样简单。例如,当存储将使用键值查找的大量对象时,可以使用哈希表或有序映射。两者都通常在库包中提供,并且都简单易用。但是,哈希表可以轻松地快 5-10 倍。因此,除非需要地图提供的排序属性,否则应始终使用哈希表。
|
||||||
|
|
||||||
|
作为另一个示例,请考虑使用诸如 C 或 C ++之类的语言分配结构数组。有两种方法可以执行此操作。一种方法是让数组保留指向结构的指针,在这种情况下,您必须首先为数组分配空间,然后为每个单独的结构分配空间。将结构存储在数组本身中效率要高得多,因此您只为所有内容分配一个大块。
|
||||||
|
|
||||||
|
如果提高效率的唯一方法是增加复杂性,那么选择就更加困难。如果更高效的设计仅增加了少量复杂性,并且复杂性是隐藏的,因此它不影响任何接口,那么它可能是值得的(但要注意:复杂性是递增的)。如果更快的设计增加了很多实现复杂性,或者导致更复杂的接口,那么最好是从更简单的方法开始,然后在性能出现问题时进行优化。但是,如果您有明确的证据表明性能在特定情况下很重要,那么您最好立即实施更快的方法。
|
||||||
|
|
||||||
|
在 RAMCloud 项目中,我们的总体目标之一是为客户端计算机通过数据中心网络访问存储系统提供尽可能低的延迟。结果,我们决定使用特殊的硬件进行联网,从而使 RAMCloud 绕过内核并直接与网络接口控制器进行通信以发送和接收数据包。即使增加了复杂性,我们还是做出了这个决定,因为我们从先前的测量中知道,基于内核的网络太慢了,无法满足我们的需求。在其余的 RAMCloud 系统中,我们能够进行简单设计。解决这个大问题“对”使其他事情变得更加容易。
|
||||||
|
|
||||||
|
通常,较简单的代码往往比复杂的代码运行更快。如果您定义了特殊情况和例外,则无需代码即可检查这些情况,并且系统运行速度更快。深层类比浅层类更有效,因为它们为每个方法调用完成了更多工作。浅类会导致更多的层交叉,并且每个层交叉都会增加开销。
|
||||||
|
|
||||||
|
## 20.2 Measure before modifying 修改前的度量
|
||||||
|
|
||||||
|
但是,即使您如上所述进行设计,也请假设您的系统仍然太慢。根据您对慢速运动的直觉,急于着手开始进行性能调整。不要这样!程序员对性能的直觉是不可靠的。即使对于有经验的开发人员也是如此。如果您开始根据直觉进行更改,则会浪费时间在实际上无法提高性能的事情上,并且可能会使系统变得更加复杂。
|
||||||
|
|
||||||
|
进行任何更改之前,请测量系统的现有行为。这有两个目的。首先,这些测量将确定性能调整将产生最大影响的地方。仅仅测量顶级系统性能是不够的。这可能会告诉您系统速度太慢,但不会告诉您原因。您需要进行更深入的衡量,以详细确定影响整体绩效的因素;目标是确定系统当前花费大量时间的少量非常具体的地方,以及您有改进想法的地方。测量的第二个目的是提供基线,以便您可以在进行更改后重新测量性能,以确保性能得到实际改善。如果这些更改并未在效果上带来可衡量的变化,然后将其退出(除非它们使系统更简单)。除非能够显着提高速度,否则保持复杂性毫无意义。
|
||||||
|
|
||||||
|
## 20.3 Design around the critical path 围绕关键路径进行设计
|
||||||
|
|
||||||
|
在这一点上,我们假设您已经仔细分析了性能,并确定了一段缓慢的代码来影响整个系统的性能。改善其性能的最佳方法是进行“根本”更改,例如引入缓存,或使用其他算法方法(例如,平衡树与列表)。我们决定绕过内核进行 RAMCloud 中的网络通信的决定是一个基本修补程序的示例。如果您可以确定基本修复程序,则可以使用前面各章中讨论的设计技术来实施它。
|
||||||
|
|
||||||
|
不幸的是,有时会出现一些根本无法解决的情况。这将我们带到本章的核心问题,即如何重新设计现有代码,使其运行更快。这应该是您的不得已的方法,并且不应该经常发生,但是在某些情况下它可能会带来很大的不同。关键思想是围绕关键路径设计代码。
|
||||||
|
|
||||||
|
首先,问自己在通常情况下执行所需任务必须执行的最少代码量是多少。忽略任何现有的代码结构。相反,想象一下您正在编写一个仅实现关键路径的新方法,这是在最常见的情况下必须执行的最少代码量。当前的代码可能充满特殊情况。在此练习中,请忽略它们。当前的代码可能会在关键路径上通过多个方法调用。想象一下,您可以将所有相关代码放在一个方法中。当前代码还可以使用各种变量和数据结构。仅考虑关键路径所需的数据,并假定最适合关键路径的任何数据结构。例如,将多个变量合并为一个值可能很有意义。假设您可以完全重新设计系统,以最大程度地减少必须为关键路径执行的代码。我们将此代码称为“理想”。
|
||||||
|
|
||||||
|
理想的代码可能会与您现有的类结构冲突,并且可能不切实际,但它提供了一个很好的目标:这代表了代码可能是最简单,最快的。下一步是寻找一种新设计,使其尽可能接近理想状态,同时又要保持干净的结构。您可以应用本书前面各章中的所有设计思想,但要保持(最好)保持理想代码的附加约束。您可能需要在理想情况下添加一些额外的代码,以允许使用简洁的抽象。例如,如果代码涉及哈希表查找,则可以向通用哈希表类引入额外的方法调用。以我的经验,几乎总是可以找到干净简洁的设计,但非常接近理想。
|
||||||
|
|
||||||
|
在此过程中发生的最重要的事情之一是从关键路径中除去特殊情况。当代码运行缓慢时,通常是因为它必须处理各种情况,并且代码经过结构化以简化所有不同情况的处理。每个特殊情况都以额外的条件语句和/或方法调用的形式向关键路径添加了一些代码。这些添加中的每一个都会使代码变慢。重新设计性能时,请尝试减少必须检查的特殊情况的数量。理想情况下,开头应该有一个 if 语句,该语句可以通过一个测试检测所有特殊情况。在正常情况下,只需要进行一项测试,之后就可以执行关键路径,而对于特殊情况则无需进行其他测试。如果初始测试失败(这意味着发生了特殊情况),则代码可以分支到关键路径之外的单独位置以进行处理。对于特殊情况,性能并不是那么重要,因此您可以为简化而不是性能来构造特殊情况的代码。
|
||||||
|
|
||||||
|
## 20.4 An example: RAMCloud Buffers 示例:RAMCloud 缓冲区
|
||||||
|
|
||||||
|
让我们考虑一个示例,其中 RAMCloud 存储系统的 Buffer 类经过优化,以使大多数常见操作的速度提高约 2 倍。
|
||||||
|
|
||||||
|
RAMCloud 使用 Buffer 对象管理可变长度的内存数组,例如远程过程调用的请求和响应消息。缓冲区旨在减少内存复制和动态存储分配的开销。缓冲区存储看似线性的字节数组,但是为了提高效率,它允许将底层存储划分为多个不连续的内存块,如图 20.1 所示。通过附加数据块来创建缓冲区。每个块都是外部的或内部的。如果块在外部,则其存储由调用方拥有;缓冲区保留对此存储的引用。外部块通常用于大型块,以避免内存复制。如果内部有块,则 Buffer 拥有该块的存储;调用者提供的数据将被复制到缓冲区的内部存储器中。每个缓冲区包含一个小的内置分配,这是一个内存块,可用于存储内部块。如果此空间已用完,则缓冲区将创建其他分配,销毁缓冲区时必须释放这些分配。内部块对于内存复制成本可忽略不计的小块很方便。图 20.1 显示了具有 5 个块的 Buffer:第一个块是内部的,接下来的两个块是外部的,最后两个块是内部的。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
图 20.1:Buffer 对象使用内存块的集合来存储看似线性字节数组。内部块由 Buffer 拥有,并在 Buffer 销毁时释放;外部块不属于缓冲区。
|
||||||
|
|
||||||
|
|
||||||
|
Buffer 类本身代表“根本性的修补程序”,因为它消除了没有它就需要的昂贵的内存副本。例如,在 RAMCloud 存储系统中组装包含短标头和大对象内容的响应消息时,RAMCloud 使用带有两个块的 Buffer。第一个块是包含头的内部块;第二个块是一个外部块,它引用 RAMCloud 存储系统中的对象内容。可以在不复制大对象的情况下将响应收集到缓冲区中。
|
||||||
|
|
||||||
|
|
||||||
|
除了允许不连续块的基本方法外,我们没有尝试在原始实现中优化 Buffer 类的代码。但是,随着时间的流逝,我们注意到缓冲区越来越多地被使用。例如,在每个远程过程调用的执行期间,至少创建四个缓冲区。最终,很明显,加速 Buffer 的实现可能会对整体系统性能产生显着影响。我们决定看看是否可以提高 Buffer 类的性能。
|
||||||
|
|
||||||
|
Buffer 最常见的操作是使用内部块为少量新数据分配空间。例如,在为请求和响应消息创建标题时,就会发生这种情况。我们决定将此操作用作优化的关键路径。在最简单的情况下,可以通过扩大 Buffer 中最后存在的块来分配空间。但是,只有在最后一个现有块位于内部,并且其分配中有足够的空间来容纳新数据时,才有可能这样做。理想的代码将执行一次检查以确认简单方法是否可行,然后将调整现有块的大小。
|
||||||
|
|
||||||
|
图 20.2 显示了关键路径的原始代码,该代码以 Buffer :: alloc 方法开头。在最快的情况下,Buffer :: alloc 调用 Buffer :: allocateAppend,后者调用 Buffer :: Allocation :: allocateAppend。从性能的角度来看,此代码有两个问题。第一个问题是要单独检查许多特殊情况:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- Buffer::allocateAppend 检查缓冲区当前是否有任何分配。
|
||||||
|
- 代码检查两次以查看当前分配是否有足够的空间容纳新数据:一次在 Buffer::Allocation::allocateAppend 中,一次在其返回值由 Buffer::allocateAppend 测试时。
|
||||||
|
- Buffer::alloc 测试 Buffer::allocAppend 的返回值,以再次确认分配成功。
|
||||||
|
|
||||||
|
此外,该代码没有尝试直接扩展最后一个块,而是在不考虑最后一个块的情况下分配了新空间。然后,Buffer::alloc 检查该空间是否恰好与最后一块相邻,在这种情况下,它将新空间与现有块合并。这导致其他检查。总体而言,此代码测试关键路径中的 6 种不同条件。
|
||||||
|
|
||||||
|
原始代码的第二个问题是它具有太多的层,所有层都很浅。这既是性能问题,也是设计问题。关键路径除了对 Buffer::alloc 的原始调用之外,还进行了另外两个方法调用。每个方法调用花费额外的时间,并且每个调用的结果必须由其调用者检查,这导致需要考虑更多特殊情况。第 7 章讨论了当您从一层传递到另一层时,抽象通常应该如何变化,但是图 20.2 中的所有三种方法都具有相同的签名,并且它们提供了基本相同的抽象。这是一个危险信号。Buffer::allocateAppend 几乎是一个传递方法;它的唯一作用是在需要时创建新的分配。额外的层使代码既慢又复杂。
|
||||||
|
|
||||||
|
为了解决这些问题,我们重构了 Buffer 类,使其设计围绕最关键性能的路径进行。我们不仅考虑了上面的分配代码,还考虑了其 他几种常用的执行路径,例如检索当前存储在 Buffer 中的数据的字节总数。对于这些关键路径中的每一个,我们试图确定在通常情况下必须执行的最少代码量。然后,我们围绕这些关键路径设计了课程的其余部分。我们还应用了本书中的设计原则来简化整个类。例如,我们消除了浅层并创建了更深的内部抽象。重构的类比原始版本小 20%(1476 行代码,而原始版本为 1886 行)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
图 20.2:使用内部块在 Buffer 的末尾分配新空间的原始代码。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
图 20.3:用于在 Buffer 的内部块中分配新空间的新代码。
|
||||||
|
|
||||||
|
|
||||||
|
图 20.3 显示了用于在 Buffer 中分配内部空间的新关键路径。新代码不仅速度更快,而且更容易阅读,因为它避免了浅层抽象。整个路径使用单一方法处理,并且使用单一测试排除所有特殊情况。新代码引入了新的实例变量 extraAppendBytes,以简化关键路径。此变量跟踪缓冲区中最后一个块之后立即有多少未使用空间可用。如果没有可用空间,或者 Buffer 中的最后一个块不是内部块,或者 Buffer 根本不包含任何块,则 extraAppendBytes 为零。图 20.3 中的代码表示处理这种常见情况的最少代码量。
|
||||||
|
|
||||||
|
注意:只要需要,就可以通过重新计算各个块的总缓冲区长度来消除对 totalLength 的更新。但是,这种方法对于具有许多块的大型 Buffer 而言将是昂贵的,并且获取 Buffer 的总长度是另一种常见的操作。因此,我们选择添加少量额外的开销来分配,以确保 Buffer 长度始终立即可用。
|
||||||
|
|
||||||
|
新代码的速度约为旧代码的两倍:使用内部存储将 1 字节字符串附加到缓冲区的总时间从 8.8 ns 降低到 4.75 ns。由于修订,许多其他缓冲区操作也加快了速度。例如,构建新缓冲区,在内部存储中附加一小块并销毁缓冲区所需的时间从 24 ns 降至 12 ns。
|
||||||
|
|
||||||
|
## 20.5 Conclusion 结论
|
||||||
|
|
||||||
|
本章最重要的总体教训是,干净的设计和高性能是兼容的。重写 Buffer 类可将其性能提高 2 倍,同时简化其设计并将代码大小减少 20%。复杂的代码通常会很慢,因为它会执行多余或多余的工作。另一方面,如果您编写干净,简单的代码,则系统可能会足够快,因此您一开始就不必担心性能。在少数需要优化性能的情况下,关键再次是简单性:找到对性能最重要的关键路径并使它们尽可能简单。
|
||||||
17
docs/ch21.md
Normal file
17
docs/ch21.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 第 21 章 结论
|
||||||
|
|
||||||
|
|
||||||
|
这本书是关于一件事的:复杂性。处理复杂性是软件设计中最重要的挑战。这是使系统难以构建和维护的原因,并且通常也使它们变慢。在本书的整个过程中,我试图描述导致复杂性的根本原因,例如依赖性和模糊性。我已经讨论了可以帮助您识别不必要的复杂性的危险标记,例如信息泄漏,不必要的错误情况或名称过于笼统。我已经提出了一些通用的思想,可以用来创建更简单的软件系统,例如,努力研究更深和更通用的类,定义不存在的错误以及将接口文档与实现文档分离。最后,我讨论了产生简单设计所需的投资思路。
|
||||||
|
|
||||||
|
|
||||||
|
所有这些建议的缺点是它们会在项目的早期阶段创建额外的工作。此外,如果您不习惯于思考设计问题,那么当您学习良好的设计技巧时,您甚至会放慢脚步。如果对您而言唯一重要的事情就是尽快使当前代码工作,那么思考设计就好像是在费劲工作,而这实际上妨碍了您实现真正的目标。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
另一方面,如果良好的设计对您来说是重要的目标,那么本书中的思想应使编程更有趣。设计是一个令人着迷的难题:如何用最简单的结构解决特定问题?探索不同的方法很有趣,找到一种既简单又强大的解决方案是一种很好的感觉。干净,简单和明显的设计是一件美丽的事情。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
此外,您对优质设计的投资将很快获得回报。在项目开始时仔细定义的模块将为您节省时间,因为您一遍又一遍地重复使用它们。您六个月前编写的清晰文档将为您节省返回代码添加新功能的时间。花在磨练设计技能上的时间也将有所回报:随着技能和经验的增长,您会发现可以越来越快地制作出好的设计。一旦知道了什么,一个好的设计实际上并不会比一个简单的设计花费更多的时间。
|
||||||
|
|
||||||
|
成为优秀设计师的好处是,您可以在设计阶段花费大部分时间,这很有趣。可怜的设计师花费大量时间在复杂而脆弱的代码中寻找错误。如果提高设计技能,不仅可以更快地生产出更高质量的软件,而且软件开发过程也将变得更加愉快。
|
||||||
45
docs/ch8.md
Normal file
45
docs/ch8.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 第 8 章 降低复杂性
|
||||||
|
|
||||||
|
|
||||||
|
本章介绍了有关如何创建更深层类的另一种思考方式。假设您正在开发一个新模块,并且发现了一个不可避免的复杂性。哪个更好:应该让模块用户处理复杂性,还是应该在模块内部处理复杂性?如果复杂度与模块提供的功能有关,则第二个答案通常是正确的答案。大多数模块拥有的用户多于开发人员,因此开发人员遭受的苦难要大于用户。作为模块开发人员,您应该努力使模块用户的生活尽可能轻松,即使这对您来说意味着额外的工作。表达此想法的另一种方法是,模块具有简单的接口比简单的实现更为重要。
|
||||||
|
|
||||||
|
|
||||||
|
作为开发人员,很容易以相反的方式行事:解决简单的问题,然后将困难的问题推给其他人。如果出现不确定如何处理的条件,最简单的方法是引发异常并让调用方处理它。如果不确定要实施什么策略,则可以定义一些配置参数来控制该策略,然后由系统管理员自行确定最佳策略。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
这样的方法短期内会使您的生活更轻松,但它们会加剧复杂性,因此许多人必须处理一个问题,而不仅仅是一个人。例如,如果一个类抛出异常,则该类的每个调用者都必须处理该异常。如果一个类导出配置参数,则每个安装中的每个系统管理员都必须学习如何设置它们。
|
||||||
|
|
||||||
|
#### 8.1 Example: editor text class 示例:编辑器文本类
|
||||||
|
|
||||||
|
|
||||||
|
考虑为 GUI 文本编辑器管理文件文本的类,这在第 6 章和第 7 章中讨论过。该类提供了将文件从磁盘读入内存、查询和修改文件在内存中的副本以及将修改后的版本写回磁盘的方法。当学生必须实现这个类时,他们中的许多人选择了一个面向行的接口,该接口具有读取、插入和删除整行文本的方法。这导致了类的简单实现,但也为更高级别的软件带来了复杂性。在用户界面级别,操作很少涉及整行。例如,击键会导致在现有行中插入单个字符;复制或删除选择项可以修改几个不同行的部分。使用面向行的文本界面,为了实现用户界面,高级软件必须分割和连接行。
|
||||||
|
|
||||||
|
|
||||||
|
面向字符的界面(如 6.3 节中所述)降低了复杂性。用户界面软件现在可以插入和删除任意范围的文本,而无需分割和合并行,因此变得更加简单。文本类的实现可能会变得更加复杂:如果内部将文本表示为行的集合,则必须拆分和合并行以实现面向字符的操作。这种方法更好,因为它封装了在文本类中拆分和合并的复杂性,从而降低了系统的整体复杂性。
|
||||||
|
|
||||||
|
#### 8.2 Example: configuration parameters 示例:配置参数
|
||||||
|
|
||||||
|
|
||||||
|
配置参数是提高复杂度而不是降低复杂度的一个示例。类可以在内部输出一些控制其行为的参数,而不是在内部确定特定的行为,例如高速缓存的大小或在放弃之前重试请求的次数。然后,该类的用户必须为参数指定适当的值。在当今的系统中,配置参数已变得非常流行。有些系统有数百个。
|
||||||
|
|
||||||
|
|
||||||
|
拥护者认为配置参数不错,因为它们允许用户根据他们的特定要求和工作负载来调整系统。在某些情况下,低级基础结构代码很难知道要应用的最佳策略,而用户则对其域更加熟悉。例如,用户可能知道某些请求比其他请求更紧迫,因此用户为这些请求指定更高的优先级是有意义的。在这种情况下,配置参数可以在更广泛的域中带来更好的性能。
|
||||||
|
|
||||||
|
|
||||||
|
但是,配置参数还提供了一个轻松的借口,可以避免处理重要问题并将其传递给其他人。在许多情况下,用户或管理员很难或无法确定参数的正确值。在其他情况下,可以通过在系统实现中进行一些额外的工作来自动确定正确的值。考虑必须处理丢失数据包的网络协议。如果它发送请求但在一定时间内未收到响应,则重新发送该请求。确定重试间隔的一种方法是引入配置参数。但是,传输协议可以通过测量成功请求的响应时间,然后将其倍数用于重试间隔,自己计算出一个合理的值。这种方法降低了复杂性,使用户不必找出正确的重试间隔。它具有动态计算重试间隔的其他优点,因此,如果操作条件发生变化,它将自动进行调整。相反,配置参数很容易过时。
|
||||||
|
|
||||||
|
|
||||||
|
因此,您应尽可能避免使用配置参数。在导出配置参数之前,请问自己:“用户(或更高级别的模块)是否能够确定比我们在此确定的更好的值?” 当您创建配置参数时,请查看是否可以自动计算合理的默认值,因此用户仅需在特殊情况下提供值即可。理想情况下,每个模块都应完全解决问题。配置参数导致解决方案不完整,从而增加了系统复杂性。
|
||||||
|
|
||||||
|
#### 8.3 Taking it too far 走得太远
|
||||||
|
|
||||||
|
降低复杂性时要谨慎处理;这个想法很容易被夸大。一种极端的方法是将整个应用程序的所有功能归为一个类,这显然没有意义。如果(a)被降低的复杂度与该类的现有功能密切相关,(b)降低复杂度将导致应用程序中其他地方的许多简化,则降低复杂度最有意义。简化了类的界面。请记住,目标是最大程度地降低整体系统复杂性。
|
||||||
|
|
||||||
|
|
||||||
|
第 6 章介绍了一些学生如何在文本类中定义反映用户界面的方法,例如实现退格键功能的方法。这似乎很好,因为它可以降低复杂性。但是,将用户界面的知识添加到文本类中并不会大大简化高层代码,并且用户界面的知识与文本类的核心功能无关。在这种情况下,降低复杂度只会导致信息泄漏。
|
||||||
|
|
||||||
|
#### 8.4 Conclusion 结论
|
||||||
|
|
||||||
|
|
||||||
|
在开发模块时,请寻找机会减轻自己的痛苦,以减轻用户的痛苦。
|
||||||
217
docs/ch9.md
Normal file
217
docs/ch9.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# 第 9 章 在一起更好还是分开更好?
|
||||||
|
|
||||||
|
|
||||||
|
软件设计中最基本的问题之一是:给定两个功能,它们应该在同一位置一起实现,还是应该分开实现?这个问题适用于系统中的所有级别,例如功能,方法,类和服务。例如,应该在提供面向流的文件 I/O 的类中包括缓冲,还是应该在单独的类中?HTTP 请求的解析应该完全在一种方法中实现,还是应该在多个方法(甚至多个类)之间划分?本章讨论做出这些决定时要考虑的因素。这些因素中的一些已经在前面的章节中进行了讨论,但是为了完整起见,这里将对其进行重新讨论。
|
||||||
|
|
||||||
|
|
||||||
|
在决定是合并还是分开时,目标是降低整个系统的复杂性并改善其模块化。看来实现此目标的最佳方法是将系统划分为大量的小组件:组件越小,每个单独的组件可能越简单。但是,细分的行为会带来额外的复杂性,而这在细分之前是不存在的:
|
||||||
|
|
||||||
|
|
||||||
|
- 一些组件的复杂性仅来自组件的数量:组件越多,就越难以追踪所有组件,也就越难在大型集合中找到所需的组件。细分通常会导致更多接口,并且每个新接口都会增加复杂性。
|
||||||
|
- 细分可能会导致附加代码来管理组件。例如,在细分之前使用单个对象的一段代码现在可能必须管理多个对象。
|
||||||
|
- 细分产生分离:细分后的组件将比细分前的组件相距更远。例如,在细分之前位于单个类中的方法可能在细分之后位于不同的类中,并且可能在不同的文件中。分离使开发人员更难于同时查看这些组件,甚至很难知道它们的存在。如果组件真正独立,那么分离是好的:它使开发人员可以一次专注于单个组件,而不会被其他组件分散注意力。另一方面,如果组件之间存在依赖性,则分离是不好的:开发人员最终将在组件之间来回翻转。更糟糕的是,他们可能不了解依赖关系,这可能导致错误。
|
||||||
|
- 细分可能导致重复:细分之前的单个实例中存在的代码可能需要存在于每个细分的组件中。
|
||||||
|
|
||||||
|
|
||||||
|
如果它们紧密相关,则将代码段组合在一起是最有益的。如果各部分无关,则最好分开。以下是两个代码相关的一些提示:
|
||||||
|
|
||||||
|
|
||||||
|
- 他们共享信息;例如,这两段代码都可能取决于特定类型文档的语法。
|
||||||
|
- 它们一起使用:任何使用其中一段代码的人都可能同时使用另一段代码。这种关系形式只有在双向关系中才具有吸引力。作为反例,磁盘块高速缓存几乎总是包含哈希表,但是哈希表可以在许多不涉及块高速缓存的情况下使用。因此,这些模块应该分开。
|
||||||
|
- 它们在概念上重叠,因为存在一个简单的更高级别的类别,其中包括这两段代码。例如,搜索子字符串和大小写转换都属于字符串操作类别。流控制和可靠的交付都属于网络通信的范畴。
|
||||||
|
- 不看其中的一段代码就很难理解。
|
||||||
|
|
||||||
|
本章的其余部分使用更具体的规则以及示例来说明何时将代码段组合在一起以及何时将它们分开是有意义的。
|
||||||
|
|
||||||
|
## 9.1 Bring together if information is shared 如果信息共享则汇聚在一起
|
||||||
|
|
||||||
|
5.4 节在实现 HTTP 服务器的项目上下文中介绍了此原理。在其第一个实现中,该项目在不同的类中使用了两种不同的方法来读取和解析 HTTP 请求。第一种方法从网络套接字读取传入请求的文本,并将其放置在字符串对象中。第二种方法解析字符串以提取请求的各个组成部分。经过这种分解,这两种方法最终都对 HTTP 请求的格式有了相当的了解:第一种方法只是尝试读取请求,而不是解析请求,但是如果不执行大多数操作,就无法确定请求的结束解析它的工作(例如,它必须解析标头行才能识别包含整个请求长度的标头)。由于此共享信息,最好在同一位置读取和解析请求;当两个类合而为一时,代码变得更短,更简单。
|
||||||
|
|
||||||
|
## 9.2 Bring together if it will simplify the interface 汇集在一起 是否可以简化接口
|
||||||
|
|
||||||
|
|
||||||
|
当两个或多个模块组合成一个模块时,可以为新模块定义一个比原始接口更简单或更易于使用的接口。当原始模块各自实现问题解决方案的一部分时,通常会发生这种情况。在上一部分的 HTTP 服务器示例中,原始方法需要一个接口来从第一个方法返回 HTTP 请求字符串并将其传递给第二个方法。当这些方法结合在一起时,这些接口就被淘汰了。
|
||||||
|
|
||||||
|
另外,将两个或更多类的功能组合在一起时,可能会自动执行某些功能,因此大多数用户无需了解它们。Java I/O 库说明了这种机会。如果将 FileInputStream 和 BufferedInputStream 类组合在一起,并且默认情况下提供了缓冲,则绝大多数用户甚至都不需要知道缓冲的存在。组合的 FileInputStream 类可能提供禁用或替换默认缓冲机制的方法,但是大多数用户不需要了解它们。
|
||||||
|
|
||||||
|
## 9.3 Bring together to eliminate duplication 消除重复
|
||||||
|
|
||||||
|
如果发现反复重复相同的代码模式,请查看是否可以重新组织代码以消除重复。一种方法是将重复的代码分解为一个单独的方法,并用对该方法的调用替换重复的代码段。如果重复的代码段很长并且替换方法具有简单的签名,则此方法最有效。如果代码段只有一两行,那么用方法调用替换它可能不会有太多好处。如果代码段与其环境以复杂的方式进行交互(例如,通过访问多个局部变量),则替换方法可能需要复杂的签名(例如,许多“按引用传递”参数),这会降低其价值。
|
||||||
|
|
||||||
|
消除重复的另一种方法是重构代码,使相关代码段仅需要在一个地方执行。假设您正在编写一种方法,该方法需要在几个不同的点返回错误,并且在返回之前需要在每个这些点执行相同的清除操作(示例请参见图 9.1)。如果编程语言支持 goto,则可以将清除代码移到方法的最后,然后在需要返回错误的每个点处转到该片段,如图 9.2 所示。Goto 语句通常被认为是一个坏主意,如果不加选择地使用它们,可能会导致无法识别的代码,但是在诸如此类的情况下,它们可用于从嵌套代码中转义,因此它们非常有用。
|
||||||
|
|
||||||
|
## 9.4 Separate general-purpose and special-purpose code 单独的通用代码和专用代码
|
||||||
|
|
||||||
|
如果模块包含可用于多种不同目的的机制,则它应仅提供一种通用机制。它不应包含专门针对特定用途的机制的代码,也不应包含其他通用机制。与通用机制关联的专用代码通常应放在不同的模块中(通常是与特定用途关联的模块)。第 6 章中的 GUI 编辑器讨论阐明了这一原理:最佳设计是文本类提供通用文本操作,而特定于用户界面的操作(例如删除所选内容)则在用户界面模块中实现。
|
||||||
|
|
||||||
|
如果相同的代码(或几乎相同的代码)一遍又一遍地出现,那是一个危险信号,您没有找到正确的抽象。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
图 9.1:此代码处理不同类型的传入网络数据包。对于每种类型,如果数据包对于该类型而言太短,则会记录一条消息。在此版本的代码中,LOG 语句对于几种不同的数据包类型是重复的。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
图 9.2:对图 9.1 中的代码进行了重新组织,因此只有 LOG 语句的一个副本。
|
||||||
|
|
||||||
|
通常,系统的下层倾向于更通用,而上层则更专用。例如,应用程序的最顶层包含完全特定于该应用程序的功能。将专用代码与通用代码分开的方法是将专用代码向上拉到较高的层,而将较低的层保留为通用。当您遇到同时包含通用功能和专用功能的同一类的类时,请查看该类是否可以分为两个类,一个包含通用功能,另一个在其上分层以提供特殊功能
|
||||||
|
|
||||||
|
## 9.5 Example: insertion cursor and selection 示例:插入光标和选择
|
||||||
|
|
||||||
|
下一节将通过三个示例说明上述原理。在两个示例中,最好的方法是分离相关的代码段。在第三个示例中,最好将它们结合在一起。
|
||||||
|
|
||||||
|
第一个示例由插入光标和第 6 章的 GUI 编辑器项目中的选择组成。编辑器显示闪烁的垂直线,指示用户键入的文本将出现在文档中的何处。它还显示了一个突出显示的字符范围,称为选择,用于复制或删除文本。插入光标始终可见,但是有时可能没有选择文本。如果存在选择,则插入光标始终位于其一端。
|
||||||
|
|
||||||
|
选择和插入光标在某些方面相关。例如,光标始终位于所选内容的一端,并且倾向于将光标和所选内容一起操作:单击并拖动鼠标将它们都设置,然后插入文本会首先删除所选的文本(如果有),然后在光标位置插入新文本。因此,使用单个对象管理选择和光标似乎合乎逻辑,并且一个项目团队采用了这种方法。该对象在文件中存储了两个位置,以及布尔值,它们指示光标的哪一端以及选择是否存在。
|
||||||
|
|
||||||
|
但是,合并的对象很尴尬。它对高级代码没有任何好处,因为高级代码仍然需要将选择和游标视为不同的实体,并且对它们进行单独操作(在插入文本期间,它首先在组合对象上调用一个方法来删除选定的文本;然后调用另一个方法来检索光标位置,以插入新文本)。实际上,组合对象比单独的对象实现起来要复杂得多。它避免了将光标位置存储为单独的实体,而是不得不存储一个布尔值,该布尔值指示选择的哪一端是光标。为了检索光标位置,组合对象必须首先测试布尔值,然后选择选择的适当结尾。
|
||||||
|
|
||||||
|
当通用机制还包含专门用于该机制的特定用途的代码时,就会出现此红色标志。这使该机制更加复杂,并在该机制与特定用例之间造成了信息泄漏:对用例的未来修改也可能需要对基础机制进行更改。
|
||||||
|
|
||||||
|
在这种情况下,选择和光标之间的关联度不足以将它们组合在一起。当修改代码以分隔选择和光标时,用法和实现都变得更加简单。与必须从中提取选择和光标信息的组合对象相比,单独的对象提供了更简单的接口。游标的实现也变得更加简单,因为游标的位置是直接表示的,而不是通过选择和布尔值间接表示的。实际上,在修订版中,没有特殊的类用于选择或游标。相反,引入了一个新的 Position 类来表示文件中的位置(行号和行内的字符)。选择用两个位置表示,光标用一个位置表示。职位还在项目中找到了其他用途。
|
||||||
|
|
||||||
|
## 9.6 Example: separate class for logging 示例:用于记录的单独类
|
||||||
|
|
||||||
|
第二个示例涉及学生项目中的错误记录。一个类包含几个代码序列,如下所示:
|
||||||
|
|
||||||
|
```java
|
||||||
|
try {
|
||||||
|
rpcConn = connectionPool.getConnection(dest);
|
||||||
|
} catch (IOException e) {
|
||||||
|
NetworkErrorLogger.logRpcOpenError(req, dest, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
而不是在检测到错误时记录错误,而是调用特殊错误记录类中的单独方法。错误记录类是在同一源文件的末尾定义的:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private static class NetworkErrorLogger {
|
||||||
|
/**
|
||||||
|
* Output information relevant to an error that occurs when trying
|
||||||
|
* to open a connection to send an RPC.
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* The RPC request that would have been sent through the connection
|
||||||
|
* @param dest
|
||||||
|
* The destination of the RPC
|
||||||
|
* @param e
|
||||||
|
* The caught error
|
||||||
|
*/
|
||||||
|
public static void logRpcOpenError(RpcRequest req, AddrPortTuple dest, Exception e) {
|
||||||
|
logger.log(Level.WARNING, "Cannot send message: " + req + ". \n" + "Unable to find or open connection to " + dest + " :" + e);
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
NetworkErrorLogger 类包含几个方法,例如 logRpcSendError 和 logRpcReceiveError,每个方法都记录了不同类型的错误。
|
||||||
|
|
||||||
|
这种分离增加了复杂性,没有任何好处。日志记录方法很浅:大多数只包含一行代码,但是它们需要大量的文档。每个方法仅在单个位置调用。日志记录方法高度依赖于它们的调用:读取调用的人很可能会切换到日志记录方法,以确保记录了正确的信息。同样,阅读日志记录方法的人可能会转到调用站点以了解该方法的目的。
|
||||||
|
|
||||||
|
在此示例中,最好消除日志记录方法,并将日志记录语句放置在检测到错误的位置。这将使代码更易于阅读,并消除了日志记录方法所需的接口。
|
||||||
|
|
||||||
|
## 9.7 Example: editor undo mechanism 示例:编辑器撤消机制
|
||||||
|
|
||||||
|
在 6.2 节的 GUI 编辑器项目中,要求之一是支持多级撤消/重做,不仅要更改文本本身,还要更改选择,插入光标和视图。例如,如果用户选择了一些文本,将其删除,滚动到文件中的其他位置,然后调用 undo,则编辑器必须将其状态恢复为删除前的状态。这包括还原已删除的文本,再次选择它,并使所选的文本在窗口中可见。
|
||||||
|
|
||||||
|
一些学生项目将整个撤消机制实现为文本类的一部分。文本类维护所有不可撤消更改的列表。每当更改文本时,它将自动将条目添加到此列表中。为了更改选择,插入光标和视图,用户界面代码调用了文本类中的其他方法,然后将这些更改的条目添加到撤消列表中。当用户请求撤消或重做时,用户界面代码将调用文本类中的方法,该方法然后处理撤消列表中的条目。对于与文本相关的条目,它更新了文本类的内部。对于与其他事物(例如选择)相关的条目,将调用返回到用户界面代码的文本类来执行撤消或重做。
|
||||||
|
|
||||||
|
这种方法在文本类中导致了一系列尴尬的功能。撤消/重做的核心由通用机制组成,用于管理已执行的动作列表,并在撤消和重做操作期间逐步执行这些动作。核心与专用处理程序一起位于 text 类中,该专用处理程序对诸如文本和选择之类的特定内容实现了撤消和重做。用于选择和光标的专用撤消处理程序与文本类中的任何其他内容均无关。它们导致文本类和用户界面之间的信息泄漏,以及每个模块中来回传递撤消信息的额外方法。如果将来将新的可撤消实体添加到系统中,则将需要更改文本类,包括特定于该实体的新方法。
|
||||||
|
|
||||||
|
通过提取撤消/重做机制的通用核心并将其放在单独的类中,可以解决这些问题:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class History {
|
||||||
|
public interface Action {
|
||||||
|
public void redo();
|
||||||
|
public void undo();
|
||||||
|
}
|
||||||
|
History() {...}
|
||||||
|
void addAction(Action action) {...}
|
||||||
|
void addFence() {...}
|
||||||
|
void undo() {...}
|
||||||
|
void redo() {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在此设计中,History 类管理实现接口 History.Action 的对象的集合。每个 History.Action 描述一个操作,例如插入文本或更改光标位置,并且它提供了可以撤消或重做该操作的方法。History 类对操作中存储的信息或它们如何实现其撤消和重做方法一无所知。历史记录维护一个历史记录列表,该列表描述了应用程序整个生命周期中执行的所有操作,并且它提供了撤消和重做方法,以响应用户请求的撤消和重做而在列表中前后移动,并在应用程序中调用撤消和重做方法。历史动作。
|
||||||
|
|
||||||
|
History.Actions are special-purpose objects: each one understands a particular kind of undoable operation. They are implemented outside the History class, in modules that understand particular kinds of undoable actions. The text class might implement UndoableInsert and UndoableDelete objects to describe text insertions and deletions. Whenever it inserts text, the text class creates a new UndoableInsert object describing the insertion and invokes History.addAction to add it to the history list. The editor’s user interface code might create UndoableSelection and UndoableCursor objects that describe changes to the selection and insertion cursor.
|
||||||
|
|
||||||
|
> 历史。动作是特殊目的的对象:每个人都了解一种特殊的不可操作。它们在 History 类之外的模块中实现,这些模块可以理解特定类型的可撤销操作。文本类可能实现 UndoableInsert 和 UndoableDelete 对象,以描述文本的插入和删除。每当插入文本时,文本类都会创建一个描述该插入的新 UndoableInsert 对象,并调用 History.addAction 将其添加到历史列表中。编辑器的用户界面代码可能会创建 UndoableSelection 和 UndoableCursor 对象,这些对象描述对选择和插入光标的更改。
|
||||||
|
|
||||||
|
The History class also allows actions to be grouped so that, for example, a single undo request from the user can restore deleted text, reselect the deleted text, and reposition the insertion cursor. There are a number of ways to group actions; the History class uses fences, which are markers placed in the history list to separate groups of related actions. Each call to History.redo walks backwards through the history list, undoing actions until it reaches the next fence. The placement of fences is determined by higher-level code by invoking History.addFence.
|
||||||
|
|
||||||
|
> History 类还允许对操作进行分组,例如,来自用户的单个撤消请求可以恢复已删除的文本,重新选择已删除的文本以及重新放置插入光标。有多种将动作分组的方法。历史记录类使用围栏,围栏是放置在历史记录列表中的标记,用于分隔相关动作的组。每次对 History.redo 的调用都会向后浏览历史记录列表,撤消操作,直到到达下一个栅栏。围栏的位置由更高级别的代码通过调用 History.addFence 确定。
|
||||||
|
|
||||||
|
This approach divides the functionality of undo into three categories, each of which is implemented in a different place:
|
||||||
|
|
||||||
|
> 这种方法将撤消功能分为三类,每类都在不同的地方实现:
|
||||||
|
|
||||||
|
A general-purpose mechanism for managing and grouping actions and invoking undo/redo operations (implemented by the History class).
|
||||||
|
The specifics of particular actions (implemented by a variety of classes, each of which understands a small number of action types).
|
||||||
|
The policy for grouping actions (implemented by high-level user interface code to provide the right overall application behavior).
|
||||||
|
Each of these categories can be implemented without any understanding of the other categories. The History class does not know what kind of actions are being undone; it could be used in a variety of applications. Each action class understands only a single kind of action, and neither the History class nor the action classes needs to be aware of the policy for grouping actions.
|
||||||
|
|
||||||
|
> 一种用于管理和分组动作以及调用撤消/重做操作的通用机制(由 History 类实现)。特定操作的细节(由各种类实现,每个类都了解少量的操作类型)。分组操作的策略(由高级用户界面代码实现,以提供正确的整体应用程序行为)。这些类别中的每一个都可以在不了解其他类别的情况下实施。历史课不知道要撤消哪种操作;它可以用于多种应用。每个动作类仅理解一种动作,并且历史记录类和动作类都不需要知道将动作分组的策略。
|
||||||
|
|
||||||
|
The key design decision was the one that separated the general-purpose part of the undo mechanism from the special-purpose parts and put the general-purpose part in a class by itself. Once that was done, the rest of the design fell out naturally.
|
||||||
|
|
||||||
|
> 关键的设计决策是将撤消机制的通用部分与专用部分分开,然后将通用部分单独放在一个类中的决定。一旦完成,其余的设计就会自然消失。
|
||||||
|
|
||||||
|
Note: the suggestion to separate general-purpose code from special-purpose code refers to code related to a particular mechanism. For example, special-purpose undo code (such as code to undo a text insertion) should be separated from general-purpose undo code (such as code to manage the history list). However, it often makes sense to combine special-purpose code for one mechanism with general-purpose code for another. The text class is an example of this: it implements a general-purpose mechanism for managing text, but it includes special-purpose code related to undoing. The undo code is special-purpose because it only handles undo operations for text modifications. It doesn’t make sense to combine this code with the general-purpose undo infrastructure in the History class, but it does make sense to put it in the text class, since it is closely related to other text functions.
|
||||||
|
|
||||||
|
> 注意:将通用代码与专用代码分离的建议是指与特定机制相关的代码。例如,特殊用途的撤消代码(例如撤消文本插入的代码)应该与通用用途的撤消代码(例如管理历史记录列表的代码)分开。然而,将一种机制的专用代码与另一种机制的通用代码组合起来通常是有意义的。text 类就是这样一个例子:它实现了一种管理文本的通用机制,但是它包含了与撤销相关的专用代码。撤消代码是专用的,因为它只处理文本修改的撤消操作。将这段代码与 History 类中通用的 undo 基础结构结合在一起是没有意义的,但是将它放在 text 类中是有意义的,因为它与其他文本函数密切相关。
|
||||||
|
|
||||||
|
## 9.8 Splitting and joining methods 拆分和合并方法
|
||||||
|
|
||||||
|
The issue of when to subdivide applies not just to classes, but also to methods: are there times when it is better to divide an existing method into multiple smaller methods? Or, should two smaller methods be combined into one larger one? Long methods tend to be more difficult to understand than shorter ones, so many people argue that length alone is a good justification for breaking up a method. Students in classes are often given rigid criteria, such as “Split up any method longer than 20 lines!”
|
||||||
|
|
||||||
|
> 何时细分的问题不仅适用于类,而且还适用于方法:是否有时最好将现有方法分为多个较小的方法?还是应该将两种较小的方法合并为一种较大的方法?长方法比短方法更难于理解,因此许多人认为仅长度是分解方法的一个很好的理由。课堂上的学生通常会获得严格的标准,例如“拆分超过 20 行的任何方法!”
|
||||||
|
|
||||||
|
However, length by itself is rarely a good reason for splitting up a method. In general, developers tend to break up methods too much. Splitting up a method introduces additional interfaces, which add to complexity. It also separates the pieces of the original method, which makes the code harder to read if the pieces are actually related. You shouldn’t break up a method unless it makes the overall system simpler; I’ll discuss how this might happen below.
|
||||||
|
|
||||||
|
> 但是,长度本身很少是拆分方法的一个很好的理由。通常,开发人员倾向于过多地分解方法。拆分方法会引入其他接口,从而增加了复杂性。它还将原始方法的各个部分分开,如果这些部分实际上是相关的,则使代码更难阅读。您不应该分解一种方法,除非它使整个系统更加简单;我将在下面讨论这种情况。
|
||||||
|
|
||||||
|
Long methods aren’t always bad. For example, suppose a method contains five 20-line blocks of code that are executed in order. If the blocks are relatively independent, then the method can be read and understood one block at a time; there’s not much benefit in moving each of the blocks into a separate method. If the blocks have complex interactions, it’s even more important to keep them together so readers can see all of the code at once; if each block is in a separate method, readers will have to flip back and forth between these spread-out methods in order to understand how they work together. Methods containing hundreds of lines of code are fine if they have a simple signature and are easy to read. These methods are deep (lots of functionality, simple interface), which is good.
|
||||||
|
|
||||||
|
> 长方法并不总是坏的。例如,假设一个方法包含按顺序执行的五个 20 行代码块。如果这些块是相对独立的,则可以一次读取并理解该方法的一个块。将每个块移动到单独的方法中并没有太大的好处。如果这些块具有复杂的交互作用,则将它们保持在一起就显得尤为重要,这样读者就可以一次看到所有代码。如果每个块使用单独的方法,则读者将不得不在这些扩展方法之间来回切换,以了解它们如何协同工作。如果方法具有简单的签名并且易于阅读,则包含数百行代码的方法就可以了。这些方法很深入(很多功能,简单的接口),很好。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Figure 9.3: A method (a) can be split either by by extracting a subtask (b) or by dividing its functionality into two separate methods (c). A method should not be split if it results in shallow methods, as in (d).
|
||||||
|
|
||||||
|
> 图 9.3:方法(a)可以通过提取子任务(b)或将其功能划分为两个单独的方法(c)进行拆分。如果方法导致浅层方法,则不应拆分该方法,如(d)所示。
|
||||||
|
|
||||||
|
When designing methods, the most important goal is to provide clean and simple abstractions. Each method should do one thing and do it completely. The method should have a clean and simple interface, so that users don’t need to have much information in their heads in order to use it correctly. The method should be deep: its interface should be much simpler than its implementation. If a method has all of these properties, then it probably doesn’t matter whether it is long or not.
|
||||||
|
|
||||||
|
> 设计方法时,最重要的目标是提供简洁的抽象。每种方法都应该做一件事并且完全做到这一点。该方法应该具有简洁的接口,以便用户无需费神就可以正确使用它。该方法应该很深:其接口应该比其实现简单得多。如果一个方法具有所有这些属性,那么它的长短与否可能无关紧要。
|
||||||
|
|
||||||
|
Splitting up a method only makes sense if it results in cleaner abstractions, overall. There are two ways to do this, which are diagrammed in Figure 9.3. The best way is by factoring out a subtask into a separate method, as shown in Figure 9.3(b). The subdivision results in a child method containing the subtask and a parent method containing the remainder of the original method; the parent invokes the child. The interface of the new parent method is the same as the original method. This form of subdivision makes sense if there is a subtask that is cleanly separable from the rest of the original method, which means (a) someone reading the child method doesn’t need to know anything about the parent method and (b) someone reading the parent method doesn’t need to understand the implementation of the child method. Typically this means that the child method is relatively general-purpose: it could conceivably be used by other methods besides the parent. If you make a split of this form and then find yourself flipping back and forth between the parent and child to understand how they work together, that is a red flag (“Conjoined Methods”) indicating that the split was probably a bad idea.
|
||||||
|
|
||||||
|
> 总体而言,拆分方法只有在其导致更抽象的抽象时才有意义。有两种方法可以做到这一点,如图 9.3 所示。最佳方法是将子任务分解为单独的方法,如图 9.3(b)所示。该细分产生一个包含该子任务的子方法和一个包含原始方法其余部分的父方法;父级调用子级。新的父方法的接口与原始方法的接口相同。如果存在一个与原始方法的其余部分完全可分离的子任务,则这种细分形式是有意义的,这意味着(a)读取子方法的某人不需要了解有关父方法的任何信息,以及(b)某人在阅读父方法不需要了解子方法的实现。通常,这意味着子方法是相对通用的:可以想象除父方法外,其他方法也可以使用它。如果您对这种形式进行拆分,然后发现自己在父母和孩子之间来回翻转以了解他们如何一起工作,那是一个红色标记(“联合方法”),表明拆分可能不是一个好主意。
|
||||||
|
|
||||||
|
The second way to break up a method is to split it into two separate methods, each visible to callers of the original method, as in Figure 9.3(c). This makes sense if the original method had an overly complex interface because it tried to do multiple things that were not closely related. If this is the case, it may be possible to divide the method’s functionality into two or more smaller methods, each of which has only a part of the original method’s functionality. If you make a split like this, the interface for each of the resulting methods should be simpler than the interface of the original method. Ideally, most callers should only need to invoke one of the two new methods; if callers must invoke both of the new methods, then that adds complexity, which makes it less likely that the split is a good idea. The new methods will be more focused in what they do. It is a good sign if the new methods are more general-purpose than the original method (i.e., you can imagine using them separately in other situations).
|
||||||
|
|
||||||
|
> 分解方法的第二种方法是将其拆分为两个单独的方法,每个方法对原始方法的调用者可见,如图 9.3(c)所示。如果原始方法的接口过于复杂,这是有道理的,因为该接口试图执行不密切相关的多项操作。在这种情况下,可以将方法的功能划分为两个或更多个较小的方法,每个方法仅具有原始方法功能的一部分。如果进行这样的拆分,则每个结果方法的接口应该比原始方法的接口更简单。理想情况下,大多数调用者只需要调用两个新方法之一即可;如果调用者必须同时调用这两个新方法,则将增加复杂性,从而降低拆分是个好主意的可能性。新方法将更加专注于它们的工作。如果新方法比原始方法更具通用性,那么这是一个好兆头(例如,您可以想象在其他情况下单独使用它们)。
|
||||||
|
|
||||||
|
Splits of the form shown in Figure 9.3(c) don’t make sense very often, because they result in callers having to deal with multiple methods instead of one. When you split this way, you run the risk of ending up with several shallow methods, as in Figure 9.3(d). If the caller has to invoke each of the separate methods, passing state back and forth between them, then splitting is not a good idea. If you’re considering a split like the one in Figure 9.3(c), you should judge it based on whether it simplifies things for callers.
|
||||||
|
|
||||||
|
> 图 9.3(c)所示形式的拆分并不是很有意义,因为它们导致调用者不得不处理多个方法而不是一个方法。当您以这种方式拆分时,您可能会遇到几种浅层方法的风险,如图 9.3(d)所示。如果调用者必须调用每个单独的方法,并在它们之间来回传递状态,则拆分不是一个好主意。如果您正在考虑像图 9.3(c)所示的拆分,则应基于它是否简化了呼叫者的情况来进行判断。
|
||||||
|
|
||||||
|
There are also situations where a system can be made simpler by joining methods together. For example, joining methods might replace two shallow methods with one deeper method; it might eliminate duplication of code; it might eliminate dependencies between the original methods, or intermediate data structures; it might result in better encapsulation, so that knowledge that was previously present in multiple places is now isolated in a single place; or it might result in a simpler interface, as discussed in Section 9.2.
|
||||||
|
|
||||||
|
> 在某些情况下,通过将方法结合在一起可以简化系统。例如,连接方法可以用一种更深的方法代替两种浅的方法。它可以消除重复的代码;它可以消除原始方法或中间数据结构之间的依赖关系;它可能导致更好的封装,从而使以前在多个位置存在的知识现在被隔离在一个位置;否则可能会导致接口更简单,如 9.2 节所述。
|
||||||
|
|
||||||
|
img Red Flag: Conjoined Methods img
|
||||||
|
|
||||||
|
It should be possible to understand each method independently. If you can’t understand the implementation of one method without also understanding the implementation of another, that’s a red flag. This red flag can occur in other contexts as well: if two pieces of code are physically separated, but each can only be understood by looking at the other, that is a red flag.
|
||||||
|
|
||||||
|
> 应该有可能独立地理解每种方法。如果您不能不理解另一种方法的实现而无法理解一种方法的实现,那就是一个危险信号。该危险信号也可以在其他情况下发生:如果两段代码在物理上是分开的,但是只有通过查看另一段代码才能理解它们,这就是危险信号。
|
||||||
|
|
||||||
|
## 9.9 Conclusion 结论
|
||||||
|
|
||||||
|
The decision to split or join modules should be based on complexity. Pick the structure that results in the best information hiding, the fewest dependencies, and the deepest interfaces.
|
||||||
|
|
||||||
|
> 拆分或加入模块的决定应基于复杂性。选择一种结构,它可以隐藏最佳的信息,最少的依赖关系和最深的接口。
|
||||||
85
docs/summary.md
Normal file
85
docs/summary.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# 总结
|
||||||
|
|
||||||
|
## Summary of Design Principles 设计原则摘要
|
||||||
|
|
||||||
|
Here are the most important software design principles discussed in this book:
|
||||||
|
|
||||||
|
> 这是本书中讨论的最重要的软件设计原则:
|
||||||
|
|
||||||
|
1. Complexity is incremental: you have to sweat the small stuff (see p. 11).
|
||||||
|
2. Working code isn’t enough (see p. 14).
|
||||||
|
3. Make continual small investments to improve system design (see p. 15).
|
||||||
|
4. Modules should be deep (see p. 22)
|
||||||
|
5. Interfaces should be designed to make the most common usage as simple as possible (see p. 27).
|
||||||
|
6. It’s more important for a module to have a simple interface than a simple implementation (see pp. 55, 71).
|
||||||
|
7. General-purpose modules are deeper (see p. 39).
|
||||||
|
8. Separate general-purpose and special-purpose code (see p. 62).
|
||||||
|
9. Different layers should have different abstractions (see p. 45).
|
||||||
|
10. Pull complexity downward (see p. 55).
|
||||||
|
11. Define errors (and special cases) out of existence (see p. 79).
|
||||||
|
12. Design it twice (see p. 91).
|
||||||
|
13. Comments should describe things that are not obvious from the code (see p. 101).
|
||||||
|
14. Software should be designed for ease of reading, not ease of writing (see p. 149).
|
||||||
|
15. The increments of software development should be abstractions, not features (see p. 154).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 1. 复杂性是逐步增加的:您必须流汗一些小东西(请参阅第 11 页)。
|
||||||
|
> 2. 工作代码还不够(请参阅第 14 页)。
|
||||||
|
> 3. 持续进行少量投资以改善系统设计(请参阅第 15 页)。
|
||||||
|
> 4. 模块应较深(请参见第 22 页)
|
||||||
|
> 5. 接口的设计应尽可能简化最常见的用法(请参阅第 27 页)。
|
||||||
|
> 6. 一个模块具有一个简单的接口比一个简单的实现更重要(请参阅第 55、71 页)。
|
||||||
|
> 7. 通用模块更深入(请参阅第 39 页)。
|
||||||
|
> 8. 通用和专用代码分开(请参见第 62 页)。
|
||||||
|
> 9. 不同的层应具有不同的抽象(请参见第 45 页)。
|
||||||
|
> 10. 降低复杂度(请参阅第 55 页)。
|
||||||
|
> 11. 定义不存在的错误(和特殊情况)(请参阅第 79 页)。
|
||||||
|
> 12. 设计两次(请参阅第 91 页)。
|
||||||
|
> 13. 注释应描述代码中不明显的内容(请参见第 101 页)。
|
||||||
|
> 14. 软件的设计应易于阅读而不是易于编写(请参见第 149 页)。
|
||||||
|
> 15. 软件开发的增量应该是抽象而不是功能(请参见第 154 页)。
|
||||||
|
|
||||||
|
## Summary of Red Flags 红旗摘要
|
||||||
|
|
||||||
|
Here are a few of of the most important red flags discussed in this book. The presence of any of these symptoms in a system suggests that there is a problem with the system’s design:
|
||||||
|
|
||||||
|
> 这是本书中讨论的一些最重要的危险信号。系统中任何这些症状的存在表明系统的设计存在问题:
|
||||||
|
|
||||||
|
- Shallow Module: the interface for a class or method isn’t much simpler than its implementation (see pp. 25, 110).
|
||||||
|
- Information Leakage: a design decision is reflected in multiple modules (see p. 31).
|
||||||
|
- Temporal Decomposition: the code structure is based on the order in which operations are executed, not on information hiding (see p. 32).
|
||||||
|
- Overexposure: An API forces callers to be aware of rarely used features in order to use commonly used features (see p. 36).
|
||||||
|
- Pass-Through Method: a method does almost nothing except pass its arguments to another method with a similar signature (see p. 46).
|
||||||
|
- Repetition: a nontrivial piece of code is repeated over and over (see p. 62).
|
||||||
|
- Special-General Mixture: special-purpose code is not cleanly separated from general purpose code (see p. 65).
|
||||||
|
- Conjoined Methods: two methods have so many dependencies that its hard to understand the implementation of one without understanding the implementation of the other (see p. 72).
|
||||||
|
- Comment Repeats Code: all of the information in a comment is immediately obvious from the code next to the comment (see p. 104).
|
||||||
|
- Implementation Documentation Contaminates Interface: an interface comment describes implementation details not needed by users of the thing being documented (see p. 114).
|
||||||
|
- Vague Name: the name of a variable or method is so imprecise that it doesn’t convey much useful information (see p. 123).
|
||||||
|
- Hard to Pick Name: it is difficult to come up with a precise and intuitive name for an entity (see p. 125).
|
||||||
|
- Hard to Describe: in order to be complete, the documentation for a variable or method must be long. (see p. 131).
|
||||||
|
- Nonobvious Code: the behavior or meaning of a piece of code cannot be understood easily. (see p. 148).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> - 浅模块:类或方法的接口并不比其实现简单得多(请参见第 25、110 页)。
|
||||||
|
> - 信息泄漏:设计决策反映在多个模块中(请参阅第 31 页)。
|
||||||
|
> - 时间分解:代码结构基于执行操作的顺序,而不是信息隐藏(请参见第 32 页)。
|
||||||
|
> - 过度暴露:API 强制调用者注意很少使用的功能,以便使用常用功能(请参阅第 36 页)。
|
||||||
|
> - Pass-Through Method:一种方法几乎不执行任何操作,只是将其参数传递给具有相似签名的另一种方法(请参见第 46 页)。
|
||||||
|
> - 重复:一遍又一遍的重复代码(请参见第 62 页)。
|
||||||
|
> - 特殊通用混合物:特殊用途代码未与通用代码完全分开(请参阅第 65 页)。
|
||||||
|
> - 联合方法:两种方法之间的依赖性很大,以至于很难理解一种方法的实现而又不理解另一种方法的实现(请参阅第 72 页)。
|
||||||
|
> - 注释重复代码:注释旁边的代码会立即显示注释中的所有信息(请参阅第 104 页)。
|
||||||
|
> - 实施文档污染了界面:界面注释描述了所记录事物的用户不需要的实施细节(请参阅第 114 页)。
|
||||||
|
> - 含糊不清的名称:变量或方法的名称过于精确,以至于它不能传达很多有用的信息(请参阅第 123 页)。
|
||||||
|
> - 难以选择的名称:很难为实体提供准确而直观的名称(请参见第 125 页)。
|
||||||
|
> - 难以描述:为了完整起见,变量或方法的文档必须很长。(请参阅第 131 页)。
|
||||||
|
> - 非显而易见的代码:一段代码的行为或含义不容易理解。(请参阅第 148 页)。
|
||||||
|
|
||||||
|
## About the Author 关于作者
|
||||||
|
|
||||||
|
John Ousterhout is the Bosack Lerner Professor of Computer Science at Stanford University. He is the creator of the Tcl scripting language and is also well known for his work in distributed operating systems and storage systems. Ousterhout received a BS degree in Physics from Yale University and a PhD in Computer Science from Carnegie Mellon University. He is a member of the National Academy of Engineering and has received numerous awards, including the ACM Software System Award, the ACM Grace Murray Hopper Award, the National Science Foundation Presidential Young Investigator Award, and the U.C. Berkeley Distinguished Teaching Award.
|
||||||
|
|
||||||
|
> John Ousterhout 是斯坦福大学的 Bosack Lerner 计算机科学教授。他是 Tcl 脚本语言的创建者,并且以在分布式操作系统和存储系统中的工作而闻名。Ousterhout 在耶鲁大学获得了物理学学士学位,并在卡内基梅隆大学获得了计算机科学博士学位。他是美国国家工程院院士,并获得了无数奖项,包括 ACM 软件系统奖,ACM Grace Murray Hopper 奖,美国国家科学基金会总统年轻研究者奖和 UC Berkeley 杰出教学奖。
|
||||||
Reference in New Issue
Block a user