Repository: glen9527/Clean-Code-zh Branch: master Commit: 28a2ca4069d9 Files: 25 Total size: 704.8 KB Directory structure: gitextract_0vuhkduc/ ├── .gitignore ├── LICENSE ├── README.md ├── docs/ │ ├── .vuepress/ │ │ └── config.js │ ├── README.md │ ├── apA.md │ ├── ch1.md │ ├── ch10.md │ ├── ch11.md │ ├── ch12.md │ ├── ch13.md │ ├── ch14.md │ ├── ch15.md │ ├── ch16.md │ ├── ch17.md │ ├── ch2.md │ ├── ch3.md │ ├── ch4.md │ ├── ch5.md │ ├── ch6.md │ ├── ch7.md │ ├── ch8.md │ └── ch9.md ├── gitee-deploy.sh └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ docs/.vuepress/dist/ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018-present, Yuxi (Evan) You Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Clean-Code-zh 《代码整洁之道》中文翻译 在线阅读:[http://gdut_yy.gitee.io/doc-cleancode/](http://gdut_yy.gitee.io/doc-cleancode/) ## 前言 ## Index - [第 1 章 整洁代码](docs/ch1.md) - [第 2 章 有意义的命名](docs/ch2.md) - [第 3 章 函数](docs/ch3.md) - [第 4 章 注释](docs/ch4.md) - [第 5 章 格式](docs/ch5.md) - [第 6 章 对象和数据结构](docs/ch6.md) - [第 7 章 错误处理](docs/ch7.md) - [第 8 章 边界](docs/ch8.md) - [第 9 章 单元测试](docs/ch9.md) - [第 10 章 类](docs/ch10.md) - [第 11 章 系统](docs/ch11.md) - [第 12 章 迭进](docs/ch12.md) - [第 13 章 并发编程](docs/ch13.md) - [第 14 章 逐步改进](docs/ch14.md) - [第 15 章 JUnit 内幕](docs/ch15.md) - [第 16 章 重构 SerialDate](docs/ch16.md) - [第 17 章 味道与启发](docs/ch17.md) - [附录 A 并发编程 II](docs/apA.md) ## 本地开发 & 阅读 本项目基于 vuepress 进行开发,以提供比 github mardown 更佳的阅读体验 依赖于 `node.js`、`yarn`、`vuepress` 等环境 ```sh # vuepress yarn global add vuepress # 本地开发 git clone https://github.com/gdut-yy/Clean-Code-zh.git cd Clean-Code-zh/ yarn docs:dev # 本地阅读 http://localhost:8080/doc-cleancode/ ``` ## License [MIT](https://github.com/gdut-yy/Clean-Code-zh/blob/master/LICENSE) ================================================ FILE: docs/.vuepress/config.js ================================================ // .vuepress/config.js module.exports = { // 网站的标题 title: "Clean Code 中文", // 上下文根 base: "/doc-cleancode/", themeConfig: { // 假定是 GitHub. 同时也可以是一个完整的 GitLab URL repo: "gdut-yy/Clean-Code-zh", // 自定义仓库链接文字。默认从 `themeConfig.repo` 中自动推断为 // "GitHub"/"GitLab"/"Bitbucket" 其中之一,或是 "Source"。 repoLabel: "Github", // 以下为可选的编辑链接选项 // 假如你的文档仓库和项目本身不在一个仓库: docsRepo: "gdut-yy/Clean-Code-zh", // 假如文档放在一个特定的分支下: docsBranch: "master/docs", // 默认是 false, 设置为 true 来启用 editLinks: true, // 默认为 "Edit this page" editLinkText: "帮助我们改善此页面!", // 最后更新时间 lastUpdated: "Last Updated", // 最大深度 sidebarDepth: 2, // 导航栏 nav: [], // 侧边栏 sidebar: { "/": [ "", "ch1.md", "ch2.md", "ch3.md", "ch4.md", "ch5.md", "ch6.md", "ch7.md", "ch8.md", "ch9.md", "ch10.md", "ch11.md", "ch12.md", "ch13.md", "ch14.md", "ch15.md", "ch16.md", "ch17.md", "apA.md" ] } } }; ================================================ FILE: docs/README.md ================================================ # Clean Code 中文
## 序 ## 目录 - [第 1 章 整洁代码](ch1.md) - [第 2 章 有意义的命名](ch2.md) - [第 3 章 函数](ch3.md) - [第 4 章 注释](ch4.md) - [第 5 章 格式](ch5.md) - [第 6 章 对象和数据结构](ch6.md) - [第 7 章 错误处理](ch7.md) - [第 8 章 边界](ch8.md) - [第 9 章 单元测试](ch9.md) - [第 10 章 类](ch10.md) - [第 11 章 系统](ch11.md) - [第 12 章 迭进](ch12.md) - [第 13 章 并发编程](ch13.md) - [第 14 章 逐步改进](ch14.md) - [第 15 章 JUnit 内幕](ch15.md) - [第 16 章 重构 SerialDate](ch16.md) - [第 17 章 味道与启发](ch17.md) - [附录 A 并发编程 II](apA.md) ================================================ FILE: docs/apA.md ================================================ # Appendix A Concurrency II by Brett L. Schuchert This appendix supports and amplifies the Concurrency chapter on page 177. It is written as a series of independent topics and you can generally read them in any order. There is some duplication between sections to allow for such reading. CLIENT/SERVER EXAMPLE Imagine a simple client/server application. A server sits and waits listening on a socket for a client to connect. A client connects and sends a request. The Server Here is a simplified version of a server application. Full source for this example is available starting on page 343, Client/Server Nonthreaded. ```java ServerSocket serverSocket = new ServerSocket(8009); while (keepProcessing) { try { Socket socket = serverSocket.accept(); process(socket); } catch (Exception e) { handle(e); } } ``` This simple application waits for a connection, processes an incoming message, and then again waits for the next client request to come in. Here’s client code that connects to this server: ```java private void connectSendReceive(int i) { try { Socket socket = new Socket(“localhost”, PORT); MessageUtils.sendMessage(socket, Integer.toString(i)); MessageUtils.getMessage(socket); socket.close(); } catch (Exception e) { e.printStackTrace(); } } ``` How well does this client/server pair perform? How can we formally describe that performance? Here’s a test that asserts that the performance is “acceptable”: ```java @Test(timeout = 10000) public void shouldRunInUnder10Seconds() throws Exception { Thread[] threads = createThreads(); startAllThreadsw(threads); waitForAllThreadsToFinish(threads); } ``` The setup is left out to keep the example simple (see “ClientTest.java” on page 344). This test asserts that it should complete within 10,000 milliseconds. This is a classic example of validating the throughput of a system. This system should complete a series of client requests in ten seconds. So long as the server can process each individual client request in time, the test will pass. What happens if the test fails? Short of developing some kind of event polling loop, there is not much to do within a single thread that will make this code any faster. Will using multiple threads solve the problem? It might, but we need to know where the time is being spent. There are two possibilities: - I/O—using a socket, connecting to a database, waiting for virtual memory swapping, and so on. - Processor—numerical calculations, regular expression processing, garbage collection, and so on. Systems typically have some of each, but for a given operation one tends to dominate. If the code is processor bound, more processing hardware can improve throughput, making our test pass. But there are only so many CPU cycles available, so adding threads to a processor-bound problem will not make it go faster. On the other hand, if the process is I/O bound, then concurrency can increase efficiency. When one part of the system is waiting for I/O, another part can use that wait time to process something else, making more effective use of the available CPU. Adding Threading Assume for the moment that the performance test fails. How can we improve the throughput so that the performance test passes? If the process method of the server is I/O bound, then here is one way to make the server use threads (just change the processMessage): ```java void process(final Socket socket) { if (socket == null) return; Runnable clientHandler = new Runnable() { public void run() { try { String message = MessageUtils.getMessage(socket); MessageUtils.sendMessage(socket, “Processed: ” + message); closeIgnoringException(socket); } catch (Exception e) { e.printStackTrace(); } } }; Thread clientConnection = new Thread(clientHandler); clientConnection.start(); } ``` Assume that this change causes the test to pass;1 the code is complete, correct? 1. You can verify that for yourself by trying out the before and after code. Review the nonthreaded code starting on page 343. Review the threaded code starting on page 346. Server Observations The updated server completes the test successfully in just over one second. Unfortunately, this solution is a bit naive and introduces some new problems. How many threads might our server create? The code sets no limit, so the we could feasibly hit the limit imposed by the Java Virtual Machine (JVM). For many simple systems this may suffice. But what if the system is meant to support many users on the public net? If too many users connect at the same time, the system might grind to a halt. But set the behavioral problem aside for the moment. The solution shown has problems of cleanliness and structure. How many responsibilities does the server code have? - Socket connection management - Client processing - Threading policy - Server shutdown policy Unfortunately, all these responsibilities live in the process function. In addition, the code crosses many different levels of abstraction. So, small as the process function is, it needs to be repartitioned. The server has several reasons to change; therefore it violates the Single Responsibility Principle. To keep concurrent systems clean, thread management should be kept to a few, well-controlled places. What’s more, any code that manages threads should do nothing other than thread management. Why? If for no other reason than that tracking down concurrency issues is hard enough without having to unwind other nonconcurrency issues at the same time. If we create a separate class for each of the responsibilities listed above, including the thread management responsibility, then when we change the thread management strategy, the change will impact less overall code and will not pollute the other responsibilities. This also makes it much easier to test all the other responsibilities without having to worry about threading. Here is an updated version that does just that: ```java public void run() { while (keepProcessing) { try { ClientConnection clientConnection = connectionManager.awaitClient(); ClientRequestProcessor requestProcessor = new ClientRequestProcessor(clientConnection); clientScheduler.schedule(requestProcessor); } catch (Exception e) { e.printStackTrace(); } } connectionManager.shutdown(); } ``` This now focuses all things thread-related into one place, clientScheduler. If there are concurrency problems, there is just one place to look: ```java public interface ClientScheduler { void schedule(ClientRequestProcessor requestProcessor); } ``` The current policy is easy to implement: ```java public class ThreadPerRequestScheduler implements ClientScheduler { public void schedule(final ClientRequestProcessor requestProcessor) { Runnable runnable = new Runnable() { public void run() { requestProcessor.process(); } }; Thread thread = new Thread(runnable); thread.start(); } } ``` Having isolated all the thread management into a single place, it is much easier to change the way we control threads. For example, moving to the Java 5 Executor framework involves writing a new class and plugging it in (Listing A-1). Listing A-1 ExecutorClientScheduler.java ```java import java.util.concurrent.Executor; import java.util.concurrent.Executors; public class ExecutorClientScheduler implements ClientScheduler { Executor executor; public ExecutorClientScheduler(int availableThreads) { executor = Executors.newFixedThreadPool(availableThreads); } public void schedule(final ClientRequestProcessor requestProcessor) { Runnable runnable = new Runnable() { public void run() { requestProcessor.process(); } }; executor.execute(runnable); } } ``` Conclusion Introducing concurrency in this particular example demonstrates a way to improve the throughput of a system and one way of validating that throughput through a testing framework. Focusing all concurrency code into a small number of classes is an example of applying the Single Responsibility Principle. In the case of concurrent programming, this becomes especially important because of its complexity. POSSIBLE PATHS OF EXECUTION Review the method incrementValue, a one-line Java method with no looping or branching: ```java public class IdGenerator { int lastIdUsed; public int incrementValue() { return ++lastIdUsed; } } ``` Ignore integer overflow and assume that only one thread has access to a single instance of IdGenerator. In this case there is a single path of execution and a single guaranteed result: - The value returned is equal to the value of lastIdUsed, both of which are one greater than just before calling the method. What happens if we use two threads and leave the method unchanged? What are the possible outcomes if each thread calls incrementValue once? How many possible paths of execution are there? First, the outcomes (assume lastIdUsed starts with a value of 93): - Thread 1 gets the value of 94, thread 2 gets the value of 95, and lastIdUsed is now 95. - Thread 1 gets the value of 95, thread 2 gets the value of 94, and lastIdUsed is now 95. - Thread 1 gets the value of 94, thread 2 gets the value of 94, and lastIdUsed is now 94. The final result, while surprising, is possible. To see how these different results are possible, we need to understand the number of possible paths of execution and how the Java Virtual Machine executes them. Number of Paths To calculate the number of possible execution paths, we’ll start with the generated byte-code. The one line of java (return ++lastIdUsed;) becomes eight byte-code instructions. It is possible for the two threads to interleave the execution of these eight instructions the way a card dealer interleaves cards as he shuffles a deck.2 Even with only eight cards in each hand, there are a remarkable number of shuffled outcomes. 2. This is a bit of a simplification. However, for the purpose of this discussion, we can use this simplifying model. For this simple case of N instructions in a sequence, no looping or conditionals, and T threads, the total number of possible execution paths is equal to ![](figures/apA/322equ01.jpg) Calculating the Possible Orderings This comes from an email from Uncle Bob to Brett: With N steps and T threads there are T* N total steps. Prior to each step there is a context switch that chooses between the T threads. Each path can thus be represented as a string of digits denoting the context switches. Given steps A and B and threads 1 and 2, the six possible paths are 1122, 1212, 1221, 2112, 2121, and 2211. Or, in terms of steps it is A1B1A2B2, A1A2B1B2, A1A2B2B1, A2A1B1B2, A2A1B2B1, and A2B2A1B1. For three threads the sequence is 112233, 112323, 113223, 113232, 112233, 121233, 121323, 121332, 123132, 123123, …. One characteristic of these strings is that there must always be N instances of each T. So the string 111111 is invalid because it has six instances of 1 and zero instances of 2 and 3. So we want the permutations of N 1’s, N 2’s, … and N T’s. This is really just the permutations of N* T things taken N* T at a time, which is (N* T)!, but with all the duplicates removed. So the trick is to count the duplicates and subtract that from (N* T)!. Given two steps and two threads, how many duplicates are there? Each four-digit string has two 1s and two 2s. Each of those pairs could be swapped without changing the sense of the string. You could swap the 1s or the 2s both, or neither. So there are four isomorphs for each string, which means that there are three duplicates. So three out of four of the options are duplicates; alternatively one of four of the permutations are NOT duplicates. 4! * .25 = 6. So this reasoning seems to work. How many duplicates are there? In the case where N = 2 and T = 2, I could swap the 1s, the 2s, or both. In the case where N = 2 and T = 3, I could swap the 1s, the 2s, the 3s, 1s and 2s, 1s and 3s, or 2s and 3s. Swapping is just the permutations of N. Let’s say there are P permutations of N. The number of different ways to arrange those permutations are P**T. So the number of possible isomorphs is N!**T. And so the number of paths is (T*N)!/(N!**T). Again, in our T = 2, N = 2 case we get 6 (24/4). For N = 2 and T = 3 we get 720/8 = 90. For N = 3 and T = 3 we get 9!/6^3 = 1680. For our simple case of one line of Java code, which equates to eight lines of byte-code and two threads, the total number of possible paths of execution is 12,870. If the type of lastIdUsed is a long, then every read/write becomes two operations instead of one, and the number of possible orderings becomes 2,704,156. What happens if we make one change to this method? ```java public synchronized void incrementValue() { ++lastIdUsed; } The number of possible execution pathways becomes two for two threads and N! in the general case. Digging Deeper What about the surprising result that two threads could both call the method once (before we added synchronized) and get the same numeric result? How is that possible? First things first. What is an atomic operation? We can define an atomic operation as any operation that is uninterruptable. For example, in the following code, line 5, where 0 is assigned to lastid, is atomic because according to the Java Memory model, assignment to a 32-bit value is uninterruptable. ```java 01: public class Example { 02: int lastId; 03: 04: public void resetId() { 05: value = 0; 06: } 07: 08: public int getNextId() { 09: ++value; 10: } 11:} ``` What happens if we change type of lastId from int to long? Is line 5 still atomic? Not according to the JVM specification. It could be atomic on a particular processor, but according to the JVM specification, assignment to any 64-bit value requires two 32-bit assignments. This means that between the first 32-bit assignment and the second 32-bit assignment, some other thread could sneak in and change one of the values. What about the pre-increment operator, ++, on line 9? The pre-increment operator can be interrupted, so it is not atomic. To understand, let’s review the byte-code of both of these methods in detail. Before we go any further, here are three definitions that will be important: - Frame—Every method invocation requires a frame. The frame includes the return address, any parameters passed into the method and the local variables defined in the method. This is a standard technique used to define a call stack, which is used by modern languages to allow for basic function/method invocation and to allow for recursive invocation. - Local variable—Any variables defined in the scope of the method. All nonstatic methods have at least one variable, this, which represents the current object, the object that received the most recent message (in the current thread), which caused the method invocation. - Operand stack—Many of the instructions in the Java Virtual Machine take parameters. The operand stack is where those parameters are put. The stack is a standard last-in, first-out (LIFO) data structure. Here is the byte-code generated for resetId(): ![](figures/apA/0324tab01.jpg) ![](figures/apA/0325tab01.jpg) These three instructions are guaranteed to be atomic because, although the thread executing them could be interrupted after any one of them, the information for the PUTFIELD instruction (the constant value 0 on the top of the stack and the reference to this one below the top, along with the field value) cannot be touched by another thread. So when the assignment occurs, we are guaranteed that the value 0 will be stored in the field value. The operation is atomic. The operands all deal with information local to the method, so there is no interference between multiple threads. So if these three instructions are executed by ten threads, there are 4.38679733629e+24 possible orderings. However, there is only one possible outcome, so the different orderings are irrelevant. It just so happens that the same outcome is guaranteed for longs in this case as well. Why? All ten threads are assigning a constant value. Even if they interleave with each other, the end result is the same. With the ++ operation in the getNextId method, there are going to be problems. Assume that lastId holds 42 at the beginning of this method. Here is the byte-code for this new method: ![](figures/apA/0325tab02.jpg) Imagine the case where the first thread completes the first three instructions, up to and including GETFIELD, and then it is interrupted. A second thread takes over and performs the entire method, incrementing lastId by one; it gets 43 back. Then the first thread picks up where it left off; 42 is still on the operand stack because that was the value of lastId when it executed GETFIELD. It adds one to get 43 again and stores the result. The value 43 is returned to the first thread as well. The result is that one of the increments is lost because the first thread stepped on the second thread after the second thread interrupted the first thread. Making the getNexId() method synchronized fixes this problem. Conclusion An intimate understanding of byte-code is not necessary to understand how threads can step on each other. If you can understand this one example, it should demonstrate the possibility of multiple threads stepping on each other, which is enough knowledge. That being said, what this trivial example demonstrates is a need to understand the memory model enough to know what is and is not safe. It is a common misconception that the ++ (pre- or post-increment) operator is atomic, and it clearly is not. This means you need to know: - Where there are shared objects/values - The code that can cause concurrent read/update issues - How to guard such concurrent issues from happening KNOWING YOUR LIBRARY Executor Framework As demonstrated in the ExecutorClientScheduler.java on page 321, the Executor framework introduced in Java 5 allows for sophisticated execution using thread pools. This is a class in the java.util.concurrent package. If you are creating threads and are not using a thread pool or are using a hand-written one, you should consider using the Executor. It will make your code cleaner, easier to follow, and smaller. The Executor framework will pool threads, resize automatically, and recreate threads if necessary. It also supports futures, a common concurrent programming construct. The Executor framework works with classes that implement Runnable and also works with classes that implement the Callable interface. A Callable looks like a Runnable, but it can return a result, which is a common need in multithreaded solutions. A future is handy when code needs to execute multiple, independent operations and wait for both to finish: ```java public String processRequest(String message) throws Exception { Callable makeExternalCall = new Callable() { public String call() throws Exception { String result = “”; // make external request return result; } }; Future result = executorService.submit(makeExternalCall); String partialResult = doSomeLocalProcessing(); return result.get() + partialResult; } ``` In this example, the method starts executing the makeExternalCall object. The method continues other processing. The final line calls result.get(), which blocks until the future completes. Nonblocking Solutions The Java 5 VM takes advantage of modern processor design, which supports reliable, nonblocking updates. Consider, for example, a class that uses synchronization (and therefore blocking) to provide a thread-safe update of a value: ```java public class ObjectWithValue { private int value; public void synchronized incrementValue() { ++value; } public int getValue() { return value; } } ``` Java 5 has a series of new classes for situations like this: AtomicBoolean, AtomicInteger, and AtomicReference are three examples; there are several more. We can rewrite the above code to use a nonblocking approach as follows: ```java public class ObjectWithValue { private AtomicInteger value = new AtomicInteger(0); public void incrementValue() { value.incrementAndGet(); } public int getValue() { return value.get(); } } ``` Even though this uses an object instead of a primitive and sends messages like incrementAndGet() instead of ++, the performance of this class will nearly always beat the previous version. In some cases it will only be slightly faster, but the cases where it will be slower are virtually nonexistent. How is this possible? Modern processors have an operation typically called Compare and Swap (CAS). This operation is analogous to optimistic locking in databases, whereas the synchronized version is analogous to pessimistic locking. The synchronized keyword always acquires a lock, even when a second thread is not trying to update the same value. Even though the performance of intrinsic locks has improved from version to version, they are still costly. The nonblocking version starts with the assumption that multiple threads generally do not modify the same value often enough that a problem will arise. Instead, it efficiently detects whether such a situation has occurred and retries until the update happens successfully. This detection is almost always less costly than acquiring a lock, even in moderate to high contention situations. How does the Virtual Machine accomplish this? The CAS operation is atomic. Logically, the CAS operation looks something like the following: ```java int variableBeingSet; void simulateNonBlockingSet(int newValue) { int currentValue; do { currentValue = variableBeingSet } while(currentValue != compareAndSwap(currentValue, newValue)); } int synchronized compareAndSwap(int currentValue, int newValue) { if(variableBeingSet == currentValue) { variableBeingSet = newValue; return currentValue; } return variableBeingSet; } ``` When a method attempts to update a shared variable, the CAS operation verifies that the variable getting set still has the last known value. If so, then the variable is changed. If not, then the variable is not set because another thread managed to get in the way. The method making the attempt (using the CAS operation) sees that the change was not made and retries. Nonthread-Safe Classes There are some classes that are inherently not thread safe. Here are a few examples: - SimpleDateFormat - Database Connections - Containers in java.util - Servlets Note that some collection classes have individual methods that are thread-safe. However, any operation that involves calling more than one method is not. For example, if you do not want to replace something in a HashTable because it is already there, you might write the following code: ```java if(!hashTable.containsKey(someKey)) { hashTable.put(someKey, new SomeValue()); } ``` Each individual method is thread-safe. However, another thread might add a value in between the containsKey and put calls. There are several options to fix this problem. - Lock the HashTable first, and make sure all other users of the HashTable do the same—client-based locking: ```java synchronized(map) { if(!map.conainsKey(key)) map.put(key, value); } ``` - Wrap the HashTable in its own object and use a different API—server-based locking using an ADAPTER: ```java public class WrappedHashtable { private Map map = new Hashtable(); public synchronized void putIfAbsent(K key, V value) { if (map.containsKey(key)) map.put(key, value); } } ``` - Use the thread-safe collections: ```java ConcurrentHashMap map = new ConcurrentHashMap(); map.putIfAbsent(key, value); ``` The collections in java.util.concurrent have operations like putIfAbsent() to accommodate such operations. DEPENDENCIES BETWEEN METHODS CAN BREAK CONCURRENT CODE Here is a trivial example of a way to introduce dependencies between methods: ```java public class IntegerIterator implements Iterator private Integer nextValue = 0; public synchronized boolean hasNext() { return nextValue < 100000; } public synchronized Integer next() { if (nextValue == 100000) throw new IteratorPastEndException(); return nextValue++; } public synchronized Integer getNextValue() { return nextValue; } } ``` Here is some code to use this IntegerIterator: ```java IntegerIterator iterator = new IntegerIterator(); while(iterator.hasNext()) { int nextValue = iterator.next(); // do something with nextValue } ``` If one thread executes this code, there will be no problem. But what happens if two threads attempt to share a single instance of IngeterIterator with the intent that each thread will process the values it gets, but that each element of the list is processed only once? Most of the time, nothing bad happens; the threads happily share the list, processing the elements they are given by the iterator and stopping when the iterator is complete. However, there is a small chance that, at the end of the iteration, the two threads will interfere with each other and cause one thread to go beyond the end of the iterator and throw an exception. Here’s the problem: Thread 1 asks the question hasNext(), which returns true. Thread 1 gets preempted and then Thread 2 asks the same question, which is still true. Thread 2 then calls next(), which returns a value as expected but has a side effect of making hasNext() return false. Thread 1 starts up again, thinking hasNext() is still true, and then calls next(). Even though the individual methods are synchronized, the client uses two methods. This is a real problem and an example of the kinds of problems that crop up in concurrent code. In this particular situation this problem is especially subtle because the only time where this causes a fault is when it happens during the final iteration of the iterator. If the threads happen to break just right, then one of the threads could go beyond the end of the iterator. This is the kind of bug that happens long after a system has been in production, and it is hard to track down. You have three options: - Tolerate the failure. - Solve the problem by changing the client: client-based locking - Solve the problem by changing the server, which additionally changes the client: server-based locking Tolerate the Failure Sometimes you can set things up such that the failure causes no harm. For example, the above client could catch the exception and clean up. Frankly, this is a bit sloppy. It’s rather like cleaning up memory leaks by rebooting at midnight. Client-Based Locking To make IntegerIterator work correctly with multiple threads, change this client (and every other client) as follows: ```java IntegerIterator iterator = new IntegerIterator(); while (true) { int nextValue; synchronized (iterator) { if (!iterator.hasNext()) break; nextValue = iterator.next(); } doSometingWith(nextValue); } ``` Each client introduces a lock via the synchronized keyword. This duplication violates the DRY principle, but it might be necessary if the code uses non-thread-safe third-party tools. This strategy is risky because all programmers who use the server must remember to lock it before using it and unlock it when done. Many (many!) years ago I worked on a system that employed client-based locking on a shared resource. The resource was used in hundreds of different places throughout the code. One poor programmer forgot to lock the resource in one of those places. The system was a multi-terminal time-sharing system running accounting software for Local 705 of the trucker’s union. The computer was in a raised-floor, environment-controlled room 50 miles north of the Local 705 headquarters. At the headquarters they had dozens of data entry clerks typing union dues postings into the terminals. The terminals were connected to the computer using dedicated phone lines and 600bps half-duplex modems. (This was a very, very long time ago.) About once per day, one of the terminals would “lock up.” There was no rhyme or reason to it. The lock up showed no preference for particular terminals or particular times. It was as though there were someone rolling dice choosing the time and terminal to lock up. Sometimes more than one terminal would lock up. Sometimes days would go by without any lock-ups. At first the only solution was a reboot. But reboots were tough to coordinate. We had to call the headquarters and get everyone to finish what they were doing on all the terminals. Then we could shut down and restart. If someone was doing something important that took an hour or two, the locked up terminal simply had to stay locked up. After a few weeks of debugging we found that the cause was a ring-buffer counter that had gotten out of sync with its pointer. This buffer controlled output to the terminal. The pointer value indicated that the buffer was empty, but the counter said it was full. Because it was empty, there was nothing to display; but because it was also full, nothing could be added to the buffer to be displayed on the screen. So we knew why the terminals were locking, but we didn’t know why the ring buffer was getting out of sync. So we added a hack to work around the problem. It was possible to read the front panel switches on the computer. (This was a very, very, very long time ago.) We wrote a little trap function that detected when one of these switches was thrown and then looked for a ring buffer that was both empty and full. If one was found, it reset that buffer to empty. Voila! The locked-up terminal(s) started displaying again. So now we didn’t have to reboot the system when a terminal locked up. The Local would simply call us and tell us we had a lock-up, and then we just walked into the computer room and flicked a switch. Of course sometimes they worked on the weekends, and we didn’t. So we added a function to the scheduler that checked all the ring buffers once per minute and reset any that were both empty and full. This caused the displays to unclog before the Local could even get on the phone. It was several more weeks of poring over page after page of monolithic assembly language code before we found the culprit. We had done the math and calculated that the frequency of the lock-ups was consistent with a single unprotected use of the ring buffer. So all we had to do was find that one faulty usage. Unfortunately, this was so very long ago that we didn’t have search tools or cross references or any other kind of automated help. We simply had to pore over listings. I learned an important lesson that cold Chicago winter of 1971. Client-based locking really blows. Server-Based Locking The duplication can be removed by making the following changes to IntegerIterator: ```java public class IntegerIteratorServerLocked { private Integer nextValue = 0; public synchronized Integer getNextOrNull() { if (nextValue < 100000) return nextValue++; else return null; } } ``` And the client code changes as well: ```java while (true) { Integer nextValue = iterator.getNextOrNull(); if (next == null) break; // do something with nextValue } ``` In this case we actually change the API of our class to be multithread aware.3 The client needs to perform a null check instead of checking hasNext(). 3. In fact, the Iterator interface is inherently not thread-safe. It was never designed to be used by multiple threads, so this should come as no surprise. In general you should prefer server-based locking for these reasons: - It reduces repeated code—Client-based locking forces each client to lock the server properly. By putting the locking code into the server, clients are free to use the object and not worry about writing additional locking code. - It allows for better performance—You can swap out a thread-safe server for a non-thread safe one in the case of single-threaded deployment, thereby avoiding all overhead. - It reduces the possibility of error—All it takes is for one programmer to forget to lock properly. - It enforces a single policy—The policy is in one place, the server, rather than many places, each client. - It reduces the scope of the shared variables—The client is not aware of them or how they are locked. All of that is hidden in the server. When things break, the number of places to look is smaller. What if you do not own the server code? - Use an ADAPTER to change the API and add locking ```java public class ThreadSafeIntegerIterator { private IntegerIterator iterator = new IntegerIterator(); public synchronized Integer getNextOrNull() { if(iterator.hasNext()) return iterator.next(); return null; } } ``` - OR better yet, use the thread-safe collections with extended interfaces INCREASING THROUGHPUT Let’s assume that we want to go out on the net and read the contents of a set of pages from a list of URLs. As each page is read, we will parse it to accumulate some statistics. Once all the pages are read, we will print a summary report. The following class returns the contents of one page, given a URL. ```java public class PageReader { //… public String getPageFor(String url) { HttpMethod method = new GetMethod(url); try { httpClient.executeMethod(method); String response = method.getResponseBodyAsString(); return response; } catch (Exception e) { handle(e); } finally { method.releaseConnection(); } } } ``` The next class is the iterator that provides the contents of the pages based on an iterator of URLs: ```java public class PageIterator { private PageReader reader; private URLIterator urls; public PageIterator(PageReader reader, URLIterator urls) { this.urls = urls; this.reader = reader; } public synchronized String getNextPageOrNull() { if (urls.hasNext()) getPageFor(urls.next()); else return null; } public String getPageFor(String url) { return reader.getPageFor(url); } } ``` An instance of the PageIterator can be shared between many different threads, each one using it’s own instance of the PageReader to read and parse the pages it gets from the iterator. Notice that we’ve kept the synchronized block very small. It contains just the critical section deep inside the PageIterator. It is always better to synchronize as little as possible as opposed to synchronizing as much as possible. Single-Thread Calculation of Throughput Now lets do some simple calculations. For the purpose of argument, assume the following: - I/O time to retrieve a page (average): 1 second - Processing time to parse page (average): .5 seconds - I/O requires 0 percent of the CPU while processing requires 100 percent. For N pages being processed by a single thread, the total execution time is 1.5 seconds * N. Figure A-1 shows a snapshot of 13 pages or about 19.5 seconds. Figure A-1 Single thread ![](figures/apA/x01-1single_thread.jpg) Multithread Calculation of Throughput If it is possible to retrieve pages in any order and process the pages independently, then it is possible to use multiple threads to increase throughput. What happens if we use three threads? How many pages can we acquire in the same time? As you can see in Figure A-2, the multithreaded solution allows the process-bound parsing of the pages to overlap with the I/O-bound reading of the pages. In an idealized world this means that the processor is fully utilized. Each one-second page read is overlapped with two parses. Thus, we can process two pages per second, which is three times the throughput of the single-threaded solution. Figure A-2 Three concurrent threads ![](figures/apA/x01-2multi_thread.jpg) DEADLOCK Imagine a Web application with two shared resource pools of some finite size: - A pool of database connections for local work in process storage - A pool of MQ connections to a master repository Assume there are two operations in this application, create and update: - Create—Acquire connection to master repository and database. Talk to service master repository and then store work in local work in process database. - Update—Acquire connection to database and then master repository. Read from work in process database and then send to the master repository What happens when there are more users than the pool sizes? Consider each pool has a size of ten. - Ten users attempt to use create, so all ten database connections are acquired, and each thread is interrupted after acquiring a database connection but before acquiring a connection to the master repository. - Ten users attempt to use update, so all ten master repository connections are acquired, and each thread is interrupted after acquiring the master repository but before acquiring a database connection. - Now the ten “create” threads must wait to acquire a master repository connection, but the ten “update” threads must wait to acquire a database connection. - Deadlock. The system never recovers. This might sound like an unlikely situation, but who wants a system that freezes solid every other week? Who wants to debug a system with symptoms that are so difficult to reproduce? This is the kind of problem that happens in the field, then takes weeks to solve. A typical “solution” is to introduce debugging statements to find out what is happening. Of course, the debug statements change the code enough so that the deadlock happens in a different situation and takes months to again occur.4 4. For example, someone adds some debugging output and the problem “disappears.” The debugging code “fixes” the problem so it remains in the system. To really solve the problem of deadlock, we need to understand what causes it. There are four conditions required for deadlock to occur: - Mutual exclusion - Lock & wait - No preemption - Circular wait Mutual Exclusion Mutual exclusion occurs when multiple threads need to use the same resources and those resources - Cannot be used by multiple threads at the same time. - Are limited in number. A common example of such a resource is a database connection, a file open for write, a record lock, or a semaphore. Lock & Wait Once a thread acquires a resource, it will not release the resource until it has acquired all of the other resources it requires and has completed its work. No Preemption One thread cannot take resources away from another thread. Once a thread holds a resource, the only way for another thread to get it is for the holding thread to release it. Circular Wait This is also referred to as the deadly embrace. Imagine two threads, T1 and T2, and two resources, R1 and R2. T1 has R1, T2 has R2. T1 also requires R2, and T2 also requires R1. This gives something like Figure A-3: Figure A-3 ![](figures/apA/x01-3breaking_cycle.jpg) All four of these conditions must hold for deadlock to be possible. Break any one of these conditions and deadlock is not possible. Breaking Mutual Exclusion One strategy for avoiding deadlock is to sidestep the mutual exclusion condition. You might be able to do this by - Using resources that allow simultaneous use, for example, AtomicInteger. - Increasing the number of resources such that it equals or exceeds the number of competing threads. - Checking that all your resources are free before seizing any. Unfortunately, most resources are limited in number and don’t allow simultaneous use. And it’s not uncommon for the identity of the second resource to be predicated on the results of operating on the first. But don’t be discouraged; there are three conditions left. Breaking Lock & Wait You can also eliminate deadlock if you refuse to wait. Check each resource before you seize it, and release all resources and start over if you run into one that’s busy. This approach introduces several potential problems: - Starvation—One thread keeps being unable to acquire the resources it needs (maybe it has a unique combination of resources that seldom all become available). - Livelock—Several threads might get into lockstep and all acquire one resource and then release one resource, over and over again. This is especially likely with simplistic CPU scheduling algorithms (think embedded devices or simplistic hand-written thread balancing algorithms). Both of these can cause poor throughput. The first results in low CPU utilization, whereas the second results in high and useless CPU utilization. As inefficient as this strategy sounds, it’s better than nothing. It has the benefit that it can almost always be implemented if all else fails. Breaking Preemption Another strategy for avoiding deadlock is to allow threads to take resources away from other threads. This is usually done through a simple request mechanism. When a thread discovers that a resource is busy, it asks the owner to release it. If the owner is also waiting for some other resource, it releases them all and starts over. This is similar to the previous approach but has the benefit that a thread is allowed to wait for a resource. This decreases the number of startovers. Be warned, however, that managing all those requests can be tricky. Breaking Circular Wait This is the most common approach to preventing deadlock. For most systems it requires no more than a simple convention agreed to by all parties. In the example above with Thread 1 wanting both Resource 1 and Resource 2 and Thread 2 wanting both Resource 2 and then Resource 1, simply forcing both Thread 1 and Thread 2 to allocate resources in the same order makes circular wait impossible. More generally, if all threads can agree on a global ordering of resources and if they all allocate resources in that order, then deadlock is impossible. Like all the other strategies, this can cause problems: - The order of acquisition might not correspond to the order of use; thus a resource acquired at the start might not be used until the end. This can cause resources to be locked longer than strictly necessary. - Sometimes you cannot impose an order on the acquisition of resources. If the ID of the second resource comes from an operation performed on the first, then ordering is not feasible. So there are many ways to avoid deadlock. Some lead to starvation, whereas others make heavy use of the CPU and reduce responsiveness. TANSTAAFL!5 5. There ain’t no such thing as a free lunch. Isolating the thread-related part of your solution to allow for tuning and experimentation is a powerful way to gain the insights needed to determine the best strategies. TESTING MULTITHREADED CODE How can we write a test to demonstrate the following code is broken? ```java 01: public class ClassWithThreadingProblem { 02: int nextId; 03: 04: public int takeNextId() { 05: return nextId++; 06: } 07:} ``` Here’s a description of a test that will prove the code is broken: - Remember the current value of nextId. - Create two threads, both of which call takeNextId() once. - Verify that nextId is two more than what we started with. - Run this until we demonstrate that nextId was only incremented by one instead of two. Listing A-2 shows such a test: Listing A-2 ClassWithThreadingProblemTest.java ```java 01: package example; 02: 03: import static org.junit.Assert.fail; 04: 05: import org.junit.Test; 06: 07: public class ClassWithThreadingProblemTest { 08: @Test 09: public void twoThreadsShouldFailEventually() throws Exception { 10: final ClassWithThreadingProblem classWithThreadingProblem = new ClassWithThreadingProblem(); 11: 12: Runnable runnable = new Runnable() { 13: public void run() { 14: classWithThreadingProblem.takeNextId(); 15: } 16: }; 17: 18: for (int i = 0; i < 50000; ++i) { 19: int startingId = classWithThreadingProblem.lastId; 20: int expectedResult = 2 + startingId; 21: 22: Thread t1 = new Thread(runnable); 23: Thread t2 = new Thread(runnable); 24: t1.start(); 25: t2.start(); 26: t1.join(); 27: t2.join(); 28: 29: int endingId = classWithThreadingProblem.lastId; 30: 31: if (endingId != expectedResult) 32: return; 33: } 34: 35: fail(“Should have exposed a threading issue but it did not.”); 36: } 37: } ``` ![](figures/apA/0340tab01.jpg) ![](figures/apA/0341tab01.jpg) This test certainly sets up the conditions for a concurrent update problem. However, the problem occurs so infrequently that the vast majority of times this test won’t detect it. Indeed, to truly detect the problem we need to set the number of iterations to over one million. Even then, in ten executions with a loop count of 1,000,000, the problem occurred only once. That means we probably ought to set the iteration count to well over one hundred million to get reliable failures. How long are we prepared to wait? Even if we tuned the test to get reliable failures on one machine, we’ll probably have to retune the test with different values to demonstrate the failure on another machine, operating system, or version of the JVM. And this is a simple problem. If we cannot demonstrate broken code easily with this problem, how will we ever detect truly complex problems? So what approaches can we take to demonstrate this simple failure? And, more importantly, how can we write tests that will demonstrate failures in more complex code? How will we be able to discover if our code has failures when we do not know where to look? Here are a few ideas: - Monte Carlo Testing. Make tests flexible, so they can be tuned. Then run the test over and over—say on a test server—randomly changing the tuning values. If the tests ever fail, the code is broken. Make sure to start writing those tests early so a continuous integration server starts running them soon. By the way, make sure you carefully log the conditions under which the test failed. - Run the test on every one of the target deployment platforms. Repeatedly. Continuously. The longer the tests run without failure, the more likely that – The production code is correct or – The tests aren’t adequate to expose problems. - Run the tests on a machine with varying loads. If you can simulate loads close to a production environment, do so. Yet, even if you do all of these things, you still don’t stand a very good chance of finding threading problems with your code. The most insidious problems are the ones that have such a small cross section that they only occur once in a billion opportunities. Such problems are the terror of complex systems. TOOL SUPPORT FOR TESTING THREAD-BASED CODE IBM has created a tool called ConTest.6 It instruments classes to make it more likely that non-thread-safe code fails. 6. http://www.haifa.ibm.com/projects/verification/contest/index.html We do not have any direct relationship with IBM or the team that developed ConTest. A colleague of ours pointed us to it. We noticed vast improvement in our ability to find threading issues after a few minutes of using it. Here’s an outline of how to use ConTest: - Write tests and production code, making sure there are tests specifically designed to simulate multiple users under varying loads, as mentioned above. - Instrument test and production code with ConTest. - Run the tests. When we instrumented code with ConTest, our success rate went from roughly one failure in ten million iterations to roughly one failure in thirty iterations. Here are the loop values for several runs of the test after instrumentation: 13, 23, 0, 54, 16, 14, 6, 69, 107, 49, 2. So clearly the instrumented classes failed much earlier and with much greater reliability. CONCLUSION This chapter has been a very brief sojourn through the large and treacherous territory of concurrent programming. We barely scratched the surface. Our emphasis here was on disciplines to help keep concurrent code clean, but there is much more you should learn if you are going to be writing concurrent systems. We recommend you start with Doug Lea’s wonderful book Concurrent Programming in Java: Design Principles and Patterns.7 7. See [Lea99] p. 191. In this chapter we talked about concurrent update, and the disciplines of clean synchronization and locking that can prevent it. We talked about how threads can enhance the throughput of an I/O-bound system and showed the clean techniques for achieving such improvements. We talked about deadlock and the disciplines for preventing it in a clean way. Finally, we talked about strategies for exposing concurrent problems by instrumenting your code. TUTORIAL: FULL CODE EXAMPLES Client/Server Nonthreaded Listing A-3 Server.java ```java package com.objectmentor.clientserver.nonthreaded; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import common.MessageUtils; public class Server implements Runnable { ServerSocket serverSocket; volatile boolean keepProcessing = true; public Server(int port, int millisecondsTimeout) throws IOException { serverSocket = new ServerSocket(port); serverSocket.setSoTimeout(millisecondsTimeout); } public void run() { System.out.printf(“Server Starting\n”); while (keepProcessing) { try { System.out.printf(“accepting client\n”); Socket socket = serverSocket.accept(); System.out.printf(“got client\n”); process(socket); } catch (Exception e) { handle(e); } } } private void handle(Exception e) { if (!(e instanceof SocketException)) { e.printStackTrace(); } } public void stopProcessing() { keepProcessing = false; closeIgnoringException(serverSocket); } void process(Socket socket) { if (socket == null) return; try { System.out.printf(“Server: getting message\n”); String message = MessageUtils.getMessage(socket); System.out.printf(“Server: got message: %s\n”, message); Thread.sleep(1000); System.out.printf(“Server: sending reply: %s\n”, message); MessageUtils.sendMessage(socket, “Processed: ” + message); System.out.printf(“Server: sent\n”); closeIgnoringException(socket); } catch (Exception e) { e.printStackTrace(); } } private void closeIgnoringException(Socket socket) { if (socket != null) try { socket.close(); } catch (IOException ignore) { } } private void closeIgnoringException(ServerSocket serverSocket) { if (serverSocket != null) try { serverSocket.close(); } catch (IOException ignore) { } } } ``` Listing A-4 ClientTest.java ```java package com.objectmentor.clientserver.nonthreaded; import java.io.IOException; import java.net.Socket; import org.junit.After; import org.junit.Before; import org.junit.Test; import common.MessageUtils; public class ClientTest { private static final int PORT = 8009; private static final int TIMEOUT = 2000; Server server; Thread serverThread; @Before public void createServer() throws Exception { try { server = new Server(PORT, TIMEOUT); serverThread = new Thread(server); serverThread.start(); } catch (Exception e) { e.printStackTrace(System.err); throw e; } } @After public void shutdownServer() throws InterruptedException { if (server != null) { server.stopProcessing(); serverThread.join(); } } class TrivialClient implements Runnable { int clientNumber; TrivialClient(int clientNumber) { this.clientNumber = clientNumber; } public void run() { try { connectSendReceive(clientNumber); } catch (IOException e) { e.printStackTrace(); } } } @Test(timeout = 10000) public void shouldRunInUnder10Seconds() throws Exception { Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; ++i) { threads[i] = new Thread(new TrivialClient(i)); threads[i].start(); } for (int i = 0; i < threads.length; ++i) { threads[i].join(); } } private void connectSendReceive(int i) throws IOException { System.out.printf("Client %2d: connecting\n", i); Socket socket = new Socket("localhost", PORT); System.out.printf("Client %2d: sending message\n", i); MessageUtils.sendMessage(socket, Integer.toString(i)); System.out.printf("Client %2d: getting reply\n", i); MessageUtils.getMessage(socket); System.out.printf("Client %2d: finished\n", i); socket.close(); } } ``` Listing A-5 MessageUtils.java ```java package common; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.net.Socket; public class MessageUtils { public static void sendMessage(Socket socket, String message) throws IOException { OutputStream stream = socket.getOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(stream); oos.writeUTF(message); oos.flush(); } public static String getMessage(Socket socket) throws IOException { InputStream stream = socket.getInputStream(); ObjectInputStream ois = new ObjectInputStream(stream); return ois.readUTF(); } } ``` Client/Server Using Threads Changing the server to use threads simply requires a change to the process message (new lines are emphasized to stand out): ```java void process(final Socket socket) { if (socket == null) return; Runnable clientHandler = new Runnable() { public void run() { try { System.out.printf("Server: getting message\n"); String message = MessageUtils.getMessage(socket); System.out.printf("Server: got message: %s\n", message); Thread.sleep(1000); System.out.printf("Server: sending reply: %s\n", message); MessageUtils.sendMessage(socket, "Processed: " + message); System.out.printf("Server: sent\n"); closeIgnoringException(socket); } catch (Exception e) { e.printStackTrace(); } } }; Thread clientConnection = new Thread(clientHandler); clientConnection.start(); } ``` ================================================ FILE: docs/ch1.md ================================================ # 第 1 章 Clean Code 整洁代码 ![](figures/ch1/1-1fig_martin.jpg) You are reading this book for two reasons. First, you are a programmer. Second, you want to be a better programmer. Good. We need better programmers. > 阅读本书有两种原因:第一,你是个程序员;第二,你想成为更好的程序员。很好。我们需要更好的程序员。 This is a book about good programming. It is filled with code. We are going to look at code from every different direction. We’ll look down at it from the top, up at it from the bottom, and through it from the inside out. By the time we are done, we’re going to know a lot about code. What’s more, we’ll be able to tell the difference between good code and bad code. We’ll know how to write good code. And we’ll know how to transform bad code into good code. > 这是本有关编写好程序的书。它充斥着代码。我们要从各个方向来考察这些代码。从顶向下,从底往上,从里而外。读完后,就能知道许多关于代码的事了。而且,我们还能说出好代码和糟糕的代码之间的差异。我们将了解到如何写出好代码。我们也会知道,如何将糟糕的代码改成好代码。 ## 1.1 THERE WILL BE CODE 要有代码 One might argue that a book about code is somehow behind the times—that code is no longer the issue; that we should be concerned about models and requirements instead. Indeed some have suggested that we are close to the end of code. That soon all code will be generated instead of written. That programmers simply won’t be needed because business people will generate programs from specifications. > 有人也许会以为,关于代码的书有点儿落后于时代——代码不再是问题;我们应当关注模型和需求。确实,有人说过我们正在临近代码的终结点。很快,代码就会自动产生出来,不需要再人工编写。程序员完全没用了,因为商务人士可以从规约直接生成程序。 Nonsense! We will never be rid of code, because code represents the details of the requirements. At some level those details cannot be ignored or abstracted; they have to be specified. And specifying requirements in such detail that a machine can execute them is programming. Such a specification is code. > 扯淡!我们永远抛不掉代码,因为代码呈现了需求的细节。在某些层面上,这些细节无法被忽略或抽象,必须明确之。将需求明确到机器可以执行的细节程度,就是编程要做的事。而这种规约正是代码。 I expect that the level of abstraction of our languages will continue to increase. I also expect that the number of domain-specific languages will continue to grow. This will be a good thing. But it will not eliminate code. Indeed, all the specifications written in these higher level and domain-specific language will be code! It will still need to be rigorous, accurate, and so formal and detailed that a machine can understand and execute it. > 我期望语言的抽象程度继续提升。我也期望领域特定语言的数量继续增加。那会是好事一桩。但那终结不了代码。实际上,在较高层次上用领域特定语言撰写的规约也将是代码!它也得严谨、精确、规范和详细,好让机器理解和执行。 The folks who think that code will one day disappear are like mathematicians who hope one day to discover a mathematics that does not have to be formal. They are hoping that one day we will discover a way to create machines that can do what we want rather than what we say. These machines will have to be able to understand us so well that they can translate vaguely specified needs into perfectly executing programs that precisely meet those needs. > 那帮以为代码终将消失的伙计,就像是巴望着发现一种无规范数学的数学家们一般。他们巴望着,总有一天能创造出某种机器,我们只要想想、嘴都不用张就能叫它依计行事。那机器要能透彻理解我们,只有这样,它才能把含糊不清的需求翻译为可完美执行的程序,精确满足需求。 This will never happen. Not even humans, with all their intuition and creativity, have been able to create successful systems from the vague feelings of their customers. Indeed, if the discipline of requirements specification has taught us anything, it is that well-specified requirements are as formal as code and can act as executable tests of that code! > 这种事永远不会发生。即便是人类,倾其全部的直觉和创造力,也造不出满足客户模糊感觉的成功系统来。如果说需求规约原则教给了我们什么,那就是归置良好的需求就像代码一样正式,也能作为代码的可执行测试来使用。 Remember that code is really the language in which we ultimately express the requirements. We may create languages that are closer to the requirements. We may create tools that help us parse and assemble those requirements into formal structures. But we will never eliminate necessary precision—so there will always be code. > 记住,代码确然是我们最终用来表达需求的那种语言。我们可以创造各种与需求接近的语言。我们可以创造帮助把需求解析和汇整为正式结构的各种工具。然而,我们永远无法抛弃必要的精确性——所以代码永存。 ## 1.2 BAD CODE 糟糕的代码 I was recently reading the preface to Kent Beck’s book Implementation Patterns.1 He says, “… this book is based on a rather fragile premise: that good code matters….” A fragile premise? I disagree! I think that premise is one of the most robust, supported, and overloaded of all the premises in our craft (and I think Kent knows it). We know good code matters because we’ve had to deal for so long with its lack. > 最近我在读 Kent Beck 著 Implementation Patterns(中译版《实现模式》)[1]一书的序言。他这样写道:“……本书基于一种不太牢靠的前提:好代码的确重要……”这前提不牢靠?我反对!我认为这是该领域最强固、最受支持、最被强调的前提了(我想 Kent 也知道)。我们知道好代码重要,是因为其短缺实在困扰了我们太久。 1. [Beck07]. I know of one company that, in the late 80s, wrote a killer app. It was very popular, and lots of professionals bought and used it. But then the release cycles began to stretch. Bugs were not repaired from one release to the next. Load times grew and crashes increased. I remember the day I shut the product down in frustration and never used it again. The company went out of business a short time after that. > 20 世纪 80 年代末,有家公司写了个很流行的杀手应用,许多专业人士都买来用。然后,发布周期开始拉长。缺陷总是不能修复。装载时间越来越久,崩溃的几率也越来越大。至今我还记得自己在某天沮丧地关掉那个程序,从此再不用它。在那之后不久,该公司就关门大吉了。 ![](figures/ch1/1-2fig_martin.jpg) Two decades later I met one of the early employees of that company and asked him what had happened. The answer confirmed my fears. They had rushed the product to market and had made a huge mess in the code. As they added more and more features, the code got worse and worse until they simply could not manage it any longer. It was the bad code that brought the company down. > 20 年后,我见到那家公司的一位早期雇员,问他当年发生了什么事。他的回答叫我愈发恐惧起来。原来,当时他们赶着推出产品,代码写得乱七八糟。特性越加越多,代码也越来越烂,最后再也没法管理这些代码了。是糟糕的代码毁了这家公司。 Have you ever been significantly impeded by bad code? If you are a programmer of any experience then you’ve felt this impediment many times. Indeed, we have a name for it. We call it wading. We wade through bad code. We slog through a morass of tangled brambles and hidden pitfalls. We struggle to find our way, hoping for some hint, some clue, of what is going on; but all we see is more and more senseless code. > 你是否曾为糟糕的代码所深深困扰?如果你是位有点儿经验的程序员,定然多次遇到过这类困境。我们有专用来形容这事的词:沼泽(wading)。我们趟过代码的水域。我们穿过灌木密布、瀑布暗藏的沼泽地。我们拼命想找到出路,期望有点什么线索能启发我们到底发生了什么事;但目光所及,只是越来越多死气沉沉的代码。 Of course you have been impeded by bad code. So then—why did you write it? > 你当然曾为糟糕的代码所困扰过。那么——为什么要写糟糕的代码呢? Were you trying to go fast? Were you in a rush? Probably so. Perhaps you felt that you didn’t have time to do a good job; that your boss would be angry with you if you took the time to clean up your code. Perhaps you were just tired of working on this program and wanted it to be over. Or maybe you looked at the backlog of other stuff that you had promised to get done and realized that you needed to slam this module together so you could move on to the next. We’ve all done it. > 是想快点完成吗?是要赶时间吗?有可能。或许你觉得自己要干好所需的时间不够;假使花时间清理代码,老板就会大发雷霆。或许你只是不耐烦再搞这套程序,期望早点结束。或许你看了看自己承诺要做的其他事,意识到得赶紧弄完手上的东西,好接着做下一件工作。这种事我们都干过。 We’ve all looked at the mess we’ve just made and then have chosen to leave it for another day. We’ve all felt the relief of seeing our messy program work and deciding that a working mess is better than nothing. We’ve all said we’d go back and clean it up later. Of course, in those days we didn’t know LeBlanc’s law: Later equals never. > 我们都曾经瞟一眼自己亲手造成的混乱,决定弃之而不顾,走向新一天。我们都曾经看到自己的烂程序居然能运行,然后断言能运行的烂程序总比什么都没有强。我们都曾经说过有朝一日再回头清理。当然,在那些日子里,我们都没听过勒布朗(LeBlanc)法则:稍后等于永不(Later equals never)。 ## 1.3 THE TOTAL COST OF OWNING A MESS 混乱的代价 If you have been a programmer for more than two or three years, you have probably been significantly slowed down by someone else’s messy code. If you have been a programmer for longer than two or three years, you have probably been slowed down by messy code. The degree of the slowdown can be significant. Over the span of a year or two, teams that were moving very fast at the beginning of a project can find themselves moving at a snail’s pace. Every change they make to the code breaks two or three other parts of the code. No change is trivial. Every addition or modification to the system requires that the tangles, twists, and knots be “understood” so that more tangles, twists, and knots can be added. Over time the mess becomes so big and so deep and so tall, they can not clean it up. There is no way at all. > 只要你干过两三年编程,就有可能曾被某人的糟糕的代码绊倒过。如果你编程不止两三年,也有可能被这种代码拖过后腿。进度延缓的程度会很严重。有些团队在项目初期进展迅速,但有那么一两年的时间却慢如蜗行。对代码的每次修改都影响到其他两三处代码。修改无小事。每次添加或修改代码,都得对那堆扭纹柴了然于心,这样才能往上扔更多的扭纹柴。这团乱麻越来越大,再也无法理清,最后束手无策。 As the mess builds, the productivity of the team continues to decrease, asymptotically approaching zero. As productivity decreases, management does the only thing they can; they add more staff to the project in hopes of increasing productivity. But that new staff is not versed in the design of the system. They don’t know the difference between a change that matches the design intent and a change that thwarts the design intent. Furthermore, they, and everyone else on the team, are under horrific pressure to increase productivity. So they all make more and more messes, driving the productivity ever further toward zero. (See Figure 1-1.) > 随着混乱的增加,团队生产力也持续下降,趋向于零。当生产力下降时,管理层就只有一件事可做了:增加更多人手到项目中,期望提升生产力。可是新人并不熟悉系统的设计。他们搞不清楚什么样的修改符合设计意图,什么样的修改违背设计意图。而且,他们以及团队中的其他人都背负着提升生产力的可怕压力。于是,他们制造更多的混乱,驱动生产力向零那端不断下降。如图 1-1 所示 Figure 1-1 Productivity vs. time ![](figures/ch1/1-4fig_martin.jpg) ### 1.3.1 The Grand Redesign in the Sky 华丽新设计 Eventually the team rebels. They inform management that they cannot continue to develop in this odious code base. They demand a redesign. Management does not want to expend the resources on a whole new redesign of the project, but they cannot deny that productivity is terrible. Eventually they bend to the demands of the developers and authorize the grand redesign in the sky. > 最后,开发团队造反了,他们告诉管理层,再也无法在这令人生厌的代码基础上做开发。他们要求做全新的设计。管理层不愿意投入资源完全重启炉灶,但他们也不能否认生产力低得可怕。他们只好同意开发者的要求,授权去做一套看上去很美的华丽新设计。 A new tiger team is selected. Everyone wants to be on this team because it’s a green-field project. They get to start over and create something truly beautiful. But only the best and brightest are chosen for the tiger team. Everyone else must continue to maintain the current system. > 于是就组建了一支新军。谁都想加入这个团队,因为它是张白纸。他们可以重新来过,搞出点真正漂亮的东西来。但只有最优秀、最聪明的家伙被选中。其余人等则继续维护现有系统。 Now the two teams are in a race. The tiger team must build a new system that does everything that the old system does. Not only that, they have to keep up with the changes that are continuously being made to the old system. Management will not replace the old system until the new system can do everything that the old system does. > 现在有两支队伍在竞赛了。新团队必须搭建一套新系统,要能实现旧系统的所有功能。另外,还得跟上对旧系统的持续改动。在新系统功能足以抗衡旧系统之前,管理层不会替换掉旧系统。 This race can go on for a very long time. I’ve seen it take 10 years. And by the time it’s done, the original members of the tiger team are long gone, and the current members are demanding that the new system be redesigned because it’s such a mess. > 竞赛可能会持续极长时间。我就见过延续了十年之久的。到了完成的时候,新团队的老成员早已不知去向,而现有成员则要求重新设计一套新系统,因为这套系统太烂了。 If you have experienced even one small part of the story I just told, then you already know that spending time keeping your code clean is not just cost effective; it’s a matter of professional survival. > 假使你经历过哪怕是一小段我谈到的这种事,那么你一定知道,花时间保持代码整洁不但有关效率,还有关生存。 ### 1.3.2 Attitude 态度 Have you ever waded through a mess so grave that it took weeks to do what should have taken hours? Have you seen what should have been a one-line change, made instead in hundreds of different modules? These symptoms are all too common. > 你是否遇到过某种严重到要花数个星期来做本来只需数小时即可完成的事的混乱状况?你是否见过本来只需做一行修改,结果却涉及上百个模块的情况?这种事太常见了。 Why does this happen to code? Why does good code rot so quickly into bad code? We have lots of explanations for it. We complain that the requirements changed in ways that thwart the original design. We bemoan the schedules that were too tight to do things right. We blather about stupid managers and intolerant customers and useless marketing types and telephone sanitizers. But the fault, dear Dilbert, is not in our stars, but in ourselves. We are unprofessional. > 怎么会发生这种事?为什么好代码会这么快就变质成糟糕的代码?理由多得很。我们抱怨需求变化背离了初期设计。我们哀叹进度太紧张,没法干好活。我们把问题归咎于那些愚蠢的经理、苛求的用户、没用的营销方式和那些电话消毒剂。不过,亲爱的呆伯特(Dilbert)[2],我们是自作自受[3]。我们太不专业了。 This may be a bitter pill to swallow. How could this mess be our fault? What about the requirements? What about the schedule? What about the stupid managers and the useless marketing types? Don’t they bear some of the blame? > 这话可不太中听。怎么会是自作自受呢?难道不关需求的事?难道不关进度的事?难道不关那些蠢经理和没用的营销手段的事?难道他们就不该负点责吗? No. The managers and marketers look to us for the information they need to make promises and commitments; and even when they don’t look to us, we should not be shy about telling them what we think. The users look to us to validate the way the requirements will fit into the system. The project managers look to us to help work out the schedule. We are deeply complicit in the planning of the project and share a great deal of the responsibility for any failures; especially if those failures have to do with bad code! > 不。经理和营销人员指望从我们这里得到必须的信息,然后才能做出承诺和保证;即便他们没开口问,我们也不该羞于告知自己的想法。用户指望我们验证需求是否都在系统中实现了。项目经理指望我们遵守进度。我们与项目的规划脱不了干系,对失败负有极大的责任;特别是当失败与糟糕的代码有关时尤为如此! “But wait!” you say. “If I don’t do what my manager says, I’ll be fired.” Probably not. Most managers want the truth, even when they don’t act like it. Most managers want good code, even when they are obsessing about the schedule. They may defend the schedule and requirements with passion; but that’s their job. It’s your job to defend the code with equal passion. > “且慢!”你说。“不听经理的,我就会被炒鱿鱼。”多半不会。多数经理想要知道实情,即便他们看起来不喜欢实情。多数经理想要好代码,即便他们总是痴缠于进度。他们会奋力卫护进度和需求;那是他们该干的。你则当以同等的热情卫护代码。 To drive this point home, what if you were a doctor and had a patient who demanded that you stop all the silly hand-washing in preparation for surgery because it was taking too much time?2 Clearly the patient is the boss; and yet the doctor should absolutely refuse to comply. Why? Because the doctor knows more than the patient about the risks of disease and infection. It would be unprofessional (never mind criminal) for the doctor to comply with the patient. > 再说明白些,假使你是位医生,病人请求你在给他做手术前别洗手,因为那会花太多时间,你会照办吗[4]?本该是病人说了算;但医生却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病和感染的风险。医生如果按病人说的办,就是一种不专业的态度(更别说是犯罪了)。 2. When hand-washing was first recommended to physicians by Ignaz Semmelweis in 1847, it was rejected on the basis that doctors were too busy and wouldn’t have time to wash their hands between patient visits. So too it is unprofessional for programmers to bend to the will of managers who don’t understand the risks of making messes. > 同理,程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法。 ### 1.3.3 The Primal Conundrum 迷题 Programmers face a conundrum of basic values. All developers with more than a few years experience know that previous messes slow them down. And yet all developers feel the pressure to make messes in order to meet deadlines. In short, they don’t take the time to go fast! > 程序员面临着一种基础价值谜题。有那么几年经验的开发者都知道,之前的混乱拖了自己的后腿。但开发者们背负期限的压力,只好制造混乱。简言之,他们没花时间让自己做得更快! True professionals know that the second part of the conundrum is wrong. You will not make the deadline by making the mess. Indeed, the mess will slow you down instantly, and will force you to miss the deadline. The only way to make the deadline—the only way to go fast—is to keep the code as clean as possible at all times. > 真正的专业人士明白,这道谜题的第二部分说错了。制造混乱无助于赶上期限。混乱只会立刻拖慢你,叫你错过期限。赶上期限的唯一方法——做得快的唯一方法 ——就是始终尽可能保持代码整洁。 ### 1.3.4 The Art of Clean Code? 整洁代码的艺术 Let’s say you believe that messy code is a significant impediment. Let’s say that you accept that the only way to go fast is to keep your code clean. Then you must ask yourself: “How do I write clean code?” It’s no good trying to write clean code if you don’t know what it means for code to be clean! > 假设你相信混乱的代码是祸首,假设你接受做得快的唯一方法是保持代码整洁的说法,你一定会自问:“我怎么才能写出整洁的代码?”不过,如果你不明白整洁对代码有何意义,尝试去写整洁代码就毫无所益! The bad news is that writing clean code is a lot like painting a picture. Most of us know when a picture is painted well or badly. But being able to recognize good art from bad does not mean that we know how to paint. So too being able to recognize clean code from dirty code does not mean that we know how to write clean code! > 坏消息是写整洁代码很像是绘画。多数人都知道一幅画是好还是坏。但能分辨优劣并不表示懂得绘画。能分辨整洁代码和肮脏代码,也不意味着会写整洁代码! Writing clean code requires the disciplined use of a myriad little techniques applied through a painstakingly acquired sense of “cleanliness.” This “code-sense” is the key. Some of us are born with it. Some of us have to fight to acquire it. Not only does it let us see whether code is good or bad, but it also shows us the strategy for applying our discipline to transform bad code into clean code. > 写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的“整洁感”。这种“代码感”就是关键所在。有些人生而有之。有些人费点劲才能得到。它不仅让我们看到代码的优劣,还予我们以借戒规之力化劣为优的攻略。 A programmer without “code-sense” can look at a messy module and recognize the mess but will have no idea what to do about it. A programmer with “code-sense” will look at a messy module and see options and variations. The “code-sense” will help that programmer choose the best variation and guide him or her to plot a sequence of behavior preserving transformations to get from here to there. > 缺乏“代码感”的程序员,看混乱是混乱,无处着手。有“代码感”的程序员能从混乱中看出其他的可能与变化。“代码感”帮助程序员选出最好的方案,并指导程序员制订修改行动计划,按图索骥。 In short, a programmer who writes clean code is an artist who can take a blank screen through a series of transformations until it is an elegantly coded system. > 简言之,编写整洁代码的程序员就像是艺术家,他能用一系列变换把一块白板变作由优雅代码构成的系统。 ### 1.3.5 What Is Clean Code? 什么是整洁代码 There are probably as many definitions as there are programmers. So I asked some very well-known and deeply experienced programmers what they thought. > 有多少程序员,就有多少定义。所以我只询问了一些非常知名且经验丰富的程序员。 ![](figures/ch1/1-5fig_martin.jpg) Bjarne Stroustrup, inventor of C++ and author of The C++ Programming Language > Bjarne Stroustrup,C++语言发明者,C++Programming Language(中译版《C++程序设计语言》)一书作者。 I like my code to be elegant and efficient. The logic should be straightforward to make it hard for bugs to hide, the dependencies minimal to ease maintenance, error handling complete according to an articulated strategy, and performance close to optimal so as not to tempt people to make the code messy with unprincipled optimizations. Clean code does one thing well. > 我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。 Bjarne uses the word “elegant.” That’s quite a word! The dictionary in my MacBook® provides the following definitions: pleasingly graceful and stylish in appearance or manner; pleasingly ingenious and simple. Notice the emphasis on the word “pleasing.” Apparently Bjarne thinks that clean code is pleasing to read. Reading it should make you smile the way a well-crafted music box or well-designed car would. > Bjarne 用了“优雅”一词。说得好!我 MacBook 上的词典提供了如下定义:外表或举止上令人愉悦的优美和雅观;令人愉悦的精致和简单。注意对“愉悦”一词的强调。Bjarne 显然认为整洁的代码读起来令人愉悦。读这种代码,就像见到手工精美的音乐盒或者设计精良的汽车一般,让你会心一笑。 Bjarne also mentions efficiency—twice. Perhaps this should not surprise us coming from the inventor of C++; but I think there’s more to it than the sheer desire for speed. Wasted cycles are inelegant, they are not pleasing. And now note the word that Bjarne uses to describe the consequence of that inelegance. He uses the word “tempt.” There is a deep truth here. Bad code tempts the mess to grow! When others change bad code, they tend to make it worse. > Bjarne 也提到效率——而且两次提及。这话出自 C++发明者之口,或许并不出奇;不过我认为并非是在单纯追求速度。被浪费掉的运算周期并不雅观,并不令人愉悦。留意 Bjarne 怎么描述那种不雅观的结果。他用了“引诱”这个词。诚哉斯言。糟糕的代码引发混乱!别人修改糟糕的代码时,往往会越改越烂。 Pragmatic Dave Thomas and Andy Hunt said this a different way. They used the metaphor of broken windows.3 A building with broken windows looks like nobody cares about it. So other people stop caring. They allow more windows to become broken. Eventually they actively break them. They despoil the facade with graffiti and allow garbage to collect. One broken window starts the process toward decay. > 务实的 Dave Thomas 和 Andy Hunt 从另一角度阐述了这种情况。他们提到破窗理论[5]。窗户破损了的建筑让人觉得似乎无人照管。于是别人也再不关心。他们放任窗户继续破损。最终自己也参加破坏活动,在外墙上涂鸦,任垃圾堆积。一扇破损的窗户开辟了大厦走向倾颓的道路。 3. http://www.pragmaticprogrammer.com/booksellers/2004-12.html Bjarne also mentions that error handing should be complete. This goes to the discipline of paying attention to details. Abbreviated error handling is just one way that programmers gloss over details. Memory leaks are another, race conditions still another. Inconsistent naming yet another. The upshot is that clean code exhibits close attention to detail. > Bjarne 也提到完善错误处理代码。往深处说就是在细节上花心思。敷衍了事的错误处理代码只是程序员忽视细节的一种表现。此外还有内存泄漏,还有竞态条件代码。还有前后不一致的命名方式。结果就是凸现出整洁代码对细节的重视。 Bjarne closes with the assertion that clean code does one thing well. It is no accident that there are so many principles of software design that can be boiled down to this simple admonition. Writer after writer has tried to communicate this thought. Bad code tries to do too much, it has muddled intent and ambiguity of purpose. Clean code is focused. Each function, each class, each module exposes a single-minded attitude that remains entirely undistracted, and unpolluted, by the surrounding details. > Bjarne 以“整洁的代码只做好一件事”结束论断。毋庸置疑,软件设计的许多原则最终都会归结为这句警语。有那么多人发表过类似的言论。糟糕的代码想做太多事,它意图混乱、目的含混。整洁的代码力求集中。每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染。 Grady Booch, author of Object Oriented Analysis and Design with Applications > Grady Booch,Object Oriented Analysis and Design with Applications(中译版《面向对象分析与设计》)一书作者。 ![](figures/ch1/1-6fig_martin.jpg) Clean code is simple and direct. Clean code reads like well-written prose. Clean code never obscures the designer’s intent but rather is full of crisp abstractions and straightforward lines of control. > 整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。 Grady makes some of the same points as Bjarne, but he takes a readability perspective. I especially like his view that clean code should read like well-written prose. Think back on a really good book that you’ve read. Remember how the words disappeared to be replaced by images! It was like watching a movie, wasn’t it? Better! You saw the characters, you heard the sounds, you experienced the pathos and the humor. > Grady 的观点与 Bjarne 的观点有类似之处,但他从可读性的角度来定义。我特别喜欢“整洁的代码如同优美的散文”这种看法。想想你读过的某本好书。回忆一下,那些文字是如何在脑中形成影像!就像是看了场电影,对吧?还不止!你还看到那些人物,听到那些声音,体验到那些喜怒哀乐。 Reading clean code will never be quite like reading Lord of the Rings. Still, the literary metaphor is not a bad one. Like a good novel, clean code should clearly expose the tensions in the problem to be solved. It should build those tensions to a climax and then give the reader that “Aha! Of course!” as the issues and tensions are resolved in the revelation of an obvious solution. > 阅读整洁的代码和阅读 Lord of the Rings(中译版《指环王》)自然不同。不过,仍有可类比之处。如同一本好的小说般,整洁的代码应当明确地展现出要解决问题的张力。它应当将这种张力推至高潮,以某种显而易见的方案解决问题和张力,使读者发出“啊哈!本当如此!”的感叹。 I find Grady’s use of the phrase “crisp abstraction” to be a fascinating oxymoron! After all the word “crisp” is nearly a synonym for “concrete.” My MacBook’s dictionary holds the following definition of “crisp”: briskly decisive and matter-of-fact, without hesitation or unnecessary detail. Despite this seeming juxtaposition of meaning, the words carry a powerful message. Our code should be matter-of-fact as opposed to speculative. It should contain only what is necessary. Our readers should perceive us to have been decisive. > 窃以为 Grady 所谓“干净利落的抽象”(crisp abstraction),乃是绝妙的矛盾修辞法。毕竟 crisp 几乎就是“具体”(concrete)的同义词。我 MacBook 上的词典这样定义 crisp 一词:果断决绝,就事论事,没有犹豫或不必要的细节。尽管有两种不同的定义,该词还是承载了有力的信息。代码应当讲述事实,不引人猜测。它只该包含必需之物。读者应当感受到我们的果断决绝。 “Big” Dave Thomas, founder of OTI, godfather of the Eclipse strategy > “老大”Dave Thomas,OTI 公司创始人,Eclipse 战略教父。 ![](figures/ch1/1-7fig_martin.jpg) Clean code can be read, and enhanced by a developer other than its original author. It has unit and acceptance tests. It has meaningful names. It provides one way rather than many ways for doing one thing. It has minimal dependencies, which are explicitly defined, and provides a clear and minimal API. Code should be literate since depending on the language, not all necessary information can be expressed clearly in code alone. > 整洁的代码应可由作者之外的开发者阅读和增补。它应当有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽量少的 API。代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达。 Big Dave shares Grady’s desire for readability, but with an important twist. Dave asserts that clean code makes it easy for other people to enhance it. This may seem obvious, but it cannot be overemphasized. There is, after all, a difference between code that is easy to read and code that is easy to change. > Dave 老大在可读性上和 Grady 持相同观点,但有一个重要的不同之处。Dave 断言,整洁的代码便于其他人加以增补。这看似显而易见,但亦不可过分强调。毕竟易读的代码和易修改的代码之间还是有区别的。 Dave ties cleanliness to tests! Ten years ago this would have raised a lot of eyebrows. But the discipline of Test Driven Development has made a profound impact upon our industry and has become one of our most fundamental disciplines. Dave is right. Code, without tests, is not clean. No matter how elegant it is, no matter how readable and accessible, if it hath not tests, it be unclean. > Dave 将整洁系于测试之上!要在十年之前,这会让人大跌眼镜。但测试驱动开发(Test Driven Development)已在行业中造成了深远影响,成为基础规程之一。Dave 说得对。没有测试的代码不干净。不管它有多优雅,不管有多可读、多易理解,微乎测试,其不洁亦可知也。 Dave uses the word minimal twice. Apparently he values code that is small, rather than code that is large. Indeed, this has been a common refrain throughout software literature since its inception. Smaller is better. > Dave 两次提及“尽量少”。显然,他推崇小块的代码。实际上,从有软件起人们就在反复强调这一点。越小越好。 Dave also says that code should be literate. This is a soft reference to Knuth’s literate programming.4 The upshot is that the code should be composed in such a form as to make it readable by humans. > Dave 也提到,代码应在字面上表达其含义。这一观点源自 Knuth 的“字面编程”(literate programming)[6]。结论就是应当用人类可读的方式来写代码。 4. [Knuth92]. Michael Feathers, author of Working Effectively with Legacy Code > Michael Feathers,Working Effectively with Legacy Code(中译版《修改代码的艺术》)一书作者。 ![](figures/ch1/1-8fig_martin.jpg) I could list all of the qualities that I notice in clean code, but there is one overarching quality that leads to all of them. Clean code always looks like it was written by someone who cares. There is nothing obvious that you can do to make it better. All of those things were thought about by the code’s author, and if you try to imagine improvements, you’re led back to where you are, sitting in appreciation of the code someone left for you—code left by someone who cares deeply about the craft. > 我可以列出我留意到的整洁代码的所有特点,但其中有一条是根本性的。整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有改进的余地。代码作者什么都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入的某人留下的代码。 One word: care. That’s really the topic of this book. Perhaps an appropriate subtitle would be How to Care for Code. > 一言以蔽之:在意。这就是本书的题旨所在。或许该加个副标题,如何在意代码。 Michael hit it on the head. Clean code is code that has been taken care of. Someone has taken the time to keep it simple and orderly. They have paid appropriate attention to details. They have cared. > Michael 一针见血。整洁代码就是作者着力照料的代码。有人曾花时间让它保持简单有序。他们适当地关注到了细节。他们在意过。 Ron Jeffries, author of Extreme Programming Installed and Extreme Programming Adventures in C# > Ron Jeffries,Extreme Programming Installed(中译版《极限编程实施》)以及 Extreme Programming Adventures in C#(中译版《C#极限编程探险》)作者。 Ron began his career programming in Fortran at the Strategic Air Command and has written code in almost every language and on almost every machine. It pays to consider his words carefully. > Ron 初入行就在战略空军司令部(Strategic Air Command)编写 Fortran 程序,此后几乎在每种机器上编写过每种语言的代码。他的言论值得咀嚼。 ![](figures/ch1/1-9fig_martin.jpg) In recent years I begin, and nearly end, with Beck’s rules of simple code. In priority order, simple code: > 近年来,我开始研究贝克的简单代码规则,差不多也都琢磨透了。简单代码,依其重要顺序: - Runs all the tests; - Contains no duplication; - Expresses all the design ideas that are in the system; - Minimizes the number of entities such as classes, methods, functions, and the like. --- > - 能通过所有测试; > - 没有重复代码; > - 体现系统中的全部设计理念; > - 包括尽量少的实体,比如类、方法、函数等。 Of these, I focus mostly on duplication. When the same thing is done over and over, it’s a sign that there is an idea in our mind that is not well represented in the code. I try to figure out what it is. Then I try to express that idea more clearly. > 在以上诸项中,我最在意代码重复。如果同一段代码反复出现,就表示某种想法未在代码中得到良好的体现。我尽力去找出到底那是什么,然后再尽力更清晰地表达出来。 Expressiveness to me includes meaningful names, and I am likely to change the names of things several times before I settle in. With modern coding tools such as Eclipse, renaming is quite inexpensive, so it doesn’t trouble me to change. Expressiveness goes beyond names, however. I also look at whether an object or method is doing more than one thing. If it’s an object, it probably needs to be broken into two or more objects. If it’s a method, I will always use the Extract Method refactoring on it, resulting in one method that says more clearly what it does, and some submethods saying how it is done. > 在我看来,有意义的命名是体现表达力的一种方式,我往往会修改好几次才会定下名字来。借助 Eclipse 这样的现代编码工具,重命名代价极低,所以我无所顾忌。然而,表达力还不只体现在命名上。我也会检查对象或方法是否想做的事太多。如果对象功能太多,最好是切分为两个或多个对象。如果方法功能太多,我总是使用抽取手段(Extract Method)重构之,从而得到一个能较为清晰地说明自身功能的方法,以及另外数个说明如何实现这些功能的方法。 Duplication and expressiveness take me a very long way into what I consider clean code, and improving dirty code with just these two things in mind can make a huge difference. There is, however, one other thing that I’m aware of doing, which is a bit harder to explain. > 消除重复和提高表达力让我在整洁代码方面获益良多,只要铭记这两点,改进脏代码时就会大有不同。不过,我时常关注的另一规则就不太好解释了。 After years of doing this work, it seems to me that all programs are made up of very similar elements. One example is “find things in a collection.” Whether we have a database of employee records, or a hash map of keys and values, or an array of items of some kind, we often find ourselves wanting a particular item from that collection. When I find that happening, I will often wrap the particular implementation in a more abstract method or class. That gives me a couple of interesting advantages. > 这么多年下来,我发现所有程序都由极为相似的元素构成。例如“在集合中查找某物”。不管是雇员记录数据库还是名-值对哈希表,或者某类条目的数组,我们都会发现自己想要从集合中找到某一特定条目。一旦出现这种情况,我通常会把实现手段封装到更抽象的方法或类中。这样做好处多多。 I can implement the functionality now with something simple, say a hash map, but since now all the references to that search are covered by my little abstraction, I can change the implementation any time I want. I can go forward quickly while preserving my ability to change later. > 可以先用某种简单的手段,比如哈希表来实现这一功能,由于对搜索功能的引用指向了我那个小小的抽象,就能随需应变,修改实现手段。这样就既能快速前进,又能为未来的修改预留余地。 In addition, the collection abstraction often calls my attention to what’s “really” going on, and keeps me from running down the path of implementing arbitrary collection behavior when all I really need is a few fairly simple ways of finding what I want. > 另外,该集合抽象常常提醒我留意“真正”在发生的事,避免随意实现集合行为,因为我真正需要的不过是某种简单的查找手段。 Reduced duplication, high expressiveness, and early building of simple abstractions. That’s what makes clean code for me. > 减少重复代码,提高表达力,提早构建简单抽象。这就是我写整洁代码的方法。 Here, in a few short paragraphs, Ron has summarized the contents of this book. No duplication, one thing, expressiveness, tiny abstractions. Everything is there. > Ron 以寥寥数段文字概括了本书的全部内容。不要重复代码,只做一件事,表达力,小规模抽象。该有的都有了。 Ward Cunningham, inventor of Wiki, inventor of Fit, coinventor of eXtreme Programming. Motive force behind Design Patterns. Smalltalk and OO thought leader. The godfather of all those who care about code. > Ward Cunningham,Wiki 发明者,eXtreme Programming (极限编程)的创始人之一,Smalltalk 语言和面向对象的思想领袖。所有在意代码者的教父。 ![](figures/ch1/1-10fig_martin.jpg) You know you are working on clean code when each routine you read turns out to be pretty much what you expected. You can call it beautiful code when the code also makes it look like the language was made for the problem. > 如果每个例程都让你感到深合己意,那就是整洁代码。如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码。 Statements like this are characteristic of Ward. You read it, nod your head, and then go on to the next topic. It sounds so reasonable, so obvious, that it barely registers as something profound. You might think it was pretty much what you expected. But let’s take a closer look. > 这种说法很 Ward。它教你听了之后就点头,然后继续听下去。如此在理,如此浅显,绝不故作高深。你大概以为此言深合己意吧。再走近点看看。 “… pretty much what you expected.” When was the last time you saw a module that was pretty much what you expected? Isn’t it more likely that the modules you look at will be puzzling, complicated, tangled? Isn’t misdirection the rule? Aren’t you used to flailing about trying to grab and hold the threads of reasoning that spew forth from the whole system and weave their way through the module you are reading? When was the last time you read through some code and nodded your head the way you might have nodded your head at Ward’s statement? > “……深合己意”。你最近一次看到深合己意的模块是什么时候?模块多半都繁复难解吧?难道没有触犯规则吗?你不是也曾挣扎着想抓住些从整个系统中散落而出的线索,编织进你在读的那个模块吗?你最近一次读到某段代码、并且如同对 Ward 的说法点头一般对这段代码点头,是什么时候的事了? Ward expects that when you read clean code you won’t be surprised at all. Indeed, you won’t even expend much effort. You will read it, and it will be pretty much what you expected. It will be obvious, simple, and compelling. Each module will set the stage for the next. Each tells you how the next will be written. Programs that are that clean are so profoundly well written that you don’t even notice it. The designer makes it look ridiculously simple like all exceptional designs. > Ward 期望你不会为整洁代码所震惊。你无需花太多力气。那代码就是深合你意。它明确、简单、有力。每个模块都为下一个模块做好准备。每个模块都告诉你下一个模块会是怎样的。整洁的程序好到你根本不会注意到它。设计者把它做得像一切其他设计般简单。 And what about Ward’s notion of beauty? We’ve all railed against the fact that our languages weren’t designed for our problems. But Ward’s statement puts the onus back on us. He says that beautiful code makes the language look like it was made for the problem! So it’s our responsibility to make the language look simple! Language bigots everywhere, beware! It is not the language that makes programs appear simple. It is the programmer that make the language appear simple! > 那 Ward 有关“美”的说法又如何呢?我们都曾面临语言不是为要解决的问题所设计的困境。但 Ward 的说法又把球踢回我们这边。他说,漂亮的代码让编程语言像是专为解决那个问题而存在!所以,让语言变得简单的责任就在我们身上了!当心,语言是冥顽不化的!是程序员让语言显得简单。 ## 1.4 SCHOOLS OF THOUGHT 思想流派 What about me (Uncle Bob)? What do I think clean code is? This book will tell you, in hideous detail, what I and my compatriots think about clean code. We will tell you what we think makes a clean variable name, a clean function, a clean class, etc. We will present these opinions as absolutes, and we will not apologize for our stridence. To us, at this point in our careers, they are absolutes. They are our school of thought about clean code. > 我(鲍勃大叔)又是怎么想的呢?在我眼中整洁代码是什么样的?本书将以详细到吓死人的程度告诉你,我和我的同道对整洁代码的看法。我们会告诉你关于整洁变量名的想法,关于整洁函数的想法,关于整洁类的想法,如此等等。我们视这些观点为当然,且不为其逆耳而致歉。对我们而言,在职业生涯的这个阶段,这些观点确属当然,也是我们整洁代码派的圭旨。 ![](figures/ch1/1-11fig_martin.jpg) Martial artists do not all agree about the best martial art, or the best technique within a martial art. Often master martial artists will form their own schools of thought and gather students to learn from them. So we see Gracie Jiu Jistu, founded and taught by the Gracie family in Brazil. We see Hakkoryu Jiu Jistu, founded and taught by Okuyama Ryuho in Tokyo. We see Jeet Kune Do, founded and taught by Bruce Lee in the United States. > 武术家从不认同所谓最好的武术,也不认同所谓绝招。武术大师们常常创建自己的流派,聚徒而授。因此我们才看到格雷西家族在巴西开创并传授的格雷西柔术(Gracie Jiu Jistu),看到奥山龙峰(Okuyama Ryuho)在东京开创并传授的八光流柔术(Hakkoryu Jiu Jistu),看到李小龙(Bruce Lee)在美国开创并传授的截拳道(Jeet Kune Do)。 Students of these approaches immerse themselves in the teachings of the founder. They dedicate themselves to learn what that particular master teaches, often to the exclusion of any other master’s teaching. Later, as the students grow in their art, they may become the student of a different master so they can broaden their knowledge and practice. Some eventually go on to refine their skills, discovering new techniques and founding their own schools. > 弟子们沉浸于创始人的授业。他们全心师从某位师傅,排斥其他师傅。弟子有所成就后,可以转投另一位师傅,扩展自己的知识与技能。有些弟子最终百炼成钢,创出新招数,开宗立派。 None of these different schools is absolutely right. Yet within a particular school we act as though the teachings and techniques are right. After all, there is a right way to practice Hakkoryu Jiu Jitsu, or Jeet Kune Do. But this rightness within a school does not invalidate the teachings of a different school. > 任何门派都并非绝对正确。不过,身处某一门派时,我们总以其所传之技为善。归根结底,练习八光流柔术或截拳道,自有其善法,但这并不能否定其他门派所授之法。 Consider this book a description of the Object Mentor School of Clean Code. The techniques and teachings within are the way that we practice our art. We are willing to claim that if you follow these teachings, you will enjoy the benefits that we have enjoyed, and you will learn to write code that is clean and professional. But don’t make the mistake of thinking that we are somehow “right” in any absolute sense. There are other schools and other masters that have just as much claim to professionalism as we. It would behoove you to learn from them as well. > 可以把本书看作是对象导师(Object Mentor)[7]整洁代码派的说明。里面要传授的就是我们勤操己艺的方法。如果你遵从这些教诲,你就会如我们一般乐受其益,你将学会如何编写整洁而专业的代码。但无论如何也别错以为我们是“正确的”。其他门派和师傅和我们一样专业。你有必要也向他们学习。 Indeed, many of the recommendations in this book are controversial. You will probably not agree with all of them. You might violently disagree with some of them. That’s fine. We can’t claim final authority. On the other hand, the recommendations in this book are things that we have thought long and hard about. We have learned them through decades of experience and repeated trial and error. So whether you agree or disagree, it would be a shame if you did not see, and respect, our point of view. > 实际上,书中很多建议都存在争议。或许你并不完全同意这些建议。你可能会强烈反对其中一些建议。这样挺好的。我们不能要求做最终权威。另外一方面,书中列出的建议,乃是我们长久苦思、从数十年的从业经验和无数尝试与错误中得来。无论你同意与否,如果你没看到或是不尊敬我们的观点,就真该自己害臊。 ## 1.5 WE ARE AUTHORS 我们是作者 The @author field of a Javadoc tells us who we are. We are authors. And one thing about authors is that they have readers. Indeed, authors are responsible for communicating well with their readers. The next time you write a line of code, remember you are an author, writing for readers who will judge your effort. > Javadoc 中的@author 字段告诉我们自己是什么人。我们是作者。作者都有读者。实际上,作者有责任与读者做良好沟通。下次你写代码的时候,记得自己是作者,要为评判你工作的读者写代码。 You might ask: How much is code really read? Doesn’t most of the effort go into writing it? > 你或许会问:代码真正“读”的成分有多少呢?难道力量主要不是用在“写”上吗? Have you ever played back an edit session? In the 80s and 90s we had editors like Emacs that kept track of every keystroke. You could work for an hour and then play back your whole edit session like a high-speed movie. When I did this, the results were fascinating. > 你是否玩过“编辑器回放”?20 世纪 80、90 年代,Emac 之类编辑器记录每次击键动作。你可以在一小时工作之后,回放击键过程,就像是看一部高速电影。我这么做过,结果很有趣。 - The vast majority of the playback was scrolling and navigating to other modules! - Bob enters the module. - He scrolls down to the function needing change. - He pauses, considering his options. - Oh, he’s scrolling up to the top of the module to check the initialization of a variable. - Now he scrolls back down and begins to type. - Ooops, he’s erasing what he typed! - He types it again. - He erases it again! - He types half of something else but then erases that! - He scrolls down to another function that calls the function he’s changing to see how it is called. - He scrolls back up and types the same code he just erased. - He pauses. - He erases that code again! - He pops up another window and looks at a subclass. Is that function overridden? - … --- > - 回放过程显示,多数时间都是在滚动屏幕、浏览其他模块! > - 鲍勃进入模块。 > - 他向下滚动到要修改的函数。 > - 他停下来考虑可以做什么。 > - 哦,他滚动到模块顶端,检查变量初始化。 > - 现在他回到修改处,开始键入。 > - 喔,他删掉了键入的内容。 > - 他重新键入。 > - 他又删除了! > - 他键入了一半什么东西,又删除掉。 > - 他滚动到调用要修改函数的另一函数,看看是怎么调用的。 > - 他回到修改处,重新键入刚才删掉的代码。 > - 他停下来。 > - 他再一次删掉代码! > - 他打开另一个窗口,查看别的子类。那是个复载函数吗? > - …… You get the drift. Indeed, the ratio of time spent reading vs. writing is well over 10:1. We are constantly reading old code as part of the effort to write new code. > 你该明白了。读与写花费时间的比例超过 10:1。写新代码时,我们一直在读旧代码。 Because this ratio is so high, we want the reading of code to be easy, even if it makes the writing harder. Of course there’s no way to write code without reading it, so making it easy to read actually makes it easier to write. > 既然比例如此之高,我们就想让读的过程变得轻松,即便那会使得编写过程更难。没可能光写不读,所以使之易读实际也使之易写。 There is no escape from this logic. You cannot write code if you cannot read the surrounding code. The code you are trying to write today will be hard or easy to write depending on how hard or easy the surrounding code is to read. So if you want to go fast, if you want to get done quickly, if you want your code to be easy to write, make it easy to read. > 这事概无例外。不读周边代码的话就没法写代码。编写代码的难度,取决于读周边代码的难度。要想干得快,要想早点做完,要想轻松写代码,先让代码易读吧。 ## 1.6 THE BOY SCOUT RULE 童子军军规 It’s not enough to write the code well. The code has to be kept clean over time. We’ve all seen code rot and degrade as time passes. So we must take an active role in preventing this degradation. > 光把代码写好可不够。必须时时保持代码整洁。我们都见过代码随时间流逝而腐坏。我们应当更积极地阻止腐坏的发生。 The Boy Scouts of America have a simple rule that we can apply to our profession. > 借用美国童子军一条简单的军规,应用到我们的专业领域: Leave the campground cleaner than you found it.5 > 让营地比你来时更干净。 5. This was adapted from Robert Stephenson Smyth Baden-Powell’s farewell message to the Scouts: “Try and leave this world a little better than you found it…” If we all checked-in our code a little cleaner than when we checked it out, the code simply could not rot. The cleanup doesn’t have to be something big. Change one variable name for the better, break up one function that’s a little too large, eliminate one small bit of duplication, clean up one composite if statement. > 如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。清理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复代码,清理一个嵌套 if 语句。 Can you imagine working on a project where the code simply got better as time passed? Do you believe that any other option is professional? Indeed, isn’t continuous improvement an intrinsic part of professionalism? > 你想要为一个代码随时间流逝而越变越好的项目工作吗?你还能相信有其他更专业的做法吗?难道持续改进不是专业性的内在组成部分吗? ## 1.7 PREQUEL AND PRINCIPLES 前传与原则 In many ways this book is a “prequel” to a book I wrote in 2002 entitled Agile Software Development: Principles, Patterns, and Practices (PPP). The PPP book concerns itself with the principles of object-oriented design, and many of the practices used by professional developers. If you have not read PPP, then you may find that it continues the story told by this book. If you have already read it, then you’ll find many of the sentiments of that book echoed in this one at the level of code. > 从许多角度看,本书都是我 2002 年写那本 Agile SoftwareDevelopment:Principles,Patterns,and Practices(中译版《敏捷软件开发:原则、模式与实践》,简称 PPP)的“前传”。PPP 关注面向对象设计的原则,以及专业开发者采用的许多实践方法。假如你没读过 PPP,你会发现它像这本书的延续。如果你读过,会发现那本书的主张在代码层面于本书中回响。 In this book you will find sporadic references to various principles of design. These include the Single Responsibility Principle (SRP), the Open Closed Principle (OCP), and the Dependency Inversion Principle (DIP) among others. These principles are described in depth in PPP. > 在本书中,你会发现对不同设计原则的引用,包括单一权责原则(Single Responsibility Principle,SRP)、开放闭合原则(Open Closed Principle,OCP)和依赖倒置原则(Dependency Inversion Principle,DIP)等。 ## 1.8 CONCLUSION 小结 Books on art don’t promise to make you an artist. All they can do is give you some of the tools, techniques, and thought processes that other artists have used. So too this book cannot promise to make you a good programmer. It cannot promise to give you “code-sense.” All it can do is show you the thought processes of good programmers and the tricks, techniques, and tools that they use. > 艺术书并不保证你读过之后能成为艺术家,只能告诉你其他艺术家用过的工具、技术和思维过程。本书同样也不担保让你成为好程序员。它不担保能给你“代码感”。它所能做的,只是展示好程序员的思维过程,还有他们使用的技巧、技术和工具。 Just like a book on art, this book will be full of details. There will be lots of code. You’ll see good code and you’ll see bad code. You’ll see bad code transformed into good code. You’ll see lists of heuristics, disciplines, and techniques. You’ll see example after example. After that, it’s up to you. > 和艺术书一样,本书也充满了细节。代码会很多。你会看到好代码,也会看到糟糕的代码。你会看到糟糕的代码如何转化为好代码。你会看到启发、规条和技巧的列表。你会看到一个又一个例子。但最终结果取决于你自己。 Remember the old joke about the concert violinist who got lost on his way to a performance? He stopped an old man on the corner and asked him how to get to Carnegie Hall. The old man looked at the violinist and the violin tucked under his arm, and said: “Practice, son. Practice!” > 还记得那个关于小提琴家在去表演的路上迷路的老笑话吗?他在街角拦住一位长者,问他怎么才能去卡耐基音乐厅(Carnegie Hall)。长者看了看小提琴家,又看了看他手中的琴,说道:“你还得练,孩子,还得练!” ================================================ FILE: docs/ch10.md ================================================ # 第 10 章 Classes with Jeff Langr ![](figures/ch10/10_1fig_martin.jpg) So far in this book we have focused on how to write lines and blocks of code well. We have delved into proper composition of functions and how they interrelate. But for all the attention to the expressiveness of code statements and the functions they comprise, we still don’t have clean code until we’ve paid attention to higher levels of code organization. Let’s talk about clean classes. CLASS ORGANIZATION Following the standard Java convention, a class should begin with a list of variables. Public static constants, if any, should come first. Then private static variables, followed by private instance variables. There is seldom a good reason to have a public variable. Public functions should follow the list of variables. We like to put the private utilities called by a public function right after the public function itself. This follows the stepdown rule and helps the program read like a newspaper article. Encapsulation We like to keep our variables and utility functions private, but we’re not fanatic about it. Sometimes we need to make a variable or utility function protected so that it can be accessed by a test. For us, tests rule. If a test in the same package needs to call a function or access a variable, we’ll make it protected or package scope. However, we’ll first look for a way to maintain privacy. Loosening encapsulation is always a last resort. CLASSES SHOULD BE SMALL! The first rule of classes is that they should be small. The second rule of classes is that they should be smaller than that. No, we’re not going to repeat the exact same text from the Functions chapter. But as with functions, smaller is the primary rule when it comes to designing classes. As with functions, our immediate question is always “How small?” With functions we measured size by counting physical lines. With classes we use a different measure. We count responsibilities.1 1. [RDD] Listing 10-1 outlines a class, SuperDashboard, that exposes about 70 public methods. Most developers would agree that it’s a bit too super in size. Some developers might refer to SuperDashboard as a “God class.” Listing 10-1 Too Many Responsibilities ```java public class SuperDashboard extends JFrame implements MetaDataUser public String getCustomizerLanguagePath() public void setSystemConfigPath(String systemConfigPath) public String getSystemConfigDocument() public void setSystemConfigDocument(String systemConfigDocument) public boolean getGuruState() public boolean getNoviceState() public boolean getOpenSourceState() public void showObject(MetaObject object) public void showProgress(String s) public boolean isMetadataDirty() public void setIsMetadataDirty(boolean isMetadataDirty) public Component getLastFocusedComponent() public void setLastFocused(Component lastFocused) public void setMouseSelectState(boolean isMouseSelected) public boolean isMouseSelected() public LanguageManager getLanguageManager() public Project getProject() public Project getFirstProject() public Project getLastProject() public String getNewProjectName() public void setComponentSizes(Dimension dim) public String getCurrentDir() public void setCurrentDir(String newDir) public void updateStatus(int dotPos, int markPos) public Class[] getDataBaseClasses() public MetadataFeeder getMetadataFeeder() public void addProject(Project project) public boolean setCurrentProject(Project project) public boolean removeProject(Project project) public MetaProjectHeader getProgramMetadata() public void resetDashboard() public Project loadProject(String fileName, String projectName) public void setCanSaveMetadata(boolean canSave) public MetaObject getSelectedObject() public void deselectObjects() public void setProject(Project project) public void editorAction(String actionName, ActionEvent event) public void setMode(int mode) public FileManager getFileManager() public void setFileManager(FileManager fileManager) public ConfigManager getConfigManager() public void setConfigManager(ConfigManager configManager) public ClassLoader getClassLoader() public void setClassLoader(ClassLoader classLoader) public Properties getProps() public String getUserHome() public String getBaseDir() public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() public MetaObject pasting( MetaObject target, MetaObject pasted, MetaProject project) public void processMenuItems(MetaObject metaObject) public void processMenuSeparators(MetaObject metaObject) public void processTabPages(MetaObject metaObject) public void processPlacement(MetaObject object) public void processCreateLayout(MetaObject object) public void updateDisplayLayer(MetaObject object, int layerIndex) public void propertyEditedRepaint(MetaObject object) public void processDeleteObject(MetaObject object) public boolean getAttachedToDesigner() public void processProjectChangedState(boolean hasProjectChanged) public void processObjectNameChanged(MetaObject object) public void runProject() public void setAçowDragging(boolean allowDragging) public boolean allowDragging() public boolean isCustomizing() public void setTitle(String title) public IdeMenuBar getIdeMenuBar() public void showHelper(MetaObject metaObject, String propertyName) // … many non-public methods follow … } ``` But what if SuperDashboard contained only the methods shown in Listing 10-2? Listing 10-2 Small Enough? ```java public class SuperDashboard extends JFrame implements MetaDataUser public Component getLastFocusedComponent() public void setLastFocused(Component lastFocused) public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() } ``` Five methods isn’t too much, is it? In this case it is because despite its small number of methods, SuperDashboard has too many responsibilities. The name of a class should describe what responsibilities it fulfills. In fact, naming is probably the first way of helping determine class size. If we cannot derive a concise name for a class, then it’s likely too large. The more ambiguous the class name, the more likely it has too many responsibilities. For example, class names including weasel words like Processor or Manager or Super often hint at unfortunate aggregation of responsibilities. We should also be able to write a brief description of the class in about 25 words, without using the words “if,” “and,” “or,” or “but.” How would we describe the SuperDashboard? “The SuperDashboard provides access to the component that last held the focus, and it also allows us to track the version and build numbers.” The first “and” is a hint that SuperDashboard has too many responsibilities. The Single Responsibility Principle The Single Responsibility Principle (SRP)2 states that a class or module should have one, and only one, reason to change. This principle gives us both a definition of responsibility, and a guidelines for class size. Classes should have one responsibility—one reason to change. 2. You can read much more about this principle in [PPP]. The seemingly small SuperDashboard class in Listing 10-2 has two reasons to change. First, it tracks version information that would seemingly need to be updated every time the software gets shipped. Second, it manages Java Swing components (it is a derivative of JFrame, the Swing representation of a top-level GUI window). No doubt we’ll want to update the version number if we change any of the Swing code, but the converse isn’t necessarily true: We might change the version information based on changes to other code in the system. Trying to identify responsibilities (reasons to change) often helps us recognize and create better abstractions in our code. We can easily extract all three SuperDashboard methods that deal with version information into a separate class named Version. (See Listing 10-3.) The Version class is a construct that has a high potential for reuse in other applications! Listing 10-3 A single-responsibility class ```java public class Version { public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() } ``` SRP is one of the more important concept in OO design. It’s also one of the simpler concepts to understand and adhere to. Yet oddly, SRP is often the most abused class design principle. We regularly encounter classes that do far too many things. Why? Getting software to work and making software clean are two very different activities. Most of us have limited room in our heads, so we focus on getting our code to work more than organization and cleanliness. This is wholly appropriate. Maintaining a separation of concerns is just as important in our programming activities as it is in our programs. The problem is that too many of us think that we are done once the program works. We fail to switch to the other concern of organization and cleanliness. We move on to the next problem rather than going back and breaking the overstuffed classes into decoupled units with single responsibilities. At the same time, many developers fear that a large number of small, single-purpose classes makes it more difficult to understand the bigger picture. They are concerned that they must navigate from class to class in order to figure out how a larger piece of work gets accomplished. However, a system with many small classes has no more moving parts than a system with a few large classes. There is just as much to learn in the system with a few large classes. So the question is: Do you want your tools organized into toolboxes with many small drawers each containing well-defined and well-labeled components? Or do you want a few drawers that you just toss everything into? Every sizable system will contain a large amount of logic and complexity. The primary goal in managing such complexity is to organize it so that a developer knows where to look to find things and need only understand the directly affected complexity at any given time. In contrast, a system with larger, multipurpose classes always hampers us by insisting we wade through lots of things we don’t need to know right now. To restate the former points for emphasis: We want our systems to be composed of many small classes, not a few large ones. Each small class encapsulates a single responsibility, has a single reason to change, and collaborates with a few others to achieve the desired system behaviors. Cohesion Classes should have a small number of instance variables. Each of the methods of a class should manipulate one or more of those variables. In general the more variables a method manipulates the more cohesive that method is to its class. A class in which each variable is used by each method is maximally cohesive. In general it is neither advisable nor possible to create such maximally cohesive classes; on the other hand, we would like cohesion to be high. When cohesion is high, it means that the methods and variables of the class are co-dependent and hang together as a logical whole. Consider the implementation of a Stack in Listing 10-4. This is a very cohesive class. Of the three methods only size() fails to use both the variables. Listing 10-4 Stack.java A cohesive class. ```java public class Stack { private int topOfStack = 0; List elements = new LinkedList(); public int size() { return topOfStack; } public void push(int element) { topOfStack++; elements.add(element); } public int pop() throws PoppedWhenEmpty { if (topOfStack == 0) throw new PoppedWhenEmpty(); int element = elements.get(--topOfStack); elements.remove(topOfStack); return element; } } ``` The strategy of keeping functions small and keeping parameter lists short can sometimes lead to a proliferation of instance variables that are used by a subset of methods. When this happens, it almost always means that there is at least one other class trying to get out of the larger class. You should try to separate the variables and methods into two or more classes such that the new classes are more cohesive. Maintaining Cohesion Results in Many Small Classes Just the act of breaking large functions into smaller functions causes a proliferation of classes. Consider a large function with many variables declared within it. Let’s say you want to extract one small part of that function into a separate function. However, the code you want to extract uses four of the variables declared in the function. Must you pass all four of those variables into the new function as arguments? Not at all! If we promoted those four variables to instance variables of the class, then we could extract the code without passing any variables at all. It would be easy to break the function up into small pieces. Unfortunately, this also means that our classes lose cohesion because they accumulate more and more instance variables that exist solely to allow a few functions to share them. But wait! If there are a few functions that want to share certain variables, doesn’t that make them a class in their own right? Of course it does. When classes lose cohesion, split them! So breaking a large function into many smaller functions often gives us the opportunity to split several smaller classes out as well. This gives our program a much better organization and a more transparent structure. As a demonstration of what I mean, let’s use a time-honored example taken from Knuth’s wonderful book Literate Programming.3 Listing 10-5 shows a translation into Java of Knuth’s PrintPrimes program. To be fair to Knuth, this is not the program as he wrote it but rather as it was output by his WEB tool. I’m using it because it makes a great starting place for breaking up a big function into many smaller functions and classes. 3. [Knuth92]. Listing 10-5 PrintPrimes.java ```java package literatePrimes; public class PrintPrimes { public static void main(String[] args) { final int M = 1000; final int RR = 50; final int CC = 4; final int WW = 10; final int ORDMAX = 30; int P[] = new int[M + 1]; int PAGENUMBER; int PAGEOFFSET; int ROWOFFSET; int C; int J; int K; boolean JPRIME; int ORD; int SQUARE; int N; int MULT[] = new int[ORDMAX + 1]; J = 1; K = 1; P[1] = 2; ORD = 2; SQUARE = 9; while (K < M) { do { J = J + 2; if (J == SQUARE) { ORD = ORD + 1; SQUARE = P[ORD] * P[ORD]; MULT[ORD - 1] = J; } N = 2; JPRIME = true; while (N < ORD && JPRIME) { while (MULT[N] < J) MULT[N] = MULT[N] + P[N] + P[N]; if (MULT[N] == J) JPRIME = false; N = N + 1; } } while (!JPRIME); K = K + 1; P[K] = J; } { PAGENUMBER = 1; PAGEOFFSET = 1; while (PAGEOFFSET <= M) { System.out.println(”The First ” + M + ” Prime Numbers --- Page ” + PAGENUMBER); System.out.println(””); for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++){ for (C = 0; C < CC;C++) if (ROWOFFSET + C * RR <= M) System.out.format(”%10d”, P[ROWOFFSET + C * RR]); System.out.println(””); } System.out.println(”\f”); PAGENUMBER = PAGENUMBER + 1; PAGEOFFSET = PAGEOFFSET + RR * CC; } } } } ``` This program, written as a single function, is a mess. It has a deeply indented structure, a plethora of odd variables, and a tightly coupled structure. At the very least, the one big function should be split up into a few smaller functions. Listing 10-6 through Listing 10-8 show the result of splitting the code in Listing 10-5 into smaller classes and functions, and choosing meaningful names for those classes, functions, and variables. Listing 10-6 PrimePrinter.java (refactored) ```java package literatePrimes; public class PrimePrinter { public static void main(String[] args) { final int NUMBER_OF_PRIMES = 1000; int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES); final int ROWS_PER_PAGE = 50; final int COLUMNS_PER_PAGE = 4; RowColumnPagePrinter tablePrinter = new RowColumnPagePrinter(ROWS_PER_PAGE, COLUMNS_PER_PAGE, ”The First ” + NUMBER_OF_PRIMES + ” Prime Numbers”); tablePrinter.print(primes); } } ``` Listing 10-7 RowColumnPagePrinter.java ```java package literatePrimes; import java.io.PrintStream; public class RowColumnPagePrinter { private int rowsPerPage; private int columnsPerPage; private int numbersPerPage; private String pageHeader; private PrintStream printStream; public RowColumnPagePrinter(int rowsPerPage, int columnsPerPage, String pageHeader) { this.rowsPerPage = rowsPerPage; this.columnsPerPage = columnsPerPage; this.pageHeader = pageHeader; numbersPerPage = rowsPerPage * columnsPerPage; printStream = System.out; } public void print(int data[]) { int pageNumber = 1; for (int firstIndexOnPage = 0; firstIndexOnPage < data.length; firstIndexOnPage += numbersPerPage) { int lastIndexOnPage = Math.min(firstIndexOnPage + numbersPerPage - 1, data.length - 1); printPageHeader(pageHeader, pageNumber); printPage(firstIndexOnPage, lastIndexOnPage, data); printStream.println(”\f”); pageNumber++; } } private void printPage(int firstIndexOnPage, int lastIndexOnPage, int[] data) { int firstIndexOfLastRowOnPage = firstIndexOnPage + rowsPerPage - 1; for (int firstIndexInRow = firstIndexOnPage; firstIndexInRow <= firstIndexOfLastRowOnPage; firstIndexInRow++) { printRow(firstIndexInRow, lastIndexOnPage, data); printStream.println(””); } } private void printRow(int firstIndexInRow, int lastIndexOnPage, int[] data) { for (int column = 0; column < columnsPerPage; column++) { int index = firstIndexInRow + column * rowsPerPage; if (index <= lastIndexOnPage) printStream.format(”%10d”, data[index]); } } private void printPageHeader(String pageHeader, int pageNumber) { printStream.println(pageHeader + ” --- Page ” + pageNumber); printStream.println(””); } public void setOutput(PrintStream printStream) { this.printStream = printStream; } } ``` Listing 10-8 PrimeGenerator.java ```java package literatePrimes; import java.util.ArrayList; public class PrimeGenerator { private static int[] primes; private static ArrayList multiplesOfPrimeFactors; protected static int[] generate(int n) { primes = new int[n]; multiplesOfPrimeFactors = new ArrayList(); set2AsFirstPrime(); checkOddNumbersForSubsequentPrimes(); return primes; } private static void set2AsFirstPrime() { primes[0] = 2; multiplesOfPrimeFactors.add(2); } private static void checkOddNumbersForSubsequentPrimes() { int primeIndex = 1; for (int candidate = 3; primeIndex < primes.length; candidate += 2) { if (isPrime(candidate)) primes[primeIndex++] = candidate; } } private static boolean isPrime(int candidate) { if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) { multiplesOfPrimeFactors.add(candidate); return false; } return isNotMultipleOfAnyPreviousPrimeFactor(candidate); } private static boolean isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) { int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()]; int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor; return candidate == leastRelevantMultiple; } private static boolean isNotMultipleOfAnyPreviousPrimeFactor(int candidate) { for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) { if (isMultipleOfNthPrimeFactor(candidate, n)) return false; } return true; } private static boolean isMultipleOfNthPrimeFactor(int candidate, int n) { return candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n); } private static int smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) { int multiple = multiplesOfPrimeFactors.get(n); while (multiple < candidate) multiple += 2 * primes[n]; multiplesOfPrimeFactors.set(n, multiple); return multiple; } } ``` The first thing you might notice is that the program got a lot longer. It went from a little over one page to nearly three pages in length. There are several reasons for this growth. First, the refactored program uses longer, more descriptive variable names. Second, the refactored program uses function and class declarations as a way to add commentary to the code. Third, we used whitespace and formatting techniques to keep the program readable. Notice how the program has been split into three main responsibilities. The main program is contained in the PrimePrinter class all by itself. Its responsibility is to handle the execution environment. It will change if the method of invocation changes. For example, if this program were converted to a SOAP service, this is the class that would be affected. The RowColumnPagePrinter knows all about how to format a list of numbers into pages with a certain number of rows and columns. If the formatting of the output needed changing, then this is the class that would be affected. The PrimeGenerator class knows how to generate a list prime numbers. Notice that it is not meant to be instantiated as an object. The class is just a useful scope in which its variables can be declared and kept hidden. This class will change if the algorithm for computing prime numbers changes. This was not a rewrite! We did not start over from scratch and write the program over again. Indeed, if you look closely at the two different programs, you’ll see that they use the same algorithm and mechanics to get their work done. The change was made by writing a test suite that verified the precise behavior of the first program. Then a myriad of tiny little changes were made, one at a time. After each change the program was executed to ensure that the behavior had not changed. One tiny step after another, the first program was cleaned up and transformed into the second. ORGANIZING FOR CHANGE For most systems, change is continual. Every change subjects us to the risk that the remainder of the system no longer works as intended. In a clean system we organize our classes so as to reduce the risk of change. The Sql class in Listing 10-9 is used to generate properly formed SQL strings given appropriate metadata. It’s a work in progress and, as such, doesn’t yet support SQL functionality like update statements. When the time comes for the Sql class to support an update statement, we’ll have to “open up” this class to make modifications. The problem with opening a class is that it introduces risk. Any modifications to the class have the potential of breaking other code in the class. It must be fully retested. Listing 10-9 A class that must be opened for change ```java public class Sql { public Sql(String table, Column[] columns) public String create() public String insert(Object[] fields) public String selectAll() public String findByKey(String keyColumn, String keyValue) public String select(Column column, String pattern) public String select(Criteria criteria) public String preparedInsert() private String columnList(Column[] columns) private String valuesList(Object[] fields, final Column[] columns) private String selectWithCriteria(String criteria) private String placeholderList(Column[] columns) } ``` The Sql class must change when we add a new type of statement. It also must change when we alter the details of a single statement type—for example, if we need to modify the select functionality to support subselects. These two reasons to change mean that the Sql class violates the SRP. We can spot this SRP violation from a simple organizational standpoint. The method outline of Sql shows that there are private methods, such as selectWithCriteria, that appear to relate only to select statements. Private method behavior that applies only to a small subset of a class can be a useful heuristic for spotting potential areas for improvement. However, the primary spur for taking action should be system change itself. If the Sql class is deemed logically complete, then we need not worry about separating the responsibilities. If we won’t need update functionality for the foreseeable future, then we should leave Sql alone. But as soon as we find ourselves opening up a class, we should consider fixing our design. What if we considered a solution like that in Listing 10-10? Each public interface method defined in the previous Sql from Listing 10-9 is refactored out to its own derivative of the Sql class. Note that the private methods, such as valuesList, move directly where they are needed. The common private behavior is isolated to a pair of utility classes, Where and ColumnList. Listing 10-10 A set of closed classes ```java abstract public class Sql { public Sql(String table, Column[] columns) abstract public String generate(); } public class CreateSql extends Sql { public CreateSql(String table, Column[] columns) @Override public String generate() } public class SelectSql extends Sql { public SelectSql(String table, Column[] columns) @Override public String generate() } public class InsertSql extends Sql { public InsertSql(String table, Column[] columns, Object[] fields) @Override public String generate() private String valuesList(Object[] fields, final Column[] columns) } public class SelectWithCriteriaSql extends Sql { public SelectWithCriteriaSql( String table, Column[] columns, Criteria criteria) @Override public String generate() } public class SelectWithMatchSql extends Sql { public SelectWithMatchSql( String table, Column[] columns, Column column, String pattern) @Override public String generate() } public class FindByKeySql extends Sql public FindByKeySql( String table, Column[] columns, String keyColumn, String keyValue) @Override public String generate() } public class PreparedInsertSql extends Sql { public PreparedInsertSql(String table, Column[] columns) @Override public String generate() { private String placeholderList(Column[] columns) } public class Where { public Where(String criteria) public String generate() } public class ColumnList { public ColumnList(Column[] columns) public String generate() } ``` The code in each class becomes excruciatingly simple. Our required comprehension time to understand any class decreases to almost nothing. The risk that one function could break another becomes vanishingly small. From a test standpoint, it becomes an easier task to prove all bits of logic in this solution, as the classes are all isolated from one another. Equally important, when it’s time to add the update statements, none of the existing classes need change! We code the logic to build update statements in a new subclass of Sql named UpdateSql. No other code in the system will break because of this change. Our restructured Sql logic represents the best of all worlds. It supports the SRP. It also supports another key OO class design principle known as the Open-Closed Principle, or OCP:4 Classes should be open for extension but closed for modification. Our restructured Sql class is open to allow new functionality via subclassing, but we can make this change while keeping every other class closed. We simply drop our UpdateSql class in place. 4. [PPP]. We want to structure our systems so that we muck with as little as possible when we update them with new or changed features. In an ideal system, we incorporate new features by extending the system, not by making modifications to existing code. Isolating from Change Needs will change, therefore code will change. We learned in OO 101 that there are concrete classes, which contain implementation details (code), and abstract classes, which represent concepts only. A client class depending upon concrete details is at risk when those details change. We can introduce interfaces and abstract classes to help isolate the impact of those details. Dependencies upon concrete details create challenges for testing our system. If we’re building a Portfolio class and it depends upon an external TokyoStockExchange API to derive the portfolio’s value, our test cases are impacted by the volatility of such a lookup. It’s hard to write a test when we get a different answer every five minutes! Instead of designing Portfolio so that it directly depends upon TokyoStockExchange, we create an interface, StockExchange, that declares a single method: ```java public interface StockExchange { Money currentPrice(String symbol); } ``` We design TokyoStockExchange to implement this interface. We also make sure that the constructor of Portfolio takes a StockExchange reference as an argument: ```java public Portfolio { private StockExchange exchange; public Portfolio(StockExchange exchange) { this.exchange = exchange; } // … } ``` Now our test can create a testable implementation of the StockExchange interface that emulates the TokyoStockExchange. This test implementation will fix the current value for any symbol we use in testing. If our test demonstrates purchasing five shares of Microsoft for our portfolio, we code the test implementation to always return $100 per share of Microsoft. Our test implementation of the StockExchange interface reduces to a simple table lookup. We can then write a test that expects $500 for our overall portfolio value. ```java public class PortfolioTest { private FixedStockExchangeStub exchange; private Portfolio portfolio; @Before protected void setUp() throws Exception { exchange = new FixedStockExchangeStub(); exchange.fix(”MSFT”, 100); portfolio = new Portfolio(exchange); } @Test public void GivenFiveMSFTTotalShouldBe500() throws Exception { portfolio.add(5, ”MSFT”); Assert.assertEquals(500, portfolio.value()); } } ``` If a system is decoupled enough to be tested in this way, it will also be more flexible and promote more reuse. The lack of coupling means that the elements of our system are better isolated from each other and from change. This isolation makes it easier to understand each element of the system. By minimizing coupling in this way, our classes adhere to another class design principle known as the Dependency Inversion Principle (DIP).5 In essence, the DIP says that our classes should depend upon abstractions, not on concrete details. 5. [PPP]. Instead of being dependent upon the implementation details of the TokyoStock-Exchange class, our Portfolio class is now dependent upon the StockExchange interface. The StockExchange interface represents the abstract concept of asking for the current price of a symbol. This abstraction isolates all of the specific details of obtaining such a price, including from where that price is obtained. ================================================ FILE: docs/ch11.md ================================================ # 第 11 章 Systems by Dr. Kevin Dean Wampler ![](figures/ch11/11_1fig_martin.jpg) “Complexity kills. It sucks the life out of developers, it makes products difficult to plan, build, and test.” —Ray Ozzie, CTO, Microsoft Corporation HOW WOULD YOU BUILD A CITY? Could you manage all the details yourself? Probably not. Even managing an existing city is too much for one person. Yet, cities work (most of the time). They work because cities have teams of people who manage particular parts of the city, the water systems, power systems, traffic, law enforcement, building codes, and so forth. Some of those people are responsible for the big picture, while others focus on the details. Cities also work because they have evolved appropriate levels of abstraction and modularity that make it possible for individuals and the “components” they manage to work effectively, even without understanding the big picture. Although software teams are often organized like that too, the systems they work on often don’t have the same separation of concerns and levels of abstraction. Clean code helps us achieve this at the lower levels of abstraction. In this chapter let us consider how to stay clean at higher levels of abstraction, the system level. SEPARATE CONSTRUCTING A SYSTEM FROM USING IT First, consider that construction is a very different process from use. As I write this, there is a new hotel under construction that I see out my window in Chicago. Today it is a bare concrete box with a construction crane and elevator bolted to the outside. The busy people there all wear hard hats and work clothes. In a year or so the hotel will be finished. The crane and elevator will be gone. The building will be clean, encased in glass window walls and attractive paint. The people working and staying there will look a lot different too. Software systems should separate the startup process, when the application objects are constructed and the dependencies are “wired” together, from the runtime logic that takes over after startup. The startup process is a concern that any application must address. It is the first concern that we will examine in this chapter. The separation of concerns is one of the oldest and most important design techniques in our craft. Unfortunately, most applications don’t separate this concern. The code for the startup process is ad hoc and it is mixed in with the runtime logic. Here is a typical example: ```java public Service getService() { if (service == null) service = new MyServiceImpl(…); // Good enough default for most cases? return service; } ``` This is the LAZY INITIALIZATION/EVALUATION idiom, and it has several merits. We don’t incur the overhead of construction unless we actually use the object, and our startup times can be faster as a result. We also ensure that null is never returned. However, we now have a hard-coded dependency on MyServiceImpl and everything its constructor requires (which I have elided). We can’t compile without resolving these dependencies, even if we never actually use an object of this type at runtime! Testing can be a problem. If MyServiceImpl is a heavyweight object, we will need to make sure that an appropriate TEST DOUBLE1 or MOCK OBJECT gets assigned to the service field before this method is called during unit testing. Because we have construction logic mixed in with normal runtime processing, we should test all execution paths (for example, the null test and its block). Having both of these responsibilities means that the method is doing more than one thing, so we are breaking the Single Responsibility Principle in a small way. 1. [Mezzaros07]. Perhaps worst of all, we do not know whether MyServiceImpl is the right object in all cases. I implied as much in the comment. Why does the class with this method have to know the global context? Can we ever really know the right object to use here? Is it even possible for one type to be right for all possible contexts? One occurrence of LAZY-INITIALIZATION isn’t a serious problem, of course. However, there are normally many instances of little setup idioms like this in applications. Hence, the global setup strategy (if there is one) is scattered across the application, with little modularity and often significant duplication. If we are diligent about building well-formed and robust systems, we should never let little, convenient idioms lead to modularity breakdown. The startup process of object construction and wiring is no exception. We should modularize this process separately from the normal runtime logic and we should make sure that we have a global, consistent strategy for resolving our major dependencies. Separation of Main One way to separate construction from use is simply to move all aspects of construction to main, or modules called by main, and to design the rest of the system assuming that all objects have been constructed and wired up appropriately. (See Figure 11-1.) The flow of control is easy to follow. The main function builds the objects necessary for the system, then passes them to the application, which simply uses them. Notice the direction of the dependency arrows crossing the barrier between main and the application. They all go one direction, pointing away from main. This means that the application has no knowledge of main or of the construction process. It simply expects that everything has been built properly. Factories Sometimes, of course, we need to make the application responsible for when an object gets created. For example, in an order processing system the application must create the Figure 11-1 Separating construction in main() ![](figures/ch11/11_2fig_martin.jpg) LineItem instances to add to an Order. In this case we can use the ABSTRACT FACTORY2 pattern to give the application control of when to build the LineItems, but keep the details of that construction separate from the application code. (See Figure 11-2.) 2. [GOF]. Figure 11-2 Separation construction with factory ![](figures/ch11/11_3fig_martin.jpg) Again notice that all the dependencies point from main toward the OrderProcessing application. This means that the application is decoupled from the details of how to build a LineItem. That capability is held in the LineItemFactoryImplementation, which is on the main side of the line. And yet the application is in complete control of when the LineItem instances get built and can even provide application-specific constructor arguments. Dependency Injection A powerful mechanism for separating construction from use is Dependency Injection (DI), the application of Inversion of Control (IoC) to dependency management.3 Inversion of Control moves secondary responsibilities from an object to other objects that are dedicated to the purpose, thereby supporting the Single Responsibility Principle. In the context of dependency management, an object should not take responsibility for instantiating dependencies itself. Instead, it should pass this responsibility to another “authoritative” mechanism, thereby inverting the control. Because setup is a global concern, this authoritative mechanism will usually be either the “main” routine or a special-purpose container. 3. See, for example, [Fowler]. JNDI lookups are a “partial” implementation of DI, where an object asks a directory server to provide a “service” matching a particular name. ```java MyService myService = (MyService)(jndiContext.lookup(“NameOfMyService”)); ``` The invoking object doesn’t control what kind of object is actually returned (as long it implements the appropriate interface, of course), but the invoking object still actively resolves the dependency. True Dependency Injection goes one step further. The class takes no direct steps to resolve its dependencies; it is completely passive. Instead, it provides setter methods or constructor arguments (or both) that are used to inject the dependencies. During the construction process, the DI container instantiates the required objects (usually on demand) and uses the constructor arguments or setter methods provided to wire together the dependencies. Which dependent objects are actually used is specified through a configuration file or programmatically in a special-purpose construction module. The Spring Framework provides the best known DI container for Java.4 You define which objects to wire together in an XML configuration file, then you ask for particular objects by name in Java code. We will look at an example shortly. 4. See [Spring]. There is also a Spring.NET framework. But what about the virtues of LAZY-INITIALIZATION? This idiom is still sometimes useful with DI. First, most DI containers won’t construct an object until needed. Second, many of these containers provide mechanisms for invoking factories or for constructing proxies, which could be used for LAZY-EVALUATION and similar optimizations.5 5. Don’t forget that lazy instantiation/evaluation is just an optimization and perhaps premature! SCALING UP Cities grow from towns, which grow from settlements. At first the roads are narrow and practically nonexistent, then they are paved, then widened over time. Small buildings and empty plots are filled with larger buildings, some of which will eventually be replaced with skyscrapers. At first there are no services like power, water, sewage, and the Internet (gasp!). These services are also added as the population and building densities increase. This growth is not without pain. How many times have you driven, bumper to bumper through a road “improvement” project and asked yourself, “Why didn’t they build it wide enough the first time!?” But it couldn’t have happened any other way. Who can justify the expense of a six-lane highway through the middle of a small town that anticipates growth? Who would want such a road through their town? It is a myth that we can get systems “right the first time.” Instead, we should implement only today’s stories, then refactor and expand the system to implement new stories tomorrow. This is the essence of iterative and incremental agility. Test-driven development, refactoring, and the clean code they produce make this work at the code level. But what about at the system level? Doesn’t the system architecture require preplanning? Certainly, it can’t grow incrementally from simple to complex, can it? Software systems are unique compared to physical systems. Their architectures can grow incrementally, ifwe maintain the proper separation of concerns. The ephemeral nature of software systems makes this possible, as we will see. Let us first consider a counterexample of an architecture that doesn’t separate concerns adequately. The original EJB1 and EJB2 architectures did not separate concerns appropriately and thereby imposed unnecessary barriers to organic growth. Consider an Entity Bean for a persistent Bank class. An entity bean is an in-memory representation of relational data, in other words, a table row. First, you had to define a local (in process) or remote (separate JVM) interface, which clients would use. Listing 11-1 shows a possible local interface: Listing 11-1 An EJB2 local interface for a Bank EJB ```java package com.example.banking; import java.util.Collections; import javax.ejb.*; public interface BankLocal extends java.ejb.EJBLocalObject { String getStreetAddr1() throws EJBException; String getStreetAddr2() throws EJBException; String getCity() throws EJBException; String getState() throws EJBException; String getZipCode() throws EJBException; void setStreetAddr1(String street1) throws EJBException; void setStreetAddr2(String street2) throws EJBException; void setCity(String city) throws EJBException; void setState(String state) throws EJBException; void setZipCode(String zip) throws EJBException; Collection getAccounts() throws EJBException; void setAccounts(Collection accounts) throws EJBException; void addAccount(AccountDTO accountDTO) throws EJBException; } ``` I have shown several attributes for the Bank’s address and a collection of accounts that the bank owns, each of which would have its data handled by a separate Account EJB. Listing 11-2 shows the corresponding implementation class for the Bank bean. Listing 11-2 The corresponding EJB2 Entity Bean Implementation ```java package com.example.banking; import java.util.Collections; import javax.ejb.*; public abstract class Bank implements javax.ejb.EntityBean { // Business logic… public abstract String getStreetAddr1(); public abstract String getStreetAddr2(); public abstract String getCity(); public abstract String getState(); public abstract String getZipCode(); public abstract void setStreetAddr1(String street1); public abstract void setStreetAddr2(String street2); public abstract void setCity(String city); public abstract void setState(String state); public abstract void setZipCode(String zip); public abstract Collection getAccounts(); public abstract void setAccounts(Collection accounts); public void addAccount(AccountDTO accountDTO) { InitialContext context = new InitialContext(); AccountHomeLocal accountHome = context.lookup(”AccountHomeLocal”); AccountLocal account = accountHome.create(accountDTO); Collection accounts = getAccounts(); accounts.add(account); } // EJB container logic public abstract void setId(Integer id); public abstract Integer getId(); public Integer ejbCreate(Integer id) { … } public void ejbPostCreate(Integer id) { … } // The rest had to be implemented but were usually empty: public void setEntityContext(EntityContext ctx) {} public void unsetEntityContext() {} public void ejbActivate() {} public void ejbPassivate() {} public void ejbLoad() {} public void ejbStore() {} public void ejbRemove() {} } ``` I haven’t shown the corresponding LocalHome interface, essentially a factory used to create objects, nor any of the possible Bank finder (query) methods you might add. Finally, you had to write one or more XML deployment descriptors that specify the object-relational mapping details to a persistence store, the desired transactional behavior, security constraints, and so on. The business logic is tightly coupled to the EJB2 application “container.” You must subclass container types and you must provide many lifecycle methods that are required by the container. Because of this coupling to the heavyweight container, isolated unit testing is difficult. It is necessary to mock out the container, which is hard, or waste a lot of time deploying EJBs and tests to a real server. Reuse outside of the EJB2 architecture is effectively impossible, due to the tight coupling. Finally, even object-oriented programming is undermined. One bean cannot inherit from another bean. Notice the logic for adding a new account. It is common in EJB2 beans to define “data transfer objects” (DTOs) that are essentially “structs” with no behavior. This usually leads to redundant types holding essentially the same data, and it requires boilerplate code to copy data from one object to another. Cross-Cutting Concerns The EJB2 architecture comes close to true separation of concerns in some areas. For example, the desired transactional, security, and some of the persistence behaviors are declared in the deployment descriptors, independently of the source code. Note that concerns like persistence tend to cut across the natural object boundaries of a domain. You want to persist all your objects using generally the same strategy, for example, using a particular DBMS6 versus flat files, following certain naming conventions for tables and columns, using consistent transactional semantics, and so on. 6. Database management system. In principle, you can reason about your persistence strategy in a modular, encapsulated way. Yet, in practice, you have to spread essentially the same code that implements the persistence strategy across many objects. We use the term cross-cutting concerns for concerns like these. Again, the persistence framework might be modular and our domain logic, in isolation, might be modular. The problem is the fine-grained intersection of these domains. In fact, the way the EJB architecture handled persistence, security, and transactions, “anticipated” aspect-oriented programming (AOP),7 which is a general-purpose approach to restoring modularity for cross-cutting concerns. 7. See [AOSD] for general information on aspects and [AspectJ]] and [Colyer] for AspectJ-specific information. In AOP, modular constructs called aspects specify which points in the system should have their behavior modified in some consistent way to support a particular concern. This specification is done using a succinct declarative or programmatic mechanism. Using persistence as an example, you would declare which objects and attributes (or patterns thereof) should be persisted and then delegate the persistence tasks to your persistence framework. The behavior modifications are made noninvasively8 to the target code by the AOP framework. Let us look at three aspects or aspect-like mechanisms in Java. 8. Meaning no manual editing of the target source code is required. JAVA PROXIES Java proxies are suitable for simple situations, such as wrapping method calls in individual objects or classes. However, the dynamic proxies provided in the JDK only work with interfaces. To proxy classes, you have to use a byte-code manipulation library, such as CGLIB, ASM, or Javassist.9 9. See [CGLIB], [ASM], and [Javassist]. Listing 11-3 shows the skeleton for a JDK proxy to provide persistence support for our Bank application, covering only the methods for getting and setting the list of accounts. Listing 11-3 JDK Proxy Example ```java // Bank.java (suppressing package names…) import java.utils.*; // The abstraction of a bank. public interface Bank { Collection getAccounts(); void setAccounts(Collection accounts); } // BankImpl.java import java.utils.*; // The “Plain Old Java Object” (POJO) implementing the abstraction. public class BankImpl implements Bank { private List accounts; public Collection getAccounts() { return accounts; } public void setAccounts(Collection accounts) { this.accounts = new ArrayList(); for (Account account: accounts) { this.accounts.add(account); } } } // BankProxyHandler.java import java.lang.reflect.*; import java.util.*; // “InvocationHandler” required by the proxy API. public class BankProxyHandler implements InvocationHandler { private Bank bank; public BankHandler (Bank bank) { this.bank = bank; } // Method defined in InvocationHandler public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (methodName.equals(”getAccounts”)) { bank.setAccounts(getAccountsFromDatabase()); return bank.getAccounts(); } else if (methodName.equals(”setAccounts”)) { bank.setAccounts((Collection) args[0]); setAccountsToDatabase(bank.getAccounts()); return null; } else { … } } // Lots of details here: protected Collection getAccountsFromDatabase() { … } protected void setAccountsToDatabase(Collection accounts) { … } } // Somewhere else… Bank bank = (Bank) Proxy.newProxyInstance( Bank.class.getClassLoader(), new Class[] { Bank.class }, new BankProxyHandler(new BankImpl())); ``` We defined an interface Bank, which will be wrapped by the proxy, and a Plain-Old Java Object (POJO), BankImpl, that implements the business logic. (We will revisit POJOs shortly.) The Proxy API requires an InvocationHandler object that it calls to implement any Bank method calls made to the proxy. Our BankProxyHandler uses the Java reflection API to map the generic method invocations to the corresponding methods in BankImpl, and so on. There is a lot of code here and it is relatively complicated, even for this simple case.10 Using one of the byte-manipulation libraries is similarly challenging. This code “volume” 10. For more detailed examples of the Proxy API and examples of its use, see, for example, [Goetz]. and complexity are two of the drawbacks of proxies. They make it hard to create clean code! Also, proxies don’t provide a mechanism for specifying system-wide execution “points” of interest, which is needed for a true AOP solution.11 11. AOP is sometimes confused with techniques used to implement it, such as method interception and “wrapping” through proxies. The real value of an AOP system is the ability to specify systemic behaviors in a concise and modular way. PURE JAVA AOP FRAMEWORKS Fortunately, most of the proxy boilerplate can be handled automatically by tools. Proxies are used internally in several Java frameworks, for example, Spring AOP and JBoss AOP, to implement aspects in pure Java.12 In Spring, you write your business logic as Plain-Old Java Objects. POJOs are purely focused on their domain. They have no dependencies on enterprise frameworks (or any other domains). Hence, they are conceptually simpler and easier to test drive. The relative simplicity makes it easier to ensure that you are implementing the corresponding user stories correctly and to maintain and evolve the code for future stories. 12. See [Spring] and [JBoss]. “Pure Java” means without the use of AspectJ. You incorporate the required application infrastructure, including cross-cutting concerns like persistence, transactions, security, caching, failover, and so on, using declarative configuration files or APIs. In many cases, you are actually specifying Spring or JBoss library aspects, where the framework handles the mechanics of using Java proxies or byte-code libraries transparently to the user. These declarations drive the dependency injection (DI) container, which instantiates the major objects and wires them together on demand. Listing 11-4 shows a typical fragment of a Spring V2.5 configuration file, app.xml13: 13. Adapted from http://www.theserverside.com/tt/articles/article.tss?l=IntrotoSpring25. Listing 11-4 Spring 2.X configuration file ```xml ``` Each “bean” is like one part of a nested “Russian doll,” with a domain object for a Bank proxied (wrapped) by a data accessor object (DAO), which is itself proxied by a JDBC driver data source. (See Figure 11-3.) Figure 11-3 The “Russian doll” of decorators ![](figures/ch11/11_4fig_martin.jpg) The client believes it is invoking getAccounts() on a Bank object, but it is actually talking to the outermost of a set of nested DECORATOR14 objects that extend the basic behavior of the Bank POJO. We could add other decorators for transactions, caching, and so forth. 14. [GOF]. In the application, a few lines are needed to ask the DI container for the top-level objects in the system, as specified in the XML file. ```java XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource(”app.xml”, getClass())); Bank bank = (Bank) bf.getBean(”bank”); ``` Because so few lines of Spring-specific Java code are required, the application is almost completely decoupled from Spring, eliminating all the tight-coupling problems of systems like EJB2. Although XML can be verbose and hard to read,15 the “policy” specified in these configuration files is simpler than the complicated proxy and aspect logic that is hidden from view and created automatically. This type of architecture is so compelling that frameworks like Spring led to a complete overhaul of the EJB standard for version 3. EJB3 15. The example can be simplified using mechanisms that exploit convention over configuration and Java 5 annotations to reduce the amount of explicit “wiring” logic required. largely follows the Spring model of declaratively supporting cross-cutting concerns using XML configuration files and/or Java 5 annotations. Listing 11-5 shows our Bank object rewritten in EJB316. 16. Adapted from http://www.onjava.com/pub/a/onjava/2006/05/17/standardizing-with-ejb3-java-persistence-api.html Listing 11-5 An EBJ3 Bank EJB ```java package com.example.banking.model; import javax.persistence.*; import java.util.ArrayList; import java.util.Collection; @Entity @Table(name = “BANKS”) public class Bank implements java.io.Serializable { @Id @GeneratedValue(strategy=GenerationType.AUTO) private int id; @Embeddable // An object “inlined” in Bank’s DB row public class Address { protected String streetAddr1; protected String streetAddr2; protected String city; protected String state; protected String zipCode; } @Embedded private Address address; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy=”bank”) private Collection accounts = new ArrayList(); public int getId() { return id; } public void setId(int id) { this.id = id; } public void addAccount(Account account) { account.setBank(this); accounts.add(account); } public Collection getAccounts() { return accounts; } public void setAccounts(Collection accounts) { this.accounts = accounts; } } ``` This code is much cleaner than the original EJB2 code. Some of the entity details are still here, contained in the annotations. However, because none of that information is outside of the annotations, the code is clean, clear, and hence easy to test drive, maintain, and so on. Some or all of the persistence information in the annotations can be moved to XML deployment descriptors, if desired, leaving a truly pure POJO. If the persistence mapping details won’t change frequently, many teams may choose to keep the annotations, but with far fewer harmful drawbacks compared to the EJB2 invasiveness. ASPECTJ ASPECTS Finally, the most full-featured tool for separating concerns through aspects is the AspectJ language,17 an extension of Java that provides “first-class” support for aspects as modularity constructs. The pure Java approaches provided by Spring AOP and JBoss AOP are sufficient for 80–90 percent of the cases where aspects are most useful. However, AspectJ provides a very rich and powerful tool set for separating concerns. The drawback of AspectJ is the need to adopt several new tools and to learn new language constructs and usage idioms. 17. See [AspectJ] and [Colyer]. The adoption issues have been partially mitigated by a recently introduced “annotation form” of AspectJ, where Java 5 annotations are used to define aspects using pure Java code. Also, the Spring Framework has a number of features that make incorporation of annotation-based aspects much easier for a team with limited AspectJ experience. A full discussion of AspectJ is beyond the scope of this book. See [AspectJ], [Colyer], and [Spring] for more information. TEST DRIVE THE SYSTEM ARCHITECTURE The power of separating concerns through aspect-like approaches can’t be overstated. If you can write your application’s domain logic using POJOs, decoupled from any architecture concerns at the code level, then it is possible to truly test drive your architecture. You can evolve it from simple to sophisticated, as needed, by adopting new technologies on demand. It is not necessary to do a Big Design Up Front18 (BDUF). In fact, BDUF is even harmful because it inhibits adapting to change, due to the psychological resistance to discarding prior effort and because of the way architecture choices influence subsequent thinking about the design. 18. Not to be confused with the good practice of up-front design, BDUF is the practice of designing everything up front before implementing anything at all. Building architects have to do BDUF because it is not feasible to make radical architectural changes to a large physical structure once construction is well underway.19 Although software has its own physics,20 it is economically feasible to make radical change, if the structure of the software separates its concerns effectively. 19. There is still a significant amount of iterative exploration and discussion of details, even after construction starts. 20. The term software physics was first used by [Kolence]. This means we can start a software project with a “naively simple” but nicely decoupled architecture, delivering working user stories quickly, then adding more infrastructure as we scale up. Some of the world’s largest Web sites have achieved very high availability and performance, using sophisticated data caching, security, virtualization, and so forth, all done efficiently and flexibly because the minimally coupled designs are appropriately simple at each level of abstraction and scope. Of course, this does not mean that we go into a project “rudderless.” We have some expectations of the general scope, goals, and schedule for the project, as well as the general structure of the resulting system. However, we must maintain the ability to change course in response to evolving circumstances. The early EJB architecture is but one of many well-known APIs that are over-engineered and that compromise separation of concerns. Even well-designed APIs can be overkill when they aren’t really needed. A good API should largely disappear from view most of the time, so the team expends the majority of its creative efforts focused on the user stories being implemented. If not, then the architectural constraints will inhibit the efficient delivery of optimal value to the customer. To recap this long discussion, An optimal system architecture consists of modularized domains of concern, each of which is implemented with Plain Old Java (or other) Objects. The different domains are integrated together with minimally invasive Aspects or Aspect-like tools. This architecture can be test-driven, just like the code. OPTIMIZE DECISION MAKING Modularity and separation of concerns make decentralized management and decision making possible. In a sufficiently large system, whether it is a city or a software project, no one person can make all the decisions. We all know it is best to give responsibilities to the most qualified persons. We often forget that it is also best to postpone decisions until the last possible moment. This isn’t lazy or irresponsible; it lets us make informed choices with the best possible information. A premature decision is a decision made with suboptimal knowledge. We will have that much less customer feedback, mental reflection on the project, and experience with our implementation choices if we decide too soon. The agility provided by a POJO system with modularized concerns allows us to make optimal, just-in-time decisions, based on the most recent knowledge. The complexity of these decisions is also reduced. USE STANDARDS WISELY, WHEN THEY ADD DEMONSTRABLE VALUE Building construction is a marvel to watch because of the pace at which new buildings are built (even in the dead of winter) and because of the extraordinary designs that are possible with today’s technology. Construction is a mature industry with highly optimized parts, methods, and standards that have evolved under pressure for centuries. Many teams used the EJB2 architecture because it was a standard, even when lighter-weight and more straightforward designs would have been sufficient. I have seen teams become obsessed with various strongly hyped standards and lose focus on implementing value for their customers. Standards make it easier to reuse ideas and components, recruit people with relevant experience, encapsulate good ideas, and wire components together. However, the process of creating standards can sometimes take too long for industry to wait, and some standards lose touch with the real needs of the adopters they are intended to serve. SYSTEMS NEED DOMAIN-SPECIFIC LANGUAGES Building construction, like most domains, has developed a rich language with a vocabulary, idioms, and patterns21 that convey essential information clearly and concisely. In software, there has been renewed interest recently in creating Domain-Specific Languages (DSLs),22 which are separate, small scripting languages or APIs in standard languages that permit code to be written so that it reads like a structured form of prose that a domain expert might write. 21. The work of [Alexander] has been particularly influential on the software community. 22. See, for example, [DSL]. [JMock] is a good example of a Java API that creates a DSL. A good DSL minimizes the “communication gap” between a domain concept and the code that implements it, just as agile practices optimize the communications within a team and with the project’s stakeholders. If you are implementing domain logic in the same language that a domain expert uses, there is less risk that you will incorrectly translate the domain into the implementation. DSLs, when used effectively, raise the abstraction level above code idioms and design patterns. They allow the developer to reveal the intent of the code at the appropriate level of abstraction. Domain-Specific Languages allow all levels of abstraction and all domains in the application to be expressed as POJOs, from high-level policy to low-level details. CONCLUSION Systems must be clean too. An invasive architecture overwhelms the domain logic and impacts agility. When the domain logic is obscured, quality suffers because bugs find it easier to hide and stories become harder to implement. If agility is compromised, productivity suffers and the benefits of TDD are lost. At all levels of abstraction, the intent should be clear. This will only happen if you write POJOs and you use aspect-like mechanisms to incorporate other implementation concerns noninvasively. Whether you are designing systems or individual modules, never forget to use the simplest thing that can possibly work. ================================================ FILE: docs/ch12.md ================================================ # 第 12 章 Emergence by Jeff Langr ![](figures/ch12/12_1fig_martin.jpg) GETTING CLEAN VIA EMERGENT DESIGN What if there were four simple rules that you could follow that would help you create good designs as you worked? What if by following these rules you gained insights into the structure and design of your code, making it easier to apply principles such as SRP and DIP? What if these four rules facilitated the emergence of good designs? Many of us feel that Kent Beck’s four rules of Simple Design1 are of significant help in creating well-designed software. 1. [XPE]. According to Kent, a design is “simple” if it follows these rules: - Runs all the tests - Contains no duplication - Expresses the intent of the programmer - Minimizes the number of classes and methods The rules are given in order of importance. SIMPLE DESIGN RULE 1: RUNS ALL THE TESTS First and foremost, a design must produce a system that acts as intended. A system might have a perfect design on paper, but if there is no simple way to verify that the system actually works as intended, then all the paper effort is questionable. A system that is comprehensively tested and passes all of its tests all of the time is a testable system. That’s an obvious statement, but an important one. Systems that aren’t testable aren’t verifiable. Arguably, a system that cannot be verified should never be deployed. Fortunately, making our systems testable pushes us toward a design where our classes are small and single purpose. It’s just easier to test classes that conform to the SRP. The more tests we write, the more we’ll continue to push toward things that are simpler to test. So making sure our system is fully testable helps us create better designs. Tight coupling makes it difficult to write tests. So, similarly, the more tests we write, the more we use principles like DIP and tools like dependency injection, interfaces, and abstraction to minimize coupling. Our designs improve even more. Remarkably, following a simple and obvious rule that says we need to have tests and run them continuously impacts our system’s adherence to the primary OO goals of low coupling and high cohesion. Writing tests leads to better designs. SIMPLE DESIGN RULES 2–4: REFACTORING Once we have tests, we are empowered to keep our code and classes clean. We do this by incrementally refactoring the code. For each few lines of code we add, we pause and reflect on the new design. Did we just degrade it? If so, we clean it up and run our tests to demonstrate that we haven’t broken anything. The fact that we have these tests eliminates the fear that cleaning up the code will break it! During this refactoring step, we can apply anything from the entire body of knowledge about good software design. We can increase cohesion, decrease coupling, separate concerns, modularize system concerns, shrink our functions and classes, choose better names, and so on. This is also where we apply the final three rules of simple design: Eliminate duplication, ensure expressiveness, and minimize the number of classes and methods. NO DUPLICATION Duplication is the primary enemy of a well-designed system. It represents additional work, additional risk, and additional unnecessary complexity. Duplication manifests itself in many forms. Lines of code that look exactly alike are, of course, duplication. Lines of code that are similar can often be massaged to look even more alike so that they can be more easily refactored. And duplication can exist in other forms such as duplication of implementation. For example, we might have two methods in a collection class: ```java int size() {} boolean isEmpty() {} ``` We could have separate implementations for each method. The isEmpty method could track a boolean, while size could track a counter. Or, we can eliminate this duplication by tying isEmpty to the definition of size: ```java boolean isEmpty() { return 0 == size(); } ``` Creating a clean system requires the will to eliminate duplication, even in just a few lines of code. For example, consider the following code: ```java public void scaleToOneDimension( float desiredDimension, float imageDimension) { if (Math.abs(desiredDimension - imageDimension) < errorThreshold) return; float scalingFactor = desiredDimension / imageDimension; scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f); RenderedOp newImage = ImageUtilities.getScaledImage( image, scalingFactor, scalingFactor); image.dispose(); System.gc(); image = newImage; } public synchronized void rotate(int degrees) { RenderedOp newImage = ImageUtilities.getRotatedImage( image, degrees); image.dispose(); System.gc(); image = newImage; } ``` To keep this system clean, we should eliminate the small amount of duplication between the scaleToOneDimension and rotate methods: ```java public void scaleToOneDimension( float desiredDimension, float imageDimension) { if (Math.abs(desiredDimension - imageDimension) < errorThreshold) return; float scalingFactor = desiredDimension / imageDimension; scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f); replaceImage(ImageUtilities.getScaledImage( image, scalingFactor, scalingFactor)); } public synchronized void rotate(int degrees) { replaceImage(ImageUtilities.getRotatedImage(image, degrees)); } privatex void replaceImage(RenderedOp newImage) { image.dispose(); System.gc(); image = newImage; } ``` As we extract commonality at this very tiny level, we start to recognize violations of SRP. So we might move a newly extracted method to another class. That elevates its visibility. Someone else on the team may recognize the opportunity to further abstract the new method and reuse it in a different context. This “reuse in the small” can cause system complexity to shrink dramatically. Understanding how to achieve reuse in the small is essential to achieving reuse in the large. The TEMPLATE METHOD2 pattern is a common technique for removing higher-level duplication. For example: ```java public class VacationPolicy { public void accrueUSDivisionVacation() { // code to calculate vacation based on hours worked to date // … // code to ensure vacation meets US minimums // … // code to apply vaction to payroll record // … } public void accrueEUDivisionVacation() { // code to calculate vacation based on hours worked to date // … // code to ensure vacation meets EU minimums // … // code to apply vaction to payroll record // … } } ``` The code across accrueUSDivisionVacation and accrueEuropeanDivisionVacation is largely the same, with the exception of calculating legal minimums. That bit of the algorithm changes based on the employee type. We can eliminate the obvious duplication by applying the TEMPLATE METHOD pattern. ```java abstract public class VacationPolicy { public void accrueVacation() { calculateBaseVacationHours(); alterForLegalMinimums(); applyToPayroll(); } private void calculateBaseVacationHours() { /* … */ }; abstract protected void alterForLegalMinimums(); private void applyToPayroll() { /* … */ }; } public class USVacationPolicy extends VacationPolicy { @Override protected void alterForLegalMinimums() { // US specific logic } } public class EUVacationPolicy extends VacationPolicy { @Override protected void alterForLegalMinimums() { // EU specific logic } } ``` The subclasses fill in the “hole” in the accrueVacation algorithm, supplying the only bits of information that are not duplicated. EXPRESSIVE Most of us have had the experience of working on convoluted code. Many of us have produced some convoluted code ourselves. It’s easy to write code that we understand, because at the time we write it we’re deep in an understanding of the problem we’re trying to solve. Other maintainers of the code aren’t going to have so deep an understanding. The majority of the cost of a software project is in long-term maintenance. In order to minimize the potential for defects as we introduce change, it’s critical for us to be able to understand what a system does. As systems become more complex, they take more and more time for a developer to understand, and there is an ever greater opportunity for a misunderstanding. Therefore, code should clearly express the intent of its author. The clearer the author can make the code, the less time others will have to spend understanding it. This will reduce defects and shrink the cost of maintenance. You can express yourself by choosing good names. We want to be able to hear a class or function name and not be surprised when we discover its responsibilities. You can also express yourself by keeping your functions and classes small. Small classes and functions are usually easy to name, easy to write, and easy to understand. You can also express yourself by using standard nomenclature. Design patterns, for example, are largely about communication and expressiveness. By using the standard pattern names, such as COMMAND or VISITOR, in the names of the classes that implement those patterns, you can succinctly describe your design to other developers. Well-written unit tests are also expressive. A primary goal of tests is to act as documentation by example. Someone reading our tests should be able to get a quick understanding of what a class is all about. But the most important way to be expressive is to try. All too often we get our code working and then move on to the next problem without giving sufficient thought to making that code easy for the next person to read. Remember, the most likely next person to read the code will be you. So take a little pride in your workmanship. Spend a little time with each of your functions and classes. Choose better names, split large functions into smaller functions, and generally just take care of what you’ve created. Care is a precious resource. MINIMAL CLASSES AND METHODS Even concepts as fundamental as elimination of duplication, code expressiveness, and the SRP can be taken too far. In an effort to make our classes and methods small, we might create too many tiny classes and methods. So this rule suggests that we also keep our function and class counts low. High class and method counts are sometimes the result of pointless dogmatism. Consider, for example, a coding standard that insists on creating an interface for each and every class. Or consider developers who insist that fields and behavior must always be separated into data classes and behavior classes. Such dogma should be resisted and a more pragmatic approach adopted. Our goal is to keep our overall system small while we are also keeping our functions and classes small. Remember, however, that this rule is the lowest priority of the four rules of Simple Design. So, although it’s important to keep class and function count low, it’s more important to have tests, eliminate duplication, and express yourself. CONCLUSION Is there a set of simple practices that can replace experience? Clearly not. On the other hand, the practices described in this chapter and in this book are a crystallized form of the many decades of experience enjoyed by the authors. Following the practice of simple design can and does encourage and enable developers to adhere to good principles and patterns that otherwise take years to learn. ================================================ FILE: docs/ch13.md ================================================ # 第 13 章 Concurrency by Brett L. Schuchert ![](figures/ch13/13_1fig_martin.jpg) “Objects are abstractions of processing. Threads are abstractions of schedule.” —James O. Coplien1 1. Private correspondence. Writing clean concurrent programs is hard—very hard. It is much easier to write code that executes in a single thread. It is also easy to write multithreaded code that looks fine on the surface but is broken at a deeper level. Such code works fine until the system is placed under stress. In this chapter we discuss the need for concurrent programming, and the difficulties it presents. We then present several recommendations for dealing with those difficulties, and writing clean concurrent code. Finally, we conclude with issues related to testing concurrent code. Clean Concurrency is a complex topic, worthy of a book by itself. Our strategy in this book is to present an overview here and provide a more detailed tutorial in “Concurrency II” on page 317. If you are just curious about concurrency, then this chapter will suffice for you now. If you have a need to understand concurrency at a deeper level, then you should read through the tutorial as well. WHY CONCURRENCY? Concurrency is a decoupling strategy. It helps us decouple what gets done from when it gets done. In single-threaded applications what and when are so strongly coupled that the state of the entire application can often be determined by looking at the stack backtrace. A programmer who debugs such a system can set a breakpoint, or a sequence of breakpoints, and know the state of the system by which breakpoints are hit. Decoupling what from when can dramatically improve both the throughput and structures of an application. From a structural point of view the application looks like many little collaborating computers rather than one big main loop. This can make the system easier to understand and offers some powerful ways to separate concerns. Consider, for example, the standard “Servlet” model of Web applications. These systems run under the umbrella of a Web or EJB container that partially manages concurrency for you. The servlets are executed asynchronously whenever Web requests come in. The servlet programmer does not have to manage all the incoming requests. In principle, each servlet execution lives in its own little world and is decoupled from all the other servlet executions. Of course if it were that easy, this chapter wouldn’t be necessary. In fact, the decoupling provided by Web containers is far less than perfect. Servlet programmers have to be very aware, and very careful, to make sure their concurrent programs are correct. Still, the structural benefits of the servlet model are significant. But structure is not the only motive for adopting concurrency. Some systems have response time and throughput constraints that require hand-coded concurrent solutions. For example, consider a single-threaded information aggregator that acquires information from many different Web sites and merges that information into a daily summary. Because this system is single threaded, it hits each Web site in turn, always finishing one before starting the next. The daily run needs to execute in less than 24 hours. However, as more and more Web sites are added, the time grows until it takes more than 24 hours to gather all the data. The single-thread involves a lot of waiting at Web sockets for I/O to complete. We could improve the performance by using a multithreaded algorithm that hits more than one Web site at a time. Or consider a system that handles one user at a time and requires only one second of time per user. This system is fairly responsive for a few users, but as the number of users increases, the system’s response time increases. No user wants to get in line behind 150 others! We could improve the response time of this system by handling many users concurrently. Or consider a system that interprets large data sets but can only give a complete solution after processing all of them. Perhaps each data set could be processed on a different computer, so that many data sets are being processed in parallel. Myths and Misconceptions And so there are compelling reasons to adopt concurrency. However, as we said before, concurrency is hard. If you aren’t very careful, you can create some very nasty situations. Consider these common myths and misconceptions: - Concurrency always improves performance. Concurrency can sometimes improve performance, but only when there is a lot of wait time that can be shared between multiple threads or multiple processors. Neither situation is trivial. - Design does not change when writing concurrent programs. In fact, the design of a concurrent algorithm can be remarkably different from the design of a single-threaded system. The decoupling of what from when usually has a huge effect on the structure of the system. - Understanding concurrency issues is not important when working with a container such as a Web or EJB container. In fact, you’d better know just what your container is doing and how to guard against the issues of concurrent update and deadlock described later in this chapter. Here are a few more balanced sound bites regarding writing concurrent software: - Concurrency incurs some overhead, both in performance as well as writing additional code. - Correct concurrency is complex, even for simple problems. - Concurrency bugs aren’t usually repeatable, so they are often ignored as one-offs2 instead of the true defects they are. 2. Cosmic-rays, glitches, and so on. - Concurrency often requires a fundamental change in design strategy. CHALLENGES What makes concurrent programming so difficult? Consider the following trivial class: ```java public class X { private int lastIdUsed; public int getNextId() { return ++lastIdUsed; } } ``` Let’s say we create an instance of X, set the lastIdUsed field to 42, and then share the instance between two threads. Now suppose that both of those threads call the method getNextId(); there are three possible outcomes: - Thread one gets the value 43, thread two gets the value 44, lastIdUsed is 44. - Thread one gets the value 44, thread two gets the value 43, lastIdUsed is 44. - Thread one gets the value 43, thread two gets the value 43, lastIdUsed is 43. The surprising third result3 occurs when the two threads step on each other. This happens because there are many possible paths that the two threads can take through that one line of Java code, and some of those paths generate incorrect results. How many different paths are there? To really answer that question, we need to understand what the Just-In-Time Compiler does with the generated byte-code, and understand what the Java memory model considers to be atomic. 3. See “Digging Deeper” on page 323. A quick answer, working with just the generated byte-code, is that there are 12,870 different possible execution paths4 for those two threads executing within the getNextId method. If the type of lastIdUsed is changed from int to long, the number of possible paths increases to 2,704,156. Of course most of those paths generate valid results. The problem is that some of them don’t. 4. See “Possible Paths of Execution” on page 321. CONCURRENCY DEFENSE PRINCIPLES What follows is a series of principles and techniques for defending your systems from the problems of concurrent code. Single Responsibility Principle The SRP5 states that a given method/class/component should have a single reason to change. Concurrency design is complex enough to be a reason to change in it’s own right and therefore deserves to be separated from the rest of the code. Unfortunately, it is all too common for concurrency implementation details to be embedded directly into other production code. Here are a few things to consider: 5. [PPP] - Concurrency-related code has its own life cycle of development, change, and tuning. - Concurrency-related code has its own challenges, which are different from and often more difficult than nonconcurrency-related code. - The number of ways in which miswritten concurrency-based code can fail makes it challenging enough without the added burden of surrounding application code. Recommendation: Keep your concurrency-related code separate from other code.6 6. See “Client/Server Example” on page 317. Corollary: Limit the Scope of Data As we saw, two threads modifying the same field of a shared object can interfere with each other, causing unexpected behavior. One solution is to use the synchronized keyword to protect a critical section in the code that uses the shared object. It is important to restrict the number of such critical sections. The more places shared data can get updated, the more likely: - You will forget to protect one or more of those places—effectively breaking all code that modifies that shared data. - There will be duplication of effort required to make sure everything is effectively guarded (violation of DRY7). 7. [PRAG]. - It will be difficult to determine the source of failures, which are already hard enough to find. Recommendation: Take data encapsulation to heart; severely limit the access of any data that may be shared. Corollary: Use Copies of Data A good way to avoid shared data is to avoid sharing the data in the first place. In some situations it is possible to copy objects and treat them as read-only. In other cases it might be possible to copy objects, collect results from multiple threads in these copies and then merge the results in a single thread. If there is an easy way to avoid sharing objects, the resulting code will be far less likely to cause problems. You might be concerned about the cost of all the extra object creation. It is worth experimenting to find out if this is in fact a problem. However, if using copies of objects allows the code to avoid synchronizing, the savings in avoiding the intrinsic lock will likely make up for the additional creation and garbage collection overhead. Corollary: Threads Should Be as Independent as Possible Consider writing your threaded code such that each thread exists in its own world, sharing no data with any other thread. Each thread processes one client request, with all of its required data coming from an unshared source and stored as local variables. This makes each of those threads behave as if it were the only thread in the world and there were no synchronization requirements. For example, classes that subclass from HttpServlet receive all of their information as parameters passed in to the doGet and doPost methods. This makes each Servlet act as if it has its own machine. So long as the code in the Servlet uses only local variables, there is no chance that the Servlet will cause synchronization problems. Of course, most applications using Servlets eventually run into shared resources such as database connections. Recommendation: Attempt to partition data into independent subsets than can be operated on by independent threads, possibly in different processors. KNOW YOUR LIBRARY Java 5 offers many improvements for concurrent development over previous versions. There are several things to consider when writing threaded code in Java 5: - Use the provided thread-safe collections. - Use the executor framework for executing unrelated tasks. - Use nonblocking solutions when possible. - Several library classes are not thread safe. Thread-Safe Collections When Java was young, Doug Lea wrote the seminal book8 Concurrent Programming in Java. Along with the book he developed several thread-safe collections, which later became part of the JDK in the java.util.concurrent package. The collections in that package are safe for multithreaded situations and they perform well. In fact, the ConcurrentHashMap implementation performs better than HashMap in nearly all situations. It also allows for simultaneous concurrent reads and writes, and it has methods supporting common composite operations that are otherwise not thread safe. If Java 5 is the deployment environment, start with ConcurrentHashMap. 8. [Lea99]. There are several other kinds of classes added to support advanced concurrency design. Here are a few examples: ![](figures/ch13/t0183-01.jpg) Recommendation: Review the classes available to you. In the case of Java, become familiar with java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks. KNOW YOUR EXECUTION MODELS There are several different ways to partition behavior in a concurrent application. To discuss them we need to understand some basic definitions. ![](figures/ch13/t0183-02.jpg) Given these definitions, we can now discuss the various execution models used in concurrent programming. Producer-Consumer9 9. http://en.wikipedia.org/wiki/Producer-consumer One or more producer threads create some work and place it in a buffer or queue. One or more consumer threads acquire that work from the queue and complete it. The queue between the producers and consumers is a bound resource. This means producers must wait for free space in the queue before writing and consumers must wait until there is something in the queue to consume. Coordination between the producers and consumers via the queue involves producers and consumers signaling each other. The producers write to the queue and signal that the queue is no longer empty. Consumers read from the queue and signal that the queue is no longer full. Both potentially wait to be notified when they can continue. Readers-Writers10 10. http://en.wikipedia.org/wiki/Readers-writers_problem When you have a shared resource that primarily serves as a source of information for readers, but which is occasionally updated by writers, throughput is an issue. Emphasizing throughput can cause starvation and the accumulation of stale information. Allowing updates can impact throughput. Coordinating readers so they do not read something a writer is updating and vice versa is a tough balancing act. Writers tend to block many readers for a long period of time, thus causing throughput issues. The challenge is to balance the needs of both readers and writers to satisfy correct operation, provide reasonable throughput and avoiding starvation. A simple strategy makes writers wait until there are no readers before allowing the writer to perform an update. If there are continuous readers, however, the writers will be starved. On the other hand, if there are frequent writers and they are given priority, throughput will suffer. Finding that balance and avoiding concurrent update issues is what the problem addresses. Dining Philosophers11 11. http://en.wikipedia.org/wiki/Dining_philosophers_problem Imagine a number of philosophers sitting around a circular table. A fork is placed to the left of each philosopher. There is a big bowl of spaghetti in the center of the table. The philosophers spend their time thinking unless they get hungry. Once hungry, they pick up the forks on either side of them and eat. A philosopher cannot eat unless he is holding two forks. If the philosopher to his right or left is already using one of the forks he needs, he must wait until that philosopher finishes eating and puts the forks back down. Once a philosopher eats, he puts both his forks back down on the table and waits until he is hungry again. Replace philosophers with threads and forks with resources and this problem is similar to many enterprise applications in which processes compete for resources. Unless carefully designed, systems that compete in this way can experience deadlock, livelock, throughput, and efficiency degradation. Most concurrent problems you will likely encounter will be some variation of these three problems. Study these algorithms and write solutions using them on your own so that when you come across concurrent problems, you’ll be more prepared to solve the problem. Recommendation: Learn these basic algorithms and understand their solutions. BEWARE DEPENDENCIES BETWEEN SYNCHRONIZED METHODS Dependencies between synchronized methods cause subtle bugs in concurrent code. The Java language has the notion of synchronized, which protects an individual method. However, if there is more than one synchronized method on the same shared class, then your system may be written incorrectly.12 12. See “Dependencies Between Methods Can Break Concurrent Code” on page 329. Recommendation: Avoid using more than one method on a shared object. There will be times when you must use more than one method on a shared object. When this is the case, there are three ways to make the code correct: - Client-Based Locking—Have the client lock the server before calling the first method and make sure the lock’s extent includes code calling the last method. - Server-Based Locking—Within the server create a method that locks the server, calls all the methods, and then unlocks. Have the client call the new method. - Adapted Server—create an intermediary that performs the locking. This is an example of server-based locking, where the original server cannot be changed. KEEP SYNCHRONIZED SECTIONS SMALL The synchronized keyword introduces a lock. All sections of code guarded by the same lock are guaranteed to have only one thread executing through them at any given time. Locks are expensive because they create delays and add overhead. So we don’t want to litter our code with synchronized statements. On the other hand, critical sections13 must be guarded. So we want to design our code with as few critical sections as possible. 13. A critical section is any section of code that must be protected from simultaneous use for the program to be correct. Some naive programmers try to achieve this by making their critical sections very large. However, extending synchronization beyond the minimal critical section increases contention and degrades performance.14 14. See “Increasing Throughput” on page 333. Recommendation: Keep your synchronized sections as small as possible. WRITING CORRECT SHUT-DOWN CODE IS HARD Writing a system that is meant to stay live and run forever is different from writing something that works for awhile and then shuts down gracefully. Graceful shutdown can be hard to get correct. Common problems involve deadlock,15 with threads waiting for a signal to continue that never comes. 15. See “Deadlock” on page 335. For example, imagine a system with a parent thread that spawns several child threads and then waits for them all to finish before it releases its resources and shuts down. What if one of the spawned threads is deadlocked? The parent will wait forever, and the system will never shut down. Or consider a similar system that has been instructed to shut down. The parent tells all the spawned children to abandon their tasks and finish. But what if two of the children were operating as a producer/consumer pair. Suppose the producer receives the signal from the parent and quickly shuts down. The consumer might have been expecting a message from the producer and be blocked in a state where it cannot receive the shutdown signal. It could get stuck waiting for the producer and never finish, preventing the parent from finishing as well. Situations like this are not at all uncommon. So if you must write concurrent code that involves shutting down gracefully, expect to spend much of your time getting the shutdown to happen correctly. Recommendation: Think about shut-down early and get it working early. It’s going to take longer than you expect. Review existing algorithms because this is probably harder than you think. TESTING THREADED CODE Proving that code is correct is impractical. Testing does not guarantee correctness. However, good testing can minimize risk. This is all true in a single-threaded solution. As soon as there are two or more threads using the same code and working with shared data, things get substantially more complex. Recommendation: Write tests that have the potential to expose problems and then run them frequently, with different programatic configurations and system configurations and load. If tests ever fail, track down the failure. Don’t ignore a failure just because the tests pass on a subsequent run. That is a whole lot to take into consideration. Here are a few more fine-grained recommendations: - Treat spurious failures as candidate threading issues. - Get your nonthreaded code working first. - Make your threaded code pluggable. - Make your threaded code tunable. - Run with more threads than processors. - Run on different platforms. - Instrument your code to try and force failures. Treat Spurious Failures as Candidate Threading Issues Threaded code causes things to fail that “simply cannot fail.” Most developers do not have an intuitive feel for how threading interacts with other code (authors included). Bugs in threaded code might exhibit their symptoms once in a thousand, or a million, executions. Attempts to repeat the systems can be frustratingly. This often leads developers to write off the failure as a cosmic ray, a hardware glitch, or some other kind of “one-off.” It is best to assume that one-offs do not exist. The longer these “one-offs” are ignored, the more code is built on top of a potentially faulty approach. Recommendation: Do not ignore system failures as one-offs. Get Your Nonthreaded Code Working First This may seem obvious, but it doesn’t hurt to reinforce it. Make sure code works outside of its use in threads. Generally, this means creating POJOs that are called by your threads. The POJOs are not thread aware, and can therefore be tested outside of the threaded environment. The more of your system you can place in such POJOs, the better. Recommendation: Do not try to chase down nonthreading bugs and threading bugs at the same time. Make sure your code works outside of threads. Make Your Threaded Code Pluggable Write the concurrency-supporting code such that it can be run in several configurations: - One thread, several threads, varied as it executes - Threaded code interacts with something that can be both real or a test double. - Execute with test doubles that run quickly, slowly, variable. - Configure tests so they can run for a number of iterations. Recommendation: Make your thread-based code especially pluggable so that you can run it in various configurations. Make Your Threaded Code Tunable Getting the right balance of threads typically requires trial an error. Early on, find ways to time the performance of your system under different configurations. Allow the number of threads to be easily tuned. Consider allowing it to change while the system is running. Consider allowing self-tuning based on throughput and system utilization. Run with More Threads Than Processors Things happen when the system switches between tasks. To encourage task swapping, run with more threads than processors or cores. The more frequently your tasks swap, the more likely you’ll encounter code that is missing a critical section or causes deadlock. Run on Different Platforms In the middle of 2007 we developed a course on concurrent programming. The course development ensued primarily under OS X. The class was presented using Windows XP running under a VM. Tests written to demonstrate failure conditions did not fail as frequently in an XP environment as they did running on OS X. In all cases the code under test was known to be incorrect. This just reinforced the fact that different operating systems have different threading policies, each of which impacts the code’s execution. Multithreaded code behaves differently in different environments.16 You should run your tests in every potential deployment environment. 16. Did you know that the threading model in Java does not guarantee preemptive threading? Modern OS’s support preemptive threading, so you get that “for free.” Even so, it not guaranteed by the JVM. Recommendation: Run your threaded code on all target platforms early and often. Instrument Your Code to Try and Force Failures It is normal for flaws in concurrent code to hide. Simple tests often don’t expose them. Indeed, they often hide during normal processing. They might show up once every few hours, or days, or weeks! The reason that threading bugs can be infrequent, sporadic, and hard to repeat, is that only a very few pathways out of the many thousands of possible pathways through a vulnerable section actually fail. So the probability that a failing pathway is taken can be star-tlingly low. This makes detection and debugging very difficult. How might you increase your chances of catching such rare occurrences? You can instrument your code and force it to run in different orderings by adding calls to methods like Object.wait(), Object.sleep(), Object.yield() and Object.priority(). Each of these methods can affect the order of execution, thereby increasing the odds of detecting a flaw. It’s better when broken code fails as early and as often as possible. There are two options for code instrumentation: - Hand-coded - Automated Hand-Coded You can insert calls to wait(), sleep(), yield(), and priority() in your code by hand. It might be just the thing to do when you’re testing a particularly thorny piece of code. Here is an example of doing just that: ```java public synchronized String nextUrlOrNull() { if(hasNext()) { String url = urlGenerator.next(); Thread.yield(); // inserted for testing. updateHasNext(); return url; } return null; } ``` The inserted call to yield() will change the execution pathways taken by the code and possibly cause the code to fail where it did not fail before. If the code does break, it was not because you added a call to yield().17 Rather, your code was broken and this simply made the failure evident. 17. This is not strictly the case. Since the JVM does not guarantee preemptive threading, a particular algorithm might always work on an OS that does not preempt threads. The reverse is also possible but for different reasons. There are many problems with this approach: - You have to manually find appropriate places to do this. - How do you know where to put the call and what kind of call to use? - Leaving such code in a production environment unnecessarily slows the code down. - It’s a shotgun approach. You may or may not find flaws. Indeed, the odds aren’t with you. What we need is a way to do this during testing but not in production. We also need to easily mix up configurations between different runs, which results in increased chances of finding errors in the aggregate. Clearly, if we divide our system up into POJOs that know nothing of threading and classes that control the threading, it will be easier to find appropriate places to instrument the code. Moreover, we could create many different test jigs that invoke the POJOs under different regimes of calls to sleep, yield, and so on. Automated You could use tools like an Aspect-Oriented Framework, CGLIB, or ASM to programmatically instrument your code. For example, you could use a class with a single method: ```java public class ThreadJigglePoint { public static void jiggle() { } } ``` You can add calls to this in various places within your code: ```java public synchronized String nextUrlOrNull() { if(hasNext()) { ThreadJiglePoint.jiggle(); String url = urlGenerator.next(); ThreadJiglePoint.jiggle(); updateHasNext(); ThreadJiglePoint.jiggle(); return url; } return null; } ``` Now you use a simple aspect that randomly selects among doing nothing, sleeping, or yielding. Or imagine that the ThreadJigglePoint class has two implementations. The first implements jiggle to do nothing and is used in production. The second generates a random number to choose between sleeping, yielding, or just falling through. If you run your tests a thousand times with random jiggling, you may root out some flaws. If the tests pass, at least you can say you’ve done due diligence. Though a bit simplistic, this could be a reasonable option in lieu of a more sophisticated tool. There is a tool called ConTest,18 developed by IBM that does something similar, but it does so with quite a bit more sophistication. 18. http://www.alphaworks.ibm.com/tech/contest The point is to jiggle the code so that threads run in different orderings at different times. The combination of well-written tests and jiggling can dramatically increase the chance finding errors. Recommendation: Use jiggling strategies to ferret out errors. CONCLUSION Concurrent code is difficult to get right. Code that is simple to follow can become nightmarish when multiple threads and shared data get into the mix. If you are faced with writing concurrent code, you need to write clean code with rigor or else face subtle and infrequent failures. First and foremost, follow the Single Responsibility Principle. Break your system into POJOs that separate thread-aware code from thread-ignorant code. Make sure when you are testing your thread-aware code, you are only testing it and nothing else. This suggests that your thread-aware code should be small and focused. Know the possible sources of concurrency issues: multiple threads operating on shared data, or using a common resource pool. Boundary cases, such as shutting down cleanly or finishing the iteration of a loop, can be especially thorny. Learn your library and know the fundamental algorithms. Understand how some of the features offered by the library support solving problems similar to the fundamental algorithms. Learn how to find regions of code that must be locked and lock them. Do not lock regions of code that do not need to be locked. Avoid calling one locked section from another. This requires a deep understanding of whether something is or is not shared. Keep the amount of shared objects and the scope of the sharing as narrow as possible. Change designs of the objects with shared data to accommodate clients rather than forcing clients to manage shared state. Issues will crop up. The ones that do not crop up early are often written off as a onetime occurrence. These so-called one-offs typically only happen under load or at seemingly random times. Therefore, you need to be able to run your thread-related code in many configurations on many platforms repeatedly and continuously. Testability, which comes naturally from following the Three Laws of TDD, implies some level of plug-ability, which offers the support necessary to run code in a wider range of configurations. You will greatly improve your chances of finding erroneous code if you take the time to instrument your code. You can either do so by hand or using some kind of automated technology. Invest in this early. You want to be running your thread-based code as long as possible before you put it into production. If you take a clean approach, your chances of getting it right increase drastically. ================================================ FILE: docs/ch14.md ================================================ # 第 14 章 Successive Refinement Case Study of a Command-Line Argument Parser ![](figures/ch14/14_1fig_martin.jpg) This chapter is a case study in successive refinement. You will see a module that started well but did not scale. Then you will see how the module was refactored and cleaned. Most of us have had to parse command-line arguments from time to time. If we don’t have a convenient utility, then we simply walk the array of strings that is passed into the main function. There are several good utilities available from various sources, but none of them do exactly what I want. So, of course, I decided to write my own. I call it: Args. Args is very simple to use. You simply construct the Args class with the input arguments and a format string, and then query the Args instance for the values of the arguments. Consider the following simple example: Listing 14-1 Simple use of Args ```java public static void main(String[] args) { try { Args arg = new Args(“l,p#,d*”, args); boolean logging = arg.getBoolean(’l’); int port = arg.getInt(’p’); String directory = arg.getString(’d’); executeApplication(logging, port, directory); } catch (ArgsException e) { System.out.printf(“Argument error: %s\n”, e.errorMessage()); } } ``` You can see how simple this is. We just create an instance of the Args class with two parameters. The first parameter is the format, or schema, string: “l,p#,d*.” It defines three command-line arguments. The first, -l, is a boolean argument. The second, -p, is an integer argument. The third, -d, is a string argument. The second parameter to the Args constructor is simply the array of command-line argument passed into main. If the constructor returns without throwing an ArgsException, then the incoming command-line was parsed, and the Args instance is ready to be queried. Methods like getBoolean, getInteger, and getString allow us to access the values of the arguments by their names. If there is a problem, either in the format string or in the command-line arguments themselves, an ArgsException will be thrown. A convenient description of what went wrong can be retrieved from the errorMessage method of the exception. ARGS IMPLEMENTATION Listing 14-2 is the implementation of the Args class. Please read it very carefully. I worked hard on the style and structure and hope it is worth emulating. Listing 14-2 Args.java ```java package com.objectmentor.utilities.args; import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*; import java.util.*; public class Args { private Map marshalers; private Set argsFound; private ListIterator currentArgument; public Args(String schema, String[] args) throws ArgsException { marshalers = new HashMap(); argsFound = new HashSet(); parseSchema(schema); parseArgumentStrings(Arrays.asList(args)); } private void parseSchema(String schema) throws ArgsException { for (String element : schema.split(“,”)) if (element.length() > 0) parseSchemaElement(element.trim()); } private void parseSchemaElement(String element) throws ArgsException { char elementId = element.charAt(0); String elementTail = element.substring(1); validateSchemaElementId(elementId); if (elementTail.length() == 0) marshalers.put(elementId, new BooleanArgumentMarshaler()); else if (elementTail.equals(“*”)) marshalers.put(elementId, new StringArgumentMarshaler()); else if (elementTail.equals(“#”)) marshalers.put(elementId, new IntegerArgumentMarshaler()); else if (elementTail.equals(“##”)) marshalers.put(elementId, new DoubleArgumentMarshaler()); else if (elementTail.equals(“[*]”)) marshalers.put(elementId, new StringArrayArgumentMarshaler()); else throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail); } private void validateSchemaElementId(char elementId) throws ArgsException { if (!Character.isLetter(elementId)) throw new ArgsException(INVALID_ARGUMENT_NAME, elementId, null); } private void parseArgumentStrings(List argsList) throws ArgsException { for (currentArgument = argsList.listIterator(); currentArgument.hasNext();) { String argString = currentArgument.next(); if (argString.startsWith(“-”)) { parseArgumentCharacters(argString.substring(1)); } else { currentArgument.previous(); break; } } } private void parseArgumentCharacters(String argChars) throws ArgsException { for (int i = 0; i < argChars.length(); i++) parseArgumentCharacter(argChars.charAt(i)); } private void parseArgumentCharacter(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m == null) { throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null); } else { argsFound.add(argChar); try { m.set(currentArgument); } catch (ArgsException e) { e.setErrorArgumentId(argChar); throw e; } } } public boolean has(char arg) { return argsFound.contains(arg); } public int nextArgument() { return currentArgument.nextIndex(); } public boolean getBoolean(char arg) { return BooleanArgumentMarshaler.getValue(marshalers.get(arg)); } public String getString(char arg) { return StringArgumentMarshaler.getValue(marshalers.get(arg)); } public int getInt(char arg) { return IntegerArgumentMarshaler.getValue(marshalers.get(arg)); } public double getDouble(char arg) { return DoubleArgumentMarshaler.getValue(marshalers.get(arg)); } public String[] getStringArray(char arg) { return StringArrayArgumentMarshaler.getValue(marshalers.get(arg)); } } ``` Notice that you can read this code from the top to the bottom without a lot of jumping around or looking ahead. The one thing you may have had to look ahead for is the definition of ArgumentMarshaler, which I left out intentionally. Having read this code carefully, you should understand what the ArgumentMarshaler interface is and what its derivatives do. I’ll show a few of them to you now (Listing 14-3 through Listing 14-6). Listing 14-3 ArgumentMarshaler.java ```java public interface ArgumentMarshaler { void set(Iterator currentArgument) throws ArgsException; } ``` Listing 14-4 BooleanArgumentMarshaler.java ```java public class BooleanArgumentMarshaler implements ArgumentMarshaler { private boolean booleanValue = false; public void set(Iterator currentArgument) throws ArgsException { booleanValue = true; } public static boolean getValue(ArgumentMarshaler am) { if (am != null && am instanceof BooleanArgumentMarshaler) return ((BooleanArgumentMarshaler) am).booleanValue; else return false; } } ``` Listing 14-5 StringArgumentMarshaler.java ```java import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*; public class StringArgumentMarshaler implements ArgumentMarshaler { private String stringValue = public void set(Iterator currentArgument) throws ArgsException { try { stringValue = currentArgument.next(); } catch (NoSuchElementException e) { throw new ArgsException(MISSING_STRING); } } public static String getValue(ArgumentMarshaler am) { if (am != null && am instanceof StringArgumentMarshaler) return ((StringArgumentMarshaler) am).stringValue; else return ””; } } ``` The other ArgumentMarshaler derivatives simply replicate this pattern for doubles and String arrays and would serve to clutter this chapter. I’ll leave them to you as an exercise. One other bit of information might be troubling you: the definition of the error code constants. They are in the ArgsException class (Listing 14-7). Listing 14-6 IntegerArgumentMarshaler.java ```java import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*; public class IntegerArgumentMarshaler implements ArgumentMarshaler { private int intValue = 0; public void set(Iterator currentArgument) throws ArgsException { String parameter = null; try { parameter = currentArgument.next(); intValue = Integer.parseInt(parameter); } catch (NoSuchElementException e) { throw new ArgsException(MISSING_INTEGER); } catch (NumberFormatException e) { throw new ArgsException(INVALID_INTEGER, parameter); } } public static int getValue(ArgumentMarshaler am) { if (am != null && am instanceof IntegerArgumentMarshaler) return ((IntegerArgumentMarshaler) am).intValue; else return 0; } } ``` Listing 14-7 ArgsException.java ```java import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*; public class ArgsException extends Exception { private char errorArgumentId = ’\0’; private String errorParameter = null; private ErrorCode errorCode = OK; public ArgsException() {} public ArgsException(String message) {super(message);} public ArgsException(ErrorCode errorCode) { this.errorCode = errorCode; } public ArgsException(ErrorCode errorCode, String errorParameter) { this.errorCode = errorCode; this.errorParameter = errorParameter; } public ArgsException(ErrorCode errorCode, char errorArgumentId, String errorParameter) { this.errorCode = errorCode; this.errorParameter = errorParameter; this.errorArgumentId = errorArgumentId; } public char getErrorArgumentId() { return errorArgumentId; } public void setErrorArgumentId(char errorArgumentId) { this.errorArgumentId = errorArgumentId; } public String getErrorParameter() { return errorParameter; } public void setErrorParameter(String errorParameter) { this.errorParameter = errorParameter; } public ErrorCode getErrorCode() { return errorCode; } public void setErrorCode(ErrorCode errorCode) { this.errorCode = errorCode; } public String errorMessage() { switch (errorCode) { case OK: return “TILT: Should not get here.”; case UNEXPECTED_ARGUMENT: return String.format(“Argument -%c unexpected.”, errorArgumentId); case MISSING_STRING: return String.format(“Could not find string parameter for -%c.”, errorArgumentId); case INVALID_INTEGER: return String.format(“Argument -%c expects an integer but was ’%s’.”, errorArgumentId, errorParameter); case MISSING_INTEGER: return String.format(“Could not find integer parameter for -%c.”, errorArgumentId); case INVALID_DOUBLE: return String.format(“Argument -%c expects a double but was ’%s’.”, errorArgumentId, errorParameter); case MISSING_DOUBLE: return String.format(“Could not find double parameter for -%c.”, errorArgumentId); case INVALID_ARGUMENT_NAME: return String.format(“’%c” is not a valid argument name.”, errorArgumentId); case INVALID_ARGUMENT_FORMAT: return String.format(“’%s” is not a valid argument format.”, errorParameter); } return ””; } public enum ErrorCode { OK, INVALID_ARGUMENT_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, MISSING_DOUBLE, INVALID_DOUBLE} } ``` It’s remarkable how much code is required to flesh out the details of this simple concept. One of the reasons for this is that we are using a particularly wordy language. Java, being a statically typed language, requires a lot of words in order to satisfy the type system. In a language like Ruby, Python, or Smalltalk, this program is much smaller.1 1. I recently rewrote this module in Ruby. It was 1/7th the size and had a subtly better structure. Please read the code over one more time. Pay special attention to the way things are named, the size of the functions, and the formatting of the code. If you are an experienced programmer, you may have some quibbles here and there with various parts of the style or structure. Overall, however, I hope you conclude that this program is nicely written and has a clean structure. For example, it should be obvious how you would add a new argument type, such as a date argument or a complex number argument, and that such an addition would require a trivial amount of effort. In short, it would simply require a new derivative of Argument-Marshaler, a new getXXX function, and a new case statement in the parseSchemaElement function. There would also probably be a new ArgsException.ErrorCode and a new error message. How Did I Do This? Let me set your mind at rest. I did not simply write this program from beginning to end in its current form. More importantly, I am not expecting you to be able to write clean and elegant programs in one pass. If we have learned anything over the last couple of decades, it is that programming is a craft more than it is a science. To write clean code, you must first write dirty code and then clean it. This should not be a surprise to you. We learned this truth in grade school when our teachers tried (usually in vain) to get us to write rough drafts of our compositions. The process, they told us, was that we should write a rough draft, then a second draft, then several subsequent drafts until we had our final version. Writing clean compositions, they tried to tell us, is a matter of successive refinement. Most freshman programmers (like most grade-schoolers) don’t follow this advice particularly well. They believe that the primary goal is to get the program working. Once it’s “working,” they move on to the next task, leaving the “working” program in whatever state they finally got it to “work.” Most seasoned programmers know that this is professional suicide. ARGS: THE ROUGH DRAFT Listing 14-8 shows an earlier version of the Args class. It “works.” And it’s messy. Listing 14-8 Args.java (first draft) ```java import java.text.ParseException; import java.util.*; public class Args { private String schema; private String[] args; private boolean valid = true; private Set unexpectedArguments = new TreeSet(); private Map booleanArgs = new HashMap (); private Map stringArgs = new HashMap (); private Map intArgs = new HashMap(); private Set argsFound = new HashSet(); private int currentArgument; private char errorArgumentId = ’\0’; private String errorParameter = “TILT”; private ErrorCode errorCode = ErrorCode.OK; private enum ErrorCode { OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT} public Args(String schema, String[] args) throws ParseException { this.schema = schema; this.args = args; valid = parse(); } private boolean parse() throws ParseException { if (schema.length() == 0 && args.length == 0) return true; parseSchema(); try { parseArguments(); } catch (ArgsException e) { } return valid; } private boolean parseSchema() throws ParseException { for (String element : schema.split(“,”)) { if (element.length() > 0) { String trimmedElement = element.trim(); parseSchemaElement(trimmedElement); } } return true; } private void parseSchemaElement(String element) throws ParseException { char elementId = element.charAt(0); String elementTail = element.substring(1); validateSchemaElementId(elementId); if (isBooleanSchemaElement(elementTail)) parseBooleanSchemaElement(elementId); else if (isStringSchemaElement(elementTail)) parseStringSchemaElement(elementId); else if (isIntegerSchemaElement(elementTail)) { parseIntegerSchemaElement(elementId); } else { throw new ParseException( String.format(“Argument: %c has invalid format: %s.”, elementId, elementTail), 0); } } private void validateSchemaElementId(char elementId) throws ParseException { if (!Character.isLetter(elementId)) { throw new ParseException( “Bad character:” + elementId + “in Args format: ” + schema, 0); } } private void parseBooleanSchemaElement(char elementId) { booleanArgs.put(elementId, false); } private void parseIntegerSchemaElement(char elementId) { intArgs.put(elementId, 0); } private void parseStringSchemaElement(char elementId) { stringArgs.put(elementId, ””); } private boolean isStringSchemaElement(String elementTail) { return elementTail.equals(”*”); } private boolean isBooleanSchemaElement(String elementTail) { return elementTail.length() == 0; } private boolean isIntegerSchemaElement(String elementTail) { return elementTail.equals(”#”); } private boolean parseArguments() throws ArgsException { for (currentArgument = 0; currentArgument < args.length; currentArgument++) { String arg = args[currentArgument]; parseArgument(arg); } return true; } private void parseArgument(String arg) throws ArgsException { if (arg.startsWith(”-”)) parseElements(arg); } private void parseElements(String arg) throws ArgsException { for (int i = 1; i < arg.length(); i++) parseElement(arg.charAt(i)); } private void parseElement(char argChar) throws ArgsException { if (setArgument(argChar)) argsFound.add(argChar); else { unexpectedArguments.add(argChar); errorCode = ErrorCode.UNEXPECTED_ARGUMENT; valid = false; } } private boolean setArgument(char argChar) throws ArgsException { if (isBooleanArg(argChar)) setBooleanArg(argChar, true); else if (isStringArg(argChar)) setStringArg(argChar); else if (isIntArg(argChar)) setIntArg(argChar); else return false; return true; } private boolean isIntArg(char argChar) {return intArgs.containsKey(argChar);} private void setIntArg(char argChar) throws ArgsException { currentArgument++; String parameter = null; try { parameter = args[currentArgument]; intArgs.put(argChar, new Integer(parameter)); } catch (ArrayIndexOutOfBoundsException e) { valid = false; errorArgumentId = argChar; errorCode = ErrorCode.MISSING_INTEGER; throw new ArgsException(); } catch (NumberFormatException e) { valid = false; errorArgumentId = argChar; errorParameter = parameter; errorCode = ErrorCode.INVALID_INTEGER; throw new ArgsException(); } } private void setStringArg(char argChar) throws ArgsException { currentArgument++; try { stringArgs.put(argChar, args[currentArgument]); } catch (ArrayIndexOutOfBoundsException e) { valid = false; errorArgumentId = argChar; errorCode = ErrorCode.MISSING_STRING; throw new ArgsException(); } } private boolean isStringArg(char argChar) { return stringArgs.containsKey(argChar); } private void setBooleanArg(char argChar, boolean value) { booleanArgs.put(argChar, value); } private boolean isBooleanArg(char argChar) { return booleanArgs.containsKey(argChar); } public int cardinality() { return argsFound.size(); } public String usage() { if (schema.length() > 0) return “-[” + schema + “]”; else return ””; } public String errorMessage() throws Exception { switch (errorCode) { case OK: throw new Exception(“TILT: Should not get here.”); case UNEXPECTED_ARGUMENT: return unexpectedArgumentMessage(); case MISSING_STRING: return String.format(“Could not find string parameter for -%c.”, errorArgumentId); case INVALID_INTEGER: return String.format(“Argument -%c expects an integer but was ’%s’.”, errorArgumentId, errorParameter); case MISSING_INTEGER: return String.format(“Could not find integer parameter for -%c.”, errorArgumentId); } return ””; } private String unexpectedArgumentMessage() { StringBuffer message = new StringBuffer(“Argument(s) -”); for (char c : unexpectedArguments) { message.append(c); } message.append(“ unexpected.”); return message.toString(); } private boolean falseIfNull(Boolean b) { return b != null && b; } private int zeroIfNull(Integer i) { return i == null ? 0 : i; } private String blankIfNull(String s) { return s == null ? ”” : s; } public String getString(char arg) { return blankIfNull(stringArgs.get(arg)); } public int getInt(char arg) { return zeroIfNull(intArgs.get(arg)); } public boolean getBoolean(char arg) { return falseIfNull(booleanArgs.get(arg)); } public boolean has(char arg) { return argsFound.contains(arg); } public boolean isValid() { return valid; } private class ArgsException extends Exception { } } ``` I hope your initial reaction to this mass of code is “I’m certainly glad he didn’t leave it like that!” If you feel like this, then remember that’s how other people are going to feel about code that you leave in rough-draft form. Actually “rough draft” is probably the kindest thing you can say about this code. It’s clearly a work in progress. The sheer number of instance variables is daunting. The odd strings like “TILT,” the HashSets and TreeSets, and the try-catch-catch blocks all add up to a festering pile. I had not wanted to write a festering pile. Indeed, I was trying to keep things reasonably well organized. You can probably tell that from my choice of function and variable names and the fact that there is a crude structure to the program. But, clearly, I had let the problem get away from me. The mess built gradually. Earlier versions had not been nearly so nasty. For example, Listing 14-9 shows an earlier version in which only Boolean arguments were working. Listing 14-9 Args.java (Boolean only) ```java package com.objectmentor.utilities.getopts; import java.util.*; public class Args { private String schema; private String[] args; private boolean valid; private Set unexpectedArguments = new TreeSet(); private Map booleanArgs = new HashMap(); private int numberOfArguments = 0; public Args(String schema, String[] args) { this.schema = schema; this.args = args; valid = parse(); } public boolean isValid() { return valid; } private boolean parse() { if (schema.length() == 0 && args.length == 0) return true; parseSchema(); parseArguments(); return unexpectedArguments.size() == 0; } private boolean parseSchema() { for (String element : schema.split(”,”)) { parseSchemaElement(element); } return true; } private void parseSchemaElement(String element) { if (element.length() == 1) { parseBooleanSchemaElement(element); } } private void parseBooleanSchemaElement(String element) { char c = element.charAt(0); if (Character.isLetter(c)) { booleanArgs.put(c, false); } } private boolean parseArguments() { for (String arg : args) parseArgument(arg); return true; } private void parseArgument(String arg) { if (arg.startsWith(”-”)) parseElements(arg); } private void parseElements(String arg) { for (int i = 1; i < arg.length(); i++) parseElement(arg.charAt(i)); } private void parseElement(char argChar) { if (isBoolean(argChar)) { numberOfArguments++; setBooleanArg(argChar, true); } else unexpectedArguments.add(argChar); } private void setBooleanArg(char argChar, boolean value) { booleanArgs.put(argChar, value); } private boolean isBoolean(char argChar) { return booleanArgs.containsKey(argChar); } public int cardinality() { return numberOfArguments; } public String usage() { if (schema.length() > 0) return ”-[“+schema+”]”; else return ””; } public String errorMessage() { if (unexpectedArguments.size() > 0) { return unexpectedArgumentMessage(); } else return ””; } private String unexpectedArgumentMessage() { StringBuffer message = new StringBuffer(“Argument(s) -”); for (char c : unexpectedArguments) { message.append(c); } message.append(“ unexpected.”); return message.toString(); } public boolean getBoolean(char arg) { return booleanArgs.get(arg); } } ``` Although you can find plenty to complain about in this code, it’s really not that bad. It’s compact and simple and easy to understand. However, within this code it is easy to see the seeds of the later festering pile. It’s quite clear how this grew into the latter mess. Notice that the latter mess has only two more argument types than this: String and integer. The addition of just two more argument types had a massively negative impact on the code. It converted it from something that would have been reasonably maintainable into something that I would expect to become riddled with bugs and warts. I added the two argument types incrementally. First, I added the String argument, which yielded this: Listing 14-10 Args.java (Boolean and String) ```java package com.objectmentor.utilities.getopts; import java.text.ParseException; import java.util.*; public class Args { private String schema; private String[] args; private boolean valid = true; private Set unexpectedArguments = new TreeSet(); private Map booleanArgs = new HashMap(); private Map stringArgs = new HashMap(); private Set argsFound = new HashSet(); private int currentArgument; private char errorArgument = '\0'; enum ErrorCode { OK, MISSING_STRING} private ErrorCode errorCode = ErrorCode.OK; public Args(String schema, String[] args) throws ParseException { this.schema = schema; this.args = args; valid = parse(); } private boolean parse() throws ParseException { if (schema.length() == 0 && args.length == 0) return true; parseSchema(); parseArguments(); return valid; } private boolean parseSchema() throws ParseException { for (String element : schema.split(“,”)) { if (element.length() > 0) { String trimmedElement = element.trim(); parseSchemaElement(trimmedElement); } } return true; } private void parseSchemaElement(String element) throws ParseException { char elementId = element.charAt(0); String elementTail = element.substring(1); validateSchemaElementId(elementId); if (isBooleanSchemaElement(elementTail)) parseBooleanSchemaElement(elementId); else if (isStringSchemaElement(elementTail)) parseStringSchemaElement(elementId); } private void validateSchemaElementId(char elementId) throws ParseException { if (!Character.isLetter(elementId)) { throw new ParseException( “Bad character:” + elementId + “in Args format: ” + schema, 0); } } private void parseStringSchemaElement(char elementId) { stringArgs.put(elementId, “ ”); } private boolean isStringSchemaElement(String elementTail) { return elementTail.equals(“*”); } private boolean isBooleanSchemaElement(String elementTail) { return elementTail.length() == 0; } private void parseBooleanSchemaElement(char elementId) { booleanArgs.put(elementId, false); } private boolean parseArguments() { for (currentArgument = 0; currentArgument < args.length; currentArgument++) { String arg = args[currentArgument]; parseArgument(arg); } return true; } private void parseArgument(String arg) { if (arg.startsWith(“-”)) parseElements(arg); } private void parseElements(String arg) { for (int i = 1; i < arg.length(); i++) parseElement(arg.charAt(i)); } private void parseElement(char argChar) { if (setArgument(argChar)) argsFound.add(argChar); else { unexpectedArguments.add(argChar); valid = false; } } private boolean setArgument(char argChar) { boolean set = true; if (isBoolean(argChar)) setBooleanArg(argChar, true); else if (isString(argChar)) setStringArg(argChar, “ ”); else set = false; return set; } private void setStringArg(char argChar, String s) { currentArgument++; try { stringArgs.put(argChar, args[currentArgument]); } catch (ArrayIndexOutOfBoundsException e) { valid = false; errorArgument = argChar; errorCode = ErrorCode.MISSING_STRING; } } private boolean isString(char argChar) { return stringArgs.containsKey(argChar); } private void setBooleanArg(char argChar, boolean value) { booleanArgs.put(argChar, value); } private boolean isBoolean(char argChar) { return booleanArgs.containsKey(argChar); } public int cardinality() { return argsFound.size(); } public String usage() { if (schema.length() > 0) return “-[“ + schema + ”]”; else return “ ”; } public String errorMessage() throws Exception { if (unexpectedArguments.size() > 0) { return unexpectedArgumentMessage(); } else switch (errorCode) { case MISSING_STRING: return String.format(“Could not find string parameter for -%c.”, errorArgument); case OK: throw new Exception(“TILT: Should not get here.”); } return “ ”; } private String unexpectedArgumentMessage() { StringBuffer message = new StringBuffer(“Argument(s) -”); for (char c : unexpectedArguments) { message.append(c); } message.append(“ unexpected.”); return message.toString(); } public boolean getBoolean(char arg) { return falseIfNull(booleanArgs.get(arg)); } private boolean falseIfNull(Boolean b) { return b == null ? false : b; } public String getString(char arg) { return blankIfNull(stringArgs.get(arg)); } private String blankIfNull(String s) { return s == null ? “ ” : s; } public boolean has(char arg) { return argsFound.contains(arg); } public boolean isValid() { return valid; } } ``` You can see that this is starting to get out of hand. It’s still not horrible, but the mess is certainly starting to grow. It’s a pile, but it’s not festering quite yet. It took the addition of the integer argument type to get this pile really fermenting and festering. So I Stopped I had at least two more argument types to add, and I could tell that they would make things much worse. If I bulldozed my way forward, I could probably get them to work, but I’d leave behind a mess that was too large to fix. If the structure of this code was ever going to be maintainable, now was the time to fix it. So I stopped adding features and started refactoring. Having just added the String and integer arguments, I knew that each argument type required new code in three major places. First, each argument type required some way to parse its schema element in order to select the HashMap for that type. Next, each argument type needed to be parsed in the command-line strings and converted to its true type. Finally, each argument type needed a getXXX method so that it could be returned to the caller as its true type. Many different types, all with similar methods—that sounds like a class to me. And so the ArgumentMarshaler concept was born. On Incrementalism One of the best ways to ruin a program is to make massive changes to its structure in the name of improvement. Some programs never recover from such “improvements.” The problem is that it’s very hard to get the program working the same way it worked before the “improvement.” ARGS: THE ROUGH DRAFT To avoid this, I use the discipline of Test-Driven Development (TDD). One of the central doctrines of this approach is to keep the system running at all times. In other words, using TDD, I am not allowed to make a change to the system that breaks that system. Every change I make must keep the system working as it worked before. To achieve this, I need a suite of automated tests that I can run on a whim and that verifies that the behavior of the system is unchanged. For the Args class I had created a suite of unit and acceptance tests while I was building the festering pile. The unit tests were written in Java and administered by JUnit. The acceptance tests were written as wiki pages in FitNesse. I could run these tests any time I wanted, and if they passed, I was confident that the system was working as I specified. So I proceeded to make a large number of very tiny changes. Each change moved the structure of the system toward the ArgumentMarshaler concept. And yet each change kept the system working. The first change I made was to add the skeleton of the ArgumentMarshaller to the end of the festering pile (Listing 14-11). Listing 14-11 ArgumentMarshaller appended to Args.java ```java private class ArgumentMarshaler } private boolean booleanValue = false; public void setBoolean(boolean value) { booleanValue = value; } public boolean getBoolean() {return booleanValue;} } private class BooleanArgumentMarshaler extends ArgumentMarshaler { } private class StringArgumentMarshaler extends ArgumentMarshaler { } private class IntegerArgumentMarshaler extends ArgumentMarshaler { } } ``` Clearly, this wasn’t going to break anything. So then I made the simplest modification I could, one that would break as little as possible. I changed the HashMap for the Boolean arguments to take an ArgumentMarshaler. ```java private Map booleanArgs = new HashMap(); This broke a few statements, which I quickly fixed. ```java … private void parseBooleanSchemaElement(char elementId) { booleanArgs.put(elementId, new BooleanArgumentMarshaler()); } .. private void setBooleanArg(char argChar, boolean value) { booleanArgs.get(argChar).setBoolean(value); } … public boolean getBoolean(char arg) { return falseIfNull(booleanArgs.get(arg).getBoolean()); } ``` Notice how these changes are in exactly the areas that I mentioned before: the parse, set, and get for the argument type. Unfortunately, small as this change was, some of the tests started failing. If you look carefully at getBoolean, you’ll see that if you call it with 'y,' but there is no y argument, then booleanArgs.get('y') will return null, and the function will throw a NullPointerException. The falseIfNull function had been used to protect against this, but the change I made caused that function to become irrelevant. Incrementalism demanded that I get this working quickly before making any other changes. Indeed, the fix was not too difficult. I just had to move the check for null. It was no longer the boolean being null that I needed to check; it was the ArgumentMarshaller. First, I removed the falseIfNull call in the getBoolean function. It was useless now, so I also eliminated the function itself. The tests still failed in the same way, so I was confident that I hadn’t introduced any new errors. ```java public boolean getBoolean(char arg) { return booleanArgs.get(arg).getBoolean(); } ``` Next, I split the function into two lines and put the ArgumentMarshaller into its own variable named argumentMarshaller. I didn’t care for the long variable name; it was badly redundant and cluttered up the function. So I shortened it to am [N5]. ```java public boolean getBoolean(char arg) { Args.ArgumentMarshaler am = booleanArgs.get(arg); return am.getBoolean(); } ``` And then I put in the null detection logic. ```java public boolean getBoolean(char arg) { Args.ArgumentMarshaler am = booleanArgs.get(arg); return am != null && am.getBoolean(); } ``` STRING ARGUMENTS Addin_g String arguments was very similar to adding boolean arguments. I had to change the HashMap and get the parse, set, and get functions working. There shouldn’t be any surprises in what follows except, perhaps, that I seem to be putting all the marshalling implementation in the ArgumentMarshaller base class instead of distributing it to the derivatives. ```java private Map stringArgs = new HashMap(); … private void parseStringSchemaElement(char elementId) { stringArgs.put(elementId, new StringArgumentMarshaler()); } … private void setStringArg(char argChar) throws ArgsException { currentArgument++; try { stringArgs.get(argChar).setString(args[currentArgument]); } catch (ArrayIndexOutOfBoundsException e) { valid = false; errorArgumentId = argChar; errorCode = ErrorCode.MISSING_STRING; throw new ArgsException(); } } … public String getString(char arg) { Args.ArgumentMarshaler am = stringArgs.get(arg); return am == null ? “ ” : am.getString(); } … private class ArgumentMarshaler { private boolean booleanValue = false; private String stringValue; public void setBoolean(boolean value) { booleanValue = value; } public boolean getBoolean() { return booleanValue; } public void setString(String s) { stringValue = s; } public String getString() { return stringValue == null ? “ ” : stringValue; } } ``` Again, these changes were made one at a time and in such a way that the tests kept running, if not passing. When a test broke, I made sure to get it passing again before continuing with the next change. By now you should be able to see my intent. Once I get all the current marshalling behavior into the ArgumentMarshaler base class, I’m going to start pushing that behavior down into the derivatives. This will allow me to keep everything running while I gradually change the shape of this program. The obvious next step was to move the int argument functionality into the ArgumentMarshaler. Again, there weren’t any surprises. ```java private Map intArgs = new HashMap(); … private void parseIntegerSchemaElement(char elementId) { intArgs.put(elementId, new IntegerArgumentMarshaler()); } … private void setIntArg(char argChar) throws ArgsException { currentArgument++; String parameter = null; try { parameter = args[currentArgument]; intArgs.get(argChar).setInteger(Integer.parseInt(parameter)); } catch (ArrayIndexOutOfBoundsException e) { valid = false; errorArgumentId = argChar; errorCode = ErrorCode.MISSING_INTEGER; throw new ArgsException(); } catch (NumberFormatException e) { valid = false; errorArgumentId = argChar; errorParameter = parameter; errorCode = ErrorCode.INVALID_INTEGER; throw new ArgsException(); } } … public int getInt(char arg) { Args.ArgumentMarshaler am = intArgs.get(arg); return am == null ? 0 : am.getInteger(); } … private class ArgumentMarshaler { private boolean booleanValue = false; private String stringValue; private int integerValue; public void setBoolean(boolean value) { booleanValue = value; } public boolean getBoolean() { return booleanValue; } public void setString(String s) { stringValue = s; } public String getString() { return stringValue == null ? “ ”: stringValue; } public void setInteger(int i) { integerValue = i; } public int getInteger() { return integerValue; } } ``` With all the marshalling moved to the ArgumentMarshaler, I started pushing functionality into the derivatives. The first step was to move the setBoolean function into the BooleanArgumentMarshaller and make sure it got called correctly. So I created an abstract set method. ```java private abstract class ArgumentMarshaler { protected boolean booleanValue = false; private String stringValue; private int integerValue; public void setBoolean(boolean value) { booleanValue = value; } public boolean getBoolean() { return booleanValue; } public void setString(String s) { stringValue = s; } public String getString() { return stringValue == null ? “ ” : stringValue; } public void setInteger(int i) { integerValue = i; } public int getInteger() { return integerValue; } public abstract void set(String s); } ``` Then I implemented the set method in BooleanArgumentMarshaller. ```java private class BooleanArgumentMarshaler extends ArgumentMarshaler { public void set(String s) { booleanValue = true; } } ``` And finally I replaced the call to setBoolean with a call to set. ```java private void setBooleanArg(char argChar, boolean value) { booleanArgs.get(argChar) .set(“true”); } ``` The tests all still passed. Because this change caused set to be deployed to the Boolean-ArgumentMarshaler, I removed the setBoolean method from the ArgumentMarshaler base class. Notice that the abstract set function takes a String argument, but the implementation in the BooleanArgumentMarshaller does not use it. I put that argument in there because I knew that the StringArgumentMarshaller and IntegerArgumentMarshaller would use it. Next, I wanted to deploy the get method into BooleanArgumentMarshaler. Deploying get functions is always ugly because the return type has to be Object, and in this case needs to be cast to a Boolean. ```java public boolean getBoolean(char arg) { Args.ArgumentMarshaler am = booleanArgs.get(arg); return am != null && (Boolean)am.get(); } ``` Just to get this to compile, I added the get function to the ArgumentMarshaler. ```java private abstract class ArgumentMarshaler { … public Object get() { return null; } } ``` This compiled and obviously failed the tests. Getting the tests working again was simply a matter of making get abstract and implementing it in BooleanAgumentMarshaler. ```java private abstract class ArgumentMarshaler { protected boolean booleanValue = false; … public abstract Object get(); } private class BooleanArgumentMarshaler extends ArgumentMarshaler { public void set(String s) { booleanValue = true; } public Object get() { return booleanValue; } } ``` Once again the tests passed. So both get and set deploy to the BooleanArgumentMarshaler! This allowed me to remove the old getBoolean function from ArgumentMarshaler, move the protected booleanValue variable down to BooleanArgumentMarshaler, and make it private. I did the same pattern of changes for Strings. I deployed both set and get, deleted the unused functions, and moved the variables. ```java private void setStringArg(char argChar) throws ArgsException { currentArgument++; try { stringArgs.get(argChar).set(args[currentArgument]); } catch (ArrayIndexOutOfBoundsException e) { valid = false; errorArgumentId = argChar; errorCode = ErrorCode.MISSING_STRING; throw new ArgsException(); } } … public String getString(char arg) { Args.ArgumentMarshaler am = stringArgs.get(arg); return am == null ? “ ” : (String) am.get(); } … private abstract class ArgumentMarshaler { private int integerValue; public void setInteger(int i) { integerValue = i; } public int getInteger() { return integerValue; } public abstract void set(String s); public abstract Object get(); } private class BooleanArgumentMarshaler extends ArgumentMarshaler { private boolean booleanValue = false; public void set(String s) { booleanValue = true; } public Object get() { return booleanValue; } } private class StringArgumentMarshaler extends ArgumentMarshaler { private String stringValue = “ ”; public void set(String s) { stringValue = s; } public Object get() { return stringValue; } } private class IntegerArgumentMarshaler extends ArgumentMarshaler { public void set(String s) { } public Object get() { return null; } } } ``` Finally, I repeated the process for integers. This was just a little more complicated because integers needed to be parsed, and the parse operation can throw an exception. But the result is better because the whole concept of NumberFormatException got buried in the IntegerArgumentMarshaler. ```java private boolean isIntArg(char argChar) {return intArgs.containsKey(argChar);} private void setIntArg(char argChar) throws ArgsException { currentArgument++; String parameter = null; try { parameter = args[currentArgument]; intArgs.get(argChar).set(parameter); } catch (ArrayIndexOutOfBoundsException e) { valid = false; errorArgumentId = argChar; errorCode = ErrorCode.MISSING_INTEGER; throw new ArgsException(); } catch (ArgsException e) { valid = false; errorArgumentId = argChar; errorParameter = parameter; errorCode = ErrorCode.INVALID_INTEGER; throw e; } } … private void setBooleanArg(char argChar) { try { booleanArgs.get(argChar).set(“true”); } catch (ArgsException e) { } } … public int getInt(char arg) { Args.ArgumentMarshaler am = intArgs.get(arg); return am == null ? 0 : (Integer) am.get(); } … private abstract class ArgumentMarshaler { public abstract void set(String s) throws ArgsException; public abstract Object get(); } … private class IntegerArgumentMarshaler extends ArgumentMarshaler { private int intValue = 0; public void set(String s) throws ArgsException { try { intValue = Integer.parseInt(s); } catch (NumberFormatException e) { throw new ArgsException(); } } public Object get() { return intValue; } } ``` Of course, the tests continued to pass. Next, I got rid of the three different maps up at the top of the algorithm. This made the whole system much more generic. However, I couldn’t get rid of them just by deleting them because that would break the system. Instead, I added a new Map for the ArgumentMarshaler and then one by one changed the methods to use it instead of the three original maps. ```java public class Args { … private Map booleanArgs = new HashMap(); private Map stringArgs = new HashMap(); private Map intArgs = new HashMap(); private Map marshalers = new HashMap(); … private void parseBooleanSchemaElement(char elementId) { ArgumentMarshaler m = new BooleanArgumentMarshaler(); booleanArgs.put(elementId, m); marshalers.put(elementId, m); } private void parseIntegerSchemaElement(char elementId) { ArgumentMarshaler m = new IntegerArgumentMarshaler(); intArgs.put(elementId, m); marshalers.put(elementId, m); } private void parseStringSchemaElement(char elementId) { ArgumentMarshaler m = new StringArgumentMarshaler(); stringArgs.put(elementId, m); marshalers.put(elementId, m); } ``` Of course the tests all still passed. Next, I changed isBooleanArg from this: ```java private boolean isBooleanArg(char argChar) { return booleanArgs.containsKey(argChar); } ``` to this: ```java private boolean isBooleanArg(char argChar) { ArgumentMarshaler m = marshalers.get(argChar); return m instanceof BooleanArgumentMarshaler; } ``` The tests still passed. So I made the same change to isIntArg and isStringArg. ```java private boolean isIntArg(char argChar) { ArgumentMarshaler m = marshalers.get(argChar); return m instanceof IntegerArgumentMarshaler; } private boolean isStringArg(char argChar) { ArgumentMarshaler m = marshalers.get(argChar); return m instanceof StringArgumentMarshaler; } ``` The tests still passed. So I eliminated all the duplicate calls to marshalers.get as follows: ```java private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (isBooleanArg(m)) setBooleanArg(argChar); else if (isStringArg(m)) setStringArg(argChar); else if (isIntArg(m)) setIntArg(argChar); else return false; return true; } private boolean isIntArg(ArgumentMarshaler m) { return m instanceof IntegerArgumentMarshaler; } private boolean isStringArg(ArgumentMarshaler m) { return m instanceof StringArgumentMarshaler; } private boolean isBooleanArg(ArgumentMarshaler m) { return m instanceof BooleanArgumentMarshaler; } ``` This left no good reason for the three isxxxArg methods. So I inlined them: ```java private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m instanceof BooleanArgumentMarshaler) setBooleanArg(argChar); else if (m instanceof StringArgumentMarshaler) setStringArg(argChar); else if (m instanceof IntegerArgumentMarshaler) setIntArg(argChar); else return false; return true; } ``` Next, I started using the marshalers map in the set functions, breaking the use of the other three maps. I started with the booleans. ```java private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m instanceof BooleanArgumentMarshaler) setBooleanArg(m); else if (m instanceof StringArgumentMarshaler) setStringArg(argChar); else if (m instanceof IntegerArgumentMarshaler) setIntArg(argChar); else return false; return true; } … private void setBooleanArg(ArgumentMarshaler m) { try { m.set(“true”); // was: booleanArgs.get(argChar).set(“true”); } catch (ArgsException e) { } } ``` The tests still passed, so I did the same with Strings and Integers. This allowed me to integrate some of the ugly exception management code into the setArgument function. ```java private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); try { if (m instanceof BooleanArgumentMarshaler) setBooleanArg(m); else if (m instanceof StringArgumentMarshaler) setStringArg(m); else if (m instanceof IntegerArgumentMarshaler) setIntArg(m); else return false; } catch (ArgsException e) { valid = false; errorArgumentId = argChar; throw e; } return true; } private void setIntArg(ArgumentMarshaler m) throws ArgsException { currentArgument++; String parameter = null; try { parameter = args[currentArgument]; m.set(parameter); } catch (ArrayIndexOutOfBoundsException e) { errorCode = ErrorCode.MISSING_INTEGER; throw new ArgsException(); } catch (ArgsException e) { errorParameter = parameter; errorCode = ErrorCode.INVALID_INTEGER; throw e; } } private void setStringArg(ArgumentMarshaler m) throws ArgsException { currentArgument++; try { m.set(args[currentArgument]); } catch (ArrayIndexOutOfBoundsException e) { errorCode = ErrorCode.MISSING_STRING; throw new ArgsException(); } } ``` I was close to being able to remove the three old maps. First, I needed to change the getBoolean function from this: ```java public boolean getBoolean(char arg) { Args.ArgumentMarshaler am = booleanArgs.get(arg); return am != null && (Boolean) am.get(); } ``` to this: ```java public boolean getBoolean(char arg) { Args.ArgumentMarshaler am = marshalers.get(arg); boolean b = false; try { b = am != null && (Boolean) am.get(); } catch (ClassCastException e) { b = false; } return b; } ``` This last change might have been a surprise. Why did I suddenly decide to deal with the ClassCastException? The reason is that I have a set of unit tests and a separate set of acceptance tests written in FitNesse. It turns out that the FitNesse tests made sure that if you called getBoolean on a nonboolean argument, you got a false. The unit tests did not. Up to this point I had only been running the unit tests.2 2. To prevent further surprises of this kind, I added a new unit test that invoked all the FitNesse tests. This last change allowed me to pull out another use of the boolean map: ```java private void parseBooleanSchemaElement(char elementId) { ArgumentMarshaler m = new BooleanArgumentMarshaler(); booleanArgs.put(elementId, m); marshalers.put(elementId, m); } ``` And now we can delete the boolean map. ```java public class Args { … private Map booleanArgs = new HashMap(); private Map stringArgs = new HashMap(); private Map intArgs = new HashMap(); private Map marshalers = new HashMap(); … ``` Next, I migrated the String and Integer arguments in the same manner and did a little cleanup with the booleans. ```java private void parseBooleanSchemaElement(char elementId) { marshalers.put(elementId, new BooleanArgumentMarshaler()); } private void parseIntegerSchemaElement(char elementId) { marshalers.put(elementId, new IntegerArgumentMarshaler()); } private void parseStringSchemaElement(char elementId) { marshalers.put(elementId, new StringArgumentMarshaler()); } … public String getString(char arg) { Args.ArgumentMarshaler am = marshalers.get(arg); try { return am == null ? “ ” : (String) am.get(); } catch (ClassCastException e) { return “ ”; } } public int getInt(char arg) { Args.ArgumentMarshaler am = marshalers.get(arg); try { return am == null ? 0 : (Integer) am.get(); } catch (Exception e) { return 0; } } … public class Args { … private Map stringArgs = new HashMap(); private Map intArgs = new HashMap(); private Map marshalers = new HashMap(); … ``` Next, I inlined the three parse methods because they didn’t do much anymore: ```java private void parseSchemaElement(String element) throws ParseException { char elementId = element.charAt(0); String elementTail = element.substring(1); validateSchemaElementId(elementId); if (isBooleanSchemaElement(elementTail)) marshalers.put(elementId, new BooleanArgumentMarshaler()); else if (isStringSchemaElement(elementTail)) marshalers.put(elementId, new StringArgumentMarshaler()); else if (isIntegerSchemaElement(elementTail)) { marshalers.put(elementId, new IntegerArgumentMarshaler()); } else { throw new ParseException(String.format( “Argument: %c has invalid format: %s.”, elementId, elementTail), 0); } } ``` Okay, so now let’s look at the whole picture again. Listing 14-12 shows the current form of the Args class. Listing 14-12 Args.java (After first refactoring) ```java package com.objectmentor.utilities.getopts; import java.text.ParseException; import java.util.*; public class Args { private String schema; private String[] args; private boolean valid = true; private Set unexpectedArguments = new TreeSet(); private Map marshalers = new HashMap(); private Set argsFound = new HashSet(); private int currentArgument; private char errorArgumentId = '\0'; private String errorParameter = “TILT”; private ErrorCode errorCode = ErrorCode.OK; private enum ErrorCode { OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT} public Args(String schema, String[] args) throws ParseException { this.schema = schema; this.args = args; valid = parse(); } private boolean parse() throws ParseException { if (schema.length() == 0 && args.length == 0) return true; parseSchema(); try { parseArguments(); } catch (ArgsException e) { } return valid; } private boolean parseSchema() throws ParseException { for (String element : schema.split(“,”)) { if (element.length() > 0) { String trimmedElement = element.trim(); parseSchemaElement(trimmedElement); } } return true; } private void parseSchemaElement(String element) throws ParseException { char elementId = element.charAt(0); String elementTail = element.substring(1); validateSchemaElementId(elementId); if (isBooleanSchemaElement(elementTail)) marshalers.put(elementId, new BooleanArgumentMarshaler()); else if (isStringSchemaElement(elementTail)) marshalers.put(elementId, new StringArgumentMarshaler()); else if (isIntegerSchemaElement(elementTail)) { marshalers.put(elementId, new IntegerArgumentMarshaler()); } else { throw new ParseException(String.format( “Argument: %c has invalid format: %s.”, elementId, elementTail), 0); } } private void validateSchemaElementId(char elementId) throws ParseException { if (!Character.isLetter(elementId)) { throw new ParseException( “Bad character:” + elementId + “in Args format: ” + schema, 0); } } private boolean isStringSchemaElement(String elementTail) { return elementTail.equals(“*”); } private boolean isBooleanSchemaElement(String elementTail) { return elementTail.length() == 0; } private boolean isIntegerSchemaElement(String elementTail) { return elementTail.equals(“-”); } private boolean parseArguments() throws ArgsException { for (currentArgument=0; currentArgument 0) return “-[“ + schema + ”]”; else return “ ”; } public String errorMessage() throws Exception { switch (errorCode) { case OK: throw new Exception(“TILT: Should not get here.”); case UNEXPECTED_ARGUMENT: return unexpectedArgumentMessage(); case MISSING_STRING: return String.format(“Could not find string parameter for -%c.”, errorArgumentId); case INVALID_INTEGER: return String.format(“Argument -%c expects an integer but was '%s'.”, errorArgumentId, errorParameter); case MISSING_INTEGER: return String.format(“Could not find integer parameter for -%c.”, errorArgumentId); } return “ ”; } private String unexpectedArgumentMessage() { StringBuffer message = new StringBuffer(“Argument(s) -”); for (char c : unexpectedArguments) { message.append(c); } message.append(“ unexpected.”); return message.toString(); } public boolean getBoolean(char arg) { Args.ArgumentMarshaler am = marshalers.get(arg); boolean b = false; try { b = am != null && (Boolean) am.get(); } catch (ClassCastException e) { b = false; } return b; } public String getString(char arg) { Args.ArgumentMarshaler am = marshalers.get(arg); try { return am == null ? “ ” : (String) am.get(); } catch (ClassCastException e) { return “ ”; } } public int getInt(char arg) { Args.ArgumentMarshaler am = marshalers.get(arg); try { return am == null ? 0 : (Integer) am.get(); } catch (Exception e) { return 0; } } public boolean has(char arg) { return argsFound.contains(arg); } public boolean isValid() { return valid; } private class ArgsException extends Exception { } private abstract class ArgumentMarshaler { public abstract void set(String s) throws ArgsException; public abstract Object get(); } private class BooleanArgumentMarshaler extends ArgumentMarshaler { private boolean booleanValue = false; public void set(String s) { booleanValue = true; } public Object get() { return booleanValue; } } private class StringArgumentMarshaler extends ArgumentMarshaler { private String stringValue = “ ”; public void set(String s) { stringValue = s; } public Object get() { return stringValue; } } private class IntegerArgumentMarshaler extends ArgumentMarshaler { private int intValue = 0; public void set(String s) throws ArgsException { try { intValue = Integer.parseInt(s); } catch (NumberFormatException e) { throw new ArgsException(); } } public Object get() { return intValue; } } } ``` After all that work, this is a bit disappointing. The structure is a bit better, but we still have all those variables up at the top; there’s still a horrible type-case in setArgument; and all those set functions are really ugly. Not to mention all the error processing. We still have a lot of work ahead of us. I’d really like to get rid of that type-case up in setArgument [G23]. What I’d like in setArgument is a single call to ArgumentMarshaler.set. This means I need to push setIntArg, setStringArg, and setBooleanArg down into the appropriate ArgumentMarshaler derivatives. But there is a problem. If you look closely at setIntArg, you’ll notice that it uses two instance variables: args and currentArg. To move setIntArg down into BooleanArgumentMarshaler, I’ll have to pass both args and currentArgs as function arguments. That’s dirty [F1]. I’d rather pass one argument instead of two. Fortunately, there is a simple solution. We can convert the args array into a list and pass an Iterator down to the set functions. The following took me ten steps, passing all the tests after each. But I’ll just show you the result. You should be able to figure out what most of the tiny little steps were. ```java public class Args { private String schema; private String[] args; private boolean valid = true; private Set unexpectedArguments = new TreeSet(); private Map marshalers = new HashMap(); private Set argsFound = new HashSet(); private Iterator currentArgument; private char errorArgumentId = ’\0’; private String errorParameter = “TILT”; private ErrorCode errorCode = ErrorCode.OK; private List argsList; private enum ErrorCode { OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT} public Args(String schema, String[] args) throws ParseException { this.schema = schema; argsList = Arrays.asList(args); valid = parse(); } private boolean parse() throws ParseException { if (schema.length() == 0 && argsList.size() == 0) return true; parseSchema(); try { parseArguments(); } catch (ArgsException e) { } return valid; } --- private boolean parseArguments() throws ArgsException { for (currentArgument = argsList.iterator(); currentArgument.hasNext();) { String arg = currentArgument.next(); parseArgument(arg); } return true; } --- private void setIntArg(ArgumentMarshaler m) throws ArgsException { String parameter = null; try { parameter = currentArgument.next(); m.set(parameter); } catch (NoSuchElementException e) { errorCode = ErrorCode.MISSING_INTEGER; throw new ArgsException(); } catch (ArgsException e) { errorParameter = parameter; errorCode = ErrorCode.INVALID_INTEGER; throw e; } } private void setStringArg(ArgumentMarshaler m) throws ArgsException { try { m.set(currentArgument.next()); } catch (NoSuchElementException e) { errorCode = ErrorCode.MISSING_STRING; throw new ArgsException(); } } ``` These were simple changes that kept all the tests passing. Now we can start moving the set functions down into the appropriate derivatives. First, I need to make the following change in setArgument: ```java private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m == null) return false; try { if (m instanceof BooleanArgumentMarshaler) setBooleanArg(m); else if (m instanceof StringArgumentMarshaler) setStringArg(m); else if (m instanceof IntegerArgumentMarshaler) setIntArg(m); else return false; } catch (ArgsException e) { valid = false; errorArgumentId = argChar; throw e; } return true; } ``` This change is important because we want to completely eliminate the if-else chain. Therefore, we needed to get the error condition out of it. Now we can start to move the set functions. The setBooleanArg function is trivial, so we’ll prepare that one first. Our goal is to change the setBooleanArg function to simply forward to the BooleanArgumentMarshaler. ```java private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m == null) return false; try { if (m instanceof BooleanArgumentMarshaler) setBooleanArg(m, currentArgument); else if (m instanceof StringArgumentMarshaler) setStringArg(m); else if (m instanceof IntegerArgumentMarshaler) setIntArg(m); } catch (ArgsException e) { valid = false; errorArgumentId = argChar; throw e; } return true; } --- private void setBooleanArg(ArgumentMarshaler m, Iterator currentArgument) throws ArgsException { try { m.set(”true”); catch (ArgsException e) { } } ``` Didn’t we just put that exception processing in? Putting things in so you can take them out again is pretty common in refactoring. The smallness of the steps and the need to keep the tests running means that you move things around a lot. Refactoring is a lot like solving a Rubik’s cube. There are lots of little steps required to achieve a large goal. Each step enables the next. Why did we pass that iterator when setBooleanArg certainly doesn’t need it? Because setIntArg and setStringArg will! And because I want to deploy all three of these functions through an abstract method in ArgumentMarshaller, I need to pass it to setBooleanArg. So now setBooleanArg is useless. If there were a set function in ArgumentMarshaler, we could call it directly. So it’s time to make that function! The first step is to add the new abstract method to ArgumentMarshaler. ```java private abstract class ArgumentMarshaler { public abstract void set(Iterator currentArgument) throws ArgsException; public abstract void set(String s) throws ArgsException; public abstract Object get(); } ``` Of course this breaks all the derivatives. So let’s implement the new method in each. ```java private class BooleanArgumentMarshaler extends ArgumentMarshaler { private boolean booleanValue = false; public void set(Iterator currentArgument) throws ArgsException { booleanValue = true; } public void set(String s) { booleanValue = true; } public Object get() { return booleanValue; } } private class StringArgumentMarshaler extends ArgumentMarshaler { private String stringValue = ””; public void set(Iterator currentArgument) throws ArgsException { } public void set(String s) { stringValue = s; } public Object get() { return stringValue; } } private class IntegerArgumentMarshaler extends ArgumentMarshaler { private int intValue = 0; public void set(Iterator currentArgument) throws ArgsException { } public void set(String s) throws ArgsException { try { intValue = Integer.parseInt(s); } catch (NumberFormatException e) { throw new ArgsException(); } } public Object get() { return intValue; } } ``` And now we can eliminate setBooleanArg! ```java private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m == null) return false; try { if (m instanceof BooleanArgumentMarshaler) m.set(currentArgument); else if (m instanceof StringArgumentMarshaler) setStringArg(m); else if (m instanceof IntegerArgumentMarshaler) setIntArg(m); } catch (ArgsException e) { valid = false; errorArgumentId = argChar; throw e; } return true; } ``` The tests all pass, and the set function is deploying to BooleanArgumentMarshaler! Now we can do the same for Strings and Integers. ```java private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m == null) return false; try { if (m instanceof BooleanArgumentMarshaler) m.set(currentArgument); else if (m instanceof StringArgumentMarshaler) m.set(currentArgument); else if (m instanceof IntegerArgumentMarshaler) m.set(currentArgument); } catch (ArgsException e) { valid = false; errorArgumentId = argChar; throw e; } return true; } --- private class StringArgumentMarshaler extends ArgumentMarshaler { private String stringValue = ””; public void set(Iterator currentArgument) throws ArgsException { try { stringValue = currentArgument.next(); } catch (NoSuchElementException e) { errorCode = ErrorCode.MISSING_STRING; throw new ArgsException(); } } public void set(String s) { } public Object get() { return stringValue; } } private class IntegerArgumentMarshaler extends ArgumentMarshaler { private int intValue = 0; public void set(Iterator currentArgument) throws ArgsException { String parameter = null; try { parameter = currentArgument.next(); set(parameter); } catch (NoSuchElementException e) { errorCode = ErrorCode.MISSING_INTEGER; throw new ArgsException(); } catch (ArgsException e) { errorParameter = parameter; errorCode = ErrorCode.INVALID_INTEGER; throw e; } } public void set(String s) throws ArgsException { try { intValue = Integer.parseInt(s); } catch (NumberFormatException e) { throw new ArgsException(); } } public Object get() { return intValue; } } ``` And so the coup de grace: The type-case can be removed! Touche! ```java private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m == null) return false; try { m.set(currentArgument); return true; } catch (ArgsException e) { valid = false; errorArgumentId = argChar; throw e; } } ``` Now we can get rid of some crufty functions in IntegerArgumentMarshaler and clean it up a bit. ```java private class IntegerArgumentMarshaler extends ArgumentMarshaler { private int intValue = 0 public void set(Iterator currentArgument) throws ArgsException { String parameter = null; try { parameter = currentArgument.next(); intValue = Integer.parseInt(parameter); } catch (NoSuchElementException e) { errorCode = ErrorCode.MISSING_INTEGER; throw new ArgsException(); } catch (NumberFormatException e) { errorParameter = parameter; errorCode = ErrorCode.INVALID_INTEGER; throw new ArgsException(); } } public Object get() { return intValue; } } ``` We can also turn ArgumentMarshaler into an interface. ```java private interface ArgumentMarshaler { void set(Iterator currentArgument) throws ArgsException; Object get(); } So now let’s see how easy it is to add a new argument type to our structure. It should require very few changes, and those changes should be isolated. First, we begin by adding a new test case to check that the double argument works correctly. ```java public void testSimpleDoublePresent() throws Exception { Args args = new Args(”x##”, new String[] {”-x”,”42.3”}); assertTrue(args.isValid()); assertEquals(1, args.cardinality()); assertTrue(args.has(’x’)); assertEquals(42.3, args.getDouble(’x’), .001); } ``` Now we clean up the schema parsing code and add the ## detection for the double argument type. ```java private void parseSchemaElement(String element) throws ParseException { char elementId = element.charAt(0); String elementTail = element.substring(1); validateSchemaElementId(elementId); if (elementTail.length() == 0) marshalers.put(elementId, new BooleanArgumentMarshaler()); else if (elementTail.equals(”*”)) marshalers.put(elementId, new StringArgumentMarshaler()); else if (elementTail.equals(”#”)) marshalers.put(elementId, new IntegerArgumentMarshaler()); else if (elementTail.equals(”##”)) marshalers.put(elementId, new DoubleArgumentMarshaler()); else throw new ParseException(String.format( ”Argument: %c has invalid format: %s.”, elementId, elementTail), 0); } ``` Next, we write the DoubleArgumentMarshaler class. ```java private class DoubleArgumentMarshaler implements ArgumentMarshaler { private double doubleValue = 0; public void set(Iterator currentArgument) throws ArgsException { String parameter = null; try { parameter = currentArgument.next(); doubleValue = Double.parseDouble(parameter); } catch (NoSuchElementException e) { errorCode = ErrorCode.MISSING_DOUBLE; throw new ArgsException(); } catch (NumberFormatException e) { errorParameter = parameter; errorCode = ErrorCode.INVALID_DOUBLE; throw new ArgsException(); } } public Object get() { return doubleValue; } } ``` This forces us to add a new ErrorCode. ```java private enum ErrorCode { OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT, MISSING_DOUBLE, INVALID_DOUBLE} And we need a getDouble function. ```java public double getDouble(char arg) { Args.ArgumentMarshaler am = marshalers.get(arg); try { return am == null ? 0 : (Double) am.get(); } catch (Exception e) { return 0.0; } } ``` And all the tests pass! That was pretty painless. So now let’s make sure all the error processing works correctly. The next test case checks that an error is declared if an unparseable string is fed to a ## argument. ```java public void testInvalidDouble() throws Exception { Args args = new Args(”x##”, new String[] {”-x”,”Forty two”}); assertFalse(args.isValid()); assertEquals(0, args.cardinality()); assertFalse(args.has(’x’)); assertEquals(0, args.getInt(’x’)); assertEquals(”Argument -x expects a double but was ‘Forty two’.”, args.errorMessage()); } --- public String errorMessage() throws Exception { switch (errorCode) { case OK: throw new Exception(”TILT: Should not get here.”); case UNEXPECTED_ARGUMENT: return unexpectedArgumentMessage(); case MISSING_STRING: return String.format(”Could not find string parameter for -%c.”, errorArgumentId); case INVALID_INTEGER: return String.format(”Argument -%c expects an integer but was ‘%s’.”, errorArgumentId, errorParameter); case MISSING_INTEGER: return String.format(”Could not find integer parameter for -%c.”, errorArgumentId); case INVALID_DOUBLE: return String.format(”Argument -%c expects a double but was ‘%s’.”, errorArgumentId, errorParameter); case MISSING_DOUBLE: return String.format(”Could not find double parameter for -%c.”, errorArgumentId); } return””; } ``` And the tests pass. The next test makes sure we detect a missing double argument properly. ```java public void testMissingDouble() throws Exception { Args args = new Args(”x##”, new String[]{”-x”}); assertFalse(args.isValid()); assertEquals(0, args.cardinality()); assertFalse(args.has(’x’)); assertEquals(0.0, args.getDouble(’x’), 0.01); assertEquals(”Could not find double parameter for -x.”, args.errorMessage()); } ``` This passes as expected. We wrote it simply for completeness. The exception code is pretty ugly and doesn’t really belong in the Args class. We are also throwing out ParseException, which doesn’t really belong to us. So let’s merge all the exceptions into a single ArgsException class and move it into its own module. ```java public class ArgsException extends Exception { private char errorArgumentId = ’\0’; private String errorParameter = ”TILT”; private ErrorCode errorCode = ErrorCode.OK; public ArgsException() {} public ArgsException(String message) {super(message);} public enum ErrorCode { OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT, MISSING_DOUBLE, INVALID_DOUBLE} } --- public class Args { … private char errorArgumentId = ’\0’; private String errorParameter = ”TILT”; private ArgsException.ErrorCode errorCode = ArgsException.ErrorCode.OK; private List argsList; public Args(String schema, String[] args) throws ArgsException { this.schema = schema; argsList = Arrays.asList(args); valid = parse(); } private boolean parse() throws ArgsException { if (schema.length() == 0 && argsList.size() == 0) return true; parseSchema(); try { parseArguments(); } catch (ArgsException e) { } return valid; } private boolean parseSchema() throws ArgsException { … } private void parseSchemaElement(String element) throws ArgsException { … else throw new ArgsException( String.format(”Argument: %c has invalid format: %s.”, elementId,elementTail)); } private void validateSchemaElementId(char elementId) throws ArgsException { if (!Character.isLetter(elementId)) { throw new ArgsException( ”Bad character:” + elementId + ”in Args format: ” + schema); } } … private void parseElement(char argChar) throws ArgsException { if (setArgument(argChar)) argsFound.add(argChar); else { unexpectedArguments.add(argChar); errorCode = ArgsException.ErrorCode.UNEXPECTED_ARGUMENT; valid = false; } } … private class StringArgumentMarshaler implements ArgumentMarshaler { private String stringValue = ””; public void set(Iterator currentArgument) throws ArgsException { try { stringValue = currentArgument.next(); } catch (NoSuchElementException e) { errorCode = ArgsException.ErrorCode.MISSING_STRING; throw new ArgsException(); } } public Object get() { return stringValue; } } private class IntegerArgumentMarshaler implements ArgumentMarshaler { private int intValue = 0; public void set(Iterator currentArgument) throws ArgsException { String parameter = null; try { parameter = currentArgument.next(); intValue = Integer.parseInt(parameter); } catch (NoSuchElementException e) { errorCode = ArgsException.ErrorCode.MISSING_INTEGER; throw new ArgsException(); } catch (NumberFormatException e) { errorParameter = parameter; errorCode = ArgsException.ErrorCode.INVALID_INTEGER; throw new ArgsException(); } } public Object get() { return intValue; } } private class DoubleArgumentMarshaler implements ArgumentMarshaler { private double doubleValue = 0; public void set(Iterator currentArgument) throws ArgsException { String parameter = null; try { parameter = currentArgument.next(); doubleValue = Double.parseDouble(parameter); } catch (NoSuchElementException e) { errorCode = ArgsException.ErrorCode.MISSING_DOUBLE; throw new ArgsException(); } catch (NumberFormatException e) { errorParameter = parameter; errorCode = ArgsException.ErrorCode.INVALID_DOUBLE; throw new ArgsException(); } } public Object get() { return doubleValue; } } } ``` This is nice. Now the only exception thrown by Args is ArgsException. Moving ArgsException into its own module means that we can move a lot of the miscellaneous error support code into that module and out of the Args module. It provides a natural and obvious place to put all that code and will really help us clean up the Args module going forward. So now we have completely separated the exception and error code from the Args module. (See Listing 14-13 through Listing 14-16.) This was achieved through a series of about 30 tiny steps, keeping the tests passing between each step. Listing 14-13 ArgsTest.java ```java package com.objectmentor.utilities.args; import junit.framework.TestCase; public class ArgsTest extends TestCase { public void testCreateWithNoSchemaOrArguments() throws Exception { Args args = new Args(“”, new String[0]); assertEquals(0, args.cardinality()); } public void testWithNoSchemaButWithOneArgument() throws Exception { try { new Args(“”, new String[]{“-x”}); fail(); } catch (ArgsException e) { assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, e.getErrorCode()); assertEquals(‘x’, e.getErrorArgumentId()); } } public void testWithNoSchemaButWithMultipleArguments() throws Exception { try { new Args(“”, new String[]{“-x”, “-y”}); fail(); } catch (ArgsException e) { assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, e.getErrorCode()); assertEquals(‘x’, e.getErrorArgumentId()); } } public void testNonLetterSchema() throws Exception { try { new Args(“*”, new String[]{}); fail(“Args constructor should have thrown exception”); } catch (ArgsException e) { assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME, e.getErrorCode()); assertEquals(‘*’, e.getErrorArgumentId()); } } public void testInvalidArgumentFormat() throws Exception { try { new Args(“f~”, new String[]{}); fail(“Args constructor should have throws exception”); } catch (ArgsException e) { assertEquals(ArgsException.ErrorCode.INVALID_FORMAT, e.getErrorCode()); assertEquals(‘f’, e.getErrorArgumentId()); } } public void testSimpleBooleanPresent() throws Exception { Args args = new Args(“x”, new String[]{“-x”}); assertEquals(1, args.cardinality()); assertEquals(true, args.getBoolean(‘x’)); } public void testSimpleStringPresent() throws Exception { Args args = new Args(“x*”, new String[]{“-x”, “param”}); assertEquals(1, args.cardinality()); assertTrue(args.has(‘x’)); assertEquals(“param”, args.getString(‘x’)); } public void testMissingStringArgument() throws Exception { try { new Args(“x*”, new String[]{“-x”}); fail(); } catch (ArgsException e) { assertEquals(ArgsException.ErrorCode.MISSING_STRING, e.getErrorCode()); assertEquals(‘x’, e.getErrorArgumentId()); } } public void testSpacesInFormat() throws Exception { Args args = new Args(“x, y”, new String[]{“-xy”}); assertEquals(2, args.cardinality()); assertTrue(args.has(‘x’)); assertTrue(args.has(‘y’)); } public void testSimpleIntPresent() throws Exception { Args args = new Args(“x#”, new String[]{“-x”, “42”}); assertEquals(1, args.cardinality()); assertTrue(args.has(‘x’)); assertEquals(42, args.getInt(‘x’)); } public void testInvalidInteger() throws Exception { try { new Args(“x#”, new String[]{“-x”, “Forty two”}); fail(); } catch (ArgsException e) { assertEquals(ArgsException.ErrorCode.INVALID_INTEGER, e.getErrorCode()); assertEquals(‘x’, e.getErrorArgumentId()); assertEquals(”Forty two”, e.getErrorParameter()); } } public void testMissingInteger() throws Exception { try { new Args(“x#”, new String[]{“-x”}); fail(); } catch (ArgsException e) { assertEquals(ArgsException.ErrorCode.MISSING_INTEGER, e.getErrorCode()); assertEquals(‘x’, e.getErrorArgumentId()); } } public void testSimpleDoublePresent() throws Exception { Args args = new Args(“x##”, new String[]{“-x”, “42.3”}); assertEquals(1, args.cardinality()); assertTrue(args.has(‘x’)); assertEquals(42.3, args.getDouble(‘x’), .001); } public void testInvalidDouble() throws Exception { try { new Args(“x##”, new String[]{“-x”, “Forty two”}); fail(); } catch (ArgsException e) { assertEquals(ArgsException.ErrorCode.INVALID_DOUBLE, e.getErrorCode()); assertEquals(‘x’, e.getErrorArgumentId()); assertEquals(“Forty two”, e.getErrorParameter()); } } public void testMissingDouble() throws Exception { try { new Args(“x##”, new String[]{“-x”}); fail(); } catch (ArgsException e) { assertEquals(ArgsException.ErrorCode.MISSING_DOUBLE, e.getErrorCode()); assertEquals(‘x’, e.getErrorArgumentId()); } } } ``` Listing 14-14 ArgsExceptionTest.java ```java public class ArgsExceptionTest extends TestCase { public void testUnexpectedMessage() throws Exception { ArgsException e = new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, ‘x’, null); assertEquals(“Argument -x unexpected.”, e.errorMessage()); } public void testMissingStringMessage() throws Exception { ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_STRING, ‘x’, null); assertEquals(“Could not find string parameter for -x.”, e.errorMessage()); } public void testInvalidIntegerMessage() throws Exception { ArgsException e = new ArgsException(ArgsException.ErrorCode.INVALID_INTEGER, ‘x’, “Forty two”); assertEquals(“Argument -x expects an integer but was ‘Forty two’.“, e.errorMessage()); } public void testMissingIntegerMessage() throws Exception { ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_INTEGER, ‘x’, null); assertEquals(“Could not find integer parameter for -x.”, e.errorMessage()); } public void testInvalidDoubleMessage() throws Exception { ArgsException e = new ArgsException(ArgsException.ErrorCode.INVALID_DOUBLE, ‘x’, “Forty two”); assertEquals(“Argument -x expects a double but was ‘Forty two’.”, e.errorMessage()); } public void testMissingDoubleMessage() throws Exception { ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_DOUBLE, ‘x’, null); assertEquals(“Could not find double parameter for -x.”, e.errorMessage()); } } ``` Listing 14-15 ArgsException.java ```java public class ArgsException extends Exception { private char errorArgumentId = ‘\0’; private String errorParameter = “TILT”; private ErrorCode errorCode = ErrorCode.OK; public ArgsException() {} public ArgsException(String message) {super(message);} public ArgsException(ErrorCode errorCode) { this.errorCode = errorCode; } public ArgsException(ErrorCode errorCode, String errorParameter) { this.errorCode = errorCode; this.errorParameter = errorParameter; } public ArgsException(ErrorCode errorCode, char errorArgumentId, String errorParameter) { this.errorCode = errorCode; this.errorParameter = errorParameter; this.errorArgumentId = errorArgumentId; } public char getErrorArgumentId() { return errorArgumentId; } public void setErrorArgumentId(char errorArgumentId) { this.errorArgumentId = errorArgumentId; } public String getErrorParameter() { return errorParameter; } public void setErrorParameter(String errorParameter) { this.errorParameter = errorParameter; } public ErrorCode getErrorCode() { return errorCode; } public void setErrorCode(ErrorCode errorCode) { this.errorCode = errorCode; } public String errorMessage() throws Exception { switch (errorCode) { case OK: throw new Exception(“TILT: Should not get here.”); case UNEXPECTED_ARGUMENT: return String.format(“Argument -%c unexpected.”, errorArgumentId); case MISSING_STRING: return String.format(“Could not find string parameter for -%c.”, errorArgumentId); case INVALID_INTEGER: return String.format(“Argument -%c expects an integer but was ‘%s’.”, errorArgumentId, errorParameter); case MISSING_INTEGER: return String.format(“Could not find integer parameter for -%c.”, errorArgumentId); case INVALID_DOUBLE: return String.format(“Argument -%c expects a double but was ‘%s’.”, errorArgumentId, errorParameter); case MISSING_DOUBLE: return String.format(“Could not find double parameter for -%c.”, errorArgumentId); } return “”; } public enum ErrorCode { OK, INVALID_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, MISSING_DOUBLE, INVALID_DOUBLE} } ``` Listing 14-16 Args.java ```java public class Args { private String schema; private Map marshalers = new HashMap(); private Set argsFound = new HashSet(); private Iterator currentArgument; private List argsList; public Args(String schema, String[] args) throws ArgsException { this.schema = schema; argsList = Arrays.asList(args); parse(); } private void parse() throws ArgsException { parseSchema(); parseArguments(); } private boolean parseSchema() throws ArgsException { for (String element : schema.split(“,”)) { if (element.length() > 0) { parseSchemaElement(element.trim()); } } return true; } private void parseSchemaElement(String element) throws ArgsException { char elementId = element.charAt(0); String elementTail = element.substring(1); validateSchemaElementId(elementId); if (elementTail.length() == 0) marshalers.put(elementId, new BooleanArgumentMarshaler()); else if (elementTail.equals(“*”)) marshalers.put(elementId, new StringArgumentMarshaler()); else if (elementTail.equals(“#”)) marshalers.put(elementId, new IntegerArgumentMarshaler()); else if (elementTail.equals(“##”)) marshalers.put(elementId, new DoubleArgumentMarshaler()); else throw new ArgsException(ArgsException.ErrorCode.INVALID_FORMAT, elementId, elementTail); } private void validateSchemaElementId(char elementId) throws ArgsException { if (!Character.isLetter(elementId)) { throw new ArgsException(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME, elementId, null); } } private void parseArguments() throws ArgsException { for (currentArgument = argsList.iterator(); currentArgument.hasNext();) { String arg = currentArgument.next(); parseArgument(arg); } } private void parseArgument(String arg) throws ArgsException { if (arg.startsWith(“-”)) parseElements(arg); } private void parseElements(String arg) throws ArgsException { for (int i = 1; i < arg.length(); i++) parseElement(arg.charAt(i)); } private void parseElement(char argChar) throws ArgsException { if (setArgument(argChar)) argsFound.add(argChar); else { throw new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, argChar, null); } } private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m == null) return false; try { m.set(currentArgument); return true; } catch (ArgsException e) { e.setErrorArgumentId(argChar); throw e; } } public int cardinality() { return argsFound.size(); } public String usage() { if (schema.length() > 0) return “-[” + schema + “]”; else return “”; } public boolean getBoolean(char arg) { ArgumentMarshaler am = marshalers.get(arg); boolean b = false; try { b = am != null && (Boolean) am.get(); } catch (ClassCastException e) { b = false; } return b; } public String getString(char arg) { ArgumentMarshaler am = marshalers.get(arg); try { return am == null ? “” : (String) am.get(); } catch (ClassCastException e) { return “”; } } public int getInt(char arg) { ArgumentMarshaler am = marshalers.get(arg); try { return am == null ? 0 : (Integer) am.get(); } catch (Exception e) { return 0; } } public double getDouble(char arg) { ArgumentMarshaler am = marshalers.get(arg); try { return am == null ? 0 : (Double) am.get(); } catch (Exception e) { return 0.0; } } public boolean has(char arg) { return argsFound.contains(arg); } } ``` The majority of the changes to the Args class were deletions. A lot of code just got moved out of Args and put into ArgsException. Nice. We also moved all the ArgumentMarshaller s into their own files. Nicer! Much of good software design is simply about partitioning—creating appropriate places to put different kinds of code. This separation of concerns makes the code much simpler to understand and maintain. Of special interest is the errorMessage method of ArgsException. Clearly it was a violation of the SRP to put the error message formatting into Args. Args should be about the processing of arguments, not about the format of the error messages. However, does it really make sense to put the error message formatting code into ArgsException? Frankly, it’s a compromise. Users who don’t like the error messages supplied by ArgsException will have to write their own. But the convenience of having canned error messages already prepared for you is not insignificant. By now it should be clear that we are within striking distance of the final solution that appeared at the start of this chapter. I’ll leave the final transformations to you as an exercise. CONCLUSION It is not enough for code to work. Code that works is often badly broken. Programmers who satisfy themselves with merely working code are behaving unprofessionally. They may fear that they don’t have time to improve the structure and design of their code, but I disagree. Nothing has a more profound and long-term degrading effect upon a development project than bad code. Bad schedules can be redone, bad requirements can be redefined. Bad team dynamics can be repaired. But bad code rots and ferments, becoming an inexorable weight that drags the team down. Time and time again I have seen teams grind to a crawl because, in their haste, they created a malignant morass of code that forever thereafter dominated their destiny. Of course bad code can be cleaned up. But it’s very expensive. As code rots, the modules insinuate themselves into each other, creating lots of hidden and tangled dependencies. Finding and breaking old dependencies is a long and arduous task. On the other hand, keeping code clean is relatively easy. If you made a mess in a module in the morning, it is easy to clean it up in the afternoon. Better yet, if you made a mess five minutes ago, it’s very easy to clean it up right now. So the solution is to continuously keep your code as clean and simple as it can be. Never let the rot get started. ================================================ FILE: docs/ch15.md ================================================ # 第 15 章 JUnit Internals ![](figures/ch15/15_1fig_martin.jpg) JUnit is one of the most famous of all Java frameworks. As frameworks go, it is simple in conception, precise in definition, and elegant in implementation. But what does the code look like? In this chapter we’ll critique an example drawn from the JUnit framework. THE JUNIT FRAMEWORK JUnit has had many authors, but it began with Kent Beck and Eric Gamma together on a plane to Atlanta. Kent wanted to learn Java, and Eric wanted to learn about Kent’s Smalltalk testing framework. “What could be more natural to a couple of geeks in cramped quarters than to pull out our laptops and start coding?”1 After three hours of high-altitude work, they had written the basics of JUnit. 1. JUnit Pocket Guide, Kent Beck, O’Reilly, 2004, p. 43. The module we’ll look at is the clever bit of code that helps identify string comparison errors. This module is called ComparisonCompactor. Given two strings that differ, such as ABCDE and ABXDE, it will expose the difference by generating a string such as <…B[X]D…>. I could explain it further, but the test cases do a better job. So take a look at Listing 15-1 and you will understand the requirements of this module in depth. While you are at it, critique the structure of the tests. Could they be simpler or more obvious? Listing 15-1 ComparisonCompactorTest.java ```java package junit.tests.framework; import junit.framework.ComparisonCompactor; import junit.framework.TestCase; public class ComparisonCompactorTest extends TestCase { public void testMessage() { String failure= new ComparisonCompactor(0, “b”, “c”).compact(“a”); assertTrue(“a expected:<[b]> but was:<[c]>”.equals(failure)); } public void testStartSame() { String failure= new ComparisonCompactor(1, “ba”, “bc”).compact(null); assertEquals(“expected: but was:”, failure); } public void testEndSame() { String failure= new ComparisonCompactor(1, “ab”, “cb”).compact(null); assertEquals(“expected:<[a]b> but was:<[c]b>”, failure); } public void testSame() { String failure= new ComparisonCompactor(1, “ab”, “ab”).compact(null); assertEquals(“expected: but was:”, failure); } public void testNoContextStartAndEndSame() { String failure= new ComparisonCompactor(0, “abc”, “adc”).compact(null); assertEquals(“expected:<…[b]…> but was:<…[d]…>”, failure); } public void testStartAndEndContext() { String failure= new ComparisonCompactor(1, “abc”, “adc”).compact(null); assertEquals(“expected: but was:”, failure); } public void testStartAndEndContextWithEllipses() { String failure= new ComparisonCompactor(1, “abcde”, “abfde”).compact(null); assertEquals(“expected:<…b[c]d…> but was:<…b[f]d…>”, failure); } public void testComparisonErrorStartSameComplete() { String failure= new ComparisonCompactor(2, “ab”, “abc”).compact(null); assertEquals(“expected: but was:”, failure); } public void testComparisonErrorEndSameComplete() { String failure= new ComparisonCompactor(0, “bc”, “abc”).compact(null); assertEquals(“expected:<[]…> but was:<[a]…>”, failure); } public void testComparisonErrorEndSameCompleteContext() { String failure= new ComparisonCompactor(2, “bc”, “abc”).compact(null); assertEquals(“expected:<[]bc> but was:<[a]bc>”, failure); } public void testComparisonErrorOverlapingMatches() { String failure= new ComparisonCompactor(0, “abc”, “abbc”).compact(null); assertEquals(“expected:<…[]…> but was:<…[b]…>”, failure); } public void testComparisonErrorOverlapingMatchesContext() { String failure= new ComparisonCompactor(2, “abc”, “abbc”).compact(null); assertEquals(“expected: but was:”, failure); } public void testComparisonErrorOverlapingMatches2() { String failure= new ComparisonCompactor(0, “abcdde”, “abcde”).compact(null); assertEquals(“expected:<…[d]…> but was:<…[]…>”, failure); } public void testComparisonErrorOverlapingMatches2Context() { String failure= new ComparisonCompactor(2, “abcdde”, “abcde”).compact(null); assertEquals(“expected:<…cd[d]e> but was:<…cd[]e>”, failure); } public void testComparisonErrorWithActualNull() { String failure= new ComparisonCompactor(0, “a”, null).compact(null); assertEquals(“expected: but was:”, failure); } public void testComparisonErrorWithActualNullContext() { String failure= new ComparisonCompactor(2, “a”, null).compact(null); assertEquals(“expected: but was:”, failure); } public void testComparisonErrorWithExpectedNull() { String failure= new ComparisonCompactor(0, null, “a”).compact(null); assertEquals(“expected: but was:”, failure); } public void testComparisonErrorWithExpectedNullContext() { String failure= new ComparisonCompactor(2, null, “a”).compact(null); assertEquals(“expected: but was:”, failure); } public void testBug609972() { String failure= new ComparisonCompactor(10, “S&P500”, “0”).compact(null); assertEquals(“expected:<[S&P50]0> but was:<[]0>”, failure); } } ``` I ran a code coverage analysis on the ComparisonCompactor using these tests. The code is 100 percent covered. Every line of code, every if statement and for loop, is executed by the tests. This gives me a high degree of confidence that the code works and a high degree of respect for the craftsmanship of the authors. The code for ComparisonCompactor is in Listing 15-2. Take a moment to look over this code. I think you’ll find it to be nicely partitioned, reasonably expressive, and simple in structure. Once you are done, then we’ll pick the nits together. Listing 15-2 ComparisonCompactor.java (Original) ```java package junit.framework; public class ComparisonCompactor { private static final String ELLIPSIS = “…”; private static final String DELTA_END = “]”; private static final String DELTA_START = “[”; private int fContextLength; private String fExpected; private String fActual; private int fPrefix; private int fSuffix; public ComparisonCompactor(int contextLength, String expected, String actual) { fContextLength = contextLength; fExpected = expected; fActual = actual; } public String compact(String message) { if (fExpected == null || fActual == null || areStringsEqual()) return Assert.format(message, fExpected, fActual); findCommonPrefix(); findCommonSuffix(); String expected = compactString(fExpected); String actual = compactString(fActual); return Assert.format(message, expected, actual); } private String compactString(String source) { String result = DELTA_START + source.substring(fPrefix, source.length() - fSuffix + 1) + DELTA_END; if (fPrefix > 0) result = computeCommonPrefix() + result; if (fSuffix > 0) result = result + computeCommonSuffix(); return result; } private void findCommonPrefix() { fPrefix = 0; int end = Math.min(fExpected.length(), fActual.length()); for (; fPrefix < end; fPrefix++) { if (fExpected.charAt(fPrefix) != fActual.charAt(fPrefix)) break; } } private void findCommonSuffix() { int expectedSuffix = fExpected.length() - 1; int actualSuffix = fActual.length() - 1; for (; actualSuffix >= fPrefix && expectedSuffix >= fPrefix; actualSuffix--, expectedSuffix--) { if (fExpected.charAt(expectedSuffix) != fActual.charAt(actualSuffix)) break; } fSuffix = fExpected.length() - expectedSuffix; } private String computeCommonPrefix() { return (fPrefix > fContextLength ? ELLIPSIS : “”) + fExpected.substring(Math.max(0, fPrefix - fContextLength), fPrefix); } private String computeCommonSuffix() { int end = Math.min(fExpected.length() - fSuffix + 1 + fContextLength, fExpected.length()); return fExpected.substring(fExpected.length() - fSuffix + 1, end) + (fExpected.length() - fSuffix + 1 < fExpected.length() - fContextLength ? ELLIPSIS : “”); } private boolean areStringsEqual() { return fExpected.equals(fActual); } } ``` You might have a few complaints about this module. There are some long expressions and some strange +1s and so forth. But overall this module is pretty good. After all, it might have looked like Listing 15-3. Listing 15-3 ComparisonCompator.java (defactored) ```java package junit.framework; public class ComparisonCompactor { private int ctxt; private String s1; private String s2; private int pfx; private int sfx; public ComparisonCompactor(int ctxt, String s1, String s2) { this.ctxt = ctxt; this.s1 = s1; this.s2 = s2; } public String compact(String msg) { if (s1 == null || s2 == null || s1.equals(s2)) return Assert.format(msg, s1, s2); pfx = 0; for (; pfx < Math.min(s1.length(), s2.length()); pfx++) { if (s1.charAt(pfx) != s2.charAt(pfx)) break; } int sfx1 = s1.length() - 1; int sfx2 = s2.length() - 1; for (; sfx2 >= pfx && sfx1 >= pfx; sfx2--, sfx1--) { if (s1.charAt(sfx1) != s2.charAt(sfx2)) break; } sfx = s1.length() - sfx1; String cmp1 = compactString(s1); String cmp2 = compactString(s2); return Assert.format(msg, cmp1, cmp2); } private String compactString(String s) { String result = “[“ + s.substring(pfx, s.length() - sfx + 1) + “]”; if (pfx > 0) result = (pfx > ctxt ? “…” : “”) + s1.substring(Math.max(0, pfx - ctxt), pfx) + result; if (sfx > 0) { int end = Math.min(s1.length() - sfx + 1 + ctxt, s1.length()); result = result + (s1.substring(s1.length() - sfx + 1, end) + (s1.length() - sfx + 1 < s1.length() - ctxt ? “…” : “”)); } return result; } } ``` Even though the authors left this module in very good shape, the Boy Scout Rule2 tells us we should leave it cleaner than we found it. So, how can we improve on the original code in Listing 15-2? 2. See “The Boy Scout Rule” on page 14. The first thing I don’t care for is the f prefix for the member variables [N6]. Today’s environments make this kind of scope encoding redundant. So let’s eliminate all the f’s. ```java private int contextLength; private String expected; private String actual; private int prefix; private int suffix; ``` Next, we have an unencapsulated conditional at the beginning of the compact function [G28]. ```java public String compact(String message) { if (expected == null || actual == null || areStringsEqual()) return Assert.format(message, expected, actual); findCommonPrefix(); findCommonSuffix(); String expected = compactString(this.expected); String actual = compactString(this.actual); return Assert.format(message, expected, actual); } ``` This conditional should be encapsulated to make our intent clear. So let’s extract a method that explains it. ```java public String compact(String message) { if (shouldNotCompact()) return Assert.format(message, expected, actual); findCommonPrefix(); findCommonSuffix(); String expected = compactString(this.expected); String actual = compactString(this.actual); return Assert.format(message, expected, actual); } private boolean shouldNotCompact() { return expected == null || actual == null || areStringsEqual(); } ``` I don’t much care for the this.expected and this.actual notation in the compact function. This happened when we changed the name of fExpected to expected. Why are there variables in this function that have the same names as the member variables? Don’t they represent something else [N4]? We should make the names unambiguous. String compactExpected = compactString(expected); String compactActual = compactString(actual); Negatives are slightly harder to understand than positives [G29]. So let’s turn that if statement on its head and invert the sense of the conditional. ```java public String compact(String message) { if (canBeCompacted()) { findCommonPrefix(); findCommonSuffix(); String compactExpected = compactString(expected); String compactActual = compactString(actual); return Assert.format(message, compactExpected, compactActual); } else { return Assert.format(message, expected, actual); } } private boolean canBeCompacted() { return expected != null && actual != null && ! areStringsEqual(); } ``` The name of the function is strange [N7]. Although it does compact the strings, it actually might not compact the strings if canBeCompacted returns false. So naming this function compact hides the side effect of the error check. Notice also that the function returns a formatted message, not just the compacted strings. So the name of the function should really be formatCompactedComparison. That makes it read a lot better when taken with the function argument: ```java public String formatCompactedComparison(String message) { The body of the if statement is where the true compacting of the expected and actual strings is done. We should extract that as a method named compactExpectedAndActual. However, we want the formatCompactedComparison function to do all the formatting. The compact… function should do nothing but compacting [G30]. So let’s split it up as follows: ```java … private String compactExpected; private String compactActual; … public String formatCompactedComparison(String message) { if (canBeCompacted()) { compactExpectedAndActual(); return Assert.format(message, compactExpected, compactActual); } else { return Assert.format(message, expected, actual); } } private void compactExpectedAndActual() { findCommonPrefix(); findCommonSuffix(); compactExpected = compactString(expected); compactActual = compactString(actual); } ``` Notice that this required us to promote compactExpected and compactActual to member variables. I don’t like the way that the last two lines of the new function return variables, but the first two don’t. They aren’t using consistent conventions [G11]. So we should change findCommonPrefix and findCommonSuffix to return the prefix and suffix values. ```java private void compactExpectedAndActual() { prefixIndex = findCommonPrefix(); suffixIndex = findCommonSuffix(); compactExpected = compactString(expected); compactActual = compactString(actual); } private int findCommonPrefix() { int prefixIndex = 0; int end = Math.min(expected.length(), actual.length()); for (; prefixIndex < end; prefixIndex++) { if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex)) break; } return prefixIndex; } private int findCommonSuffix() { int expectedSuffix = expected.length() - 1; int actualSuffix = actual.length() - 1; for (; actualSuffix >= prefixIndex && expectedSuffix >= prefixIndex; actualSuffix--, expectedSuffix--) { if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) break; } return expected.length() - expectedSuffix; } ``` We should also change the names of the member variables to be a little more accurate [N1]; after all, they are both indices. Careful inspection of findCommonSuffix exposes a hidden temporal coupling [G31]; it depends on the fact that prefixIndex is calculated by findCommonPrefix. If these two functions were called out of order, there would be a difficult debugging session ahead. So, to expose this temporal coupling, let’s have findCommonSuffix take the prefixIndex as an argument. ```java private void compactExpectedAndActual() { prefixIndex = findCommonPrefix(); suffixIndex = findCommonSuffix(prefixIndex); compactExpected = compactString(expected); compactActual = compactString(actual); } private int findCommonSuffix(int prefixIndex) { int expectedSuffix = expected.length() - 1; int actualSuffix = actual.length() - 1; for (; actualSuffix >= prefixIndex && expectedSuffix >= prefixIndex; actualSuffix--, expectedSuffix--) { if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) break; } return expected.length() - expectedSuffix; } ``` I’m not really happy with this. The passing of the prefixIndex argument is a bit arbitrary [G32]. It works to establish the ordering but does nothing to explain the need for that ordering. Another programmer might undo what we have done because there’s no indication that the parameter is really needed. So let’s take a different tack. ```java private void compactExpectedAndActual() { findCommonPrefixAndSuffix(); compactExpected = compactString(expected); compactActual = compactString(actual); } private void findCommonPrefixAndSuffix() { findCommonPrefix(); int expectedSuffix = expected.length() - 1; int actualSuffix = actual.length() - 1; for (; actualSuffix >= prefixIndex && expectedSuffix >= prefixIndex; actualSuffix--, expectedSuffix-- ) { if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) break; } suffixIndex = expected.length() - expectedSuffix; } private void findCommonPrefix() { prefixIndex = 0; int end = Math.min(expected.length(), actual.length()); for (; prefixIndex < end; prefixIndex++) if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex)) break; } ``` We put findCommonPrefix and findCommonSuffix back the way they were, changing the name of findCommonSuffix to findCommonPrefixAnd Suffix and having it call findCommon-Prefix before doing anything else. That establishes the temporal nature of the two functions in a much more dramatic way than the previous solution. It also points out how ugly findCommonPrefixAndSuffix is. Let’s clean it up now. ```java private void findCommonPrefixAndSuffix() { findCommonPrefix(); int suffixLength = 1; for (; !suffixOverlapsPrefix(suffixLength); suffixLength++) { if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) break; } suffixIndex = suffixLength; } private char charFromEnd(String s, int i) { return s.charAt(s.length()-i);} private boolean suffixOverlapsPrefix(int suffixLength) { return actual.length() - suffixLength < prefixLength || expected.length() - suffixLength < prefixLength; } ``` This is much better. It exposes that the suffixIndex is really the length of the suffix and is not well named. The same is true of the prefixIndex, though in that case “index” and “length” are synonymous. Even so, it is more consistent to use “length.” The problem is that the suffixIndex variable is not zero based; it is 1 based and so is not a true length. This is also the reason that there are all those +1s in computeCommonSuffix [G33]. So let’s fix that. The result is in Listing 15-4. Listing 15-4 ComparisonCompactor.java (interim) ```java public class ComparisonCompactor { … private int suffixLength; … private void findCommonPrefixAndSuffix() { findCommonPrefix(); suffixLength = 0; for (; !suffixOverlapsPrefix(suffixLength); suffixLength++) { if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) break; } } private char charFromEnd(String s, int i) { return s.charAt(s.length() - i - 1); } private boolean suffixOverlapsPrefix(int suffixLength) { return actual.length() - suffixLength <= prefixLength || expected.length() - suffixLength <= prefixLength; } … private String compactString(String source) { String result = DELTA_START + source.substring(prefixLength, source.length() - suffixLength) + DELTA_END; if (prefixLength > 0) result = computeCommonPrefix() + result; if (suffixLength > 0) result = result + computeCommonSuffix(); return result; } … private String computeCommonSuffix() { int end = Math.min(expected.length() - suffixLength + contextLength, expected.length() ); return expected.substring(expected.length() - suffixLength, end) + (expected.length() - suffixLength < expected.length() - contextLength ? ELLIPSIS : “”); } ``` We replaced the +1s in computeCommonSuffix with a -1 in charFromEnd, where it makes perfect sense, and two <= operators in suffixOverlapsPrefix, where they also make perfect sense. This allowed us to change the name of suffixIndex to suffixLength, greatly enhancing the readability of the code. There is a problem however. As I was eliminating the +1s, I noticed the following line in compactString: if (suffixLength > 0) Take a look at it in Listing 15-4. By rights, because suffixLength is now one less than it used to be, I should change the > operator to a >= operator. But that makes no sense. It makes sense now! This means that it didn’t use to make sense and was probably a bug. Well, not quite a bug. Upon further analysis we see that the if statement now prevents a zero length suffix from being appended. Before we made the change, the if statement was nonfunctional because suffixIndex could never be less than one! This calls into question both if statements in compactString! It looks as though they could both be eliminated. So let’s comment them out and run the tests. They passed! So let’s restructure compactString to eliminate the extraneous if statements and make the function much simpler [G9]. ```java private String compactString(String source) { return computeCommonPrefix() + DELTA_START + source.substring(prefixLength, source.length() - suffixLength) + DELTA_END + computeCommonSuffix(); } ``` This is much better! Now we see that the compactString function is simply composing the fragments together. We can probably make this even clearer. Indeed, there are lots of little cleanups we could do. But rather than drag you through the rest of the changes, I’ll just show you the result in Listing 15-5. Listing 15-5 ComparisonCompactor.java (final) ```java package junit.framework; public class ComparisonCompactor { private static final String ELLIPSIS = “…”; private static final String DELTA_END = “]”; private static final String DELTA_START = “[”; private int contextLength; private String expected; private String actual; private int prefixLength; private int suffixLength; public ComparisonCompactor( int contextLength, String expected, String actual ) { this.contextLength = contextLength; this.expected = expected; this.actual = actual; } public String formatCompactedComparison(String message) { String compactExpected = expected; String compactActual = actual; if (shouldBeCompacted()) { findCommonPrefixAndSuffix(); compactExpected = compact(expected); compactActual = compact(actual); } return Assert.format(message, compactExpected, compactActual); } private boolean shouldBeCompacted() { return !shouldNotBeCompacted(); } private boolean shouldNotBeCompacted() { return expected == null || actual == null || expected.equals(actual); } private void findCommonPrefixAndSuffix() { findCommonPrefix(); suffixLength = 0; for (; !suffixOverlapsPrefix(); suffixLength++) { if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength) ) break; } } private char charFromEnd(String s, int i) { return s.charAt(s.length() - i - 1); } private boolean suffixOverlapsPrefix() { return actual.length() - suffixLength <= prefixLength || expected.length() - suffixLength <= prefixLength; } private void findCommonPrefix() { prefixLength = 0; int end = Math.min(expected.length(), actual.length()); for (; prefixLength < end; prefixLength++) if (expected.charAt(prefixLength) != actual.charAt(prefixLength)) break; } private String compact(String s) { return new StringBuilder() .append(startingEllipsis()) .append(startingContext()) .append(DELTA_START) .append(delta(s)) .append(DELTA_END) .append(endingContext()) .append(endingEllipsis()) .toString(); } private String startingEllipsis() { return prefixLength > contextLength ? ELLIPSIS : “”; } private String startingContext() { int contextStart = Math.max(0, prefixLength - contextLength); int contextEnd = prefixLength; return expected.substring(contextStart, contextEnd); } private String delta(String s) { int deltaStart = prefixLength; int deltaEnd = s.length() - suffixLength; return s.substring(deltaStart, deltaEnd); } private String endingContext() { int contextStart = expected.length() - suffixLength; int contextEnd = Math.min(contextStart + contextLength, expected.length()); return expected.substring(contextStart, contextEnd); } private String endingEllipsis() { return (suffixLength > contextLength ? ELLIPSIS : “”); } } ``` This is actually quite pretty. The module is separated into a group of analysis functions and another group of synthesis functions. They are topologically sorted so that the definition of each function appears just after it is used. All the analysis functions appear first, and all the synthesis functions appear last. If you look carefully, you will notice that I reversed several of the decisions I made earlier in this chapter. For example, I inlined some extracted methods back into formatCompactedComparison, and I changed the sense of the shouldNotBeCompacted expression. This is typical. Often one refactoring leads to another that leads to the undoing of the first. Refactoring is an iterative process full of trial and error, inevitably converging on something that we feel is worthy of a professional. CONCLUSION And so we have satisfied the Boy Scout Rule. We have left this module a bit cleaner than we found it. Not that it wasn’t clean already. The authors had done an excellent job with it. But no module is immune from improvement, and each of us has the responsibility to leave the code a little better than we found it. ================================================ FILE: docs/ch16.md ================================================ # 第 16 章 Refactoring SerialDate ![](figures/ch16/16_1fig_martin.jpg) If you go to http://www.jfree.org/jcommon/index.php, you will find the JCommon library. Deep within that library there is a package named org.jfree.date. Within that package there is a class named SerialDate. We are going to explore that class. The author of SerialDate is David Gilbert. David is clearly an experienced and competent programmer. As we shall see, he shows a significant degree of professionalism and discipline within his code. For all intents and purposes, this is “good code.” And I am going to rip it to pieces. This is not an activity of malice. Nor do I think that I am so much better than David that I somehow have a right to pass judgment on his code. Indeed, if you were to find some of my code, I’m sure you could find plenty of things to complain about. No, this is not an activity of nastiness or arrogance. What I am about to do is nothing more and nothing less than a professional review. It is something that we should all be comfortable doing. And it is something we should welcome when it is done for us. It is only through critiques like these that we will learn. Doctors do it. Pilots do it. Lawyers do it. And we programmers need to learn how to do it too. One more thing about David Gilbert: David is more than just a good programmer. David had the courage and good will to offer his code to the community at large for free. He placed it out in the open for all to see and invited public usage and public scrutiny. This was well done! SerialDate (Listing B-1, page 349) is a class that represents a date in Java. Why have a class that represents a date, when Java already has java.util.Date and java.util.Calendar, and others? The author wrote this class in response to a pain that I have often felt myself. The comment in his opening Javadoc (line 67) explains it well. We could quibble about his intention, but I have certainly had to deal with this issue, and I welcome a class that is about dates instead of times. FIRST, MAKE IT WORK There are some unit tests in a class named SerialDateTests (Listing B-2, page 366). The tests all pass. Unfortunately a quick inspection of the tests shows that they don’t test everything [T1]. For example, doing a “Find Usages” search on the method MonthCodeToQuarter (line 334) indicates that it is not used [F4]. Therefore, the unit tests don’t test it. So I fired up Clover to see what the unit tests covered and what they didn’t. Clover reported that the unit tests executed only 91 of the 185 executable statements in SerialDate (~50 percent) [T2]. The coverage map looks like a patchwork quilt, with big gobs of unexecuted code littered all through the class. It was my goal to completely understand and also refactor this class. I couldn’t do that without much greater test coverage. So I wrote my own suite of completely independent unit tests (Listing B-4, page 374). As you look through these tests, you will note that many of them are commented out. These tests didn’t pass. They represent behavior that I think SerialDate should have. So as I refactor SerialDate, I’ll be working to make these tests pass too. Even with some of the tests commented out, Clover reports that the new unit tests are executing 170 (92 percent) out of the 185 executable statements. This is pretty good, and I think we’ll be able to get this number higher. The first few commented-out tests (lines 23-63) were a bit of conceit on my part. The program was not designed to pass these tests, but the behavior seemed obvious [G2] to me. I’m not sure why the testWeekdayCodeToString method was written in the first place, but because it is there, it seems obvious that it should not be case sensitive. Writing these tests was trivial [T3]. Making them pass was even easier; I just changed lines 259 and 263 to use equalsIgnoreCase. I left the tests at line 32 and line 45 commented out because it’s not clear to me that the “tues” and “thurs” abbreviations ought to be supported. The tests on line 153 and line 154 don’t pass. Clearly, they should [G2]. We can easily fix this, and the tests on line 163 through line 213, by making the following changes to the stringToMonthCode function. ```java 457 if ((result < 1) || (result > 12)) { result = -1; 458 for (int i = 0; i < monthNames.length; i++) { 459 if (s.equalsIgnoreCase(shortMonthNames[i])) { 460 result = i + 1; 461 break; 462 } 463 if (s.equalsIgnoreCase(monthNames[i])) { 464 result = i + 1; 465 break; 466 } 467 } 468 } ``` The commented test on line 318 exposes a bug in the getFollowingDayOfWeek method (line 672). December 25th, 2004, was a Saturday. The following Saturday was January 1st, 2005. However, when we run the test, we see that getFollowingDayOfWeek returns December 25th as the Saturday that follows December 25th. Clearly, this is wrong [G3],[T1]. We see the problem in line 685. It is a typical boundary condition error [T5]. It should read as follows: ```java 685 if (baseDOW >= targetWeekday) { ``` It is interesting to note that this function was the target of an earlier repair. The change history (line 43) shows that “bugs” were fixed in getPreviousDayOfWeek, getFollowingDayOfWeek, and getNearestDayOfWeek [T6]. The testGetNearestDayOfWeek unit test (line 329), which tests the getNearestDayOfWeek method (line 705), did not start out as long and exhaustive as it currently is. I added a lot of test cases to it because my initial test cases did not all pass [T6]. You can see the pattern of failure by looking at which test cases are commented out. That pattern is revealing [T7]. It shows that the algorithm fails if the nearest day is in the future. Clearly there is some kind of boundary condition error [T5]. The pattern of test coverage reported by Clover is also interesting [T8]. Line 719 never gets executed! This means that the if statement in line 718 is always false. Sure enough, a look at the code shows that this must be true. The adjust variable is always negative and so cannot be greater or equal to 4. So this algorithm is just wrong. The right algorithm is shown below: ```java int delta = targetDOW - base.getDayOfWeek(); int positiveDelta = delta + 7; int adjust = positiveDelta % 7; if (adjust > 3) adjust -= 7; return SerialDate.addDays(adjust, base); ``` Finally, the tests at line 417 and line 429 can be made to pass simply by throwing an IllegalArgumentException instead of returning an error string from weekInMonthToString and relativeToString. With these changes all the unit tests pass, and I believe SerialDate now works. So now it’s time to make it “right.” THEN MAKE IT RIGHT We are going to walk from the top to the bottom of SerialDate, improving it as we go along. Although you won’t see this in the discussion, I will be running all of the JCommon unit tests, including my improved unit test for SerialDate, after every change I make. So rest assured that every change you see here works for all of JCommon. Starting at line 1, we see a ream of comments with license information, copyrights, authors, and change history. I acknowledge that there are certain legalities that need to be addressed, and so the copyrights and licenses must stay. On the other hand, the change history is a leftover from the 1960s. We have source code control tools that do this for us now. This history should be deleted [C1]. The import list starting at line 61 could be shortened by using java.text.* and java.util.*. [J1] I wince at the HTML formatting in the Javadoc (line 67). Having a source file with more than one language in it troubles me. This comment has four languages in it: Java, English, Javadoc, and html [G1]. With that many languages in use, it’s hard to keep things straight. For example, the nice positioning of line 71 and line 72 are lost when the Javadoc is generated, and yet who wants to see `
    ` and `
  • ` in the source code? A better strategy might be to just surround the whole comment with `
    ` so that the formatting that is apparent in the source code is preserved within the Javadoc.1
    
    1. An even better solution would have been for Javadoc to present all comments as preformatted, so that comments appear the same in both code and document.
    
    Line 86 is the class declaration. Why is this class named SerialDate? What is the significance of the word “serial”? Is it because the class is derived from Serializable? That doesn’t seem likely.
    
    I won’t keep you guessing. I know why (or at least I think I know why) the word “serial” was used. The clue is in the constants SERIAL_LOWER_BOUND and SERIAL_UPPER_BOUND on line 98 and line 101. An even better clue is in the comment that begins on line 830. This class is named SerialDate because it is implemented using a “serial number,” which happens to be the number of days since December 30th, 1899.
    
    I have two problems with this. First, the term “serial number” is not really correct. This may be a quibble, but the representation is more of a relative offset than a serial number. The term “serial number” has more to do with product identification markers than dates. So I don’t find this name particularly descriptive [N1]. A more descriptive term might be “ordinal.”
    
    The second problem is more significant. The name SerialDate implies an implementation. This class is an abstract class. There is no need to imply anything at all about the implementation. Indeed, there is good reason to hide the implementation! So I find this name to be at the wrong level of abstraction [N2]. In my opinion, the name of this class should simply be Date.
    
    Unfortunately, there are already too many classes in the Java library named Date, so this is probably not the best name to choose. Because this class is all about days, instead of time, I considered naming it Day, but this name is also heavily used in other places. In the end, I chose DayDate as the best compromise.
    
    From now on in this discussion I will use the term DayDate. I leave it to you to remember that the listings you are looking at still use SerialDate.
    
    I understand why DayDate inherits from Comparable and Serializable. But why does it inherit from MonthConstants? The class MonthConstants (Listing B-3, page 372) is just a bunch of static final constants that define the months. Inheriting from classes with constants is an old trick that Java programmers used so that they could avoid using expressions like MonthConstants.January, but it’s a bad idea [J2]. MonthConstants should really be an enum.
    ```java
       public abstract class DayDate implements Comparable,
                                                Serializable {
         public static enum Month {
           JANUARY(1),
           FEBRUARY(2),
           MARCH(3),
           APRIL(4),
           MAY(5),
           JUNE(6),
           JULY(7),
           AUGUST(8),
           SEPTEMBER(9),
           OCTOBER(10),
           NOVEMBER(11),
           DECEMBER(12);
     
           Month(int index) {
             this.index = index;
           }
    
           public static Month make(int monthIndex) {
             for (Month m : Month.values()) {
               if (m.index == monthIndex)
                 return m;
             }
             throw new IllegalArgumentException(“Invalid month index ” + monthIndex);
           }
           public final int index;
         }
    ```
    Changing MonthConstants to this enum forces quite a few changes to the DayDate class and all it’s users. It took me an hour to make all the changes. However, any function that used to take an int for a month, now takes a Month enumerator. This means we can get rid of the isValidMonthCode method (line 326), and all the month code error checking such as that in monthCodeToQuarter (line 356) [G5].
    
    Next, we have line 91, serialVersionUID. This variable is used to control the serializer. If we change it, then any DayDate written with an older version of the software won’t be readable anymore and will result in an InvalidClassException. If you don’t declare the serialVersionUID variable, then the compiler automatically generates one for you, and it will be different every time you make a change to the module. I know that all the documents recommend manual control of this variable, but it seems to me that automatic control of serialization is a lot safer [G4]. After all, I’d much rather debug an InvalidClassException than the odd behavior that would ensue if I forgot to change the serialVersionUID. So I’m going to delete the variable—at least for the time being.2
    
    2. Several of the reviewers of this text have taken exception to this decision. They contend that in an open source framework it is better to assert manual control over the serial ID so that minor changes to the software don’t cause old serialized dates to be invalid. This is a fair point. However, at least the failure, inconvenient though it might be, has a clear-cut cause. On the other hand, if the author of the class forgets to update the ID, then the failure mode is undefined and might very well be silent. I think the real moral of this story is that you should not expect to deserialize across versions.
    
    I find the comment on line 93 redundant. Redundant comments are just places to collect lies and misinformation [C2]. So I’m going to get rid of it and its ilk.
    
    The comments at line 97 and line 100 talk about serial numbers, which I discussed earlier [C1]. The variables they describe are the earliest and latest possible dates that DayDate can describe. This can be made a bit clearer [N1].
    ```java
       public static final int EARLIEST_DATE_ORDINAL = 2;     // 1/1/1900
       public static final int LATEST_DATE_ORDINAL = 2958465; // 12/31/9999
    ```
    It’s not clear to me why EARLIEST_DATE_ORDINAL is 2 instead of 0. There is a hint in the comment on line 829 that suggests that this has something to do with the way dates are represented in Microsoft Excel. There is a much deeper insight provided in a derivative of DayDate called SpreadsheetDate (Listing B-5, page 382). The comment on line 71 describes the issue nicely.
    
    The problem I have with this is that the issue seems to be related to the implementation of SpreadsheetDate and has nothing to do with DayDate. I conclude from this that EARLIEST_DATE_ORDINAL and LATEST_DATE_ORDINAL do not really belong in DayDate and should be moved to SpreadsheetDate [G6].
    
    Indeed, a search of the code shows that these variables are used only within SpreadsheetDate. Nothing in DayDate, nor in any other class in the JCommon framework, uses them. Therefore, I’ll move them down into SpreadsheetDate.
    
    The next variables, MINIMUM_YEAR_SUPPORTED, and MAXIMUM_YEAR_SUPPORTED (line 104 and line 107), provide something of a dilemma. It seems clear that if DayDate is an abstract class that provides no foreshadowing of implementation, then it should not inform us about a minimum or maximum year. Again, I am tempted to move these variables down into SpreadsheetDate [G6]. However, a quick search of the users of these variables shows that one other class uses them: RelativeDayOfWeekRule (Listing B-6, page 390). We see that usage at line 177 and line 178 in the getDate function, where they are used to check that the argument to getDate is a valid year. The dilemma is that a user of an abstract class needs information about its implementation.
    
    What we need to do is provide this information without polluting DayDate itself. Usually, we would get implementation information from an instance of a derivative. However, the getDate function is not passed an instance of a DayDate. It does, however, return such an instance, which means that somewhere it must be creating it. Line 187 through line 205 provide the hint. The DayDate instance is being created by one of the three functions, getPreviousDayOfWeek, getNearestDayOfWeek, or getFollowingDayOfWeek. Looking back at the DayDate listing, we see that these functions (lines 638–724) all return a date created by addDays (line 571), which calls createInstance (line 808), which creates a SpreadsheetDate! [G7].
    
    It’s generally a bad idea for base classes to know about their derivatives. To fix this, we should use the ABSTRACT FACTORY3 pattern and create a DayDateFactory. This factory will create the instances of DayDate that we need and can also answer questions about the implementation, such as the maximum and minimum dates.
    
    3. [GOF].
    ```java
       public abstract class DayDateFactory {
         private static DayDateFactory factory = new SpreadsheetDateFactory();
         public static void setInstance(DayDateFactory factory) {
           DayDateFactory.factory = factory;
         }
     
         protected abstract DayDate _makeDate(int ordinal);
         protected abstract DayDate _makeDate(int day, DayDate.Month month, int year);
         protected abstract DayDate _makeDate(int day, int month, int year);
         protected abstract DayDate _makeDate(java.util.Date date);
         protected abstract int _getMinimumYear();
         protected abstract int _getMaximumYear();
     
         public static DayDate makeDate(int ordinal) {
           return factory._makeDate(ordinal);
         }
         public static DayDate makeDate(int day, DayDate.Month month, int year) {
           return factory._makeDate(day, month, year);
         }
     
         public static DayDate makeDate(int day, int month, int year) {
           return factory._makeDate(day, month, year);
         }
     
         public static DayDate makeDate(java.util.Date date) {
           return factory._makeDate(date);
         }
     
         public static int getMinimumYear() {
           return factory._getMinimumYear();
         }
     
         public static int getMaximumYear() {
           return factory._getMaximumYear();
         }
       }
    ```
    This factory class replaces the createInstance methods with makeDate methods, which improves the names quite a bit [N1]. It defaults to a SpreadsheetDateFactory but can be changed at any time to use a different factory. The static methods that delegate to abstract methods use a combination of the SINGLETON,4 DECORATOR,5 and ABSTRACT FACTORY patterns that I have found to be useful.
    
    4. Ibid.
    
    5. Ibid.
    
    The SpreadsheetDateFactory looks like this.
    ```java
       public class SpreadsheetDateFactory extends DayDateFactory {
         public DayDate _makeDate(int ordinal) {
           return new SpreadsheetDate(ordinal);
         }
     
         public DayDate _makeDate(int day, DayDate.Month month, int year) {
           return new SpreadsheetDate(day, month, year);
         }
     
         public DayDate _makeDate(int day, int month, int year) {
           return new SpreadsheetDate(day, month, year);
         }
     
         public DayDate _makeDate(Date date) {
           final GregorianCalendar calendar = new GregorianCalendar();
           calendar.setTime(date);
           return new SpreadsheetDate(
             calendar.get(Calendar.DATE),
             DayDate.Month.make(calendar.get(Calendar.MONTH) + 1),
             calendar.get(Calendar.YEAR));
         }
    
         protected int _getMinimumYear() {
           return SpreadsheetDate.MINIMUM_YEAR_SUPPORTED;
         }
     
         protected int _getMaximumYear() {
           return SpreadsheetDate.MAXIMUM_YEAR_SUPPORTED;
         }
       }
    ```
    As you can see, I have already moved the MINIMUM_YEAR_SUPPORTED and MAXIMUM_YEAR_SUPPORTED variables into SpreadsheetDate, where they belong [G6].
    
    The next issue in DayDate are the day constants beginning at line 109. These should really be another enum [J3]. We’ve seen this pattern before, so I won’t repeat it here. You’ll see it in the final listings.
    
    Next, we see a series of tables starting with LAST_DAY_OF_MONTH at line 140. My first issue with these tables is that the comments that describe them are redundant [C3]. Their names are sufficient. So I’m going to delete the comments.
    
    There seems to be no good reason that this table isn’t private [G8], because there is a static function lastDayOfMonth that provides the same data.
    
    The next table, AGGREGATE_DAYS_TO_END_OF_MONTH, is a bit more mysterious because it is not used anywhere in the JCommon framework [G9]. So I deleted it.
    
    The same goes for LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH.
    
    The next table, AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH, is used only in Spread-sheetDate (line 434 and line 473). This begs the question of whether it should be moved to SpreadsheetDate. The argument for not moving it is that the table is not specific to any particular implementation [G6]. On the other hand, no implementation other than SpreadsheetDate actually exists, and so the table should be moved close to where it is used [G10].
    
    What settles the argument for me is that to be consistent [G11], we should make the table private and expose it through a function like julianDateOfLastDayOfMonth. Nobody seems to need a function like that. Moreover, the table can be moved back to DayDate easily if any new implementation of DayDate needs it. So I moved it.
    
    The same goes for the table, LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH.
    
    Next, we see three sets of constants that can be turned into enums (lines 162–205). The first of the three selects a week within a month. I changed it into an enum named WeekInMonth.
    ```java
       public enum WeekInMonth {
           FIRST(1), SECOND(2), THIRD(3), FOURTH(4), LAST(0);
           public final int index;
     
           WeekInMonth(int index) {
             this.index = index;
           }
         }
    ```
    The second set of constants (lines 177–187) is a bit more obscure. The INCLUDE_NONE, INCLUDE_FIRST, INCLUDE_SECOND, and INCLUDE_BOTH constants are used to describe whether the defining end-point dates of a range should be included in that range. Mathematically, this is described using the terms “open interval,” “half-open interval,” and “closed interval.” I think it is clearer using the mathematical nomenclature [N3], so I changed it to an enum named DateInterval with CLOSED, CLOSED_LEFT, CLOSED_RIGHT, and OPEN enumerators.
    
    The third set of constants (lines 189–205) describe whether a search for a particular day of the week should result in the last, next, or nearest instance. Deciding what to call this is difficult at best. In the end, I settled for WeekdayRange with LAST, NEXT, and NEAREST enumerators.
    
    You might not agree with the names I’ve chosen. They make sense to me, but they may not make sense to you. The point is that they are now in a form that makes them easy to change [J3]. They aren’t passed as integers anymore; they are passed as symbols. I can use the “change name” function of my IDE to change the names, or the types, without worrying that I missed some -1 or 2 somewhere in the code or that some int argument declaration is left poorly described.
    
    The description field at line 208 does not seem to be used by anyone. I deleted it along with its accessor and mutator [G9].
    
    I also deleted the degenerate default constructor at line 213 [G12]. The compiler will generate it for us.
    
    We can skip over the isValidWeekdayCode method (lines 216–238) because we deleted it when we created the Day enumeration.
    
    This brings us to the stringToWeekdayCode method (lines 242–270). Javadocs that don’t add much to the method signature are just clutter [C3],[G12]. The only value this Javadoc adds is the description of the -1 return value. However, because we changed to the Day enumeration, the comment is actually wrong [C2]. The method now throws an IllegalArgumentException. So I deleted the Javadoc.
    
    I also deleted all the final keywords in arguments and variable declarations. As far as I could tell, they added no real value but did add to the clutter [G12]. Eliminating final flies in the face of some conventional wisdom. For example, Robert Simmons6 strongly recommends us to “. . . spread final all over your code.” Clearly I disagree. I think that there are a few good uses for final, such as the occasional final constant, but otherwise the keyword adds little value and creates a lot of clutter. Perhaps I feel this way because the kinds of errors that final might catch are already caught by the unit tests I write.
    
    6. [Simmons04], p. 73.
    
    I didn’t care for the duplicate if statements [G5] inside the for loop (line 259 and line 263), so I connected them into a single if statement using the || operator. I also used the Day enumeration to direct the for loop and made a few other cosmetic changes.
    
    It occurred to me that this method does not really belong in DayDate. It’s really the parse function of Day. So I moved it into the Day enumeration. However, that made the Day enumeration pretty large. Because the concept of Day does not depend on DayDate, I moved the Day enumeration outside of the DayDate class into its own source file [G13].
    
    I also moved the next function, weekdayCodeToString (lines 272–286) into the Day enumeration and called it toString.
    ```java
       public enum Day {
         MONDAY(Calendar.MONDAY),
         TUESDAY(Calendar.TUESDAY),
         WEDNESDAY(Calendar.WEDNESDAY),s
         THURSDAY(Calendar.THURSDAY),
         FRIDAY(Calendar.FRIDAY),
         SATURDAY(Calendar.SATURDAY),
         SUNDAY(Calendar.SUNDAY);
     
         public final int index;
         private static DateFormatSymbols dateSymbols = new DateFormatSymbols();
     
         Day(int day) {
           index = day;
         }
     
         public static Day make(int index) throws IllegalArgumentException {
           for (Day d : Day.values())
             if (d.index == index)
               return d;
           throw new IllegalArgumentException(
             String.format(“Illegal day index: %d.”, index));
         }
     
         public static Day parse(String s) throws IllegalArgumentException {
           String[] shortWeekdayNames =
             dateSymbols.getShortWeekdays();
           String[] weekDayNames =
             dateSymbols.getWeekdays();
     
           s = s.trim();
           for (Day day : Day.values()) {
             if (s.equalsIgnoreCase(shortWeekdayNames[day.index]) ||
                 s.equalsIgnoreCase(weekDayNames[day.index])) {
               return day;
             }
           }
           throw new IllegalArgumentException(
             String.format(“%s is not a valid weekday string”, s));
          }
     
          public String toString() {
            return dateSymbols.getWeekdays()[index];
          }
        }
    ```
    There are two getMonths functions (lines 288–316). The first calls the second. The second is never called by anyone but the first. Therefore, I collapsed the two into one and vastly simplified them [G9],[G12],[F4]. Finally, I changed the name to be a bit more self-descriptive [N1].
    ```java
       public static String[] getMonthNames() {
         return dateFormatSymbols.getMonths();
       }
    ```
    The isValidMonthCode function (lines 326–346) was made irrelevant by the Month enum, so I deleted it [G9].
    
    The monthCodeToQuarter function (lines 356–375) smells of FEATURE ENVY7 [G14] and probably belongs in the Month enum as a method named quarter. So I replaced it.
    
    7. [Refactoring].
    ```java
       public int quarter() {
         return 1 + (index-1)/3;
       }
    ```
    This made the Month enum big enough to be in its own class. So I moved it out of DayDate to be consistent with the Day enum [G11],[G13].
    
    The next two methods are named monthCodeToString (lines 377–426). Again, we see the pattern of one method calling its twin with a flag. It is usually a bad idea to pass a flag as an argument to a function, especially when that flag simply selects the format of the output [G15]. I renamed, simplified, and restructured these functions and moved them into the Month enum [N1],[N3],[C3],[G14].
    ```java
       public String toString() {
         return dateFormatSymbols.getMonths()[index - 1];
       }
     
       public String toShortString() {
         return dateFormatSymbols.getShortMonths()[index - 1];
       }
    ```
    The next method is stringToMonthCode (lines 428–472). I renamed it, moved it into the Month enum, and simplified it [N1],[N3],[C3],[G14],[G12].
    ```java
       public static Month parse(String s) {
         s = s.trim();
         for (Month m : Month.values())
           if (m.matches(s))
             return m;
     
         try {
           return make(Integer.parseInt(s));
         }
         catch (NumberFormatException e) {}
         throw new IllegalArgumentException(“Invalid month ” + s);
       }
    
       private boolean matches(String s) {
         return s.equalsIgnoreCase(toString()) ||
                s.equalsIgnoreCase(toShortString());
       }
    ```
    The isLeapYear method (lines 495–517) can be made a bit more expressive [G16].
    ```java
       public static boolean isLeapYear(int year) {
         boolean fourth = year % 4 == 0;
         boolean hundredth = year % 100 == 0;
         boolean fourHundredth = year % 400 == 0;
         return fourth && (!hundredth || fourHundredth);
       }
    ```
    The next function, leapYearCount (lines 519–536) doesn’t really belong in DayDate. Nobody calls it except for two methods in SpreadsheetDate. So I pushed it down [G6].
    
    The lastDayOfMonth function (lines 538–560) makes use of the LAST_DAY_OF_MONTH array. This array really belongs in the Month enum [G17], so I moved it there. I also simplified the function and made it a bit more expressive [G16].
    ```java
       public static int lastDayOfMonth(Month month, int year) {
         if (month == Month.FEBRUARY && isLeapYear(year))
           return month.lastDay() + 1;
          else
           return month.lastDay();
       }
    ```
    Now things start to get a bit more interesting. The next function is addDays (lines 562–576). First of all, because this function operates on the variables of DayDate, it should not be static [G18]. So I changed it to an instance method. Second, it calls the function toSerial. This function should be renamed toOrdinal [N1]. Finally, the method can be simplified.
    ```java
       public DayDate addDays(int days) {
         return DayDateFactory.makeDate(toOrdinal() + days);
       }
    ```
    The same goes for addMonths (lines 578–602). It should be an instance method [G18]. The algorithm is a bit complicated, so I used EXPLAINING TEMPORARY VARIABLES8 [G19] to make it more transparent. I also renamed the method getYYY to getYear [N1].
    
    8. [Beck97].
    ```java
       public DayDate addMonths(int months) {
         int thisMonthAsOrdinal = 12 * getYear() + getMonth().index - 1;
         int resultMonthAsOrdinal = thisMonthAsOrdinal + months;
         int resultYear = resultMonthAsOrdinal / 12;
         Month resultMonth = Month.make(resultMonthAsOrdinal % 12 + 1);
    
         int lastDayOfResultMonth = lastDayOfMonth(resultMonth, resultYear);
         int resultDay = Math.min(getDayOfMonth(), lastDayOfResultMonth);
         return DayDateFactory.makeDate(resultDay, resultMonth, resultYear);
       }
    ```
    The addYears function (lines 604–626) provides no surprises over the others.
    ```java
       public DayDate plusYears(int years) {
         int resultYear = getYear() + years;
         int lastDayOfMonthInResultYear = lastDayOfMonth(getMonth(), resultYear);
         int resultDay = Math.min(getDayOfMonth(), lastDayOfMonthInResultYear);
         return DayDateFactory.makeDate(resultDay, getMonth(), resultYear);
       }
    ```
    There is a little itch at the back of my mind that is bothering me about changing these methods from static to instance. Does the expression date.addDays(5) make it clear that the date object does not change and that a new instance of DayDate is returned? Or does it erroneously imply that we are adding five days to the date object? You might not think that is a big problem, but a bit of code that looks like the following can be very deceiving [G20].
    ```java
       DayDate date = DateFactory.makeDate(5, Month.DECEMBER, 1952);
       date.addDays(7); // bump date by one week.
    ```
    Someone reading this code would very likely just accept that addDays is changing the date object. So we need a name that breaks this ambiguity [N4]. So I changed the names to plusDays and plusMonths. It seems to me that the intent of the method is captured nicely by
    ```java
       DayDate date = oldDate.plusDays(5);
    ```
    whereas the following doesn’t read fluidly enough for a reader to simply accept that the date object is changed:
    ```java
       date.plusDays(5);
    ```
    The algorithms continue to get more interesting. getPreviousDayOfWeek (lines 628–660) works but is a bit complicated. After some thought about what was really going on [G21], I was able to simplify it and use EXPLAINING TEMPORARY VARIABLES [G19] to make it clearer. I also changed it from a static method to an instance method [G18], and got rid of the duplicate instance method [G5] (lines 997–1008).
    ```java
       public DayDate getPreviousDayOfWeek(Day targetDayOfWeek) {
         int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;
         if (offsetToTarget >= 0)
           offsetToTarget -= 7;
         return plusDays(offsetToTarget);
       }
    ```
    The exact same analysis and result occurred for getFollowingDayOfWeek (lines 662–693).
    ```java
       public DayDate getFollowingDayOfWeek(Day targetDayOfWeek) {
           int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;
           if (offsetToTarget <= 0)
    
             offsetToTarget += 7;
           return plusDays(offsetToTarget);
         }
    ```
    The next function is getNearestDayOfWeek (lines 695–726), which we corrected back on page 270. But the changes I made back then aren’t consistent with the current pattern in the last two functions [G11]. So I made it consistent and used some EXPLAINING TEMPORARY VARIABLES [G19] to clarify the algorithm.
    ```java
       public DayDate getNearestDayOfWeek(final Day targetDay) {
           int offsetToThisWeeksTarget = targetDay.index - getDayOfWeek().index;
           int offsetToFutureTarget = (offsetToThisWeeksTarget + 7) % 7;
           int offsetToPreviousTarget = offsetToFutureTarget - 7;
     
           if (offsetToFutureTarget > 3)
             return plusDays(offsetToPreviousTarget);
           else
             return plusDays(offsetToFutureTarget);
         }
    ```
    The getEndOfCurrentMonth method (lines 728–740) is a little strange because it is an instance method that envies [G14] its own class by taking a DayDate argument. I made it a true instance method and clarified a few names.
    ```java
       public DayDate getEndOfMonth() {
           Month month = getMonth();
           int year = getYear();
           int lastDay = lastDayOfMonth(month, year);
           return DayDateFactory.makeDate(lastDay, month, year);
         }
    ```
    Refactoring weekInMonthToString (lines 742–761) turned out to be very interesting indeed. Using the refactoring tools of my IDE, I first moved the method to the WeekInMonth enum that I created back on page 275. Then I renamed the method to toString. Next, I changed it from a static method to an instance method. All the tests still passed. (Can you guess where I am going?)
    
    Next, I deleted the method entirely! Five asserts failed (lines 411–415, Listing B-4, page 374). I changed these lines to use the names of the enumerators (FIRST, SECOND, …). All the tests passed. Can you see why? Can you also see why each of these steps was necessary? The refactoring tool made sure that all previous callers of weekInMonthToString now called toString on the weekInMonth enumerator because all enumerators implement toString to simply return their names.…
    
    Unfortunately, I was a bit too clever. As elegant as that wonderful chain of refactorings was, I finally realized that the only users of this function were the tests I had just modified, so I deleted the tests.
    
    Fool me once, shame on you. Fool me twice, shame on me! So after determining that nobody other than the tests called relativeToString (lines 765–781), I simply deleted the function and its tests.
    
    We have finally made it to the abstract methods of this abstract class. And the first one is as appropriate as they come: toSerial (lines 838–844). Back on page 279 I had changed the name to toOrdinal. Having looked at it in this context, I decided the name should be changed to getOrdinalDay.
    
    The next abstract method is toDate (lines 838–844). It converts a DayDate to a java.util.Date. Why is this method abstract? If we look at its implementation in SpreadsheetDate (lines 198–207, Listing B-5, page 382), we see that it doesn’t depend on anything in the implementation of that class [G6]. So I pushed it up.
    
    The getYYYY, getMonth, and getDayOfMonth methods are nicely abstract. However, the getDayOfWeek method is another one that should be pulled up from SpreadSheetDate because it doesn’t depend on anything that can’t be found in DayDate [G6]. Or does it?
    
    If you look carefully (line 247, Listing B-5, page 382), you’ll see that the algorithm implicitly depends on the origin of the ordinal day (in other words, the day of the week of day 0). So even though this function has no physical dependencies that couldn’t be moved to DayDate, it does have a logical dependency.
    
    Logical dependencies like this bother me [G22]. If something logical depends on the implementation, then something physical should too. Also, it seems to me that the algorithm itself could be generic with a much smaller portion of it dependent on the implementation [G6].
    
    So I created an abstract method in DayDate named getDayOfWeekForOrdinalZero and implemented it in SpreadsheetDate to return Day.SATURDAY. Then I moved the getDayOfWeek method up to DayDate and changed it to call getOrdinalDay and getDayOfWeekForOrdinal-Zero.
    ```java
       public Day getDayOfWeek() {
           Day startingDay = getDayOfWeekForOrdinalZero();
           int startingOffset = startingDay.index - Day.SUNDAY.index;
           return Day.make((getOrdinalDay() + startingOffset) % 7 + 1);
         }
    ```
    As a side note, look carefully at the comment on line 895 through line 899. Was this repetition really necessary? As usual, I deleted this comment along with all the others.
    
    The next method is compare (lines 902–913). Again, this method is inappropriately abstract [G6], so I pulled the implementation up into DayDate. Also, the name does not communicate enough [N1]. This method actually returns the difference in days since the argument. So I changed the name to daysSince. Also, I noted that there weren’t any tests for this method, so I wrote them.
    
    The next six functions (lines 915–980) are all abstract methods that should be implemented in DayDate. So I pulled them all up from SpreadsheetDate.
    
    The last function, isInRange (lines 982–995) also needs to be pulled up and refactored. The switch statement is a bit ugly [G23] and can be replaced by moving the cases into the DateInterval enum.
    ```java
       public enum DateInterval {
           OPEN {
             public boolean isIn(int d, int left, int right) {
               return d > left && d < right;
             }
           },
           CLOSED_LEFT {
             public boolean isIn(int d, int left, int right) {
               return d >= left && d < right;
             }
           },
           CLOSED_RIGHT {
             public boolean isIn(int d, int left, int right) {
               return d > left && d <= right;
             }
           },
           CLOSED {
             public boolean isIn(int d, int left, int right) {
               return d >= left && d <= right;
             }
           };
     
           public abstract boolean isIn(int d, int left, int right);
         }
    
       public boolean isInRange(DayDate d1, DayDate d2, DateInterval interval) {
           int left = Math.min(d1.getOrdinalDay(), d2.getOrdinalDay());
           int right = Math.max(d1.getOrdinalDay(), d2.getOrdinalDay());
           return interval.isIn(getOrdinalDay(), left, right);
         }
    ```
    That brings us to the end of DayDate. So now we’ll make one more pass over the whole class to see how well it flows.
    
    First, the opening comment is long out of date, so I shortened and improved it [C2].
    
    Next, I moved all the remaining enums out into their own files [G12].
    
    Next, I moved the static variable (dateFormatSymbols) and three static methods (getMonthNames, isLeapYear, lastDayOfMonth) into a new class named DateUtil [G6].
    
    I moved the abstract methods up to the top where they belong [G24].
    
    I changed Month.make to Month.fromInt [N1] and did the same for all the other enums. I also created a toInt() accessor for all the enums and made the index field private.
    
    There was some interesting duplication [G5] in plusYears and plusMonths that I was able to eliminate by extracting a new method named correctLastDayOfMonth, making the all three methods much clearer.
    
    I got rid of the magic number 1 [G25], replacing it with Month.JANUARY.toInt() or Day.SUNDAY.toInt(), as appropriate. I spent a little time with SpreadsheetDate, cleaning up the algorithms a bit. The end result is contained in Listing B-7, page 394, through Listing B-16, page 405.
    
    Interestingly the code coverage in DayDate has decreased to 84.9 percent! This is not because less functionality is being tested; rather it is because the class has shrunk so much that the few uncovered lines have a greater weight. DayDate now has 45 out of 53 executable statements covered by tests. The uncovered lines are so trivial that they weren’t worth testing.
    
    CONCLUSION
    So once again we’ve followed the Boy Scout Rule. We’ve checked the code in a bit cleaner than when we checked it out. It took a little time, but it was worth it. Test coverage was increased, some bugs were fixed, the code was clarified and shrunk. The next person to look at this code will hopefully find it easier to deal with than we did. That person will also probably be able to clean it up a bit more than we did.
    
    ================================================
    FILE: docs/ch17.md
    ================================================
    # 第 17 章 Smells and Heuristics
    
    ![](figures/ch17/17_1fig_martin.jpg)
    
    In his wonderful book Refactoring,1 Martin Fowler identified many different “Code Smells.” The list that follows includes many of Martin’s smells and adds many more of my own. It also includes other pearls and heuristics that I use to practice my trade.
    
    1. [Refactoring].
    
    I compiled this list by walking through several different programs and refactoring them. As I made each change, I asked myself why I made that change and then wrote the reason down here. The result is a rather long list of things that smell bad to me when I read code.
    
    This list is meant to be read from top to bottom and also to be used as a reference. There is a cross-reference for each heuristic that shows you where it is referenced in the rest of the text in “Appendix C” on page 409.
    
    COMMENTS
    C1: Inappropriate Information
    It is inappropriate for a comment to hold information better held in a different kind of system such as your source code control system, your issue tracking system, or any other record-keeping system. Change histories, for example, just clutter up source files with volumes of historical and uninteresting text. In general, meta-data such as authors, last-modified-date, SPR number, and so on should not appear in comments. Comments should be reserved for technical notes about the code and design.
    
    C2: Obsolete Comment
    A comment that has gotten old, irrelevant, and incorrect is obsolete. Comments get old quickly. It is best not to write a comment that will become obsolete. If you find an obsolete comment, it is best to update it or get rid of it as quickly as possible. Obsolete comments tend to migrate away from the code they once described. They become floating islands of irrelevance and misdirection in the code.
    
    C3: Redundant Comment
    A comment is redundant if it describes something that adequately describes itself. For example:
    ```java
       i++; // increment i
    ```
    Another example is a Javadoc that says nothing more than (or even less than) the function signature:
    ```java
       /**
        * @param sellRequest
        * @return
        * @throws ManagedComponentException
        */
       public SellResponse beginSellItem(SellRequest sellRequest)
         throws ManagedComponentException
    ```
    Comments should say things that the code cannot say for itself.
    
    C4: Poorly Written Comment
    A comment worth writing is worth writing well. If you are going to write a comment, take the time to make sure it is the best comment you can write. Choose your words carefully. Use correct grammar and punctuation. Don’t ramble. Don’t state the obvious. Be brief.
    
    C5: Commented-Out Code
    It makes me crazy to see stretches of code that are commented out. Who knows how old it is? Who knows whether or not it’s meaningful? Yet no one will delete it because everyone assumes someone else needs it or has plans for it.
    
    That code sits there and rots, getting less and less relevant with every passing day. It calls functions that no longer exist. It uses variables whose names have changed. It follows conventions that are long obsolete. It pollutes the modules that contain it and distracts the people who try to read it. Commented-out code is an abomination.
    
    When you see commented-out code, delete it! Don’t worry, the source code control system still remembers it. If anyone really needs it, he or she can go back and check out a previous version. Don’t suffer commented-out code to survive.
    
    ENVIRONMENT
    E1: Build Requires More Than One Step
    Building a project should be a single trivial operation. You should not have to check many little pieces out from source code control. You should not need a sequence of arcane commands or context dependent scripts in order to build the individual elements. You should not have to search near and far for all the various little extra JARs, XML files, and other artifacts that the system requires. You should be able to check out the system with one simple command and then issue one other simple command to build it.
    ```sh
       svn get mySystem
       cd mySystem
       ant all
    ```
    E2: Tests Require More Than One Step
    You should be able to run all the unit tests with just one command. In the best case you can run all the tests by clicking on one button in your IDE. In the worst case you should be able to issue a single simple command in a shell. Being able to run all the tests is so fundamental and so important that it should be quick, easy, and obvious to do.
    
    FUNCTIONS
    F1: Too Many Arguments
    Functions should have a small number of arguments. No argument is best, followed by one, two, and three. More than three is very questionable and should be avoided with prejudice. (See “Function Arguments” on page 40.)
    
    F2: Output Arguments
    Output arguments are counterintuitive. Readers expect arguments to be inputs, not outputs. If your function must change the state of something, have it change the state of the object it is called on. (See “Output Arguments” on page 45.)
    
    F3: Flag Arguments
    Boolean arguments loudly declare that the function does more than one thing. They are confusing and should be eliminated. (See “Flag Arguments” on page 41.)
    
    F4: Dead Function
    Methods that are never called should be discarded. Keeping dead code around is wasteful. Don’t be afraid to delete the function. Remember, your source code control system still remembers it.
    
    GENERAL
    G1: Multiple Languages in One Source File
    Today’s modern programming environments make it possible to put many different languages into a single source file. For example, a Java source file might contain snippets of XML, HTML, YAML, JavaDoc, English, JavaScript, and so on. For another example, in addition to HTML a JSP file might contain Java, a tag library syntax, English comments, Javadocs, XML, JavaScript, and so forth. This is confusing at best and carelessly sloppy at worst.
    
    The ideal is for a source file to contain one, and only one, language. Realistically, we will probably have to use more than one. But we should take pains to minimize both the number and extent of extra languages in our source files.
    
    G2: Obvious Behavior Is Unimplemented
    Following “The Principle of Least Surprise,”2 any function or class should implement the behaviors that another programmer could reasonably expect. For example, consider a function that translates the name of a day to an enum that represents the day.
    
    2. Or “The Principle of Least Astonishment”: http://en.wikipedia.org/wiki/
             Principle_of_least_astonishment
    ```java
       Day day = DayDate.StringToDay(String dayName);
    ```
    We would expect the string “Monday” to be translated to Day.MONDAY. We would also expect the common abbreviations to be translated, and we would expect the function to ignore case.
    
    When an obvious behavior is not implemented, readers and users of the code can no longer depend on their intuition about function names. They lose their trust in the original author and must fall back on reading the details of the code.
    
    G3: Incorrect Behavior at the Boundaries
    It seems obvious to say that code should behave correctly. The problem is that we seldom realize just how complicated correct behavior is. Developers often write functions that they think will work, and then trust their intuition rather than going to the effort to prove that their code works in all the corner and boundary cases.
    
    There is no replacement for due diligence. Every boundary condition, every corner case, every quirk and exception represents something that can confound an elegant and intuitive algorithm. Don’t rely on your intuition. Look for every boundary condition and write a test for it.
    
    G4: Overridden Safeties
    Chernobyl melted down because the plant manager overrode each of the safety mechanisms one by one. The safeties were making it inconvenient to run an experiment. The result was that the experiment did not get run, and the world saw it’s first major civilian nuclear catastrophe.
    
    It is risky to override safeties. Exerting manual control over serialVersionUID may be necessary, but it is always risky. Turning off certain compiler warnings (or all warnings!) may help you get the build to succeed, but at the risk of endless debugging sessions. Turning off failing tests and telling yourself you’ll get them to pass later is as bad as pretending your credit cards are free money.
    
    G5: Duplication
    This is one of the most important rules in this book, and you should take it very seriously. Virtually every author who writes about software design mentions this rule. Dave Thomas and Andy Hunt called it the DRY3 principle (Don’t Repeat Yourself). Kent Beck made it one of the core principles of Extreme Programming and called it: “Once, and only once.” Ron Jeffries ranks this rule second, just below getting all the tests to pass.
    
    3. [PRAG].
    
    Every time you see duplication in the code, it represents a missed opportunity for abstraction. That duplication could probably become a subroutine or perhaps another class outright. By folding the duplication into such an abstraction, you increase the vocabulary of the language of your design. Other programmers can use the abstract facilities you create. Coding becomes faster and less error prone because you have raised the abstraction level.
    
    The most obvious form of duplication is when you have clumps of identical code that look like some programmers went wild with the mouse, pasting the same code over and over again. These should be replaced with simple methods.
    
    A more subtle form is the switch/case or if/else chain that appears again and again in various modules, always testing for the same set of conditions. These should be replaced with polymorphism.
    
    Still more subtle are the modules that have similar algorithms, but that don’t share similar lines of code. This is still duplication and should be addressed by using the TEMPLATE METHOD,4 or STRATEGY5 pattern.
    
    4. [GOF].
    
    5. [GOF].
    
    Indeed, most of the design patterns that have appeared in the last fifteen years are simply well-known ways to eliminate duplication. So too the Codd Normal Forms are a strategy for eliminating duplication in database schemae. OO itself is a strategy for organizing modules and eliminating duplication. Not surprisingly, so is structured programming.
    
    I think the point has been made. Find and eliminate duplication wherever you can.
    
    G6: Code at Wrong Level of Abstraction
    It is important to create abstractions that separate higher level general concepts from lower level detailed concepts. Sometimes we do this by creating abstract classes to hold the higher level concepts and derivatives to hold the lower level concepts. When we do this, we need to make sure that the separation is complete. We want all the lower level concepts to be in the derivatives and all the higher level concepts to be in the base class.
    
    For example, constants, variables, or utility functions that pertain only to the detailed implementation should not be present in the base class. The base class should know nothing about them.
    
    This rule also pertains to source files, components, and modules. Good software design requires that we separate concepts at different levels and place them in different containers. Sometimes these containers are base classes or derivatives and sometimes they are source files, modules, or components. Whatever the case may be, the separation needs to be complete. We don’t want lower and higher level concepts mixed together.
    
    Consider the following code:
    ```java
       public interface Stack {
         Object pop() throws EmptyException;
         void push(Object o) throws FullException;
         double percentFull();
    
         class EmptyException extends Exception {}
         class FullException extends Exception {}
       }
    ```
    The percentFull function is at the wrong level of abstraction. Although there are many implementations of Stack where the concept of fullness is reasonable, there are other implementations that simply could not know how full they are. So the function would be better placed in a derivative interface such as BoundedStack.
    
    Perhaps you are thinking that the implementation could just return zero if the stack were boundless. The problem with that is that no stack is truly boundless. You cannot really prevent an OutOfMemoryException by checking for
    ```java
       stack.percentFull() < 50.0.
    ```
    Implementing the function to return 0 would be telling a lie.
    
    The point is that you cannot lie or fake your way out of a misplaced abstraction. Isolating abstractions is one of the hardest things that software developers do, and there is no quick fix when you get it wrong.
    
    G7: Base Classes Depending on Their Derivatives
    The most common reason for partitioning concepts into base and derivative classes is so that the higher level base class concepts can be independent of the lower level derivative class concepts. Therefore, when we see base classes mentioning the names of their derivatives, we suspect a problem. In general, base classes should know nothing about their derivatives.
    
    There are exceptions to this rule, of course. Sometimes the number of derivatives is strictly fixed, and the base class has code that selects between the derivatives. We see this a lot in finite state machine implementations. However, in that case the derivatives and base class are strongly coupled and always deploy together in the same jar file. In the general case we want to be able to deploy derivatives and bases in different jar files.
    
    Deploying derivatives and bases in different jar files and making sure the base jar files know nothing about the contents of the derivative jar files allow us to deploy our systems in discrete and independent components. When such components are modified, they can be redeployed without having to redeploy the base components. This means that the impact of a change is greatly lessened, and maintaining systems in the field is made much simpler.
    
    G8: Too Much Information
    Well-defined modules have very small interfaces that allow you to do a lot with a little. Poorly defined modules have wide and deep interfaces that force you to use many different gestures to get simple things done. A well-defined interface does not offer very many functions to depend upon, so coupling is low. A poorly defined interface provides lots of functions that you must call, so coupling is high.
    
    Good software developers learn to limit what they expose at the interfaces of their classes and modules. The fewer methods a class has, the better. The fewer variables a function knows about, the better. The fewer instance variables a class has, the better.
    
    Hide your data. Hide your utility functions. Hide your constants and your temporaries. Don’t create classes with lots of methods or lots of instance variables. Don’t create lots of protected variables and functions for your subclasses. Concentrate on keeping interfaces very tight and very small. Help keep coupling low by limiting information.
    
    G9: Dead Code
    Dead code is code that isn’t executed. You find it in the body of an if statement that checks for a condition that can’t happen. You find it in the catch block of a try that never throws. You find it in little utility methods that are never called or switch/case conditions that never occur.
    
    The problem with dead code is that after awhile it starts to smell. The older it is, the stronger and sourer the odor becomes. This is because dead code is not completely updated when designs change. It still compiles, but it does not follow newer conventions or rules. It was written at a time when the system was different. When you find dead code, do the right thing. Give it a decent burial. Delete it from the system.
    
    G10: Vertical Separation
    Variables and function should be defined close to where they are used. Local variables should be declared just above their first usage and should have a small vertical scope. We don’t want local variables declared hundreds of lines distant from their usages.
    
    Private functions should be defined just below their first usage. Private functions belong to the scope of the whole class, but we’d still like to limit the vertical distance between the invocations and definitions. Finding a private function should just be a matter of scanning downward from the first usage.
    
    G11: Inconsistency
    If you do something a certain way, do all similar things in the same way. This goes back to the principle of least surprise. Be careful with the conventions you choose, and once chosen, be careful to continue to follow them.
    
    If within a particular function you use a variable named response to hold an HttpServletResponse, then use the same variable name consistently in the other functions that use HttpServletResponse objects. If you name a method processVerificationRequest, then use a similar name, such as processDeletionRequest, for the methods that process other kinds of requests.
    
    Simple consistency like this, when reliably applied, can make code much easier to read and modify.
    
    G12: Clutter
    Of what use is a default constructor with no implementation? All it serves to do is clutter up the code with meaningless artifacts. Variables that aren’t used, functions that are never called, comments that add no information, and so forth. All these things are clutter and should be removed. Keep your source files clean, well organized, and free of clutter.
    
    G13: Artificial Coupling
    Things that don’t depend upon each other should not be artificially coupled. For example, general enums should not be contained within more specific classes because this forces the whole application to know about these more specific classes. The same goes for general purpose static functions being declared in specific classes.
    
    In general an artificial coupling is a coupling between two modules that serves no direct purpose. It is a result of putting a variable, constant, or function in a temporarily convenient, though inappropriate, location. This is lazy and careless.
    
    Take the time to figure out where functions, constants, and variables ought to be declared. Don’t just toss them in the most convenient place at hand and then leave them there.
    
    G14: Feature Envy
    This is one of Martin Fowler’s code smells.6 The methods of a class should be interested in the variables and functions of the class they belong to, and not the variables and functions of other classes. When a method uses accessors and mutators of some other object to manipulate the data within that object, then it envies the scope of the class of that other object. It wishes that it were inside that other class so that it could have direct access to the variables it is manipulating. For example:
    
    6. [Refactoring].
    ```java
       public class HourlyPayCalculator {
         public Money calculateWeeklyPay(HourlyEmployee e) {
           int tenthRate = e.getTenthRate().getPennies();
           int tenthsWorked = e.getTenthsWorked();
           int straightTime = Math.min(400, tenthsWorked);
           int overTime = Math.max(0, tenthsWorked - straightTime);
           int straightPay = straightTime * tenthRate;
           int overtimePay = (int)Math.round(overTime*tenthRate*1.5);
           return new Money(straightPay + overtimePay);
         }
       }
    ```
    The calculateWeeklyPay method reaches into the HourlyEmployee object to get the data on which it operates. The calculateWeeklyPay method envies the scope of HourlyEmployee. It “wishes” that it could be inside HourlyEmployee.
    
    All else being equal, we want to eliminate Feature Envy because it exposes the internals of one class to another. Sometimes, however, Feature Envy is a necessary evil. Consider the following:
    ```java
       public class HourlyEmployeeReport {
         private HourlyEmployee employee ;
     
         public HourlyEmployeeReport(HourlyEmployee e) {
           this.employee = e;
         }
     
         String reportHours() {
           return String.format(
             “Name: %s\tHours:%d.%1d\n”,
             employee.getName(),
             employee.getTenthsWorked()/10,
             employee.getTenthsWorked()%10);
         }
       }
    ```
    Clearly, the reportHours method envies the HourlyEmployee class. On the other hand, we don’t want HourlyEmployee to have to know about the format of the report. Moving that format string into the HourlyEmployee class would violate several principles of object oriented design.7 It would couple HourlyEmployee to the format of the report, exposing it to changes in that format.
    
    7. Specifically, the Single Responsibility Principle, the Open Closed Principle, and the Common Closure Principle. See [PPP].
    
    G15: Selector Arguments
    There is hardly anything more abominable than a dangling false argument at the end of a function call. What does it mean? What would it change if it were true? Not only is the purpose of a selector argument difficult to remember, each selector argument combines many functions into one. Selector arguments are just a lazy way to avoid splitting a large function into several smaller functions. Consider:
    ```java
       public int calculateWeeklyPay(boolean overtime) {
         int tenthRate = getTenthRate();
         int tenthsWorked = getTenthsWorked();
         int straightTime = Math.min(400, tenthsWorked);
         int overTime = Math.max(0, tenthsWorked - straightTime);
         int straightPay = straightTime * tenthRate;
         double overtimeRate = overtime ? 1.5 : 1.0 * tenthRate;
         int overtimePay = (int)Math.round(overTime*overtimeRate);
         return straightPay + overtimePay;
       }
    ```
    You call this function with a true if overtime is paid as time and a half, and with a false if overtime is paid as straight time. It’s bad enough that you must remember what calculateWeeklyPay(false) means whenever you happen to stumble across it. But the real shame of a function like this is that the author missed the opportunity to write the following:
    ```java
       public int straightPay() {
         return getTenthsWorked() * getTenthRate();
       }
     
       public int overTimePay() {
         int overTimeTenths = Math.max(0, getTenthsWorked() - 400);
         int overTimePay = overTimeBonus(overTimeTenths);
         return straightPay() + overTimePay;
       }
     
       private int overTimeBonus(int overTimeTenths) {
         double bonus = 0.5 * getTenthRate() * overTimeTenths;
         return (int) Math.round(bonus);
       }
    ```
    Of course, selectors need not be boolean. They can be enums, integers, or any other type of argument that is used to select the behavior of the function. In general it is better to have many functions than to pass some code into a function to select the behavior.
    
    G16: Obscured Intent
    We want code to be as expressive as possible. Run-on expressions, Hungarian notation, and magic numbers all obscure the author’s intent. For example, here is the overTimePay function as it might have appeared:
    ```java
       public int m_otCalc() {
       return iThsWkd * iThsRte +
         (int) Math.round(0.5 * iThsRte *
           Math.max(0, iThsWkd - 400)
         );
       }
    ```
    Small and dense as this might appear, it’s also virtually impenetrable. It is worth taking the time to make the intent of our code visible to our readers.
    
    G17: Misplaced Responsibility
    One of the most important decisions a software developer can make is where to put code. For example, where should the PI constant go? Should it be in the Math class? Perhaps it belongs in the Trigonometry class? Or maybe in the Circle class?
    
    The principle of least surprise comes into play here. Code should be placed where a reader would naturally expect it to be. The PI constant should go where the trig functions are declared. The OVERTIME_RATE constant should be declared in the HourlyPay-Calculator class.
    
    Sometimes we get “clever” about where to put certain functionality. We’ll put it in a function that’s convenient for us, but not necessarily intuitive to the reader. For example, perhaps we need to print a report with the total of hours that an employee worked. We could sum up those hours in the code that prints the report, or we could try to keep a running total in the code that accepts time cards.
    
    One way to make this decision is to look at the names of the functions. Let’s say that our report module has a function named getTotalHours. Let’s also say that the module that accepts time cards has a saveTimeCard function. Which of these two functions, by it’s name, implies that it calculates the total? The answer should be obvious.
    
    Clearly, there are sometimes performance reasons why the total should be calculated as time cards are accepted rather than when the report is printed. That’s fine, but the names of the functions ought to reflect this. For example, there should be a computeRunning-TotalOfHours function in the timecard module.
    
    G18: Inappropriate Static
    Math.max(double a, double b) is a good static method. It does not operate on a single instance; indeed, it would be silly to have to say new Math().max(a,b) or even a.max(b). All the data that max uses comes from its two arguments, and not from any “owning” object. More to the point, there is almost no chance that we’d want Math.max to be polymorphic.
    
    Sometimes, however, we write static functions that should not be static. For example, consider:
    ```java
       HourlyPayCalculator.calculatePay(employee, overtimeRate).
    ```
    Again, this seems like a reasonable static function. It doesn’t operate on any particular object and gets all it’s data from it’s arguments. However, there is a reasonable chance that we’ll want this function to be polymorphic. We may wish to implement several different algorithms for calculating hourly pay, for example, OvertimeHourlyPayCalculator and StraightTimeHourlyPayCalculator. So in this case the function should not be static. It should be a nonstatic member function of Employee.
    
    In general you should prefer nonstatic methods to static methods. When in doubt, make the function nonstatic. If you really want a function to be static, make sure that there is no chance that you’ll want it to behave polymorphically.
    
    G19: Use Explanatory Variables
    Kent Beck wrote about this in his great book Smalltalk Best Practice Patterns8 and again more recently in his equally great book Implementation Patterns.9 One of the more powerful ways to make a program readable is to break the calculations up into intermediate values that are held in variables with meaningful names.
    
    8. [Beck97], p. 108.
    
    9. [Beck07].
    
    Consider this example from FitNesse:
    ```java
       Matcher match = headerPattern.matcher(line);
       if(match.find())
       {
         String key = match.group(1);
         String value = match.group(2);
         headers.put(key.toLowerCase(), value);
       }
    ```
    The simple use of explanatory variables makes it clear that the first matched group is the key, and the second matched group is the value.
    
    It is hard to overdo this. More explanatory variables are generally better than fewer. It is remarkable how an opaque module can suddenly become transparent simply by breaking the calculations up into well-named intermediate values.
    
    G20: Function Names Should Say What They Do
    
    
    Look at this code:
    ```java
       Date newDate = date.add(5);
    ```
    Would you expect this to add five days to the date? Or is it weeks, or hours? Is the date instance changed or does the function just return a new Date without changing the old one? You can’t tell from the call what the function does.
    
    If the function adds five days to the date and changes the date, then it should be called addDaysTo or increaseByDays. If, on the other hand, the function returns a new date that is five days later but does not change the date instance, it should be called daysLater or daysSince.
    
    If you have to look at the implementation (or documentation) of the function to know what it does, then you should work to find a better name or rearrange the functionality so that it can be placed in functions with better names.
    
    G21: Understand the Algorithm
    Lots of very funny code is written because people don’t take the time to understand the algorithm. They get something to work by plugging in enough if statements and flags, without really stopping to consider what is really going on.
    
    Programming is often an exploration. You think you know the right algorithm for something, but then you wind up fiddling with it, prodding and poking at it, until you get it to “work.” How do you know it “works”? Because it passes the test cases you can think of.
    
    There is nothing wrong with this approach. Indeed, often it is the only way to get a function to do what you think it should. However, it is not sufficient to leave the quotation marks around the word “work.”
    
    Before you consider yourself to be done with a function, make sure you understand how it works. It is not good enough that it passes all the tests. You must know10 that the solution is correct.
    
    10. There is a difference between knowing how the code works and knowing whether the algorithm will do the job required of it. Being unsure that an algorithm is appropriate is often a fact of life. Being unsure what your code does is just laziness.
    
    Often the best way to gain this knowledge and understanding is to refactor the function into something that is so clean and expressive that it is obvious how it works.
    
    G22: Make Logical Dependencies Physical
    If one module depends upon another, that dependency should be physical, not just logical. The dependent module should not make assumptions (in other words, logical dependencies) about the module it depends upon. Rather it should explicitly ask that module for all the information it depends upon.
    
    For example, imagine that you are writing a function that prints a plain text report of hours worked by employees. One class named HourlyReporter gathers all the data into a convenient form and then passes it to HourlyReportFormatter to print it. (See Listing 17-1.)
    
    
    Listing 17-1 HourlyReporter.java
    ```java
       public class HourlyReporter {
         private HourlyReportFormatter formatter;
         private List page;
         private final int PAGE_SIZE = 55;
     
         public HourlyReporter(HourlyReportFormatter formatter) {
           this.formatter = formatter;
           page = new ArrayList();
         }
     
         public void generateReport(List employees) {
           for (HourlyEmployee e : employees) {
             addLineItemToPage(e);
             if (page.size() == PAGE_SIZE)
               printAndClearItemList();
           }
           if (page.size() > 0)
             printAndClearItemList();
         }
     
         private void printAndClearItemList() {
           formatter.format(page);
           page.clear();
         }
     
         private void addLineItemToPage(HourlyEmployee e) {
           LineItem item = new LineItem();
           item.name = e.getName();
           item.hours = e.getTenthsWorked() / 10;
    
           item.tenths = e.getTenthsWorked() % 10;
           page.add(item);
         }
     
         public class LineItem {
           public String name;
           public int hours;
           public int tenths;
         }
       }
    ```
    This code has a logical dependency that has not been physicalized. Can you spot it? It is the constant PAGE_SIZE. Why should the HourlyReporter know the size of the page? Page size should be the responsibility of the HourlyReportFormatter.
    
    The fact that PAGE_SIZE is declared in HourlyReporter represents a misplaced responsibility [G17] that causes HourlyReporter to assume that it knows what the page size ought to be. Such an assumption is a logical dependency. HourlyReporter depends on the fact that HourlyReportFormatter can deal with page sizes of 55. If some implementation of HourlyReportFormatter could not deal with such sizes, then there would be an error.
    
    We can physicalize this dependency by creating a new method in HourlyReport-Formatter named getMaxPageSize(). HourlyReporter will then call that function rather than using the PAGE_SIZE constant.
    
    G23: Prefer Polymorphism to If/Else or Switch/Case
    This might seem a strange suggestion given the topic of Chapter 6. After all, in that chapter I make the point that switch statements are probably appropriate in the parts of the system where adding new functions is more likely than adding new types.
    
    First, most people use switch statements because it’s the obvious brute force solution, not because it’s the right solution for the situation. So this heuristic is here to remind us to consider polymorphism before using a switch.
    
    Second, the cases where functions are more volatile than types are relatively rare. So every switch statement should be suspect.
    
    I use the following “ONE SWITCH” rule: There may be no more than one switch statement for a given type of selection. The cases in that switch statement must create polymorphic objects that take the place of other such switch statements in the rest of the system.
    
    G24: Follow Standard Conventions
    Every team should follow a coding standard based on common industry norms. This coding standard should specify things like where to declare instance variables; how to name classes, methods, and variables; where to put braces; and so on. The team should not need a document to describe these conventions because their code provides the examples.
    
    Everyone on the team should follow these conventions. This means that each team member must be mature enough to realize that it doesn’t matter a whit where you put your braces so long as you all agree on where to put them.
    
    If you would like to know what conventions I follow, you’ll see them in the refactored code in Listing B-7 on page 394, through Listing B-14.
    
    G25: Replace Magic Numbers with Named Constants
    
    
    This is probably one of the oldest rules in software development. I remember reading it in the late sixties in introductory COBOL, FORTRAN, and PL/1 manuals. In general it is a bad idea to have raw numbers in your code. You should hide them behind well-named constants.
    
    For example, the number 86,400 should be hidden behind the constant SECONDS_PER_DAY. If you are printing 55 lines per page, then the constant 55 should be hidden behind the constant LINES_PER_PAGE.
    
    Some constants are so easy to recognize that they don’t always need a named constant to hide behind so long as they are used in conjunction with very self-explanatory code. For example:
    ```java
       double milesWalked = feetWalked/5280.0;
       int dailyPay = hourlyRate * 8;
       double circumference = radius * Math.PI * 2;
    ```
    Do we really need the constants FEET_PER_MILE, WORK_HOURS_PER_DAY, and TWO in the above examples? Clearly, the last case is absurd. There are some formulae in which constants are simply better written as raw numbers. You might quibble about the WORK_HOURS_PER_DAY case because the laws or conventions might change. On the other hand, that formula reads so nicely with the 8 in it that I would be reluctant to add 17 extra characters to the readers’ burden. And in the FEET_PER_MILE case, the number 5280 is so very well known and so unique a constant that readers would recognize it even if it stood alone on a page with no context surrounding it.
    
    Constants like 3.141592653589793 are also very well known and easily recognizable. However, the chance for error is too great to leave them raw. Every time someone sees 3.1415927535890793, they know that it is π, and so they fail to scrutinize it. (Did you catch the single-digit error?) We also don’t want people using 3.14, 3.14159, 3.142, and so forth. Therefore, it is a good thing that Math.PI has already been defined for us.
    
    The term “Magic Number” does not apply only to numbers. It applies to any token that has a value that is not self-describing. For example:
    ```java
       assertEquals(7777, Employee.find(“John Doe”).employeeNumber());
    ```
    There are two magic numbers in this assertion. The first is obviously 7777, though what it might mean is not obvious. The second magic number is “John Doe,” and again the intent is not clear.
    
    It turns out that “John Doe” is the name of employee #7777 in a well-known test database created by our team. Everyone in the team knows that when you connect to this database, it will have several employees already cooked into it with well-known values and attributes. It also turns out that “John Doe” represents the sole hourly employee in that test database. So this test should really read:
    ```java
       assertEquals(
         HOURLY_EMPLOYEE_ID,
         Employee.find(HOURLY_EMPLOYEE_NAME).employeeNumber());
    ```
    G26: Be Precise
    Expecting the first match to be the only match to a query is probably naive. Using floating point numbers to represent currency is almost criminal. Avoiding locks and/or transaction management because you don’t think concurrent update is likely is lazy at best. Declaring a variable to be an ArrayList when a List will due is overly constraining. Making all variables protected by default is not constraining enough.
    
    When you make a decision in your code, make sure you make it precisely. Know why you have made it and how you will deal with any exceptions. Don’t be lazy about the precision of your decisions. If you decide to call a function that might return null, make sure you check for null. If you query for what you think is the only record in the database, make sure your code checks to be sure there aren’t others. If you need to deal with currency, use integers11 and deal with rounding appropriately. If there is the possibility of concurrent update, make sure you implement some kind of locking mechanism.
    
    11. Or better yet, a Money class that uses integers.
    
    Ambiguities and imprecision in code are either a result of disagreements or laziness. In either case they should be eliminated.
    
    G27: Structure over Convention
    
    
    Enforce design decisions with structure over convention. Naming conventions are good, but they are inferior to structures that force compliance. For example, switch/cases with nicely named enumerations are inferior to base classes with abstract methods. No one is forced to implement the switch/case statement the same way each time; but the base classes do enforce that concrete classes have all abstract methods implemented.
    
    G28: Encapsulate Conditionals
    Boolean logic is hard enough to understand without having to see it in the context of an if or while statement. Extract functions that explain the intent of the conditional.
    
    For example:
    ```java
       if (shouldBeDeleted(timer))
    ```
    is preferable to
    ```java
       if (timer.hasExpired() && !timer.isRecurrent())
    ```
    G29: Avoid Negative Conditionals
    Negatives are just a bit harder to understand than positives. So, when possible, conditionals should be expressed as positives. For example:
    ```java
       if (buffer.shouldCompact())
    ```
    is preferable to
    ```java
       if (!buffer.shouldNotCompact())
    ```
    G30: Functions Should Do One Thing
    It is often tempting to create functions that have multiple sections that perform a series of operations. Functions of this kind do more than one thing, and should be converted into many smaller functions, each of which does one thing.
    
    For example:
    ```java
       public void pay() {
         for (Employee e : employees) {
           if (e.isPayday()) {
             Money pay = e.calculatePay();
             e.deliverPay(pay);
           }
         }
       }
    ```
    This bit of code does three things. It loops over all the employees, checks to see whether each employee ought to be paid, and then pays the employee. This code would be better written as:
    ```java
       public void pay() {
         for (Employee e : employees)
           payIfNecessary(e);
       }
     
       private void payIfNecessary(Employee e) {
         if (e.isPayday())
           calculateAndDeliverPay(e);
       }
     
       private void calculateAndDeliverPay(Employee e) {
         Money pay = e.calculatePay();
         e.deliverPay(pay);
       }
    ```
    Each of these functions does one thing. (See “Do One Thing” on page 35.)
    
    G31: Hidden Temporal Couplings
    Temporal couplings are often necessary, but you should not hide the coupling. Structure the arguments of your functions such that the order in which they should be called is obvious. Consider the following:
    ```java
       public class MoogDiver {
         Gradient gradient;
         List splines;
     
         public void dive(String reason) {
           saturateGradient();
           reticulateSplines();
           diveForMoog(reason);
         }
         …
       }
    ```
    The order of the three functions is important. You must saturate the gradient before you can reticulate the splines, and only then can you dive for the moog. Unfortunately, the code does not enforce this temporal coupling. Another programmer could call reticulate-Splines before saturateGradient was called, leading to an UnsaturatedGradientException. A better solution is:
    ```java
       public class MoogDiver {
         Gradient gradient;
         List splines;
     
         public void dive(String reason) {
           Gradient gradient = saturateGradient();
           List splines = reticulateSplines(gradient);
           diveForMoog(splines, reason);
         }
         …
       }
    ```
    This exposes the temporal coupling by creating a bucket brigade. Each function produces a result that the next function needs, so there is no reasonable way to call them out of order.
    
    You might complain that this increases the complexity of the functions, and you’d be right. But that extra syntactic complexity exposes the true temporal complexity of the situation.
    
    Note that I left the instance variables in place. I presume that they are needed by private methods in the class. Even so, I want the arguments in place to make the temporal coupling explicit.
    
    G32: Don’t Be Arbitrary
    Have a reason for the way you structure your code, and make sure that reason is communicated by the structure of the code. If a structure appears arbitrary, others will feel empowered to change it. If a structure appears consistently throughout the system, others will use it and preserve the convention. For example, I was recently merging changes to FitNesse and discovered that one of our committers had done this:
    ```java
       public class AliasLinkWidget extends ParentWidget
       {
         public static class VariableExpandingWidgetRoot {
           …
     
         …
       }
    ```
    The problem with this was that VariableExpandingWidgetRoot had no need to be inside the scope of AliasLinkWidget. Moreover, other unrelated classes made use of AliasLinkWidget.VariableExpandingWidgetRoot. These classes had no need to know about AliasLinkWidget.
    
    Perhaps the programmer had plopped the VariableExpandingWidgetRoot into AliasWidget as a matter of convenience, or perhaps he thought it really needed to be scoped inside AliasWidget. Whatever the reason, the result wound up being arbitrary. Public classes that are not utilities of some other class should not be scoped inside another class. The convention is to make them public at the top level of their package.
    
    G33: Encapsulate Boundary Conditions
    Boundary conditions are hard to keep track of. Put the processing for them in one place. Don’t let them leak all over the code. We don’t want swarms of +1s and -1s scattered hither and yon. Consider this simple example from FIT:
    ```java
       if(level + 1 < tags.length)
       {
         parts = new Parse(body, tags, level + 1, offset + endTag);
         body = null;
       }
    ```
    Notice that level+1 appears twice. This is a boundary condition that should be encapsulated within a variable named something like nextLevel.
    ```java
       int nextLevel = level + 1;
       if(nextLevel < tags.length)
       {
         parts = new Parse(body, tags, nextLevel, offset + endTag);
         body = null;
       }
    ```
    G34: Functions Should Descend Only One Level of Abstraction
    The statements within a function should all be written at the same level of abstraction, which should be one level below the operation described by the name of the function. This may be the hardest of these heuristics to interpret and follow. Though the idea is plain enough, humans are just far too good at seamlessly mixing levels of abstraction. Consider, for example, the following code taken from FitNesse:
    ```java
       public String render() throws Exception
       {
         StringBuffer html = new StringBuffer(“ 0)
           html.append(” size=\“”).append(size + 1).append(”\“”);
         html.append(“>”);
     
         return html.toString();
       }
    ```
    A moment’s study and you can see what’s going on. This function constructs the HTML tag that draws a horizontal rule across the page. The height of that rule is specified in the size variable.
    
    Now look again. This method is mixing at least two levels of abstraction. The first is the notion that a horizontal rule has a size. The second is the syntax of the HR tag itself. This code comes from the HruleWidget module in FitNesse. This module detects a row of four or more dashes and converts it into the appropriate HR tag. The more dashes, the larger the size.
    
    I refactored this bit of code as follows. Note that I changed the name of the size field to reflect its true purpose. It held the number of extra dashes.
    ```java
       public String render() throws Exception
       {
         HtmlTag hr = new HtmlTag(“hr”);
         if (extraDashes > 0)
           hr.addAttribute(“size”, hrSize(extraDashes));
         return hr.html();
       }
     
       private String hrSize(int height)
       {
         int hrSize = height + 1;
         return String.format(“%d”, hrSize);
       }
    ```
    This change separates the two levels of abstraction nicely. The render function simply constructs an HR tag, without having to know anything about the HTML syntax of that tag. The HtmlTag module takes care of all the nasty syntax issues.
    
    Indeed, by making this change I caught a subtle error. The original code did not put the closing slash on the HR tag, as the XHTML standard would have it. (In other words, it emitted 
    instead of
    .) The HtmlTag module had been changed to conform to XHTML long ago. Separating levels of abstraction is one of the most important functions of refactoring, and it’s one of the hardest to do well. As an example, look at the code below. This was my first attempt at separating the abstraction levels in the HruleWidget.render method. ```java public String render() throws Exception { HtmlTag hr = new HtmlTag(“hr”); if (size > 0) { hr.addAttribute(“size”, “”+(size+1)); } return hr.html(); } ``` My goal, at this point, was to create the necessary separation and get the tests to pass. I accomplished that goal easily, but the result was a function that still had mixed levels of abstraction. In this case the mixed levels were the construction of the HR tag and the interpretation and formatting of the size variable. This points out that when you break a function along lines of abstraction, you often uncover new lines of abstraction that were obscured by the previous structure. G35: Keep Configurable Data at High Levels If you have a constant such as a default or configuration value that is known and expected at a high level of abstraction, do not bury it in a low-level function. Expose it as an argument to that low-level function called from the high-level function. Consider the following code from FitNesse: ```java public static void main(String[] args) throws Exception { Arguments arguments = parseCommandLine(args); … } public class Arguments { public static final String DEFAULT_PATH = “.”; public static final String DEFAULT_ROOT = “FitNesseRoot”; public static final int DEFAULT_PORT = 80; public static final int DEFAULT_VERSION_DAYS = 14; … } ``` The command-line arguments are parsed in the very first executable line of FitNesse. The default values of those arguments are specified at the top of the Argument class. You don’t have to go looking in low levels of the system for statements like this one: ```java if (arguments.port == 0) // use 80 by default ``` The configuration constants reside at a very high level and are easy to change. They get passed down to the rest of the application. The lower levels of the application do not own the values of these constants. G36: Avoid Transitive Navigation In general we don’t want a single module to know much about its collaborators. More specifically, if A collaborates with B, and B collaborates with C, we don’t want modules that use A to know about C. (For example, we don’t want a.getB().getC().doSomething();.) This is sometimes called the Law of Demeter. The Pragmatic Programmers call it “Writing Shy Code.”12 In either case it comes down to making sure that modules know only about their immediate collaborators and do not know the navigation map of the whole system. 12. [PRAG], p. 138. If many modules used some form of the statement a.getB().getC(), then it would be difficult to change the design and architecture to interpose a Q between B and C. You’d have to find every instance of a.getB().getC() and convert it to a.getB().getQ().getC(). This is how architectures become rigid. Too many modules know too much about the architecture. Rather we want our immediate collaborators to offer all the services we need. We should not have to roam through the object graph of the system, hunting for the method we want to call. Rather we should simply be able to say: ```java myCollaborator.doSomething(). ``` JAVA J1: Avoid Long Import Lists by Using Wildcards If you use two or more classes from a package, then import the whole package with ```java import package.*; ``` Long lists of imports are daunting to the reader. We don’t want to clutter up the tops of our modules with 80 lines of imports. Rather we want the imports to be a concise statement about which packages we collaborate with. Specific imports are hard dependencies, whereas wildcard imports are not. If you specifically import a class, then that class must exist. But if you import a package with a wildcard, no particular classes need to exist. The import statement simply adds the package to the search path when hunting for names. So no true dependency is created by such imports, and they therefore serve to keep our modules less coupled. There are times when the long list of specific imports can be useful. For example, if you are dealing with legacy code and you want to find out what classes you need to build mocks and stubs for, you can walk down the list of specific imports to find out the true qualified names of all those classes and then put the appropriate stubs in place. However, this use for specific imports is very rare. Furthermore, most modern IDEs will allow you to convert the wildcarded imports to a list of specific imports with a single command. So even in the legacy case it’s better to import wildcards. Wildcard imports can sometimes cause name conflicts and ambiguities. Two classes with the same name, but in different packages, will need to be specifically imported, or at least specifically qualified when used. This can be a nuisance but is rare enough that using wildcard imports is still generally better than specific imports. J2: Don’t Inherit Constants I have seen this several times and it always makes me grimace. A programmer puts some constants in an interface and then gains access to those constants by inheriting that interface. Take a look at the following code: ```java public class HourlyEmployee extends Employee { private int tenthsWorked; private double hourlyRate; public Money calculatePay() { int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK); int overTime = tenthsWorked - straightTime; return new Money( hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime) ); } … } ``` Where did the constants TENTHS_PER_WEEK and OVERTIME_RATE come from? They might have come from class Employee; so let’s take a look at that: ```java public abstract class Employee implements PayrollConstants { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } ``` Nope, not there. But then where? Look closely at class Employee. It implements PayrollConstants. ```java public interface PayrollConstants { public static final int TENTHS_PER_WEEK = 400; public static final double OVERTIME_RATE = 1.5; } ``` This is a hideous practice! The constants are hidden at the top of the inheritance hierarchy. Ick! Don’t use inheritance as a way to cheat the scoping rules of the language. Use a static import instead. ```java import static PayrollConstants.*; public class HourlyEmployee extends Employee { private int tenthsWorked; private double hourlyRate; public Money calculatePay() { int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK); int overTime = tenthsWorked - straightTime; return new Money( hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime) ); } … } ``` J3: Constants versus Enums Now that enums have been added to the language (Java 5), use them! Don’t keep using the old trick of public static final ints. The meaning of ints can get lost. The meaning of enums cannot, because they belong to an enumeration that is named. What’s more, study the syntax for enums carefully. They can have methods and fields. This makes them very powerful tools that allow much more expression and flexibility than ints. Consider this variation on the payroll code: ```java public class HourlyEmployee extends Employee { private int tenthsWorked; HourlyPayGrade grade; public Money calculatePay() { int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK); int overTime = tenthsWorked - straightTime; return new Money( grade.rate() * (tenthsWorked + OVERTIME_RATE * overTime) ); } … } public enum HourlyPayGrade { APPRENTICE { public double rate() { return 1.0; } }, LEUTENANT_JOURNEYMAN { public double rate() { return 1.2; } }, JOURNEYMAN { public double rate() { return 1.5; } }, MASTER { public double rate() { return 2.0; } }; public abstract double rate(); } ``` NAMES N1: Choose Descriptive Names Don’t be too quick to choose a name. Make sure the name is descriptive. Remember that meanings tend to drift as software evolves, so frequently reevaluate the appropriateness of the names you choose. This is not just a “feel-good” recommendation. Names in software are 90 percent of what make software readable. You need to take the time to choose them wisely and keep them relevant. Names are too important to treat carelessly. Consider the code below. What does it do? If I show you the code with well-chosen names, it will make perfect sense to you, but like this it’s just a hodge-podge of symbols and magic numbers. ```java public int x() { int q = 0; int z = 0; for (int kk = 0; kk < 10; kk++) { if (l[z] == 10) { q += 10 + (l[z + 1] + l[z + 2]); z += 1; } else if (l[z] + l[z + 1] == 10) { q += 10 + l[z + 2]; z += 2; } else { q += l[z] + l[z + 1]; z += 2; } } return q; } ``` Here is the code the way it should be written. This snippet is actually less complete than the one above. Yet you can infer immediately what it is trying to do, and you could very likely write the missing functions based on that inferred meaning. The magic numbers are no longer magic, and the structure of the algorithm is compellingly descriptive. ```java public int score() { int score = 0; int frame = 0; for (int frameNumber = 0; frameNumber < 10; frameNumber++) { if (isStrike(frame)) { score += 10 + nextTwoBallsForStrike(frame); frame += 1; } else if (isSpare(frame)) { score += 10 + nextBallForSpare(frame); frame += 2; } else { score += twoBallsInFrame(frame); frame += 2; } } return score; } ``` The power of carefully chosen names is that they overload the structure of the code with description. That overloading sets the readers’ expectations about what the other functions in the module do. You can infer the implementation of isStrike() by looking at the code above. When you read the isStrike method, it will be “pretty much what you expected.”13 13. See Ward Cunningham’s quote on page 11. ```java private boolean isStrike(int frame) { return rolls[frame] == 10; } ``` N2: Choose Names at the Appropriate Level of Abstraction Don’t pick names that communicate implementation; choose names the reflect the level of abstraction of the class or function you are working in. This is hard to do. Again, people are just too good at mixing levels of abstractions. Each time you make a pass over your code, you will likely find some variable that is named at too low a level. You should take the opportunity to change those names when you find them. Making code readable requires a dedication to continuous improvement. Consider the Modem interface below: ```java public interface Modem { boolean dial(String phoneNumber); boolean disconnect(); boolean send(char c); char recv(); String getConnectedPhoneNumber(); } ``` At first this looks fine. The functions all seem appropriate. Indeed, for many applications they are. But now consider an application in which some modems aren’t connected by dialling. Rather they are connected permanently by hard wiring them together (think of the cable modems that provide Internet access to most homes nowadays). Perhaps some are connected by sending a port number to a switch over a USB connection. Clearly the notion of phone numbers is at the wrong level of abstraction. A better naming strategy for this scenario might be: ```java public interface Modem { boolean connect(String connectionLocator); boolean disconnect(); boolean send(char c); char recv(); String getConnectedLocator(); } ``` Now the names don’t make any commitments about phone numbers. They can still be used for phone numbers, or they could be used for any other kind of connection strategy. N3: Use Standard Nomenclature Where Possible Names are easier to understand if they are based on existing convention or usage. For example, if you are using the DECORATOR pattern, you should use the word Decorator in the names of the decorating classes. For example, AutoHangupModemDecorator might be the name of a class that decorates a Modem with the ability to automatically hang up at the end of a session. Patterns are just one kind of standard. In Java, for example, functions that convert objects to string representations are often named toString. It is better to follow conventions like these than to invent your own. Teams will often invent their own standard system of names for a particular project. Eric Evans refers to this as a ubiquitous language for the project.14 Your code should use the terms from this language extensively. In short, the more you can use names that are overloaded with special meanings that are relevant to your project, the easier it will be for readers to know what your code is talking about. 14. [DDD]. N4: Unambiguous Names Choose names that make the workings of a function or variable unambiguous. Consider this example from FitNesse: ```java private String doRename() throws Exception { if(refactorReferences) renameReferences(); renamePage(); pathToRename.removeNameFromEnd(); pathToRename.addNameToEnd(newName); return PathParser.render(pathToRename); } ``` The name of this function does not say what the function does except in broad and vague terms. This is emphasized by the fact that there is a function named renamePage inside the function named doRename! What do the names tell you about the difference between the two functions? Nothing. A better name for that function is renamePageAndOptionallyAllReferences. This may seem long, and it is, but it’s only called from one place in the module, so it’s explanatory value outweighs the length. N5: Use Long Names for Long Scopes The length of a name should be related to the length of the scope. You can use very short variable names for tiny scopes, but for big scopes you should use longer names. Variable names like i and j are just fine if their scope is five lines long. Consider this snippet from the old standard “Bowling Game”: ```java private void rollMany(int n, int pins) { for (int i=0; i 软件中随处可见命名。我们给变量、函数、参数、类和封包命名。我们给源代码及源代码所在目录命名。我们给 jar 文件、war 文件和 ear 文件命名。我们命名、命名,不断命名。既然有这么多命名要做,不妨做好它。下文列出了取个好名字的几条简单规则。 ## 2.2 USE INTENTION-REVEALING NAMES 名副其实 It is easy to say that names should reveal intent. What we want to impress upon you is that we are serious about this. Choosing good names takes time but saves more than it takes. So take care with your names and change them when you find better ones. Everyone who reads your code (including you) will be happier if you do. > 名副其实说起来简单。我们想要强调,这事很严肃。选个好名字要花时间,但省下来的时间比花掉的多。注意命名,而且一旦发现有更好的名称,就换掉旧的。这么做,读你代码的人(包括你自己)都会更开心。 The name of a variable, function, or class, should answer all the big questions. It should tell you why it exists, what it does, and how it is used. If a name requires a comment, then the name does not reveal its intent. > 变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。 ```java int d; // elapsed time in days ``` The name d reveals nothing. It does not evoke a sense of elapsed time, nor of days. We should choose a name that specifies what is being measured and the unit of that measurement: > 名称 d 什么也没说明。它没有引起对时间消逝的感觉,更别说以日计了。我们应该选择指明了计量对象和计量单位的名称: ```java int elapsedTimeInDays; int daysSinceCreation; int daysSinceModification; int fileAgeInDays; ``` Choosing names that reveal intent can make it much easier to understand and change code. What is the purpose of this code? > 选择体现本意的名称能让人更容易理解和修改代码。下列代码的目的何在? ```java public List getThem() { List list1 = new ArrayList(); for (int[] x : theList) if (x[0] == 4) list1.add(x); return list1; } ``` Why is it hard to tell what this code is doing? There are no complex expressions. Spacing and indentation are reasonable. There are only three variables and two constants mentioned. There aren’t even any fancy classes or polymorphic methods, just a list of arrays (or so it seems). > 为什么难以说明上列代码要做什么事?里面并没有复杂的表达式。空格和缩进中规中矩。只用到三个变量和两个常量。甚至没有涉及任何其他类或多态方法,只是(或者看起来是)一个数组的列表而已。 The problem isn’t the simplicity of the code but the implicity of the code (to coin a phrase): the degree to which the context is not explicit in the code itself. The code implicitly requires that we know the answers to questions such as: > 问题不在于代码的简洁度,而是在于代码的模糊度:即上下文在代码中未被明确体现的程度。上列代码要求我们了解类似以下问题的答案: 1. What kinds of things are in theList? 2. What is the significance of the zeroth subscript of an item in theList? 3. What is the significance of the value 4? 4. How would I use the list being returned? --- > 1. theList 中是什么类型的东西? > 2. theList 零下标条目的意义是什么? > 3. 值 4 的意义是什么? > 4. 我怎么使用返回的列表? The answers to these questions are not present in the code sample, but they could have been. Say that we’re working in a mine sweeper game. We find that the board is a list of cells called theList. Let’s rename that to gameBoard. > 问题的答案没体现在代码段中,可那就是它们该在的地方。比方说,我们在开发一种扫雷游戏,我们发现,盘面是名为 theList 的单元格列表,那就将其名称改为 gameBoard。 Each cell on the board is represented by a simple array. We further find that the zeroth subscript is the location of a status value and that a status value of 4 means “flagged.” Just by giving these concepts names we can improve the code considerably: > 盘面上每个单元格都用一个简单数组表示。我们还发现,零下标条目是一种状态值,而该种状态值为 4 表示“已标记”。只要改为有意义的名称,代码就会得到相当程度的改进: ```java public List getFlaggedCells() { List flaggedCells = new ArrayList(); for (int[] cell : gameBoard) if (cell[STATUS_VALUE] == FLAGGED) flaggedCells.add(cell); return flaggedCells; } ``` Notice that the simplicity of the code has not changed. It still has exactly the same number of operators and constants, with exactly the same number of nesting levels. But the code has become much more explicit. > 注意,代码的简洁性并未被触及。运算符和常量的数量全然保持不变,嵌套数量也全然保持不变。但代码变得明确多了。 We can go further and write a simple class for cells instead of using an array of ints. It can include an intention-revealing function (call it isFlagged) to hide the magic numbers. It results in a new version of the function: > 还可以更进一步,不用 int 数组表示单元格,而是另写一个类。该类包括一个名副其实的函数(称为 isFlagged),从而掩盖住那个魔术数[1]。于是得到函数的新版本: ```java public List getFlaggedCells() { List flaggedCells = new ArrayList(); for (Cell cell : gameBoard) if (cell.isFlagged()) flaggedCells.add(cell); return flaggedCells; } ``` With these simple name changes, it’s not difficult to understand what’s going on. This is the power of choosing good names. > 只要简单改一下名称,就能轻易知道发生了什么。这就是选用好名称的力量。 ## 2.3 AVOID DISINFORMATION 避免误导 Programmers must avoid leaving false clues that obscure the meaning of code. We should avoid words whose entrenched meanings vary from our intended meaning. For example, hp, aix, and sco would be poor variable names because they are the names of Unix platforms or variants. Even if you are coding a hypotenuse and hp looks like a good abbreviation, it could be disinformative. > 程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。例如,hp、aix 和 sco 都不该用做变量名,因为它们都是 UNIX 平台或类 UNIX 平台的专有名称。即便你是在编写三角计算程序, hp 看起来是个不错的缩写[2],但那也可能会提供错误信息。 Do not refer to a grouping of accounts as an accountList unless it’s actually a List. The word list means something specific to programmers. If the container holding the accounts is not actually a List, it may lead to false conclusions.1 So accountGroup or bunchOfAccounts or just plain accounts would be better. > 别用 accountList 来指称一组账号,除非它真的是 List 类型。List 一词对程序员有特殊意义。如果包纳账号的容器并非真是个 List,就会引起错误的判断[3]。所以,用 accountGroup 或 bunchOfAccounts,甚至直接用 accounts 都会好一些。 1. As we’ll see later on, even if the container is a List, it’s probably better not to encode the container type into the name. Beware of using names which vary in small ways. How long does it take to spot the subtle difference between a XYZControllerForEfficientHandlingOfStrings in one module and, somewhere a little more distant, XYZControllerForEfficientStorageOfStrings? The words have frightfully similar shapes. > 提防使用不同之处较小的名称。想区分模块中某处的 XYZControllerFor EfficientHandlingOfStrings 和另一处的 XYZControllerForEfficientStorageOfStrings,会花多长时间呢?这两个词外形实在太相似了。 Spelling similar concepts similarly is information. Using inconsistent spellings is disinformation. With modern Java environments we enjoy automatic code completion. We write a few characters of a name and press some hotkey combination (if that) and are rewarded with a list of possible completions for that name. It is very helpful if names for very similar things sort together alphabetically and if the differences are very obvious, because the developer is likely to pick an object by name without seeing your copious comments or even the list of methods supplied by that class. > 以同样的方式拼写出同样的概念才是信息。拼写前后不一致就是误导。我们很享受现代 Java 编程环境的自动代码完成特性。键入某个名称的前几个字母,按一下某个热键组合(如果有的话),就能得到一列该名称的可能形式。假如相似的名称依字母顺序放在一起,且差异很明显,那就会相当有助益,因为程序员多半会压根不看你的详细注释,甚至不看该类的方法列表就直接看名字挑一个对象。 A truly awful example of disinformative names would be the use of lower-case L or uppercase O as variable names, especially in combination. The problem, of course, is that they look almost entirely like the constants one and zero, respectively. > 误导性名称真正可怕的例子,是用小写字母 l 和大写字母 O 作为变量名,尤其是在组合使用的时候。当然,问题在于它们看起来完全像是常量“壹”和“零”。 ```java int a = l; if ( O == l ) a = O1; else l = 01; ``` The reader may think this a contrivance, but we have examined code where such things were abundant. In one case the author of the code suggested using a different font so that the differences were more obvious, a solution that would have to be passed down to all future developers as oral tradition or in a written document. The problem is conquered with finality and without creating new work products by a simple renaming. > 读者可能会认为这纯属虚构,但我们确曾见过充斥这类玩意的代码。有一次,代码作者建议用不同字体写变量名,好显得更清楚些,不过这种方案得要通过口头和书面传递给未来所有的开发者才行。后来,只是做了简单的重命名操作,就解决了问题,而且也没搞出别的事。 ## 2.4 MAKE MEANINGFUL DISTINCTIONS 做有意义的区分 ![](figures/ch2/2_2fig_martin.jpg) Programmers create problems for themselves when they write code solely to satisfy a compiler or interpreter. For example, because you can’t use the same name to refer to two different things in the same scope, you might be tempted to change one name in an arbitrary way. Sometimes this is done by misspelling one, leading to the surprising situation where correcting spelling errors leads to an inability to compile.2 > 如果程序员只是为满足编译器或解释器的需要而写代码,就会制造麻烦。例如,因为同一作用范围内两样不同的东西不能重名,你可能会随手改掉其中一个的名称。有时干脆以错误的拼写充数,结果就是出现在更正拼写错误后导致编译器出错的情况。 2. Consider, for example, the truly hideous practice of creating a variable named klass just because the name class was used for something else. It is not sufficient to add number series or noise words, even though the compiler is satisfied. If names must be different, then they should also mean something different. > 光是添加数字系列或是废话远远不够,即便这足以让编译器满意。如果名称必须相异,那其意思也应该不同才对。 Number-series naming (a1, a2, .. aN) is the opposite of intentional naming. Such names are not disinformative—they are noninformative; they provide no clue to the author’s intention. Consider: > 以数字系列命名(a1、a2,……aN)是依义命名的对立面。这样的名称纯属误导——完全没有提供正确信息;没有提供导向作者意图的线索。试看: ```java public static void copyChars(char a1[], char a2[]) { for (int i = 0; i < a1.length; i++) { a2[i] = a1[i]; } } ``` This function reads much better when source and destination are used for the argument names. > 如果参数名改为 source 和 destination,这个函数就会像样许多。 Noise words are another meaningless distinction. Imagine that you have a Product class. If you have another called ProductInfo or ProductData, you have made the names different without making them mean anything different. Info and Data are indistinct noise words like a, an, and the. > 废话是另一种没意义的区分。假设你有一个 Product 类。如果还有一个 ProductInfo 或 ProductData 类,那它们的名称虽然不同,意思却无区别。Info 和 Data 就像 a、an 和 the 一样,是意义含混的废话。 Note that there is nothing wrong with using prefix conventions like a and the so long as they make a meaningful distinction. For example you might use a for all local variables and the for all function arguments.3 The problem comes in when you decide to call a variable theZork because you already have another variable named zork. > 注意,只要体现出有意义的区分,使用 a 和 the 这样的前缀就没错。例如,你可能把 a 用在域内变量,而把 the 用于函数参数[5]。但如果你已经有一个名为 zork 的变量,又想调用一个名为 theZork 的变量,麻烦就来了。 3. Uncle Bob used to do this in C++ but has given up the practice because modern IDEs make it unnecessary. Noise words are redundant. The word variable should never appear in a variable name. The word table should never appear in a table name. How is NameString better than Name? Would a Name ever be a floating point number? If so, it breaks an earlier rule about disinformation. Imagine finding one class named Customer and another named CustomerObject. What should you understand as the distinction? Which one will represent the best path to a customer’s payment history? > 废话都是冗余。Variable 一词永远不应当出现在变量名中。Table 一词永远不应当出现在表名中。NameString 会比 Name 好吗?难道 Name 会是一个浮点数不成?如果是这样,就触犯了关于误导的规则。设想有个名为 Customer 的类,还有一个名为 CustomerObject 的类。区别何在呢?哪一个是表示客户历史支付情况的最佳途径? There is an application we know of where this is illustrated. we’ve changed the names to protect the guilty, but here’s the exact form of the error: > 有个应用反映了这种状况。为当事者讳,我们改了一下,不过犯错的代码的确就是这个样子: ```java getActiveAccount(); getActiveAccounts(); getActiveAccountInfo(); ``` How are the programmers in this project supposed to know which of these functions to call? > 程序员怎么能知道该调用哪个函数呢? In the absence of specific conventions, the variable moneyAmount is indistinguishable from money, customerInfo is indistinguishable from customer, accountData is indistinguishable from account, and theMessage is indistinguishable from message. Distinguish names in such a way that the reader knows what the differences offer. > 如果缺少明确约定,变量 moneyAmount 就与 money 没区别, customerInfo 与 customer 没区别,accountData 与 account 没区别, theMessage 也与 message 没区别。要区分名称,就要以读者能鉴别不同之处的方式来区分。 ## 2.5 USE PRONOUNCEABLE NAMES 使用读得出来的名称 Humans are good at words. A significant part of our brains is dedicated to the concept of words. And words are, by definition, pronounceable. It would be a shame not to take advantage of that huge portion of our brains that has evolved to deal with spoken language. So make your names pronounceable. > 人类长于记忆和使用单词。大脑的相当一部分就是用来容纳和处理单词的。单词能读得出来。人类进化到大脑中有那么大的一块地方用来处理言语,若不善加利用,实在是种耻辱。 If you can’t pronounce it, you can’t discuss it without sounding like an idiot. “Well, over here on the bee cee arr three cee enn tee we have a pee ess zee kyew int, see?” This matters because programming is a social activity. > 如果名称读不出来,讨论的时候就会像个傻鸟。“哎,这儿,鼻涕阿三喜摁踢(bee cee arr three cee enn tee)[6]上头,有个皮挨死极翘(pee ess zee kyew)[7]整数,看见没?”这不是小事,因为编程本就是一种社会活动。 A company I know has genymdhms (generation date, year, month, day, hour, minute, and second) so they walked around saying “gen why emm dee aich emm ess”. I have an annoying habit of pronouncing everything as written, so I started saying “gen-yah-muddahims.” It later was being called this by a host of designers and analysts, and we still sounded silly. But we were in on the joke, so it was fun. Fun or not, we were tolerating poor naming. New developers had to have the variables explained to them, and then they spoke about it in silly made-up words instead of using proper English terms. Compare > 有家公司,程序里面写了个 genymdhms(生成日期,年、月、日、时、分、秒),他们一般读作“gen why emm dee aich emm ess”[8]。我有个见字照读的恶习,于是开口就念“gen-yah-mudda-hims”。后来好些设计师和分析师都有样学样,听起来傻乎乎的。我们知道典故,所以会觉得很搞笑。搞笑归搞笑,实际是在强忍糟糕的命名。在给新开发者解释变量的意义时,他们总是读出傻乎乎的自造词,而非恰当的英语词。比较 ```java class DtaRcrd102 { private Date genymdhms; private Date modymdhms; private final String pszqint = ”102”; /* … */ }; ``` to > 和 ```java class Customer { private Date generationTimestamp; private Date modificationTimestamp;; private final String recordId = ”102”; /* … */ }; ``` Intelligent conversation is now possible: “Hey, Mikey, take a look at this record! The generation timestamp is set to tomorrow’s date! How can that be?” 现在读起来就像人话了:“喂,Mikey,看看这条记录!生成时间戳(generation timestamp) [9]被设置为明天了!不能这样吧?” ## 2.6 USE SEARCHABLE NAMES 使用可搜索的名称 Single-letter names and numeric constants have a particular problem in that they are not easy to locate across a body of text. > 单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。 One might easily grep for MAX_CLASSES_PER_STUDENT, but the number 7 could be more troublesome. Searches may turn up the digit as part of file names, other constant definitions, and in various expressions where the value is used with different intent. It is even worse when a constant is a long number and someone might have transposed digits, thereby creating a bug while simultaneously evading the programmer’s search. > 找 MAX_CLASSES_PER_STUDENT 很容易,但想找数字 7 就麻烦了,它可能是某些文件名或其他常量定义的一部分,出现在因不同意图而采用的各种表达式中。如果该常量是个长数字,又被人错改过,就会逃过搜索,从而造成错误。 Likewise, the name e is a poor choice for any variable for which a programmer might need to search. It is the most common letter in the English language and likely to show up in every passage of text in every program. In this regard, longer names trump shorter names, and any searchable name trumps a constant in code. > 同样,e 也不是个便于搜索的好变量名。它是英文中最常用的字母,在每个程序、每段代码中都有可能出现。由此而见,长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。 My personal preference is that single-letter names can ONLY be used as local variables inside short methods. The length of a name should correspond to the size of its scope [N5]. If a variable or constant might be seen or used in multiple places in a body of code, it is imperative to give it a search-friendly name. Once again compare > 窃以为单字母名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应[N5]。若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。再比较 ```java for (int j=0; j<34; j++) { s += (t[j]*4)/5; } ``` to > 和 ```java int realDaysPerIdealDay = 4; const int WORK_DAYS_PER_WEEK = 5; int sum = 0; for (int j=0; j < NUMBER_OF_TASKS; j++) { int realTaskDays = taskEstimate[j] * realDaysPerIdealDay; int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK); sum += realTaskWeeks; } ``` Note that sum, above, is not a particularly useful name but at least is searchable. The intentionally named code makes for a longer function, but consider how much easier it will be to find WORK_DAYS_PER_WEEK than to find all the places where 5 was used and filter the list down to just the instances with the intended meaning. > 注意,上面代码中的 sum 并非特别有用的名称,不过它至少搜得到。采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK 要比数字 5 好找得多,而列表中也只剩下了体现作者意图的名称。 ## 2.7 AVOID ENCODINGS 避免使用编码 We have enough encodings to deal with without adding more to our burden. Encoding type or scope information into names simply adds an extra burden of deciphering. It hardly seems reasonable to require each new employee to learn yet another encoding “language” in addition to learning the (usually considerable) body of code that they’ll be working in. It is an unnecessary mental burden when trying to solve a problem. Encoded names are seldom pronounceable and are easy to mis-type. > 编码已经太多,无谓再自找麻烦。把类型或作用域编进名称里面,徒然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之外(那算是正常的),还要再搞懂另一种编码“语言”。这对于解决问题而言,纯属多余的负担。带编码的名称通常也不便发音,容易打错。 ### 2.7.1 Hungarian Notation 匈牙利语标记法 In days of old, when we worked in name-length-challenged languages, we violated this rule out of necessity, and with regret. Fortran forced encodings by making the first letter a code for the type. Early versions of BASIC allowed only a letter plus one digit. Hungarian Notation (HN) took this to a whole new level. > 在往昔名称长短很要命的时代,我们毫无必要地破坏了不编码的规矩,如今后悔不迭。Fortran 语言要求首字母体现出类型,导致了编码的产生。BASIC 早期版本只允许使用一个字母再加上一位数字。匈牙利语标记法(Hungarian Notation,HN)将这种态势愈演愈烈。 HN was considered to be pretty important back in the Windows C API, when everything was an integer handle or a long pointer or a void pointer, or one of several implementations of “string” (with different uses and attributes). The compiler did not check types in those days, so the programmers needed a crutch to help them remember the types. > 在 Windows 的 C 语言 API 的时代,HN 相当重要,那时所有名称要么是个整数句柄,要么是个长指针或者 void 指针,要不然就是 string 的几种实现(有不同的用途和属性)之一。那时候编译器并不做类型检查,程序员需要匈牙利语标记法来帮助自己记住类型。 In modern languages we have much richer type systems, and the compilers remember and enforce the types. What’s more, there is a trend toward smaller classes and shorter functions so that people can usually see the point of declaration of each variable they’re using. > 现代编程语言具有更丰富的类型系统,编译器也记得并强制使用类型。而且,人们趋向于使用更小的类、更短的方法,好让每个变量的定义都在视野范围之内。 Java programmers don’t need type encoding. Objects are strongly typed, and editing environments have advanced such that they detect a type error long before you can run a compile! So nowadays HN and other forms of type encoding are simply impediments. They make it harder to change the name or type of a variable, function, or class. They make it harder to read the code. And they create the possibility that the encoding system will mislead the reader. > Java 程序员不需要类型编码。对象是强类型的,代码编辑环境已经先进到在编译开始前就侦测到类型错误的程度!所以,如今 HN 和其他类型编码形式都纯属多余。它们增加了修改变量、函数或类的名称或类型的难度。它们增加了阅读代码的难度。它们制造了让编码系统误导读者的可能性。 ```java PhoneNumber phoneString; // name not changed when type changed! ``` ### 2.7.2 Member Prefixes 成员前缀 You also don’t need to prefix member variables with m\_ anymore. Your classes and functions should be small enough that you don’t need them. And you should be using an editing environment that highlights or colorizes members to make them distinct. > 也不必用 m\_前缀来标明成员变量。应当把类和函数做得足够小,消除对成员前缀的需要。你应当使用某种可以高亮或用颜色标出成员的编辑环境。 ```java public class Part { private String m_dsc; // The textual description void setName(String name) { m_dsc = name; } } _________________________________________________ public class Part { String description; void setDescription(String description) { this.description = description; } } ``` Besides, people quickly learn to ignore the prefix (or suffix) to see the meaningful part of the name. The more we read the code, the less we see the prefixes. Eventually the prefixes become unseen clutter and a marker of older code. > 此外,人们会很快学会无视前缀(或后缀),只看到名称中有意义的部分。代码读得越多,眼中就越没有前缀。最终,前缀变作了不入法眼的废料,变作了旧代码的标志物。 ### 2.7.3 Interfaces and Implementations 接口和实现 These are sometimes a special case for encodings. For example, say you are building an ABSTRACT FACTORY for the creation of shapes. This factory will be an interface and will be implemented by a concrete class. What should you name them? IShapeFactory and ShapeFactory? I prefer to leave interfaces unadorned. The preceding I, so common in today’s legacy wads, is a distraction at best and too much information at worst. I don’t want my users knowing that I’m handing them an interface. I just want them to know that it’s a ShapeFactory. So if I must encode either the interface or the implementation, I choose the implementation. Calling it ShapeFactoryImp, or even the hideous CShapeFactory, is preferable to encoding the interface. > 有时也会出现采用编码的特殊情形。比如,你在做一个创建形状用的抽象工厂(Abstract Factory)。该工厂是个接口,要用具体类来实现。你怎么来命名工厂和具体类呢?IShapeFactory 和 ShapeFactory 吗?我喜欢不加修饰的接口。前导字母 I 被滥用到了说好听点是干扰,说难听点根本就是废话的程度。我不想让用户知道我给他们的是接口。我就想让他们知道那是个 ShapeFactory。如果接口和实现必须选一个来编码的话,我宁肯选择实现。ShapeFactoryImp,甚至是丑陋的 CShapeFactory,都比对接口名称编码来得好。 ## 2.8 AVOID MENTAL MAPPING 避免思维映射 Readers shouldn’t have to mentally translate your names into other names they already know. This problem generally arises from a choice to use neither problem domain terms nor solution domain terms. > 不应当让读者在脑中把你的名称翻译为他们熟知的名称。这种问题经常出现在选择是使用问题领域术语还是解决方案领域术语时。 This is a problem with single-letter variable names. Certainly a loop counter may be named i or j or k (though never l!) if its scope is very small and no other names can conflict with it. This is because those single-letter names for loop counters are traditional. However, in most other contexts a single-letter name is a poor choice; it’s just a place holder that the reader must mentally map to the actual concept. There can be no worse reason for using the name c than because a and b were already taken. > 单字母变量名就是个问题。在作用域较小、也没有名称冲突时,循环计数器自然有可能被命名为 i 或 j 或 k。(但千万别用字母 l!)这是因为传统上惯用单字母名称做循环计数器。然而,在多数其他情况下,单字母名称不是个好选择;读者必须在脑中将它映射为真实概念。仅仅是因为有了 a 和 b,就要取名为 c,实在并非像样的理由。 In general programmers are pretty smart people. Smart people sometimes like to show off their smarts by demonstrating their mental juggling abilities. After all, if you can reliably remember that r is the lower-cased version of the url with the host and scheme removed, then you must clearly be very smart. > 程序员通常都是聪明人。聪明人有时会借脑筋急转弯炫耀其聪明。总而言之,假使你记得 r 代表不包含主机名和图式(scheme)的小写字母版 url 的话,那你真是太聪明了。 One difference between a smart programmer and a professional programmer is that the professional understands that clarity is king. Professionals use their powers for good and write code that others can understand. > 聪明程序员和专业程序员之间的区别在于,专业程序员了解,明确是王道。专业程序员善用其能,编写其他人能理解的代码。 ## 2.9 CLASS NAMES 类名 Classes and objects should have noun or noun phrase names like Customer, WikiPage, Account, and AddressParser. Avoid words like Manager, Processor, Data, or Info in the name of a class. A class name should not be a verb. > 类名和对象名应该是名词或名词短语,如 Customer、WikiPage、Account 和 AddressParser。避免使用 Manager、Processor、Data 或 Info 这样的类名。类名不应当是动词。 ## 2.10 METHOD NAMES 方法名 Methods should have verb or verb phrase names like postPayment, deletePage, or save. Accessors, mutators, and predicates should be named for their value and prefixed with get, set, and is according to the javabean standard.4 > 方法名应当是动词或动词短语,如 postPayment、deletePage 或 save。属性访问器、修改器和断言应该根据其值命名,并依 Javabean 标准[10]加上 get、set 和 is 前缀。 4. http://java.sun.com/products/javabeans/docs/spec.html ```java string name = employee.getName(); customer.setName(”mike”); if (paycheck.isPosted())… ``` When constructors are overloaded, use static factory methods with names that describe the arguments. For example, > 重载构造器时,使用描述了参数的静态工厂方法名。例如, ```java Complex fulcrumPoint = Complex.FromRealNumber(23.0); ``` is generally better than > 通常好于 ```java Complex fulcrumPoint = new Complex(23.0); ``` Consider enforcing their use by making the corresponding constructors private. > 可以考虑将相应的构造器设置为 private,强制使用这种命名手段。 ## 2.11 ON’T BE CUTE 别扮可爱 If names are too clever, they will be memorable only to people who share the author’s sense of humor, and only as long as these people remember the joke. Will they know what the function named HolyHandGrenade is supposed to do? Sure, it’s cute, but maybe in this case DeleteItems might be a better name. Choose clarity over entertainment value. > 如果名称太耍宝,那就只有同作者一般有幽默感的人才能记得住,而且还是在他们记得那个笑话的时候才行。谁会知道名为 HolyHandGrenade[11]的函数是用来做什么的呢?没错,这名字挺伶俐,不过 DeleteItems[12]或许是更好的名称。宁可明确,毋为好玩。 ![](figures/ch2/2_3fig_martin.jpg) Cuteness in code often appears in the form of colloquialisms or slang. For example, don’t use the name whack() to mean kill(). Don’t tell little culture-dependent jokes like eatMyShorts() to mean abort(). > 扮可爱的做法在代码中经常体现为使用俗话或俚语。例如,别用 whack( )[13]来表示 kill( )。别用 eatMyShorts( )[14]这类与文化紧密相关的笑话来表示 abort( )。 Say what you mean. Mean what you say. > 言到意到。意到言到。 ## 2.12 PICK ONE WORD PER CONCEPT 每个概念对应一个词 Pick one word for one abstract concept and stick with it. For instance, it’s confusing to have fetch, retrieve, and get as equivalent methods of different classes. How do you remember which method name goes with which class? Sadly, you often have to remember which company, group, or individual wrote the library or class in order to remember which term was used. Otherwise, you spend an awful lot of time browsing through headers and previous code samples. > 给每个抽象概念选一个词,并且一以贯之。例如,使用 fetch、 retrieve 和 get 来给在多个类中的同种方法命名。你怎么记得住哪个类中是哪个方法呢?很悲哀,你总得记住编写库或类的公司、机构或个人,才能想得起来用的是哪个术语。否则,就得耗费大把时间浏览各个文件头及前面的代码。 Modern editing environments like Eclipse and IntelliJ provide context-sensitive clues, such as the list of methods you can call on a given object. But note that the list doesn’t usually give you the comments you wrote around your function names and parameter lists. You are lucky if it gives the parameter names from function declarations. The function names have to stand alone, and they have to be consistent in order for you to pick the correct method without any additional exploration. > Eclipse 和 IntelliJ 之类现代编程环境提供了与环境相关的线索,比如某个对象能调用的方法列表。不过要注意,列表中通常不会给出你为函数名和参数列表编写的注释。如果参数名称来自函数声明,你就太幸运了。函数名称应当独一无二,而且要保持一致,这样你才能不借助多余的浏览就找到正确的方法。 Likewise, it’s confusing to have a controller and a manager and a driver in the same code base. What is the essential difference between a DeviceManager and a Protocol-Controller? Why are both not controllers or both not managers? Are they both Drivers really? The name leads you to expect two objects that have very different type as well as having different classes. > 同样,在同一堆代码中有 controller,又有 manager,还有 driver,就会令人困惑。DeviceManager 和 Protocol-Controller 之间有何根本区别?为什么不全用 controllers 或 managers?他们都是 Drivers 吗?这种名称,让人觉得这两个对象是不同类型的,也分属不同的类。 A consistent lexicon is a great boon to the programmers who must use your code. > 对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音。 ## 2.13 DON’T PUN 别用双关语 Avoid using the same word for two purposes. Using the same term for two different ideas is essentially a pun. > 避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。 If you follow the “one word per concept” rule, you could end up with many classes that have, for example, an add method. As long as the parameter lists and return values of the various add methods are semantically equivalent, all is well. > 如果遵循“一词一义”规则,可能在好多个类里面都会有 add 方法。只要这些 add 方法的参数列表和返回值在语义上等价,就一切顺利。 However one might decide to use the word add for “consistency” when he or she is not in fact adding in the same sense. Let’s say we have many classes where add will create a new value by adding or concatenating two existing values. Now let’s say we are writing a new class that has a method that puts its single parameter into a collection. Should we call this method add? It might seem consistent because we have so many other add methods, but in this case, the semantics are different, so we should use a name like insert or append instead. To call the new method add would be a pun. > 但是,可能会有人决定为“保持一致”而使用 add 这个词来命名,即便并非真的想表示这种意思。比如,在多个类中都有 add 方法,该方法通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一个方法,把单个参数放到群集(collection)中。该把这个方法叫做 add 吗?这样做貌似和其他 add 方法保持了一致,但实际上语义却不同,应该用 insert 或 append 之类词来命名才对。把该方法命名为 add,就是双关语了。 Our goal, as authors, is to make our code as easy as possible to understand. We want our code to be a quick skim, not an intense study. We want to use the popular paperback model whereby the author is responsible for making himself clear and not the academic model where it is the scholar’s job to dig the meaning out of the paper. > 代码作者应尽力写出易于理解的代码。我们想把代码写得让别人能一目尽览,而不必殚精竭虑地研究。我们想要那种大众化的作者尽责写清楚的平装书模式;我们不想要那种学者挖地三尺才能明白个中意义的学院派模式。 ## 2.14 USE SOLUTION DOMAIN NAMES 使用解决方案领域名称 Remember that the people who read your code will be programmers. So go ahead and use computer science (CS) terms, algorithm names, pattern names, math terms, and so forth. It is not wise to draw every name from the problem domain because we don’t want our coworkers to have to run back and forth to the customer asking what every name means when they already know the concept by a different name. > 记住,只有程序员才会读你的代码。所以,尽管用那些计算机科学(Computer Science,CS)术语、算法名、模式名、数学术语吧。依据问题所涉领域来命名可不算是聪明的做法,因为不该让协作者老是跑去问客户每个名称的含义,其实他们早该通过另一名称了解这个概念了。对于熟悉访问者(VISITOR)模式的程序来说,名称 AccountVisitor 富有意义。哪个程序员会不知道 JobQueue 的意思呢?程序员要做太多技术性工作。给这些事取个技术性的名称,通常是最靠谱的做法。 The name AccountVisitor means a great deal to a programmer who is familiar with the VISITOR pattern. What programmer would not know what a JobQueue was? There are lots of very technical things that programmers have to do. Choosing technical names for those things is usually the most appropriate course. ## 2.15 USE PROBLEM DOMAIN NAMES 使用源自所涉问题领域的名称 When there is no “programmer-eese” for what you’re doing, use the name from the problem domain. At least the programmer who maintains your code can ask a domain expert what it means. > 如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了。 Separating solution and problem domain concepts is part of the job of a good programmer and designer. The code that has more to do with problem domain concepts should have names drawn from the problem domain. > 优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。 ## 2.16 ADD MEANINGFUL CONTEXT 添加有意义的语境 There are a few names which are meaningful in and of themselves—most are not. Instead, you need to place names in context for your reader by enclosing them in well-named classes, functions, or namespaces. When all else fails, then prefixing the name may be necessary as a last resort. > 很少有名称是能自我说明的——多数都不能。反之,你需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。 Imagine that you have variables named firstName, lastName, street, houseNumber, city, state, and zipcode. Taken together it’s pretty clear that they form an address. But what if you just saw the state variable being used alone in a method? Would you automatically infer that it was part of an address? > 设想你有名为 firstName、lastName、street、houseNumber、city、state 和 zipcode 的变量。当它们搁一块儿的时候,很明确是构成了一个地址。不过,假使只是在某个方法中看见孤零零一个 state 变量呢?你会理所当然推断那是某个地址的一部分吗? You can add context by using prefixes: addrFirstName, addrLastName, addrState, and so on. At least readers will understand that these variables are part of a larger structure. Of course, a better solution is to create a class named Address. Then, even the compiler knows that the variables belong to a bigger concept. > 可以添加前缀 addrFirstName、addrLastName、addrState 等,以此提供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为 Address 的类。这样,即便是编译器也会知道这些变量隶属某个更大的概念了。 Consider the method in Listing 2-1. Do the variables need a more meaningful context? The function name provides only part of the context; the algorithm provides the rest. Once you read through the function, you see that the three variables, number, verb, and pluralModifier, are part of the “guess statistics” message. Unfortunately, the context must be inferred. When you first look at the method, the meanings of the variables are opaque. > 看看代码清单 2-1 中的方法。以下变量是否需要更有意义的语境呢?函数名仅给出了部分语境;算法提供了剩下的部分。遍览函数后,你会知道 number、verb 和 pluralModifier 这三个变量是“测估”信息的一部分。不幸的是这语境得靠读者推断出来。第一眼看到这个方法时,这些变量的含义完全不清楚。 Listing 2-1 Variables with unclear context. > 代码清单 2-1 语境不明确的变量 ```java private void printGuessStatistics(char candidate, int count) { String number; String verb; String pluralModifier; if (count == 0) { number = ”no”; verb = ”are”; pluralModifier = ”s”; } else if (count == 1) { number = ”1”; verb = ”is”; pluralModifier = ””; } else { number = Integer.toString(count); verb = ”are”; pluralModifier = ”s”; } String guessMessage = String.format( ”There %s %s %s%s”, verb, number, candidate, pluralModifier ); print(guessMessage); } ``` The function is a bit too long and the variables are used throughout. To split the function into smaller pieces we need to create a GuessStatisticsMessage class and make the three variables fields of this class. This provides a clear context for the three variables. They are definitively part of the GuessStatisticsMessage. The improvement of context also allows the algorithm to be made much cleaner by breaking it into many smaller functions. (See Listing 2-2.) > 上列函数有点儿过长,变量的使用贯穿始终。要分解这个函数,需要创建一个名为 GuessStatisticsMessage 的类,把三个变量做成该类的成员字段。这样它们就在定义上变作了 GuessStatisticsMessage 的一部分。语境的增强也让算法能够通过分解为更小的函数而变得更为干净利落。 Listing 2-2 Variables have a context. > (如代码清单 2-2 所示。)代码清单 ```java public class GuessStatisticsMessage { private String number; private String verb; private String pluralModifier; public String make(char candidate, int count) { createPluralDependentMessageParts(count); return String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier ); } private void createPluralDependentMessageParts(int count) { if (count == 0) { thereAreNoLetters(); } else if (count == 1) { thereIsOneLetter(); } else { thereAreManyLetters(count); } } private void thereAreManyLetters(int count) { number = Integer.toString(count); verb = "are"; pluralModifier = "s"; } private void thereIsOneLetter() { number = "1"; verb = "is"; pluralModifier = ""; } private void thereAreNoLetters() { number = "no"; verb = "are"; pluralModifier = "s"; } } ``` ## 2.17 DON’T ADD GRATUITOUS CONTEXT 不要添加没用的语境 In an imaginary application called “Gas Station Deluxe,” it is a bad idea to prefix every class with GSD. Frankly, you are working against your tools. You type G and press the completion key and are rewarded with a mile-long list of every class in the system. Is that wise? Why make it hard for the IDE to help you? > 设若有一个名为“加油站豪华版”(Gas Station Deluxe)的应用,在其中给每个类添加 GSD 前缀就不是什么好点子。说白了,你是在和自己在用的工具过不去。输入 G,按下自动完成键,结果会得到系统中全部类的列表,列表恨不得有一英里那么长。这样做聪明吗?为什么要搞得 IDE 没法帮助你? Likewise, say you invented a MailingAddress class in GSD’s accounting module, and you named it GSDAccountAddress. Later, you need a mailing address for your customer contact application. Do you use GSDAccountAddress? Does it sound like the right name? Ten of 17 characters are redundant or irrelevant. > 再比如,你在 GSD 应用程序中的记账模块创建了一个表示邮件地址的类,然后给该类命名为 GSDAccountAddress。稍后,你的客户联络应用中需要用到邮件地址,你会用 GSDAccountAddress 吗?这名字听起来没问题吗?在这 17 个字母里面,有 10 个字母纯属多余和与当前语境毫无关联。 Shorter names are generally better than longer ones, so long as they are clear. Add no more context to a name than is necessary. > 只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。 The names accountAddress and customerAddress are fine names for instances of the class Address but could be poor names for classes. Address is a fine name for a class. If I need to differentiate between MAC addresses, port addresses, and Web addresses, I might consider PostalAddress, MAC, and URI. The resulting names are more precise, which is the point of all naming. > 对于 Address 类的实体来说,accountAddress 和 customerAddress 都是不错的名称,不过用在类名上就不太好了。Address 是个好类名。如果需要与 MAC 地址、端口地址和 Web 地址相区别,我会考虑使用 PostalAddress、MAC 和 URI。这样的名称更为精确,而精确正是命名的要点。 ## 2.18 FINAL WORDS 最后的话 The hardest thing about choosing good names is that it requires good descriptive skills and a shared cultural background. This is a teaching issue rather than a technical, business, or management issue. As a result many people in this field don’t learn to do it very well. > 取好名字最难的地方在于需要良好的描述技巧和共有文化背景。与其说这是一种技术、商业或管理问题,还不如说是一种教学问题。其结果是,这个领域内的许多人都没能学会做得很好。 People are also afraid of renaming things for fear that some other developers will object. We do not share that fear and find that we are actually grateful when names change (for the better). Most of the time we don’t really memorize the names of classes and methods. We use the modern tools to deal with details like that so we can focus on whether the code reads like paragraphs and sentences, or at least like tables and data structure (a sentence isn’t always the best way to display data). You will probably end up surprising someone when you rename, just like you might with any other code improvement. Don’t let it stop you in your tracks. > 我们有时会怕其他开发者反对重命名。如果讨论一下就知道,如果名称改得更好,那大家真的会感激你。多数时候我们并不记忆类名和方法名。我们使用现代工具对付这些细节,好让自己集中精力于把代码写得就像词句篇章、至少像是表和数据结构(词句并非总是呈现数据的最佳手段)。改名可能会让某人吃惊,就像你做到其他代码改善工作一样。别让这种事阻碍你的前进步伐。 Follow some of these rules and see whether you don’t improve the readability of your code. If you are maintaining someone else’s code, use refactoring tools to help resolve these problems. It will pay off in the short term and continue to pay in the long run. > 不妨试试上面这些规则,看你的代码可读性是否有所提升。如果你是在维护别人写的代码,使用重构工具来解决问题。效果立竿见影,而且会持续下去。 ================================================ FILE: docs/ch3.md ================================================ # 第 3 章 Functions 函数 ![](figures/ch3/3_1fig_martin.jpg) In the early days of programming we composed our systems of routines and subroutines. Then, in the era of Fortran and PL/1 we composed our systems of programs, subprograms, and functions. Nowadays only the function survives from those early days. Functions are the first line of organization in any program. Writing them well is the topic of this chapter. > 在编程的早年岁月,系统由程序和子程序组成。后来,在 Fortran 和 PL/1 的年代,系统由程序、子程序和函数组成。如今,只有函数存活下来。函数是所有程序中的第一组代码。本章将讨论如何写好函数。 Consider the code in Listing 3-1. It’s hard to find a long function in FitNesse,1 but after a bit of searching I came across this one. Not only is it long, but it’s got duplicated code, lots of odd strings, and many strange and inobvious data types and APIs. See how much sense you can make of it in the next three minutes. > 请看代码清单 3-1。在 FitNesse[1]中,很难找到长函数,不过我还是搜寻到一个。它不光长,而且代码也很复杂,有大量字符串、怪异而不显见的数据类型和 API。花 3 分钟时间,看能读懂多少? 1. An open-source testing tool. www.fitnesse.org Listing 3-1 HtmlUtil.java (FitNesse 20070619) > 代码清单 3-1 HtmlUtil.java(FitNesse 20070619) ```java public static String testableHtml( PageData pageData, boolean includeSuiteSetup ) throws Exception { WikiPage wikiPage = pageData.getWikiPage(); StringBuffer buffer = new StringBuffer(); if (pageData.hasAttribute("Test")) { if (includeSuiteSetup) { WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage( SuiteResponder.SUITE_SETUP_NAME, wikiPage ); if (suiteSetup != null) { WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup); String pagePathName = PathParser.render(pagePath); buffer.append("!include -setup .") .append(pagePathName) .append("\n"); } } WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage); if (setup != null) { WikiPagePath setupPath = wikiPage.getPageCrawler().getFullPath(setup); String setupPathName = PathParser.render(setupPath); buffer.append("!include -setup .") .append(setupPathName) .append("\n"); } } buffer.append(pageData.getContent()); if (pageData.hasAttribute("Test")) { WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage); if (teardown != null) { WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown); String tearDownPathName = PathParser.render(tearDownPath); buffer.append("\n") .append("!include -teardown .") .append(tearDownPathName) .append("\n"); } if (includeSuiteSetup) { WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage( SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage ); if (suiteTeardown != null) { WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath (suiteTeardown); String pagePathName = PathParser.render(pagePath); buffer.append("!include -teardown .") .append(pagePathName) .append("\n"); } } } pageData.setContent(buffer.toString()); return pageData.getHtml(); } ``` Do you understand the function after three minutes of study? Probably not. There’s too much going on in there at too many different levels of abstraction. There are strange strings and odd function calls mixed in with doubly nested if statements controlled by flags. > 搞懂这个函数了吗?大概没有。有太多事发生,有太多不同层级的抽象。奇怪的字符串和函数调用,混以双重嵌套、用标识来控制的 if 语句等,不一而足。 However, with just a few simple method extractions, some renaming, and a little restructuring, I was able to capture the intent of the function in the nine lines of Listing 3-2. See whether you can understand that in the next 3 minutes. > 不过,只要做几个简单的方法抽离和重命名操作,加上一点点重构,就能在 9 行代码之内搞掂(如代码清单 3-2 所示)。用 3 分钟阅读以下代码,看你能理解吗? Listing 3-2 HtmlUtil.java (refactored) > 代码清单 3-2 HtmlUtil.java(重构之后) ```java public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite ) throws Exception { boolean isTestPage = pageData.hasAttribute("Test"); if (isTestPage) { WikiPage testPage = pageData.getWikiPage(); StringBuffer newPageContent = new StringBuffer(); includeSetupPages(testPage, newPageContent, isSuite); newPageContent.append(pageData.getContent()); includeTeardownPages(testPage, newPageContent, isSuite); pageData.setContent(newPageContent.toString()); } return pageData.getHtml(); } ``` Unless you are a student of FitNesse, you probably don’t understand all the details. Still, you probably understand that this function performs the inclusion of some setup and teardown pages into a test page and then renders that page into HTML. If you are familiar with JUnit,2 you probably realize that this function belongs to some kind of Web-based testing framework. And, of course, that is correct. Divining that information from Listing 3-2 is pretty easy, but it’s pretty well obscured by Listing 3-1. > 除非你正在研究 FitNesse,否则就理解不了所有细节。不过,你大概能明白,该函数包含把一些设置和拆解页放入一个测试页面,再渲染为 HTML 的操作。如果你熟悉 JUnit[2],或许会想到,该函数归属于某个基于 Web 的测试框架。而且,这当然没错。从代码清单 3-2 中获得信息很容易,而代码清单 3-1 则晦涩难明。 2. An open-source unit-testing tool for Java. www.junit.org So what is it that makes a function like Listing 3-2 easy to read and understand? How can we make a function communicate its intent? What attributes can we give our functions that will allow a casual reader to intuit the kind of program they live inside? > 是什么让代码清单 3-2 易于阅读和理解?怎么才能让函数表达其意图?该给函数赋予哪些属性,好让读者一看就明白函数是属于怎样的程序? ## 3.1 SMALL! 短小 The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. This is not an assertion that I can justify. I can’t provide any references to research that shows that very small functions are better. What I can tell you is that for nearly four decades I have written functions of all different sizes. I’ve written several nasty 3,000-line abominations. I’ve written scads of functions in the 100 to 300 line range. And I’ve written functions that were 20 to 30 lines long. What this experience has taught me, through long trial and error, is that functions should be very small. > 函数的第一规则是要短小。第二条规则是还要更短小。我无法证明这个断言。我给不出任何证实了小函数更好的研究结果。我能说的是,近 40 年来,我写过各种不同大小的函数。我写过令人憎恶的长达 3000 行的厌物,也写过许多 100 行到 300 行的函数,我还写过 20 行到 30 行的。经过漫长的试错,经验告诉我,函数就该小。 In the eighties we used to say that a function should be no bigger than a screen-full. Of course we said that at a time when VT100 screens were 24 lines by 80 columns, and our editors used 4 lines for administrative purposes. Nowadays with a cranked-down font and a nice big monitor, you can fit 150 characters on a line and a 100 lines or more on a screen. Lines should not be 150 characters long. Functions should not be 100 lines long. Functions should hardly ever be 20 lines long. > 在 20 世纪 80 年代,我们常说函数不该长于一屏。当然,说这话的时候,VT100 屏幕只有 24 行、80 列,而编辑器就得先占去 4 行空间放菜单。如今,用上了精致的字体和宽大的显示器,一屏里面可以显示 100 行,每行能容纳 150 个字符。每行都不应该有 150 个字符那么长。函数也不该有 100 行那么长,20 行封顶最佳。 How short should a function be? In 1999 I went to visit Kent Beck at his home in Oregon. We sat down and did some programming together. At one point he showed me a cute little Java/Swing program that he called Sparkle. It produced a visual effect on the screen very similar to the magic wand of the fairy godmother in the movie Cinderella. As you moved the mouse, the sparkles would drip from the cursor with a satisfying scintillation, falling to the bottom of the window through a simulated gravitational field. When Kent showed me the code, I was struck by how small all the functions were. I was used to functions in Swing programs that took up miles of vertical space. Every function in this program was just two, or three, or four lines long. Each was transparently obvious. Each told a story. And each led you to the next in a compelling order. That’s how short your functions should be!3 > 函数到底该有多长?1991 年,我去 Kent Beck 位于奥勒冈州(Oregon)的家中拜访。我们坐到一起写了些代码。他给我看一个叫做 Sparkle(火花闪耀)的有趣的 Java/Swing 小程序。程序在屏幕上描画电影 Cinderella(《灰姑娘》)中仙女用魔棒造出的那种视觉效果。只要移动鼠标,光标所在处就会爆发出一团令人欣喜的火花,沿着模拟重力场划落到窗口底部。肯特给我看代码的时候,我惊讶于其中那些函数尺寸之小。我看惯了 Swing 程序中长度数以里计的函数。但这个程序中每个函数都只有两行、三行或四行长。每个函数都一目了然。每个函数都只说一件事。而且,每个函数都依序把你带到下一个函数。这就是函数应该达到的短小程度![3] 3. I asked Kent whether he still had a copy, but he was unable to find one. I searched all my old computers too, but to no avail. All that is left now is my memory of that program. How short should your functions be? They should usually be shorter than Listing 3-2! Indeed, Listing 3-2 should really be shortened to Listing 3-3. > 函数应该有多短小?通常来说,应该短于代码清单 3-2 中的函数!代码清单 3-2 实在应该缩短成代码清单 3-3 这个样子。 Listing 3-3 HtmlUtil.java (re-refactored) > 代码清单 3-3 HtmlUtil.java(再次重构之后) ```java public static String renderPageWith SetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { if (isTestPage(pageData)) includeSetupAndTeardownPages(pageData, isSuite); return pageData.getHtml(); } ``` ### Blocks and Indenting 代码块和缩进 This implies that the blocks within if statements, else statements, while statements, and so on should be one line long. Probably that line should be a function call. Not only does this keep the enclosing function small, but it also adds documentary value because the function called within the block can have a nicely descriptive name. > if 语句、else 语句、while 语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。 This also implies that functions should not be large enough to hold nested structures. Therefore, the indent level of a function should not be greater than one or two. This, of course, makes the functions easier to read and understand. > 这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进层级不该多于一层或两层。当然,这样的函数易于阅读和理解。 ## 3.2 DO ONE THING 只做一件事 It should be very clear that Listing 3-1 is doing lots more than one thing. It’s creating buffers, fetching pages, searching for inherited pages, rendering paths, appending arcane strings, and generating HTML, among other things. Listing 3-1 is very busy doing lots of different things. On the other hand, Listing 3-3 is doing one simple thing. It’s including setups and teardowns into test pages. > 代码清单 3-1 显然想做好几件事。它创建缓冲区、获取页面、搜索继承下来的页面、渲染路径、添加神秘的字符串、生成 HTML,如此等等。代码清单 3-1 手忙脚乱。而代码清单 3-3 则只做一件简单的事。它将设置和拆解包纳到测试页面中。 The following advice has appeared in one form or another for 30 years or more. > 过去 30 年以来,以下建议以不同形式一再出现: ![](figures/ch3/3_2fig_martin.jpg) FUNCTIONS SHOULD DO ONE THING. THEY SHOULD DO IT WELL. THEY SHOULD DO IT ONLY. > 函数应该做一件事。做好这件事。只做这一件事。 The problem with this statement is that it is hard to know what “one thing” is. Does Listing 3-3 do one thing? It’s easy to make the case that it’s doing three things: > 问题在于很难知道那件该做的事是什么。代码清单 3-3 只做了一件事,对吧?其实也很容易看作是三件事: 1. Determining whether the page is a test page. 2. If so, including setups and teardowns. 3. Rendering the page in HTML. --- > 1. 判断是否为测试页面; > 2. 如果是,则容纳进设置和分拆步骤; > 3. 渲染成 HTML。 So which is it? Is the function doing one thing or three things? Notice that the three steps of the function are one level of abstraction below the stated name of the function. We can describe the function by describing it as a brief TO4 paragraph: > 那件事是什么?函数是做了一件事呢,还是做了三件事?注意,这三个步骤均在该函数名下的同一抽象层上。可以用简洁的 TO[4]起头段落来描述这个函数: 4. The LOGO language used the keyword “TO” in the same way that Ruby and Python use “def.” So every function began with the word “TO.” This had an interesting effect on the way functions were designed. TO RenderPageWithSetupsAndTeardowns, we check to see whether the page is a test page and if so, we include the setups and teardowns. In either case we render the page in HTML. > (要 RenderPageWithSetupsAndTeardowns,检查页面是否为测试页,如果是测试页,就容纳进设置和分拆步骤。无论是否测试页,都渲染成 HTML) If a function does only those steps that are one level below the stated name of the function, then the function is doing one thing. After all, the reason we write functions is to decompose a larger concept (in other words, the name of the function) into a set of steps at the next level of abstraction. > 如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。 It should be very clear that Listing 3-1 contains steps at many different levels of abstraction. So it is clearly doing more than one thing. Even Listing 3-2 has two levels of abstraction, as proved by our ability to shrink it down. But it would be very hard to meaningfully shrink Listing 3-3. We could extract the if statement into a function named includeSetupsAndTeardownsIfTestPage, but that simply restates the code without changing the level of abstraction. > 代码清单 3-1 明显包括了处于多个不同抽象层级的步骤。显然,它所做的不止一件事。即便是代码清单 3-2 也有两个抽象层,这已被我们将其缩短的能力所证明。然而,很难再将代码清单 3-3 做有意义的缩短。可以将 if 语句拆出来做一个名为 includeSetupAndTeardonwsIfTestpage 的函数,但那只是重新诠释代码,并未改变抽象层级。 So, another way to know that a function is doing more than “one thing” is if you can extract another function from it with a name that is not merely a restatement of its implementation [G34]. > 所以,要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现[G34]。 ### Sections within Functions 函数中的区段 Look at Listing 4-7 on page 71. Notice that the generatePrimes function is divided into sections such as declarations, initializations, and sieve. This is an obvious symptom of doing more than one thing. Functions that do one thing cannot be reasonably divided into sections. > 请看代码清单 4-7。注意,generatePrimes 函数被切分为 declarations、initializations 和 sieve 等区段。这就是函数做事太多的明显征兆。只做一件事的函数无法被合理地切分为多个区段。 ## 3.3 ONE LEVEL OF ABSTRACTION PER FUNCTION 每个函数一个抽象层级 In order to make sure our functions are doing “one thing,” we need to make sure that the statements within our function are all at the same level of abstraction. It is easy to see how Listing 3-1 violates this rule. There are concepts in there that are at a very high level of abstraction, such as getHtml(); others that are at an intermediate level of abstraction, such as: String pagePathName = PathParser.render(pagePath); and still others that are remarkably low level, such as: .append(”\n”). > 要确保函数只做一件事,函数中的语句都要在同一抽象层级上。一眼就能看出,代码清单 3-1 违反了这条规矩。那里面有 getHtml( )等位于较高抽象层的概念,也有 String pagePathName = PathParser.render(pagePath)等位于中间抽象层的概念,还有.append("\n") 等位于相当低的抽象层的概念。 Mixing levels of abstraction within a function is always confusing. Readers may not be able to tell whether a particular expression is an essential concept or a detail. Worse, like broken windows, once details are mixed with essential concepts, more and more details tend to accrete within the function. > 函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。 ### Reading Code from Top to Bottom: The Stepdown Rule 自顶向下读代码:向下规则 We want the code to read like a top-down narrative.5 We want every function to be followed by those at the next level of abstraction so that we can read the program, descending one level of abstraction at a time as we read down the list of functions. I call this The Step-down Rule. > 我们想要让代码拥有自顶向下的阅读顺序。[5]我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能偱抽象层级向下阅读了。我把这叫做向下规则。 5. [KP78], p. 37. To say this differently, we want to be able to read the program as though it were a set of TO paragraphs, each of which is describing the current level of abstraction and referencing subsequent TO paragraphs at the next level down. > 换一种说法。我们想要这样读程序:程序就像是一系列 TO 起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续 TO 起头段落。 - To include the setups and teardowns, we include setups, then we include the test page content, and then we include the teardowns. - To include the setups, we include the suite setup if this is a suite, then we include the regular setup. - To include the suite setup, we search the parent hierarchy for the “SuiteSetUp” page and add an include statement with the path of that page. - To search the parent… --- > - (要容纳设置和分拆步骤,就先容纳设置步骤,然后纳入测试页面内容,再纳入分拆步骤。) > - (要容纳设置步骤,如果是套件,就纳入套件设置步骤,然后再纳入普通设置步骤。) > - (要容纳套件设置步骤,先搜索“SuiteSetUp”页面的上级继承关系,再添加一个包括该页面路径的语句。) > - (要搜索……) It turns out to be very difficult for programmers to learn to follow this rule and write functions that stay at a single level of abstraction. But learning this trick is also very important. It is the key to keeping functions short and making sure they do “one thing.” Making the code read like a top-down set of TO paragraphs is an effective technique for keeping the abstraction level consistent. > 程序员往往很难学会遵循这条规则,写出只停留于一个抽象层级上的函数。尽管如此,学习这个技巧还是很重要。这是保持函数短小、确保只做一件事的要诀。让代码读起来像是一系列自顶向下的 TO 起头段落是保持抽象层级协调一致的有效技巧。 Take a look at Listing 3-7 at the end of this chapter. It shows the whole testableHtml function refactored according to the principles described here. Notice how each function introduces the next, and each function remains at a consistent level of abstraction. > 看看本章末尾的代码清单 3-7。它展示了遵循这条原则重构的完整 testableHtml 函数。留意每个函数是如何引出下一个函数,如何保持在同一抽象层上的。 ## 3.4 SWITCH STATEMENTS switch 语句 It’s hard to make a small switch statement.6 Even a switch statement with only two cases is larger than I’d like a single block or function to be. It’s also hard to make a switch statement that does one thing. By their nature, switch statements always do N things. Unfortunately we can’t always avoid switch statements, but we can make sure that each switch statement is buried in a low-level class and is never repeated. We do this, of course, with polymorphism. > 写出短小的 switch 语句很难[6]。即便是只有两种条件的 switch 语句也要比我想要的单个代码块或函数大得多。写出只做一件事的 switch 语句也很难。Switch 天生要做 N 件事。不幸我们总无法避开 switch 语句,不过还是能够确保每个 switch 都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。 6. And, of course, I include if/else chains in this. Consider Listing 3-4. It shows just one of the operations that might depend on the type of employee. > 请看代码清单 3-4。它呈现了可能依赖于雇员类型的仅仅一种操作。 Listing 3-4 Payroll.java > 代码清单 3-4 Payroll.java ```java public Money calculatePay(Employee e) throws InvalidEmployeeType { switch (e.type) { case COMMISSIONED: return calculateCommissionedPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED: return calculateSalariedPay(e); default: throw new InvalidEmployeeType(e.type); } } ``` There are several problems with this function. First, it’s large, and when new employee types are added, it will grow. Second, it very clearly does more than one thing. Third, it violates the Single Responsibility Principle7 (SRP) because there is more than one reason for it to change. Fourth, it violates the Open Closed Principle8 (OCP) because it must change whenever new types are added. But possibly the worst problem with this function is that there are an unlimited number of other functions that will have the same structure. For example we could have > 该函数有好几个问题。首先,它太长,当出现新的雇员类型时,还会变得更长。其次,它明显做了不止一件事。第三,它违反了单一权责原则(Single Responsibility Principle[7], SRP),因为有好几个修改它的理由。第四,它违反了开放闭合原则(Open Closed Principle[8],OCP),因为每当添加新类型时,就必须修改之。不过,该函数最麻烦的可能是到处皆有类似结构的函数。例如,可能会有 7. a. http://en.wikipedia.org/wiki/Single_responsibility_principle b. http://www.objectmentor.com/resources/articles/srp.pdf 8. a. http://en.wikipedia.org/wiki/Open/closed_principle b. http://www.objectmentor.com/resources/articles/ocp.pdf ```java isPayday(Employee e, Date date), ``` or > 或 ```java deliverPay(Employee e, Money pay), ``` or a host of others. All of which would have the same deleterious structure. > 如此等等。它们的结构都有同样的问题。 The solution to this problem (see Listing 3-5) is to bury the switch statement in the basement of an ABSTRACT FACTORY,9 and never let anyone see it. The factory will use the switch statement to create appropriate instances of the derivatives of Employee, and the various functions, such as calculatePay, isPayday, and deliverPay, will be dispatched polymorphically through the Employee interface. > 该问题的解决方案(如代码清单 3-5 所示)是将 switch 语句埋到抽象工厂[9]底下,不让任何人看到。该工厂使用 switch 语句为 Employee 的派生物创建适当的实体,而不同的函数,如 calculatePay、isPayday 和 deliverPay 等,则藉由 Employee 接口多态地接受派遣。 9. [GOF]. My general rule for switch statements is that they can be tolerated if they appear only once, are used to create polymorphic objects, and are hidden behind an inheritance relationship so that the rest of the system can’t see them [G23]. Of course every circumstance is unique, and there are times when I violate one or more parts of that rule. > 对于 switch 语句,我的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍 [G23]。当然也要就事论事,有时我也会部分或全部违反这条规矩。 Listing 3-5 Employee and Factory > 代码清单 3-5 Employee 与工厂 ```java public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } ----------------- public interface EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } ----------------- public class EmployeeFactoryImpl implements EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { switch (r.type) { case COMMISSIONED: return new CommissionedEmployee(r) ; case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmploye(r); default: throw new InvalidEmployeeType(r.type); } } } ``` ## 3.5 USE DESCRIPTIVE NAMES 使用描述性的名称 In Listing 3-7 I changed the name of our example function from testableHtml to SetupTeardownIncluder.render. This is a far better name because it better describes what the function does. I also gave each of the private methods an equally descriptive name such as isTestable or includeSetupAndTeardownPages. It is hard to overestimate the value of good names. Remember Ward’s principle: “You know you are working on clean code when each routine turns out to be pretty much what you expected.” Half the battle to achieving that principle is choosing good names for small functions that do one thing. The smaller and more focused a function is, the easier it is to choose a descriptive name. > 在代码清单 3-7 中,我把示例函数的名称从 testableHtml 改为 SetupTeardownIncluder.render。这个名称好得多,因为它较好地描述了函数做的事。我也给每个私有方法取个同样具有描述性的名称,如 isTestable 或 includeSetupAndTeardownPages。好名称的价值怎么好评都不为过。记住沃德原则:“如果每个例程都让你感到深合己意,那就是整洁代码。”要遵循这一原则,泰半工作都在于为只做一件事的小函数取个好名字。函数越短小、功能越集中,就越便于取个好名字。 Don’t be afraid to make a name long. A long descriptive name is better than a short enigmatic name. A long descriptive name is better than a long descriptive comment. Use a naming convention that allows multiple words to be easily read in the function names, and then make use of those multiple words to give the function a name that says what it does. > 别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。 Don’t be afraid to spend time choosing a name. Indeed, you should try several different names and read the code with each in place. Modern IDEs like Eclipse or IntelliJ make it trivial to change names. Use one of those IDEs and experiment with different names until you find one that is as descriptive as you can make it. > 别害怕花时间取名字。你当尝试不同的名称,实测其阅读效果。在 Eclipse 或 IntelliJ 等现代 IDE 中改名称易如反掌。使用这些 IDE 测试不同名称,直至找到最具有描述性的那一个为止。 Choosing descriptive names will clarify the design of the module in your mind and help you to improve it. It is not at all uncommon that hunting for a good name results in a favorable restructuring of the code. > 选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。 Be consistent in your names. Use the same phrases, nouns, and verbs in the function names you choose for your modules. Consider, for example, the names includeSetup-AndTeardownPages, includeSetupPages, includeSuiteSetupPage, and includeSetupPage. The similar phraseology in those names allows the sequence to tell a story. Indeed, if I showed you just the sequence above, you’d ask yourself: “What happened to includeTeardownPages, includeSuiteTeardownPage, and includeTeardownPage?” How’s that for being “… pretty much what you expected.” > 命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如,includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage 和 includeSetupPage 等。这些名称使用了类似的措辞,依序讲出一个故事。实际上,假使我只给你看上述函数序列,你就会自问:“includeTeardownPages、includeSuiteTeardownPages 和 includeTeardownPage 又会如何?”这就是所谓“深合己意”了。 ## 3.6 FUNCTION ARGUMENTS 函数参数 The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification—and then shouldn’t be used anyway. > 最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这么做。 ![](figures/ch3/3_3fig_martin.jpg) Arguments are hard. They take a lot of conceptual power. That’s why I got rid of almost all of them from the example. Consider, for instance, the StringBuffer in the example. We could have passed it around as an argument rather than making it an instance variable, but then our readers would have had to interpret it each time they saw it. When you are reading the story told by the module, includeSetupPage() is easier to understand than includeSetupPageInto(newPage-Content). The argument is at a different level of abstraction than the function name and forces you to know a detail (in other words, StringBuffer) that isn’t particularly important at that point. > 参数不易对付。它们带有太多概念性。所以我在代码范例中几乎不加参数。比如,以 StringBuffer 为例,我们可能不把它作为实体变量,而是当作参数来传递,那样的话,读者每次看到它都得要翻译一遍。阅读模块所讲述的故事时,includeSetupPage()要比 includeSetupPageInto(newPage-Content)易于理解。参数与函数名处在不同的抽象层级,它要求你了解目前并不特别重要的细节(即那个 StringBuffer)。 Arguments are even harder from a testing point of view. Imagine the difficulty of writing all the test cases to ensure that all the various combinations of arguments work properly. If there are no arguments, this is trivial. If there’s one argument, it’s not too hard. With two arguments the problem gets a bit more challenging. With more than two arguments, testing every combination of appropriate values can be daunting. > 从测试的角度看,参数甚至更叫人为难。想想看,要编写能确保参数的各种组合运行正常的测试用例,是多么困难的事。如果没有参数,就是小菜一碟。如果只有一个参数,也不太困难。有两个参数,问题就麻烦多了。如果参数多于两个,测试覆盖所有可能值的组合简直让人生畏。 Output arguments are harder to understand than input arguments. When we read a function, we are used to the idea of information going in to the function through arguments and out through the return value. We don’t usually expect information to be going out through the arguments. So output arguments often cause us to do a double-take. > 输出参数比输入参数还要难以理解。读函数时,我们惯于认为信息通过参数输入函数,通过返回值从函数中输出。我们不太期望信息通过参数输出。所以,输出参数往往让人苦思之后才恍然大悟。 One input argument is the next best thing to no arguments. SetupTeardown-Includer.render(pageData) is pretty easy to understand. Clearly we are going to render the data in the pageData object. > 相较于没有参数,只有一个输入参数算是第二好的做法。SetupTeardownInclude.render (pageData)也相当易于理解。很明显,我们将渲染 pageData 对象中的数据。 ### 3.6.1 Common Monadic Forms 一元函数的普遍形式 There are two very common reasons to pass a single argument into a function. You may be asking a question about that argument, as in boolean fileExists(“MyFile”). Or you may be operating on that argument, transforming it into something else and returning it. For example, InputStream fileOpen(“MyFile”) transforms a file name String into an InputStream return value. These two uses are what readers expect when they see a function. You should choose names that make the distinction clear, and always use the two forms in a consistent context. (See Command Query Separation below.) > 向函数传入单个参数有两种极普遍的理由。你也许会问关于那个参数的问题,就像在 boolean fileExists("MyFile")中那样。也可能是操作该参数,将其转换为其他什么东西,再输出之。例如,InputStream fileOpen("MyFile")把 String 类型的文件名转换为 InputStream 类型的返回值。这就是读者看到函数时所期待的东西。你应当选用较能区别这两种理由的名称,而且总在一致的上下文中使用这两种形式。 A somewhat less common, but still very useful form for a single argument function, is an event. In this form there is an input argument but no output argument. The overall program is meant to interpret the function call as an event and use the argument to alter the state of the system, for example, void passwordAttemptFailedNtimes(int attempts). Use this form with care. It should be very clear to the reader that this is an event. Choose names and contexts carefully. > 还有一种虽不那么普遍但仍极有用的单参数函数形式,那就是事件(event)。在这种形式中,有输入参数而无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态,例如 void passwordAttemptFailedNtimes(int attempts)。小心使用这种形式。应该让读者很清楚地了解它是个事件。谨慎地选用名称和上下文语境。 Try to avoid any monadic functions that don’t follow these forms, for example, void includeSetupPageInto(StringBuffer pageText). Using an output argument instead of a return value for a transformation is confusing. If a function is going to transform its input argument, the transformation should appear as the return value. Indeed, StringBuffer transform(StringBuffer in) is better than void transform(StringBuffer out), even if the implementation in the first case simply returns the input argument. At least it still follows the form of a transformation. > 尽量避免编写不遵循这些形式的一元函数,例如,void includeSetupPageInto(StringBuffer pageText)。对于转换,使用输出参数而非返回值令人迷惑。如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。实际上,StringBuffer transform(StringBuffer in)要比 void transform(StringBuffer out)强,即便第一种形式只简单地返回输参数也是这样。至少,它遵循了转换的形式。 ### 3.6.2 Flag Arguments 标识参数 Flag arguments are ugly. Passing a boolean into a function is a truly terrible practice. It immediately complicates the signature of the method, loudly proclaiming that this function does more than one thing. It does one thing if the flag is true and another if the flag is false! > 标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法。这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事。如果标识为 true 将会这样做,标识为 false 则会那样做! In Listing 3-7 we had no choice because the callers were already passing that flag in, and I wanted to limit the scope of refactoring to the function and below. Still, the method call render(true) is just plain confusing to a poor reader. Mousing over the call and seeing render(boolean isSuite) helps a little, but not that much. We should have split the function into two: renderForSuite() and renderForSingleTest(). > 在代码清单 3-7 中,我们别无选择,因为调用者已经传入了那个标识,而我想把重构范围限制在该函数及该函数以下范围之内。方法调用 render(true)对于可怜的读者来说仍然摸不着头脑。卷动屏幕,看到 render(Boolean isSuite),稍许有点帮助,不过仍然不够。应该把该函数一分为二:reanderForSuite( )和 renderForSingleTest( )。 ### 3.6.3 Dyadic Functions 二元函数 A function with two arguments is harder to understand than a monadic function. For example, writeField(name) is easier to understand than writeField(output-Stream, name).10 Though the meaning of both is clear, the first glides past the eye, easily depositing its meaning. The second requires a short pause until we learn to ignore the first parameter. And that, of course, eventually results in problems because we should never ignore any part of code. The parts we ignore are where the bugs will hide. > 有两个参数的函数要比一元函数难懂。例如,writeField(name)比 writeField(outputStream,name)[10]好懂。尽管两种情况下意义都很清楚,但第一个只要扫一眼就明白,更好地表达了其意义。第二个就得暂停一下才能明白,除非我们学会忽略第一个参数。而且最终那也会导致问题,因为我们根本就不该忽略任何代码。忽略掉的部分就是缺陷藏身之地。 10. I just finished refactoring a module that used the dyadic form. I was able to make the outputStream a field of the class and convert all the writeField calls to the monadic form. The result was much cleaner. There are times, of course, where two arguments are appropriate. For example, Point p = new Point(0,0); is perfectly reasonable. Cartesian points naturally take two arguments. Indeed, we’d be very surprised to see new Point(0). However, the two arguments in this case are ordered components of a single value! Whereas output-Stream and name have neither a natural cohesion, nor a natural ordering. > 当然,有些时候两个参数正好。例如,Point p = new Point(0,0);就相当合理。笛卡儿点天生拥有两个参数。如果看到 new Point(0),我们会倍感惊讶。然而,本例中的两个参数却只是单个值的有序组成部分!而 output-Stream 和 name 则既非自然的组合,也不是自然的排序。 Even obvious dyadic functions like assertEquals(expected, actual) are problematic. How many times have you put the actual where the expected should be? The two arguments have no natural ordering. The expected, actual ordering is a convention that requires practice to learn. > 即便是如 assertEquals(expected, actual)这样的二元函数也有其问题。你有多少次会搞错 actual 和 expected 的位置呢?这两个参数没有自然的顺序。expected 在前,actual 在后,只是一种需要学习的约定罢了。 Dyads aren’t evil, and you will certainly have to write them. However, you should be aware that they come at a cost and should take advantage of what mechanisms may be available to you to convert them into monads. For example, you might make the writeField method a member of outputStream so that you can say outputStream. writeField(name). Or you might make the outputStream a member variable of the current class so that you don’t have to pass it. Or you might extract a new class like FieldWriter that takes the outputStream in its constructor and has a write method. > 二元函数不算恶劣,而且你当然也会编写二元函数。不过,你得小心,使用二元函数要付出代价。你应该尽量利用一些机制将其转换成一元函数。例如,可以把 writeField 方法写成 outputStream 的成员之一,从而能这样用:outputStream.writeField(name)。或者,也可以把 outputStream 写成当前类的成员变量,从而无需再传递它。还可以分离出类似 FieldWriter 的新类,在其构造器中采用 outputStream,并且包含一个 write 方法。 ### 3.6.4 Triads 三元函数 Functions that take three arguments are significantly harder to understand than dyads. The issues of ordering, pausing, and ignoring are more than doubled. I suggest you think very carefully before creating a triad. > 有三个参数的函数要比二元函数难懂得多。排序、琢磨、忽略的问题都会加倍体现。建议你在写三元函数前一定要想清楚。 For example, consider the common overload of assertEquals that takes three arguments: assertEquals(message, expected, actual). How many times have you read the message and thought it was the expected? I have stumbled and paused over that particular triad many times. In fact, every time I see it, I do a double-take and then learn to ignore the message. > 例如,设想 assertEquals 有三个参数:assertEquals(message, expected, actual)。有多少次,你读到 message,错以为它是 expected 呢?我就常栽在这个三元函数上。实际上,每次我看到这里,总会绕半天圈子,最后学会了忽略 message 参数。 On the other hand, here is a triad that is not quite so insidious: assertEquals(1.0, amount, .001). Although this still requires a double-take, it’s one that’s worth taking. It’s always good to be reminded that equality of floating point values is a relative thing. > 另一方面,这里有个并不那么险恶的三元函数:assertEquals(1.0, amount, .001)。虽然也要费点神,还是值得的。得到“浮点值的等值是相对而言”的提示总是好的。 ### 3.6.5 Argument Objects 参数对象 When a function seems to need more than two or three arguments, it is likely that some of those arguments ought to be wrapped into a class of their own. Consider, for example, the difference between the two following declarations: > 如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。例如,下面两个声明的差别: ```java Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius); ``` Reducing the number of arguments by creating objects out of them may seem like cheating, but it’s not. When groups of variables are passed together, the way x and y are in the example above, they are likely part of a concept that deserves a name of its own. > 从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则并非如此。当一组参数被共同传递,就像上例中的 x 和 y 那样,往往就是该有自己名称的某个概念的一部分。 ### 3.6.6 Argument Lists 参数列表 Sometimes we want to pass a variable number of arguments into a function. Consider, for example, the String.format method: > 有时,我们想要向函数传入数量可变的参数。例如, ```java String.format(”%s worked %.2f hours.”, name, hours); ``` If the variable arguments are all treated identically, as they are in the example above, then they are equivalent to a single argument of type List. By that reasoning, String.format is actually dyadic. Indeed, the declaration of String.format as shown below is clearly dyadic. > 如果可变参数像上例中那样被同等对待,就和类型为 List 的单个参数没什么两样。这样一来,String.formate 实则是二元函数。下列 String.format 的声明也很明显是二元的: ```java public String format(String format, Object… args) ``` So all the same rules apply. Functions that take variable arguments can be monads, dyads, or even triads. But it would be a mistake to give them more arguments than that. > 同理,有可变参数的函数可能是一元、二元甚至三元。超过这个数量就可能要犯错了。 ```java void monad(Integer… args); void dyad(String name, Integer… args); void triad(String name, int count, Integer… args); ``` ### 3.6.7 Verbs and Keywords 动词与关键字 Choosing good names for a function can go a long way toward explaining the intent of the function and the order and intent of the arguments. In the case of a monad, the function and argument should form a very nice verb/noun pair. For example, write(name) is very evocative. Whatever this “name” thing is, it is being “written.” An even better name might be writeField(name), which tells us that the “name” thing is a “field.” > 给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。例如,write(name)就相当令人认同。不管这个“name”是什么,都要被“write”。更好的名称大概是 writeField(name),它告诉我们,“name”是一个“field”。 This last is an example of the keyword form of a function name. Using this form we encode the names of the arguments into the function name. For example, assertEquals might be better written as assertExpectedEqualsActual(expected, actual). This strongly mitigates the problem of having to remember the ordering of the arguments. > 最后那个例子展示了函数名称的关键字(keyword)形式。使用这种形式,我们把参数的名称编码成了函数名。例如,assertEqual 改成 assertExpectedEqualsActual(expected, actual)可能会好些。这大大减轻了记忆参数顺序的负担。 ## 3.7 HAVE NO SIDE EFFECTS 无副作用 Side effects are lies. Your function promises to do one thing, but it also does other hidden things. Sometimes it will make unexpected changes to the variables of its own class. Sometimes it will make them to the parameters passed into the function or to system globals. In either case they are devious and damaging mistruths that often result in strange temporal couplings and order dependencies. > 副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。 Consider, for example, the seemingly innocuous function in Listing 3-6. This function uses a standard algorithm to match a userName to a password. It returns true if they match and false if anything goes wrong. But it also has a side effect. Can you spot it? > 以代码清单 3-6 中看似无伤大雅的函数为例。该函数使用标准算法来匹配 userName 和 password。如果匹配成功,返回 true,如果失败则返回 false。但它会有副作用。你知道问题所在吗? Listing 3-6 UserValidator.java > 代码清单 3-6 UserValidator.java ```java public class UserValidator { private Cryptographer cryptographer; public boolean checkPassword(String userName, String password) { User user = UserGateway.findByName(userName); if (user != User.NULL) { String codedPhrase = user. getPhraseEncodedByPassword(); String phrase = cryptographer.decrypt(codedPhrase, password); if ("Valid Password".equals(phrase)) { Session.initialize(); return true; } } return false; } } ``` The side effect is the call to Session.initialize(), of course. The checkPassword function, by its name, says that it checks the password. The name does not imply that it initializes the session. So a caller who believes what the name of the function says runs the risk of erasing the existing session data when he or she decides to check the validity of the user. > 当然了,副作用就在于对 Session.initialize( )的调用。checkPassword 函数,顾名思义,就是用来检查密码的。该名称并未暗示它会初始化该次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。 This side effect creates a temporal coupling. That is, checkPassword can only be called at certain times (in other words, when it is safe to initialize the session). If it is called out of order, session data may be inadvertently lost. Temporal couplings are confusing, especially when hidden as a side effect. If you must have a temporal coupling, you should make it clear in the name of the function. In this case we might rename the function checkPasswordAndInitializeSession, though that certainly violates “Do one thing.” > 这一副作用造出了一次时序性耦合。也就是说,checkPassword 只能在特定时刻调用(换言之,在初始化会话是安全的时候调用)。如果在不合适的时候调用,会话数据就有可能沉默地丢失。时序性耦合令人迷惑,特别是当它躲在副作用后面时。如果一定要时序性耦合,就应该在函数名称中说明。在本例中,可以重命名函数为 checkPasswordAndInitializeSession,虽然那还是违反了“只做一件事”的规则。 ### Output Arguments 输出参数 Arguments are most naturally interpreted as inputs to a function. If you have been programming for more than a few years, I’m sure you’ve done a double-take on an argument that was actually an output rather than an input. For example: > 参数多数会被自然而然地看作是函数的输入。如果你编过好些年程序,我担保你一定被用作输出而非输入的参数迷惑过。例如: ```java appendFooter(s); ``` Does this function append s as the footer to something? Or does it append some footer to s? Is s an input or an output? It doesn’t take long to look at the function signature and see: > 这个函数是把 s 添加到什么东西后面吗?或者它把什么东西添加到了 s 后面?s 是输入参数还是输出参数?稍许花点时间看看函数签名: ```java public void appendFooter(StringBuffer report) ``` This clarifies the issue, but only at the expense of checking the declaration of the function. Anything that forces you to check the function signature is equivalent to a double-take. It’s a cognitive break and should be avoided. > 事情清楚了,但付出了检查函数声明的代价。你被迫检查函数签名,就得花上一点时间。应该避免这种中断思路的事。 In the days before object oriented programming it was sometimes necessary to have output arguments. However, much of the need for output arguments disappears in OO languages because this is intended to act as an output argument. In other words, it would be better for appendFooter to be invoked as > 在面向对象编程之前的岁月里,有时的确需要输出参数。然而,面向对象语言中对输出参数的大部分需求已经消失了,因为 this 也有输出函数的意味在内。换言之,最好是这样调用 appendFooter: ```java report.appendFooter(); ``` In general output arguments should be avoided. If your function must change the state of something, have it change the state of its owning object. > 在面向对象编程之前的岁月里,有时的确需要输出参数。然而,面向对象语言中对输出参数的大部分需求已经消失了,因为 this 也有输出函数的意味在内。换言之,最好是这样调用 appendFooter: ## 3.8 COMMAND QUERY SEPARATION 分隔指令与询问 Functions should either do something or answer something, but not both. Either your function should change the state of an object, or it should return some information about that object. Doing both often leads to confusion. Consider, for example, the following function: > 函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。看看下面的例子: ```java public boolean set(String attribute, String value); ``` This function sets the value of a named attribute and returns true if it is successful and false if no such attribute exists. This leads to odd statements like this: > 该函数设置某个指定属性,如果成功就返回 true,如果不存在那个属性则返回 false。这样就导致了以下语句: ```java if (set(”username”, ”unclebob”))… ``` Imagine this from the point of view of the reader. What does it mean? Is it asking whether the “username” attribute was previously set to “unclebob”? Or is it asking whether the “username” attribute was successfully set to “unclebob”? It’s hard to infer the meaning from the call because it’s not clear whether the word “set” is a verb or an adjective. > 从读者的角度考虑一下吧。这是什么意思呢?它是在问 username 属性值是否之前已设置为 unclebob 吗?或者它是在问 username 属性值是否成功设置为 unclebob 呢?从这行调用很难判断其含义,因为 set 是动词还是形容词并不清楚。 The author intended set to be a verb, but in the context of the if statement it feels like an adjective. So the statement reads as “If the username attribute was previously set to unclebob” and not “set the username attribute to unclebob and if that worked then.…” We could try to resolve this by renaming the set function to setAndCheckIfExists, but that doesn’t much help the readability of the if statement. The real solution is to separate the command from the query so that the ambiguity cannot occur. > 作者本意,set 是个动词,但在 if 语句的上下文中,感觉它像是个形容词。该语句读起来像是说“如果 username 属性值之前已被设置为 uncleob”,而不是“设置 username 属性值为 unclebob,看看是否可行,然后……”。要解决这个问题,可以将 set 函数重命名为 setAndCheckIfExists,但这对提高 if 语句的可读性帮助不大。真正的解决方案是把指令与询问分隔开来,防止混淆的发生: ```java if (attributeExists(”username”)) { setAttribute(”username”, ”unclebob”); … } ``` ## 3.9 PREFER EXCEPTIONS TO RETURNING ERROR CODES 使用异常替代返回错误码 Returning error codes from command functions is a subtle violation of command query separation. It promotes commands being used as expressions in the predicates of if statements. > 从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在 if 语句判断中把指令当作表达式使用。 ```java if (deletePage(page) == E_OK) ``` This does not suffer from verb/adjective confusion but does lead to deeply nested structures. When you return an error code, you create the problem that the caller must deal with the error immediately. > 这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。当返回错误码时,就是在要求调用者立刻处理错误。 ```java if (deletePage(page) == E_OK) { if (registry.deleteReference(page.name) == E_OK) { if (configKeys.deleteKey(page.name.makeKey()) == E_OK){ logger.log("page deleted"); } else { logger.log("configKey not deleted"); } } else { logger.log("deleteReference from registry failed"); } } else { logger.log("delete failed"); return E_ERROR; } ``` On the other hand, if you use exceptions instead of returned error codes, then the error processing code can be separated from the happy path code and can be simplified: > 另一方面,如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化: ```java try { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } catch (Exception e) { logger.log(e.getMessage()); } ``` ### 3.9.1 Extract Try/Catch Blocks 抽离 Try/Catch 代码块 Try/catch blocks are ugly in their own right. They confuse the structure of the code and mix error processing with normal processing. So it is better to extract the bodies of the try and catch blocks out into functions of their own. > Try/catch 代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把 try 和 catch 代码块的主体部分抽离出来,另外形成函数。 ```java public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } } private void deletePageAndAllReferences(Page page) throws Exception { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e) { logger.log(e.getMessage()); } ``` In the above, the delete function is all about error processing. It is easy to understand and then ignore. The deletePageAndAllReferences function is all about the processes of fully deleting a page. Error handling can be ignored. This provides a nice separation that makes the code easier to understand and modify. > 在上例中,delete 函数只与错误处理有关。很容易理解然后就忽略掉。deletePageAndAllReference 函数只与完全删除一个 page 有关。错误处理可以忽略掉。有了这样美妙的区隔,代码就更易于理解和修改了。 ### 3.9.2 Error Handling Is One Thing 错误处理就是一件事 Functions should do one thing. Error handing is one thing. Thus, a function that handles errors should do nothing else. This implies (as in the example above) that if the keyword try exists in a function, it should be the very first word in the function and that there should be nothing after the catch/finally blocks. > 函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字 try 在某个函数中存在,它就该是这个函数的第一个单词,而且在 catch/finally 代码块后面也不该有其他内容。 ### 3.9.3 The Error.java Dependency Magnet Error.java 依赖磁铁 Returning error codes usually implies that there is some class or enum in which all the error codes are defined. > 返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。 ```java public enum Error { OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT; } ``` Classes like this are a dependency magnet; many other classes must import and use them. Thus, when the Error enum changes, all those other classes need to be recompiled and redeployed.11 This puts a negative pressure on the Error class. Programmers don’t want to add new errors because then they have to rebuild and redeploy everything. So they reuse old error codes instead of adding new ones. > 这样的类就是一块依赖磁铁(dependency magnet);其他许多类都得导入和使用它。当 Error 枚举修改时,所有这些其他的类都需要重新编译和部署。[11]这对 Error 类造成了负面压力。程序员不愿增加新的错误代码,因为这样他们就得重新构建和部署所有东西。于是他们就复用旧的错误码,而不添加新的。 11. Those who felt that they could get away without recompiling and redeploying have been found—and dealt with. When you use exceptions rather than error codes, then new exceptions are derivatives of the exception class. They can be added without forcing any recompilation or redeployment.12 > 使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署[12]。 12. This is an example of the Open Closed Principle (OCP) [PPP02]. ## 3.10 DON’T REPEAT YOURSELF13 别重复自己 13. The DRY principle. [PRAG]. Look back at Listing 3-1 carefully and you will notice that there is an algorithm that gets repeated four times, once for each of the SetUp, SuiteSetUp, TearDown, and SuiteTearDown cases. It’s not easy to spot this duplication because the four instances are intermixed with other code and aren’t uniformly duplicated. Still, the duplication is a problem because it bloats the code and will require four-fold modification should the algorithm ever have to change. It is also a four-fold opportunity for an error of omission. > 回头仔细看看代码清单 3-1,你会注意到,有个算法在 SetUp、SuiteSetUp、TearDown 和 SuiteTearDown 中总共被重复了 4 次。识别重复不太容易,因为这 4 次重复与其他代码混在一起,而且也不完全一样。这样的重复还是会导致问题,因为代码因此而臃肿,且当算法改变时需要修改 4 处地方。而且也会增加 4 次放过错误的可能性。 ![](figures/ch3/3_4fig_martin.jpg) This duplication was remedied by the include method in Listing 3-7. Read through that code again and notice how the readability of the whole module is enhanced by the reduction of that duplication. > 使用代码清单 3-7 中的 include 方法修正了这些重复。再读一遍那段代码,你会注意到,整个模块的可读性因为重复的消除而得到了提升。 Duplication may be the root of all evil in software. Many principles and practices have been created for the purpose of controlling or eliminating it. Consider, for example, that all of Codd’s database normal forms serve to eliminate duplication in data. Consider also how object-oriented programming serves to concentrate code into base classes that would otherwise be redundant. Structured programming, Aspect Oriented Programming, Component Oriented Programming, are all, in part, strategies for eliminating duplication. It would appear that since the invention of the subroutine, innovations in software development have been an ongoing attempt to eliminate duplication from our source code. > 重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。例如,全部考德(Codd)[14]数据库范式都是为消灭数据重复而服务。再想想看,面向对象编程是如何将代码集中到基类,从而避免了冗余。面向方面编程(Aspect Oriented Programming)、面向组件编程(Component Oriented Programming)多少也都是消除重复的一种策略。看来,自子程序发明以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复。 ## 3.11 STRUCTURED PROGRAMMING 结构化编程 Some programmers follow Edsger Dijkstra’s rules of structured programming.14 Dijkstra said that every function, and every block within a function, should have one entry and one exit. Following these rules means that there should only be one return statement in a function, no break or continue statements in a loop, and never, ever, any goto statements. > 有些程序员遵循 Edsger Dijkstra 的结构化编程规则[15]。Dijkstra 认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个 return 语句,循环中不能有 break 或 continue 语句,而且永永远远不能有任何 goto 语句。 14. [SP72]. While we are sympathetic to the goals and disciplines of structured programming, those rules serve little benefit when functions are very small. It is only in larger functions that such rules provide significant benefit. > 我们赞成结构化编程的目标和规范,但对于小函数,这些规则助益不大。只有在大函数中,这些规则才会有明显的好处。 So if you keep your functions small, then the occasional multiple return, break, or continue statement does no harm and can sometimes even be more expressive than the single-entry, single-exit rule. On the other hand, goto only makes sense in large functions, so it should be avoided. > 所以,只要函数保持短小,偶尔出现的 return、break 或 continue 语句没有坏处,甚至还比单入单出原则更具有表达力。另外一方面,goto 只在大函数中才有道理,所以应该尽量避免使用。 ## 3.12 HOW DO YOU WRITE FUNCTIONS LIKE THIS? 如何写出这样的函数 Writing software is like any other kind of writing. When you write a paper or an article, you get your thoughts down first, then you massage it until it reads well. The first draft might be clumsy and disorganized, so you wordsmith it and restructure it and refine it until it reads the way you want it to read. > 写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。 When I write functions, they come out long and complicated. They have lots of indenting and nested loops. They have long argument lists. The names are arbitrary, and there is duplicated code. But I also have a suite of unit tests that cover every one of those clumsy lines of code. > 我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。 So then I massage and refine that code, splitting out functions, changing names, eliminating duplication. I shrink the methods and reorder them. Sometimes I break out whole classes, all the while keeping the tests passing. > 然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。最后,遵循本章列出的规则,我组装好这些函数。 In the end, I wind up with functions that follow the rules I’ve laid down in this chapter. I don’t write them that way to start. I don’t think anyone could. > 我并不从一开始就按照规则写函数。我想没人做得到。 ## 3.13 CONCLUSION 小结 Every system is built from a domain-specific language designed by the programmers to describe that system. Functions are the verbs of that language, and classes are the nouns. This is not some throwback to the hideous old notion that the nouns and verbs in a requirements document are the first guess of the classes and functions of a system. Rather, this is a much older truth. The art of programming is, and has always been, the art of language design. > 每个系统都是使用某种领域特定语言搭建,而这种语言是程序员设计来描述那个系统的。函数是语言的动词,类是名词。这并非是退回到那种认为需求文档中的名词和动词就是系统中类和函数的最初设想的可怕的旧观念。其实这是个历史更久的真理。编程艺术是且一直就是语言设计的艺术。 Master programmers think of systems as stories to be told rather than programs to be written. They use the facilities of their chosen programming language to construct a much richer and more expressive language that can be used to tell that story. Part of that domain-specific language is the hierarchy of functions that describe all the actions that take place within that system. In an artful act of recursion those actions are written to use the very domain-specific language they define to tell their own small part of the story. > 大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用来讲那个故事。那种领域特定语言的一个部分,就是描述在系统中发生的各种行为的函数层级。在一种狡猾的递归操作中,这些行为使用它们定义的与领域紧密相关的语言讲述自己那个小故事。 This chapter has been about the mechanics of writing functions well. If you follow the rules herein, your functions will be short, well named, and nicely organized. But never forget that your real goal is to tell the story of the system, and that the functions you write need to fit cleanly together into a clear and precise language to help you with that telling. > 本章所讲述的是有关编写良好函数的机制。如果你遵循这些规则,函数就会短小,有个好名字,而且被很好地归置。不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。 ## 3.14 SETUPTEARDOWNINCLUDER SetupTeardownIncluder 程序 Listing 3-7 SetupTeardownIncluder.java > 代码清单 3-7 SetupTeardownIncluder.java ```java package fitnesse.html; import fitnesse.responders.run.SuiteResponder; import fitnesse.wiki.*; public class SetupTeardownIncluder { private PageData pageData; private boolean isSuite; private WikiPage testPage; private StringBuffer newPageContent; private PageCrawler pageCrawler; public static String render(PageData pageData) throws Exception { return render(pageData, false); } public static String render(PageData pageData, boolean isSuite) throws Exception { return new SetupTeardownIncluder(pageData).render(isSuite); } private SetupTeardownIncluder(PageData pageData) { this.pageData = pageData; testPage = pageData.getWikiPage(); pageCrawler = testPage.getPageCrawler(); newPageContent = new StringBuffer(); } private String render(boolean isSuite) throws Exception { this.isSuite = isSuite; if (isTestPage()) includeSetupAndTeardownPages(); return pageData.getHtml(); } private boolean isTestPage() throws Exception { return pageData.hasAttribute("Test"); } private void includeSetupAndTeardownPages() throws Exception { includeSetupPages(); includePageContent(); includeTeardownPages(); updatePageContent(); } private void includeSetupPages() throws Exception { if (isSuite) includeSuiteSetupPage(); includeSetupPage(); } private void includeSuiteSetupPage() throws Exception { include(SuiteResponder.SUITE_SETUP_NAME, "-setup"); } private void includeSetupPage() throws Exception { include("SetUp", "-setup"); } private void includePageContent() throws Exception { newPageContent.append(pageData.getContent()); } private void includeTeardownPages() throws Exception { includeTeardownPage(); if (isSuite) includeSuiteTeardownPage(); } private void includeTeardownPage() throws Exception { include("TearDown", "-teardown"); } private void includeSuiteTeardownPage() throws Exception { include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown"); } private void updatePageContent() throws Exception { pageData.setContent(newPageContent.toString()); } private void include(String pageName, String arg) throws Exception { WikiPage inheritedPage = findInheritedPage(pageName); if (inheritedPage != null) { String pagePathName = getPathNameForPage(inheritedPage); buildIncludeDirective(pagePathName, arg); } } private WikiPage findInheritedPage(String pageName) throws Exception { return PageCrawlerImpl.getInheritedPage(pageName, testPage); } private String getPathNameForPage(WikiPage page) throws Exception { WikiPagePath pagePath = pageCrawler.getFullPath(page); return PathParser.render(pagePath); } private void buildIncludeDirective(String pagePathName, String arg) { newPageContent .append("\n!include ") .append(arg) .append(" .") .append(pagePathName) .append("\n"); } } ``` ================================================ FILE: docs/ch4.md ================================================ # 第 4 章 Comments 注释 ![](figures/ch4/4_1fig_martin.jpg) “Don’t comment bad code—rewrite it.”—Brian W. Kernighan and P. J. Plaugher1 > “别给糟糕的代码加注释——重新写吧。”——Brian W. Kernighan 与 P. J. 1. [KP78], p. 144. Nothing can be quite so helpful as a well-placed comment. Nothing can clutter up a module more than frivolous dogmatic comments. Nothing can be quite so damaging as an old crufty comment that propagates lies and misinformation. > 什么也比不上放置良好的注释来得有用。什么也不会比乱七八糟的注释更有本事搞乱一个模块。什么也不会比陈旧、提供错误信息的注释更有破坏性。 Comments are not like Schindler’s List. They are not “pure good.” Indeed, comments are, at best, a necessary evil. If our programming languages were expressive enough, or if we had the talent to subtly wield those languages to express our intent, we would not need comments very much—perhaps not at all. > 注释并不像辛德勒的名单。它们并不“纯然地好”。实际上,注释最多也就是一种必须的恶。若编程语言足够有表达力,或者我们长于用这些语言来表达意图,就不那么需要注释——也许根本不需要。 The proper use of comments is to compensate for our failure to express ourself in code. Note that I used the word failure. I meant it. Comments are always failures. We must have them because we cannot always figure out how to express ourselves without them, but their use is not a cause for celebration. > 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。注意,我用了“失败”一词。我是说真的。注释总是一种失败。我们总无法找到不用注释就能表达自我的方法,所以总要有注释,这并不值得庆贺。 So when you find yourself in a position where you need to write a comment, think it through and see whether there isn’t some way to turn the tables and express yourself in code. Every time you express yourself in code, you should pat yourself on the back. Every time you write a comment, you should grimace and feel the failure of your ability of expression. > 如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达。每次用代码表达,你都该夸奖一下自己。每次写注释,你都该做个鬼脸,感受自己在表达能力上的失败。 Why am I so down on comments? Because they lie. Not always, and not intentionally, but too often. The older a comment is, and the farther away it is from the code it describes, the more likely it is to be just plain wrong. The reason is simple. Programmers can’t realistically maintain them. > 我为什么要极力贬低注释?因为注释会撒谎。也不是说总是如此或有意如此,但出现得实在太频繁。注释存在的时间越久,就离其所描述的代码越远,越来越变得全然错误。原因很简单。程序员不能坚持维护注释。 Code changes and evolves. Chunks of it move from here to there. Those chunks bifurcate and reproduce and come together again to form chimeras. Unfortunately the comments don’t always follow them—can’t always follow them. And all too often the comments get separated from the code they describe and become orphaned blurbs of ever-decreasing accuracy. For example, look what has happened to this comment and the line it was intended to describe: > 代码在变动,在演化。从这里移到那里。彼此分离、重造又合到一处。很不幸,注释并不总是随之变动——不能总是跟着走。注释常常会与其所描述的代码分隔开来,孑然飘零,越来越不准确。例如,看看以下注释以及它本来要描述的代码行变成了什么样子: ```java MockRequest request; private final String HTTP_DATE_REGEXP = “[SMTWF][a-z]{2}\\,\\s[0-9]{2}\\s[JFMASOND][a-z]{2}\\s”+ “[0-9]{4}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\sGMT”; private Response response; private FitNesseContext context; private FileResponder responder; private Locale saveLocale; // Example: ”Tue, 02 Apr 2003 22:18:49 GMT” ``` Other instance variables that were probably added later were interposed between the HTTP_DATE_REGEXP constant and it’s explanatory comment. > 在 HTTP_DATE_REGEXP 常量及其注释之间,有可能插入其他实体变量。 It is possible to make the point that programmers should be disciplined enough to keep the comments in a high state of repair, relevance, and accuracy. I agree, they should. But I would rather that energy go toward making the code so clear and expressive that it does not need the comments in the first place. > 程序员应当负责将注释保持在可维护、有关联、精确的高度。我同意这种说法。但我更主张把力气用在写清楚代码上,直接保证无须编写注释。 Inaccurate comments are far worse than no comments at all. They delude and mislead. They set expectations that will never be fulfilled. They lay down old rules that need not, or should not, be followed any longer. > 不准确的注释要比没注释坏得多。它们满口胡言。它们预期的东西永不能实现。它们设定了无需也不应再遵循的旧规则。 Truth can only be found in one place: the code. Only the code can truly tell you what it does. It is the only source of truly accurate information. Therefore, though comments are sometimes necessary, we will expend significant energy to minimize them. > 真实只在一处地方有:代码。只有代码能忠实地告诉你它做的事。那是唯一真正准确的信息来源。所以,尽管有时也需要注释,我们也该多花心思尽量减少注释量。 ## 4.1 COMMENTS DO NOT MAKE UP FOR BAD CODE 注释不能美化糟糕的代码 One of the more common motivations for writing comments is bad code. We write a module and we know it is confusing and disorganized. We know it’s a mess. So we say to ourselves, “Ooh, I’d better comment that!” No! You’d better clean it! > 写注释的常见动机之一是糟糕的代码的存在。我们编写一个模块,发现它令人困扰、乱七八糟。我们知道,它烂透了。我们告诉自己:“喔,最好写点注释!”不!最好是把代码弄干净! Clear and expressive code with few comments is far superior to cluttered and complex code with lots of comments. Rather than spend your time writing the comments that explain the mess you’ve made, spend it cleaning that mess. > 带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多。与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。 ## 4.2 EXPLAIN YOURSELF IN CODE 用代码来阐述 There are certainly times when code makes a poor vehicle for explanation. Unfortunately, many programmers have taken this to mean that code is seldom, if ever, a good means for explanation. This is patently false. Which would you rather see? This: > 有时,代码本身不足以解释其行为。不幸的是,许多程序员据此以为代码很少——如果有的话——能做好解释工作。这种观点纯属错误。 ```java // Check to see if the employee is eligible for full benefits if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) ``` Or this? > 你愿意看到这个: ```java if (employee.isEligibleForFullBenefits()) ``` It takes only a few seconds of thought to explain most of your intent in code. In many cases it’s simply a matter of creating a function that says the same thing as the comment you want to write. > 只要想上那么几秒钟,就能用代码解释你大部分的意图。很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可。 ## 4.3 GOOD COMMENTS 好注释 Some comments are necessary or beneficial. We’ll look at a few that I consider worthy of the bits they consume. Keep in mind, however, that the only truly good comment is the comment you found a way not to write. > 有些注释是必须的,也是有利的。来看看一些我认为值得写的注释。不过要记住,唯一真正好的注释是你想办法不去写的注释。 ### 4.3.1 Legal Comments 法律信息 Sometimes our corporate coding standards force us to write certain comments for legal reasons. For example, copyright and authorship statements are necessary and reasonable things to put into a comment at the start of each source file. > 有时,公司代码规范要求编写与法律有关的注释。例如,版权及著作权声明就是必须和有理由在每个源文件开头注释处放置的内容。 Here, for example, is the standard comment header that we put at the beginning of every source file in FitNesse. I am happy to say that our IDE hides this comment from acting as clutter by automatically collapsing it. > 下例是我们在 FitNesse 项目每个源文件开头放置的标准注释。我可以很开心地说,IDE 自动卷起这些注释,这样就不会显得凌乱了。 ```java // Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved. // Released under the terms of the GNU General Public License version 2 or later. ``` Comments like this should not be contracts or legal tomes. Where possible, refer to a standard license or other external document rather than putting all the terms and conditions into the comment. > 这类注释不应是合同或法典。只要有可能,就指向一份标准许可或其他外部文档,而不要把所有条款放到注释中。 ### 4.3.2 Informative Comments 提供信息的注释 It is sometimes useful to provide basic information with a comment. For example, consider this comment that explains the return value of an abstract method: > 有时,用注释来提供基本信息也有其用处。例如,以下注释解释了某个抽象方法的返回值: ```java // Returns an instance of the Responder being tested. protected abstract Responder responderInstance(); ``` A comment like this can sometimes be useful, but it is better to use the name of the function to convey the information where possible. For example, in this case the comment could be made redundant by renaming the function: responderBeingTested. > 这类注释有时管用,但更好的方式是尽量利用函数名称传达信息。比如,在本例中,只要把函数重新命名为 responderBeingTested,注释就是多余的了。 Here’s a case that’s a bit better: > 下例稍好一些: ```java // format matched kk:mm:ss EEE, MMM dd, yyyy Pattern timeMatcher = Pattern.compile( “\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*”); ``` In this case the comment lets us know that the regular expression is intended to match a time and date that were formatted with the SimpleDateFormat.format function using the specified format string. Still, it might have been better, and clearer, if this code had been moved to a special class that converted the formats of dates and times. Then the comment would likely have been superfluous. > SimpleDateFormat.format 函数利用特定格式字符串格式化的时间和日期。同样,如果把这段代码移到某个转换日期和时间格式的类中,就会更好、更清晰,而注释也就变得多此一举了。 ### 4.3.3 Explanation of Intent 对意图的解释 Sometimes a comment goes beyond just useful information about the implementation and provides the intent behind a decision. In the following case we see an interesting decision documented by a comment. When comparing two objects, the author decided that he wanted to sort objects of his class higher than objects of any other. > 有时,注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图。在下例中,我们看到注释反映出来的一个有趣决定。在对比两个对象时,作者决定将他的类放置在比其他东西更高的位置。 ```java public int compareTo(Object o) { if(o instanceof WikiPagePath) { WikiPagePath p = (WikiPagePath) o; String compressedName = StringUtil.join(names, “”); String compressedArgumentName = StringUtil.join(p.names, “”); return compressedName.compareTo(compressedArgumentName); } return 1; // we are greater because we are the right type. } ``` Here’s an even better example. You might not agree with the programmer’s solution to the problem, but at least you know what he was trying to do. > 下面的例子甚至更好。你也许不同意程序员给这个问题提供的解决方案,但至少你知道他想干什么。 ```java public void testConcurrentAddWidgets() throws Exception { WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class}); String text = ”’’’bold text’’’”; ParentWidget parent = new BoldWidget(new MockWidgetRoot(), ”’’’bold text’’’”); AtomicBoolean failFlag = new AtomicBoolean(); failFlag.set(false); //This is our best attempt to get a race condition //by creating large number of threads. for (int i = 0; i < 25000; i++) { WidgetBuilderThread widgetBuilderThread = new WidgetBuilderThread(widgetBuilder, text, parent, failFlag); Thread thread = new Thread(widgetBuilderThread); thread.start(); } assertEquals(false, failFlag.get()); } ``` ### 4.3.4 Clarification 阐释 Sometimes it is just helpful to translate the meaning of some obscure argument or return value into something that’s readable. In general it is better to find a way to make that argument or return value clear in its own right; but when its part of the standard library, or in code that you cannot alter, then a helpful clarifying comment can be useful. > 有时,注释把某些晦涩难明的参数或返回值的意义翻译为某种可读形式,也会是有用的。通常,更好的方法是尽量让参数或返回值自身就足够清楚;但如果参数或返回值是某个标准库的一部分,或是你不能修改的代码,帮助阐释其含义的代码就会有用。 ```java public void testCompareTo() throws Exception { WikiPagePath a = PathParser.parse("PageA"); WikiPagePath ab = PathParser.parse("PageA.PageB"); WikiPagePath b = PathParser.parse("PageB"); WikiPagePath aa = PathParser.parse("PageA.PageA"); WikiPagePath bb = PathParser.parse("PageB.PageB"); WikiPagePath ba = PathParser.parse("PageB.PageA"); assertTrue(a.compareTo(a) == 0); // a == a assertTrue(a.compareTo(b) != 0); // a != b assertTrue(ab.compareTo(ab) == 0); // ab == ab assertTrue(a.compareTo(b) == -1); // a < b assertTrue(aa.compareTo(ab) == -1); // aa < ab assertTrue(ba.compareTo(bb) == -1); // ba < bb assertTrue(b.compareTo(a) == 1); // b > a assertTrue(ab.compareTo(aa) == 1); // ab > aa assertTrue(bb.compareTo(ba) == 1); // bb > ba } ``` There is a substantial risk, of course, that a clarifying comment is incorrect. Go through the previous example and see how difficult it is to verify that they are correct. This explains both why the clarification is necessary and why it’s risky. So before writing comments like this, take care that there is no better way, and then take even more care that they are accurate. > 当然,这也会冒阐释性注释本身就不正确的风险。回头看看上例,你会发现想要确认注释的正确性有多难。这一方面说明了阐释有多必要,另外也说明了它有风险。所以,在写这类注释之前,考虑一下是否还有更好的办法,然后再加倍小心地确认注释正确性。 ### 4.3.5 Warning of Consequences 警示 Sometimes it is useful to warn other programmers about certain consequences. For example, here is a comment that explains why a particular test case is turned off: > 有时,用于警告其他程序员会出现某种后果的注释也是有用的。例如,下面的注释解释了为什么要关闭某个特定的测试用例: ![](figures/ch4/4_2fig_martin.jpg) ```java // Don't run unless you // have some time to kill. public void _testWithReallyBigFile() { writeLinesToFile(10000000); response.setBody(testFile); response.readyToSend(this); String responseString = output.toString(); assertSubString("Content-Length: 1000000000", responseString); assertTrue(bytesSent > 1000000000); } ``` Nowadays, of course, we’d turn off the test case by using the @Ignore attribute with an appropriate explanatory string. @Ignore(”Takes too long to run”). But back in the days before JUnit 4, putting an underscore in front of the method name was a common convention. The comment, while flippant, makes the point pretty well. > 当然,如今我们多数会利用附上恰当解释性字符串的@Ignore 属性来关闭测试用例。比如@Ignore("Takes too long to run[2]")。但在 JUnit4 之前的日子里,惯常的做法是在方法名前面加上下划线。如果注释足够有说服力,就会很有用了。 Here’s another, more poignant example: > 这里有个更麻烦的例子: ```java public static SimpleDateFormat makeStandardHttpDateFormat() { //SimpleDateFormat is not thread safe, //so we need to create each instance independently. SimpleDateFormat df = new SimpleDateFormat(”EEE, dd MMM yyyy HH:mm:ss z”); df.setTimeZone(TimeZone.getTimeZone(”GMT”)); return df; } ``` You might complain that there are better ways to solve this problem. I might agree with you. But the comment, as given here, is perfectly reasonable. It will prevent some overly eager programmer from using a static initializer in the name of efficiency. > 你也许会抱怨说,还会有更好的解决方法。我大概会同意。不过上面的注释绝对有道理存在,它能阻止某位急切的程序员以效率之名使用静态初始器。 ### 4.3.6 TODO Comments TODO 注释 It is sometimes reasonable to leave “To do” notes in the form of //TODO comments. In the following case, the TODO comment explains why the function has a degenerate implementation and what that function’s future should be. > 有时,有理由用//TODO 形式在源代码中放置要做的工作列表。在下例中,TODO 注释解释了为什么该函数的实现部分无所作为,将来应该是怎样。 ```java //TODO-MdM these are not needed // We expect this to go away when we do the checkout model protected VersionInfo makeVersion() throws Exception { return null; } ``` TODOs are jobs that the programmer thinks should be done, but for some reason can’t do at the moment. It might be a reminder to delete a deprecated feature or a plea for someone else to look at a problem. It might be a request for someone else to think of a better name or a reminder to make a change that is dependent on a planned event. Whatever else a TODO might be, it is not an excuse to leave bad code in the system. > TODO 是一种程序员认为应该做,但由于某些原因目前还没做的工作。它可能是要提醒删除某个不必要的特性,或者要求他人注意某个问题。它可能是恳请别人取个好名字,或者提示对依赖于某个计划事件的修改。无论 TODO 的目的如何,它都不是在系统中留下糟糕的代码的借口。 Nowadays, most good IDEs provide special gestures and features to locate all the TODO comments, so it’s not likely that they will get lost. Still, you don’t want your code to be littered with TODOs. So scan through them regularly and eliminate the ones you can. > 如今,大多数好 IDE 都提供了特别的手段来定位所有 TODO 注释,这些注释看来丢不了。你不会愿意代码因为 TODO 的存在而变成一堆垃圾,所以要定期查看,删除不再需要的。 ### 4.3.7 Amplification 放大 A comment may be used to amplify the importance of something that may otherwise seem inconsequential. > 注释可以用来放大某种看来不合理之物的重要性。 ```java String listItemContent = match.group(3).trim(); // the trim is real important. It removes the starting // spaces that could cause the item to be recognized // as another list. new ListItemWidget(this, listItemContent, this.level + 1); return buildList(text.substring(match.end())); ``` ### 4.3.8 Javadocs in Public APIs 公共 API 中的 Javadoc There is nothing quite so helpful and satisfying as a well-described public API. The java-docs for the standard Java library are a case in point. It would be difficult, at best, to write Java programs without them. > 没有什么比被良好描述的公共 API 更有用和令人满意的了。标准 Java 库中的 Javadoc 就是一例。没有它们,写 Java 程序就会变得很难。 If you are writing a public API, then you should certainly write good javadocs for it. But keep in mind the rest of the advice in this chapter. Javadocs can be just as misleading, nonlocal, and dishonest as any other kind of comment. > 如果你在编写公共 API,就该为它编写良好的 Javadoc。不过要记住本章中的其他建议。就像其他注释一样,Javadoc 也可能误导、不适用或者提供错误信息。 ## 4.4 BAD COMMENTS 坏注释 Most comments fall into this category. Usually they are crutches or excuses for poor code or justifications for insufficient decisions, amounting to little more than the programmer talking to himself. > 大多数注释都属此类。通常,坏注释都是糟糕的代码的支撑或借口,或者对错误决策的修正,基本上等于程序员自说自话。 ### 4.4.1 Mumbling 喃喃自语 Plopping in a comment just because you feel you should or because the process requires it, is a hack. If you decide to write a comment, then spend the time necessary to make sure it is the best comment you can write. > 如果只是因为你觉得应该或者因为过程需要就添加注释,那就是无谓之举。如果你决定写注释,就要花必要的时间确保写出最好的注释。 Here, for example, is a case I found in FitNesse, where a comment might indeed have been useful. But the author was in a hurry or just not paying much attention. His mumbling left behind an enigma: > 例如,我在 FitNesse 中找到的这个例子,例中的注释大概确实有用。不过,作者太着急,或者没太花心思。他的喃喃自语变成了一个谜团。 ```java public void loadProperties() { try { String propertiesPath = propertiesLocation + ”/” + PROPERTIES_FILE; FileInputStream propertiesStream = new FileInputStream(propertiesPath); loadedProperties.load(propertiesStream); } catch(IOException e) { // No properties files means all defaults are loaded } } ``` What does that comment in the catch block mean? Clearly it meant something to the author, but the meaning does not come through all that well. Apparently, if we get an IOException, it means that there was no properties file; and in that case all the defaults are loaded. But who loads all the defaults? Were they loaded before the call to loadProperties.load? Or did loadProperties.load catch the exception, load the defaults, and then pass the exception on for us to ignore? Or did loadProperties.load load all the defaults before attempting to load the file? Was the author trying to comfort himself about the fact that he was leaving the catch block empty? Or—and this is the scary possibility—was the author trying to tell himself to come back here later and write the code that would load the defaults? > catch 代码块中的注释是什么意思呢?显然对于作者有其意义,不过并没有好到足够的程度。很明显,如果出现 IOException,就表示没有属性文件;在那种情况下,载入默认设置。但谁来装载默认设置呢?会在对 loadProperties.load 之前装载吗?抑或 loadProperties.load 捕获异常、装载默认设置、再向上传递异常?再或 loadProperties.load 在尝试载入文件前就装载所有默认设置?要么作者只是在安慰自己别在意 catch 代码块的留空? Our only recourse is to examine the code in other parts of the system to find out what’s going on. Any comment that forces you to look in another module for the meaning of that comment has failed to communicate to you and is not worth the bits it consumes. > 或者——这种可能最可怕——作者是想告诉自己,将来再回头写装载默认设置的代码?我们唯有检视系统其他部分的代码,弄清事情原委。任何迫使读者查看其他模块的注释,都没能与读者沟通好,不值所费。 ### 4.4.2 Redundant Comments 多余的注释 Listing 4-1 shows a simple function with a header comment that is completely redundant. The comment probably takes longer to read than the code itself. > 代码清单 4-1 展示的简单函数,其头部位置的注释全属多余。读这段注释花的时间没准比读代码花的时间还要长。 Listing 4-1 waitForClose > 代码清单 4-1 waitForClose ```java // Utility method that returns when this.closed is true. Throws an exception // if the timeout is reached. public synchronized void waitForClose(final long timeoutMillis) throws Exception { if(!closed) { wait(timeoutMillis); if(!closed) throw new Exception("MockResponseSender could not be closed"); } } ``` What purpose does this comment serve? It’s certainly not more informative than the code. It does not justify the code, or provide intent or rationale. It is not easier to read than the code. Indeed, it is less precise than the code and entices the reader to accept that lack of precision in lieu of true understanding. It is rather like a gladhanding used-car salesman assuring you that you don’t need to look under the hood. > 这段注释起了什么作用?它并不能比代码本身提供更多的信息。它没有证明代码的意义,也没有给出代码的意图或逻辑。读它并不比读代码更容易。事实上,它不如代码精确,误导读者接受不精确的信息,而不是正确地理解代码。它就像个自来熟的二手车贩子,满口保证你不用打开发动机盖查验。来看看代码清单 4-2 中摘自 Tomcat 项目的无用而多余的 Javadoc 吧。 Now consider the legion of useless and redundant javadocs in Listing 4-2 taken from Tomcat. These comments serve only to clutter and obscure the code. They serve no documentary purpose at all. To make matters worse, I only showed you the first few. There are many more in this module. > 这些注释只是一味将代码搞得含糊不明。完全没有文档上的价值。下面只列出了靠前面的一些代码,后续模块中还有许多类似情况。 Listing 4-2 ContainerBase.java (Tomcat) > 代码清单 4-2 ContainerBase.java (Tomcat) ```java public abstract class ContainerBase implements Container, Lifecycle, Pipeline, MBeanRegistration, Serializable { /** * The processor delay for this component. */ protected int backgroundProcessorDelay = -1; /** * The lifecycle event support for this component. */ protected LifecycleSupport lifecycle = new LifecycleSupport(this); /** * The container event listeners for this Container. */ protected ArrayList listeners = new ArrayList(); /** * The Loader implementation with which this Container is * associated. */ protected Loader loader = null; /** * The Logger implementation with which this Container is * associated. */ protected Log logger = null; /** * Associated logger name. */ protected String logName = null; /** * The Manager implementation with which this Container is * associated. */ protected Manager manager = null; /** * The cluster with which this Container is associated. */ protected Cluster cluster = null; /** * The human-readable name of this Container. */ protected String name = null; /** * The parent Container to which this Container is a child. */ protected Container parent = null; /** * The parent class loader to be configured when we install a * Loader. */ protected ClassLoader parentClassLoader = null; /** * The Pipeline object with which this Container is * associated. */ protected Pipeline pipeline = new StandardPipeline(this); /** * The Realm with which this Container is associated. */ protected Realm realm = null; /** * The resources DirContext object with which this Container * is associated. */ protected DirContext resources = null; ``` ### 4.4.3 Misleading Comments 误导性注释 Sometimes, with all the best intentions, a programmer makes a statement in his comments that isn’t precise enough to be accurate. Consider for another moment the badly redundant but also subtly misleading comment we saw in Listing 4-1. > 有时,尽管初衷可嘉,程序员还是会写出不够精确的注释。想想看代码清单 4-1 中那多余而又有误导嫌疑的注释吧。 Did you discover how the comment was misleading? The method does not return when this.closed becomes true. It returns if this.closed is true; otherwise, it waits for a blind time-out and then throws an exception if this.closed is still not true. > 你有没有发现那样的注释是如何误导读者的?在 this.closed 变为 true 的时候,方法并没有返回。方法只在判断到 this.closed 为 true 的时候返回,否则,就只是等待遥遥无期的超时,然后如果判断 this.closed 还是非 true,就抛出一个异常。 This subtle bit of misinformation, couched in a comment that is harder to read than the body of the code, could cause another programmer to blithely call this function in the expectation that it will return as soon as this.closed becomes true. That poor programmer would then find himself in a debugging session trying to figure out why his code executed so slowly. > 这一细微的误导信息,放在比代码本身更难阅读的注释里面,有可能导致其他程序员快活地调用这个函数,并期望在 this.closed 变为 true 时立即返回。那位可怜的程序员将会发现自己陷于调试困境之中,拼命想找出代码执行得如此之慢的原因。 ### 4.4.4 Mandated Comments 循规式注释 It is just plain silly to have a rule that says that every function must have a javadoc, or every variable must have a comment. Comments like this just clutter up the code, propagate lies, and lend to general confusion and disorganization. > 所谓每个函数都要有 Javadoc 或每个变量都要有注释的规矩全然是愚蠢可笑的。这类注释徒然让代码变得散乱,满口胡言,令人迷惑不解。 For example, required javadocs for every function lead to abominations such as Listing 4-3. This clutter adds nothing and serves only to obfuscate the code and create the potential for lies and misdirection. > 例如,要求每个函数都要有 Javadoc,就会得到类似代码清单 4-3 那样面目可憎的代码。这类废话只会搞乱代码,有可能误导读者。 Listing 4-3 > 代码清单 4-3 ```java /** * * @param title The title of the CD * @param author The author of the CD * @param tracks The number of tracks on the CD * @param durationInMinutes The duration of the CD in minutes */ public void addCD(String title, String author, int tracks, int durationInMinutes) { CD cd = new CD(); cd.title = title; cd.author = author; cd.tracks = tracks; cd.duration = duration; cdList.add(cd); } ``` ### 4.4.5 Journal Comments 日志式注释 Sometimes people add a comment to the start of a module every time they edit it. These comments accumulate as a kind of journal, or log, of every change that has ever been made. I have seen some modules with dozens of pages of these run-on journal entries. > 有人会在每次编辑代码时,在模块开始处添加一条注释。这类注释就像是一种记录每次修改的日志。我见过满篇尽是这类日志的代码模块。 ```java * Changes (from 11-Oct-2001) * -------------------------- * 11-Oct-2001 : Re-organised the class and moved it to new package * com.jrefinery.date (DG); * 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate * class (DG); * 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate * class is gone (DG); Changed getPreviousDayOfWeek(), * getFollowingDayOfWeek() and getNearestDayOfWeek() to correct * bugs (DG); * 05-Dec-2001 : Fixed bug in SpreadsheetDate class (DG); * 29-May-2002 : Moved the month constants into a separate interface * (MonthConstants) (DG); * 27-Aug-2002 : Fixed bug in addMonths() method, thanks to N???levka Petr (DG); * 03-Oct-2002 : Fixed errors reported by Checkstyle (DG); * 13-Mar-2003 : Implemented Serializable (DG); * 29-May-2003 : Fixed bug in addMonths method (DG); * 04-Sep-2003 : Implemented Comparable. Updated the isInRange javadocs (DG); * 05-Jan-2005 : Fixed bug in addYears() method (1096282) (DG); ``` Long ago there was a good reason to create and maintain these log entries at the start of every module. We didn’t have source code control systems that did it for us. Nowadays, however, these long journals are just more clutter to obfuscate the module. They should be completely removed. > 很久以前,在模块开始处创建并维护这些记录还算有道理。那时,我们还没有源代码控制系统可用。如今,这种冗长的记录只会让模块变得凌乱不堪,应当全部删除。 ### 4.4.6 Noise Comments 废话注释 Sometimes you see comments that are nothing but noise. They restate the obvious and provide no new information. > 有时,你会看到纯然是废话的注释。它们对于显然之事喋喋不休,毫无新意。 ```java /** * Default constructor. */ protected AnnualDateRule() { } ``` No, really? Or how about this: > 对吧?再看看这个: ```java /** The day of the month. */ private int dayOfMonth; ``` And then there’s this paragon of redundancy: > 这样的废话模范: ```java /** * Returns the day of the month. * * @return the day of the month. */ public int getDayOfMonth() { return dayOfMonth; } ``` These comments are so noisy that we learn to ignore them. As we read through code, our eyes simply skip over them. Eventually the comments begin to lie as the code around them changes. > 这类注释废话连篇,我们都学会了视而不见。读代码时,眼光不会停留在它们上面。最终,当代码修改之后,这类注释就变作了谎言一堆。 The first comment in Listing 4-4 seems appropriate.2 It explains why the catch block is being ignored. But the second comment is pure noise. Apparently the programmer was just so frustrated with writing try/catch blocks in this function that he needed to vent. > 代码清单 4-4 中的第一条注释貌似还行[3]。它解释了 catch 代码块为何被忽略。不过第二条注释就纯是废话了。显然,该程序员沮丧于编写函数中那些 try/catch 代码块。 2. The current trend for IDEs to check spelling in comments will be a balm for those of us who read a lot of code. Listing 4-4 startSending > 代码清单 4-4 startSending ```java private void startSending() { try { doSending(); } catch(SocketException e) { // normal. someone stopped the request. } catch(Exception e) { try { response.add(ErrorResponder.makeExceptionString(e)); response.closeAll(); } catch(Exception e1) { //Give me a break! } } } ``` Rather than venting in a worthless and noisy comment, the programmer should have recognized that his frustration could be resolved by improving the structure of his code. He should have redirected his energy to extracting that last try/catch block into a separate function, as shown in Listing 4-5. > 与其纠缠于毫无价值的废话注释,程序员应该意识到,他的挫败感可以由改进代码结构而消除。他应该把力气花在将最末一个 try/catch 代码块拆解到单独的函数中,如代码清单 4-5 所示。 Listing 4-5 startSending (refactored) > 代码清单 4-5 startSending(重构之后) ```java private void startSending() { try { doSending(); } catch(SocketException e) { // normal. someone stopped the request. } catch(Exception e) { addExceptionAndCloseResponse(e); } } private void addExceptionAndCloseResponse(Exception e) { try { response.add(ErrorResponder.makeExceptionString(e)); response.closeAll(); } catch(Exception e1) { } } ``` Replace the temptation to create noise with the determination to clean your code. You’ll find it makes you a better and happier programmer. > 用整理代码的决心替代创造废话的冲动吧。你会发现自己成为更优秀、更快乐的程序员。 ### 4.4.7 Scary Noise 可怕的废话 Javadocs can also be noisy. What purpose do the following Javadocs (from a well-known open-source library) serve? Answer: nothing. They are just redundant noisy comments written out of some misplaced desire to provide documentation. > Javadoc 也可能是废话。下列 Javadoc(来自某知名开源库)的目的是什么?答案:无。它们只是源自某种提供文档的不当愿望的废话注释。 ```java /** The name. */ private String name; /** The version. */ private String version; /** The licenceName. */ private String licenceName; /** The version. */ private String info; ``` Read these comments again more carefully. Do you see the cut-paste error? If authors aren’t paying attention when comments are written (or pasted), why should readers be expected to profit from them? > 再仔细读读这些注释。你是否发现了剪切-粘贴错误?如果作者在写(或粘贴)注释时都没花心思,怎么能指望读者从中获益呢? ### 4.4.8 Don’t Use a Comment When You Can Use a Function or a Variable 能用函数或变量时就别用注释 Consider the following stretch of code: > 看看以下代码概要: ```java // does the module from the global list depend on the // subsystem we are part of? if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem())) ``` This could be rephrased without the comment as > 可以改成以下没有注释的版本: ```java ArrayList moduleDependees = smodule.getDependSubsystems(); String ourSubSystem = subSysMod.getSubSystem(); if (moduleDependees.contains(ourSubSystem)) ``` The author of the original code may have written the comment first (unlikely) and then written the code to fulfill the comment. However, the author should then have refactored the code, as I did, so that the comment could be removed. > 代码原作者可能(不太像)是先写注释再编写代码。不过,作者应该重构代码,如我所做的那样,从而删掉注释。 ### 4.4.9 Position Markers 位置标记 Sometimes programmers like to mark a particular position in a source file. For example, I recently found this in a program I was looking through: > 有时,程序员喜欢在源代码中标记某个特别位置。例如,最近我在程序中看到这样一行: ```java // Actions ////////////////////////////////// ``` There are rare times when it makes sense to gather certain functions together beneath a banner like this. But in general they are clutter that should be eliminated—especially the noisy train of slashes at the end. > 把特定函数趸放在这种标记栏下面,多数时候实属无理。鸡零狗碎,理当删除——特别是尾部那一长串无用的斜杠。 Think of it this way. A banner is startling and obvious if you don’t see banners very often. So use them very sparingly, and only when the benefit is significant. If you overuse banners, they’ll fall into the background noise and be ignored. > 这么说吧。如果标记栏不多,就会显而易见。所以,尽量少用标记栏,只在特别有价值的时候用。如果滥用标记栏,就会沉没在背景噪音中,被忽略掉。 ### 4.4.10 Closing Brace Comments 括号后面的注释 Sometimes programmers will put special comments on closing braces, as in Listing 4-6. Although this might make sense for long functions with deeply nested structures, it serves only to clutter the kind of small and encapsulated functions that we prefer. So if you find yourself wanting to mark your closing braces, try to shorten your functions instead. > 有时,程序员会在括号后面放置特殊的注释,如代码清单 4-6 所示。尽管这对于含有深度嵌套结构的长函数可能有意义,但只会给我们更愿意编写的短小、封装的函数带来混乱。如果你发现自己想标记右括号,其实应该做的是缩短函数。 Listing 4-6 wc.java > 代码清单 4-6 wc.java ```java public class wc { public static void main(String[] args) { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); String line; int lineCount = 0; int charCount = 0; int wordCount = 0; try { while ((line = in.readLine()) != null) { lineCount++; charCount += line.length(); String words[] = line.split("\\W"); wordCount += words.length; } //while System.out.println("wordCount = " + wordCount); System.out.println("lineCount = " + lineCount); System.out.println("charCount = " + charCount); } // try catch (IOException e) { System.err.println("Error:" + e.getMessage()); } //catch } //main } ``` ### 4.4.11 Attributions and Bylines 归属与署名 ```java /* Added by Rick */ ``` Source code control systems are very good at remembering who added what, when. There is no need to pollute the code with little bylines. You might think that such comments would be useful in order to help others know who to talk to about the code. But the reality is that they tend to stay around for years and years, getting less and less accurate and relevant. > 源代码控制系统非常善于记住是谁在何时添加了什么。没必要用那些小小的签名搞脏代码。你也许会认为,这种注释大概有助于他人了解应该和谁讨论这段代码。不过,事实却是注释在那儿放了一年又一年,越来越不准确,越来越和原作者没关系。 Again, the source code control system is a better place for this kind of information. > 重申一下,源代码控制系统是这类信息最好的归属地。 ### 4.4.12 Commented-Out Code 注释掉的代码 Few practices are as odious as commenting-out code. Don’t do this! > 直接把代码注释掉是讨厌的做法。别这么干! ```java InputStreamResponse response = new InputStreamResponse(); response.setBody(formatter.getResultStream(), formatter.getByteCount()); // InputStream resultsStream = formatter.getResultStream(); // StreamReader reader = new StreamReader(resultsStream); // response.setContent(reader.read(formatter.getByteCount())); ``` Others who see that commented-out code won’t have the courage to delete it. They’ll think it is there for a reason and is too important to delete. So commented-out code gathers like dregs at the bottom of a bad bottle of wine. > 其他人不敢删除注释掉的代码。他们会想,代码依然放在那儿,一定有其原因,而且这段代码很重要,不能删除。注释掉的代码堆积在一起,就像破酒瓶底的渣滓一般。 Consider this from apache commons: > 看看以下来自 Apache 公共库的代码: ```java this.bytePos = writeBytes(pngIdBytes, 0); //hdrPos = bytePos; writeHeader(); writeResolution(); //dataPos = bytePos; if (writeImageData()) { writeEnd(); this.pngBytes = resizeByteArray(this.pngBytes, this.maxPos); } else { this.pngBytes = null; } return this.pngBytes; ``` Why are those two lines of code commented? Are they important? Were they left as reminders for some imminent change? Or are they just cruft that someone commented-out years ago and has simply not bothered to clean up. > 这两行代码为什么要注释掉?它们重要吗?它们搁在那儿,是为了给未来的修改做提示吗?或者,只是某人在多年以前注释掉、懒得清理的过时玩意? There was a time, back in the sixties, when commenting-out code might have been useful. But we’ve had good source code control systems for a very long time now. Those systems will remember the code for us. We don’t have to comment it out any more. Just delete the code. We won’t lose it. Promise. > 20 世纪 60 年代,曾经有那么一段时间,注释掉的代码可能有用。但我们已经拥有优良的源代码控制系统如此之久,这些系统可以为我们记住不要的代码。我们无需再用注释来标记,删掉即可,它们丢不了。我担保。 ### 4.4.13 HTML Comments HTML 注释 HTML in source code comments is an abomination, as you can tell by reading the code below. It makes the comments hard to read in the one place where they should be easy to read—the editor/IDE. If comments are going to be extracted by some tool (like Javadoc) to appear in a Web page, then it should be the responsibility of that tool, and not the programmer, to adorn the comments with appropriate HTML. > 源代码注释中的 HTML 标记是一种厌物,如你在下面代码中所见。编辑器/IDE 中的代码本来易于阅读,却因为 HTML 注释的存在而变得难以卒读。如果注释将由某种工具(例如 Javadoc)抽取出来,呈现到网页,那么该是工具而非程序员来负责给注释加上合适的 HTML 标签。 ```java /** * Task to run fit tests. * This task runs fitnesse tests and publishes the results. *

    *

    * Usage:
    * <taskdef name="execute-fitnesse-tests"
    *     classname="fitnesse.ant.ExecuteFitnesseTestsTask"
    *     classpathref="classpath" />
    * OR
    * <taskdef classpathref="classpath"
    *             resource="tasks.properties" />
    * 

    * <execute-fitnesse-tests * suitepage="FitNesse.SuiteAcceptanceTests" * fitnesseport="8082" * resultsdir="${results.dir}" * resultshtmlpage="fit-results.html" * classpathref="classpath" /> *

    */ ``` ### 4.4.14 Nonlocal Information 非本地信息 If you must write a comment, then make sure it describes the code it appears near. Don’t offer systemwide information in the context of a local comment. Consider, for example, the javadoc comment below. Aside from the fact that it is horribly redundant, it also offers information about the default port. And yet the function has absolutely no control over what that default is. The comment is not describing the function, but some other, far distant part of the system. Of course there is no guarantee that this comment will be changed when the code containing the default is changed. > 假如你一定要写注释,请确保它描述了离它最近的代码。别在本地注释的上下文环境中给出系统级的信息。以下面的 Javadoc 注释为例,除了那可怕的冗余之外,它还给出了有关默认端口的信息。不过该函数完全没控制到那个所谓默认值。这个注释并未描述该函数,而是在描述系统中远在他方的其他函数。当然,也无法担保在包含那个默认值的代码修改之后,这里的注释也会跟着修改。 ```java /** * Port on which fitnesse would run. Defaults to 8082. * * @param fitnessePort */ public void setFitnessePort(int fitnessePort) { this.fitnessePort = fitnessePort; } ``` ### 4.4.15 Too Much Information 信息过多 Don’t put interesting historical discussions or irrelevant descriptions of details into your comments. The comment below was extracted from a module designed to test that a function could encode and decode base64. Other than the RFC number, someone reading this code has no need for the arcane information contained in the comment. > 别在注释中添加有趣的历史性话题或者无关的细节描述。下列注释来自某个用来测试 base64 编解码函数的模块。除了 RFC 文档编号之外,注释中的其他细节信息对于读者完全没有必要。 ```java /* RFC 2045 - Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies section 6.8. Base64 Content-Transfer-Encoding The encoding process represents 24-bit groups of input bits as output strings of 4 encoded characters. Proceeding from left to right, a 24-bit input group is formed by concatenating 3 8-bit input groups. These 24 bits are then treated as 4 concatenated 6-bit groups, each of which is translated into a single digit in the base64 alphabet. When encoding a bit stream via the base64 encoding, the bit stream must be presumed to be ordered with the most-significant-bit first. That is, the first bit in the stream will be the high-order bit in the first 8-bit byte, and the eighth bit will be the low-order bit in the first 8-bit byte, and so on. */ ``` ### 4.4.16 Inobvious Connection 不明显的联系 The connection between a comment and the code it describes should be obvious. If you are going to the trouble to write a comment, then at least you’d like the reader to be able to look at the comment and the code and understand what the comment is talking about. > 注释及其描述的代码之间的联系应该显而易见。如果你不嫌麻烦要写注释,至少让读者能看着注释和代码,并且理解注释所谈何物。 Consider, for example, this comment drawn from apache commons: > 以来自 Apache 公共库的这段注释为例: ```java /* * start with an array that is big enough to hold all the pixels * (plus filter bytes), and an extra 200 bytes for header info */ this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200]; ``` What is a filter byte? Does it relate to the +1? Or to the \*3? Both? Is a pixel a byte? Why 200? The purpose of a comment is to explain code that does not explain itself. It is a pity when a comment needs its own explanation. > 过滤器字节是什么?与那个+1 有关系吗?或与\*3 有关?还是与两者皆有关?为什么用 200?注释的作用是解释未能自行解释的代码。如果注释本身还需要解释,就太遗憾了。 ### 4.4.17 Function Headers 函数头 Short functions don’t need much description. A well-chosen name for a small function that does one thing is usually better than a comment header. > 短函数不需要太多描述。为只做一件事的短函数选个好名字,通常要比写函数头注释要好。 ### 4.4.18 Javadocs in Nonpublic Code 非公共代码中的 Javadoc As useful as javadocs are for public APIs, they are anathema to code that is not intended for public consumption. Generating javadoc pages for the classes and functions inside a system is not generally useful, and the extra formality of the javadoc comments amounts to little more than cruft and distraction. > 虽然 Javadoc 对于公共 API 非常有用,但对于不打算作公共用途的代码就令人厌恶了。为系统中的类和函数生成 Javadoc 页并非总有用,而 Javadoc 注释额外的形式要求几乎等同于八股文章。 ### 4.4.19 Example 范例 I wrote the module in Listing 4-7 for the first XP Immersion. It was intended to be an example of bad coding and commenting style. Kent Beck then refactored this code into a much more pleasant form in front of several dozen enthusiastic students. Later I adapted the example for my book Agile Software Development, Principles, Patterns, and Practices and the first of my Craftsman articles published in Software Development magazine. > 我曾为首个 XP Immersion[4]课程编写了代码清单 4-7 列出的模块。这个模块几乎是糟糕的代码和坏注释风格的典范。后来 Kent Beck 当着几十位满腔热情的学生的面重构了这些代码,将其变得令人愉悦。后来,我在拙著 Agile Software Development,Principles,Patterns,and Practices(中译版《敏捷软件开发:原则、模式与实践》)和 Software Development(软件开发)杂志的“技艺”专栏的第一篇文章中引用了这个例子。 What I find fascinating about this module is that there was a time when many of us would have considered it “well documented.” Now we see it as a small mess. See how many different comment problems you can find. > 这个模块最迷人的地方是,有那么一阵,我们中的许多人都认为它“文档做得很好”。如今,我们认为它是一小团乱麻。看看你能发现多少个不同的注释问题吧。 Listing 4-7 GeneratePrimes.java > 代码清单 4-7 GeneratePrimes.java ```java /** * This class Generates prime numbers up to a user specified * maximum. The algorithm used is the Sieve of Eratosthenes. *

    * Eratosthenes of Cyrene, b. c. 276 BC, Cyrene, Libya -- * d. c. 194, Alexandria. The first man to calculate the * circumference of the Earth. Also known for working on * calendars with leap years and ran the library at Alexandria. *

    * The algorithm is quite simple. Given an array of integers * starting at 2. Cross out all multiples of 2. Find the next * uncrossed integer, and cross out all of its multiples. * Repeat untilyou have passed the square root of the maximum * value. * * @author Alphonse * @version 13 Feb 2002 atp */ import java.util.*; public class GeneratePrimes { /** * @param maxValue is the generation limit. */ public static int[] generatePrimes(int maxValue) { if (maxValue >= 2) // the only valid case { // declarations int s = maxValue + 1; // size of array boolean[] f = new boolean[s]; int i; // initialize array to true. for (i = 0; i < s; i++) f[i] = true; // get rid of known non-primes f[0] = f[1] = false; // sieve int j; for (i = 2; i < Math.sqrt(s) + 1; i++) { if (f[i]) // if i is uncrossed, cross its multiples. { for (j = 2 * i; j < s; j += i) f[j] = false; // multiple is not prime } } // how many primes are there? int count = 0; for (i = 0; i < s; i++) { if (f[i]) count++; // bump count. } int[] primes = new int[count]; // move the primes into the result for (i = 0, j = 0; i < s; i++) { if (f[i]) // if prime primes[j++] = i; } return primes; // return the primes } else // maxValue < 2 return new int[0]; // return null array if bad input. } } ``` In Listing 4-8 you can see a refactored version of the same module. Note that the use of comments is significantly restrained. There are just two comments in the whole module. Both comments are explanatory in nature. > 在代码清单 4-8 中,你可以看到该模块重构后的版本。注意,注释的使用被明显地限制了。在整个模块中只有两个注释。每个注释都足具说明意义。 Listing 4-8 PrimeGenerator.java (refactored) > 代码清单 4-8 PrimeGenerator.java(重构后) ```java /** * This class Generates prime numbers up to a user specified * maximum. The algorithm used is the Sieve of Eratosthenes. * Given an array of integers starting at 2: * Find the first uncrossed integer, and cross out all its * multiples. Repeat until there are no more multiples * in the array. */ public class PrimeGenerator { private static boolean[] crossedOut; private static int[] result; public static int[] generatePrimes(int maxValue) { if (maxValue < 2) return new int[0]; else { uncrossIntegersUpTo(maxValue); crossOutMultiples(); putUncrossedIntegersIntoResult(); return result; } } private static void uncrossIntegersUpTo(int maxValue) { crossedOut = new boolean[maxValue + 1]; for (int i = 2; i < crossedOut.length; i++) crossedOut[i] = false; } private static void crossOutMultiples() { int limit = determineIterationLimit(); for (int i = 2; i <= limit; i++) if (notCrossed(i)) crossOutMultiplesOf(i); } private static int determineIterationLimit() { // Every multiple in the array has a prime factor that // is less than or equal to the root of the array size, // so we don’t have to cross out multiples of numbers // larger than that root. double iterationLimit = Math.sqrt(crossedOut.length); return (int) iterationLimit; } private static void crossOutMultiplesOf(int i) { for (int multiple = 2*i; multiple < crossedOut.length; multiple += i) crossedOut[multiple] = true; } private static boolean notCrossed(int i) { return crossedOut[i] == false; } private static void putUncrossedIntegersIntoResult() { result = new int[numberOfUncrossedIntegers()]; for (int j = 0, i = 2; i < crossedOut.length; i++) if (notCrossed(i)) result[j++] = i; } private static int numberOfUncrossedIntegers() { int count = 0; for (int i = 2; i < crossedOut.length; i++) if (notCrossed(i)) count++; return count; } } ``` It is easy to argue that the first comment is redundant because it reads very much like the generatePrimes function itself. Still, I think the comment serves to ease the reader into the algorithm, so I’m inclined to leave it. > 很容易说明,第一个注释完全是多余的,因为它读起来非常像是 generatePrimes 函数自身。不过,我认为这段注释还是省了读者去读具体算法的精力,所以我倾向于留下它。 The second argument is almost certainly necessary. It explains the rationale behind the use of the square root as the loop limit. I could find no simple variable name, nor any different coding structure that made this point clear. On the other hand, the use of the square root might be a conceit. Am I really saving that much time by limiting the iteration to the square root? Could the calculation of the square root take more time than I’m saving? > 第二个注释显然很有必要。它解释了平方根作为循环限制的理由。我找不到能说明白这个问题的简单变量名或者其他编程结构。另外,对平方根的使用可能也有点武断。通过限制平方根循环,我是否真节省了许多时间?平方根计算所花的时间会不会比省下的时间还要多? It’s worth thinking about. Using the square root as the iteration limit satisfies the old C and assembly language hacker in me, but I’m not convinced it’s worth the time and effort that everyone else will expend to understand it. > 这些都值得考虑。使用平方根作为循环限制,满足了我这种旧式 C 语言和汇编语言黑客,不过我可不敢说抵得上其他人为理解它而花的时间和精力。 ================================================ FILE: docs/ch5.md ================================================ # 第 5 章 Formatting 格式 ![](figures/ch5/5_1fig_martin.jpg) When people look under the hood, we want them to be impressed with the neatness, consistency, and attention to detail that they perceive. We want them to be struck by the orderliness. We want their eyebrows to rise as they scroll through the modules. We want them to perceive that professionals have been at work. If instead they see a scrambled mass of code that looks like it was written by a bevy of drunken sailors, then they are likely to conclude that the same inattention to detail pervades every other aspect of the project. > 当有人查看底层代码实现时,我们希望他们为其整洁、一致及所感知到的对细节的关注而震惊。我们希望他们高高扬起眉毛,一路看下去。我们希望他们感受到那些为之劳作的专业人士们。但若他们看到的只是一堆像是由酒醉的水手写出的鬼画符,那他们多半会得出结论,认为项目其他任何部分也同样对细节漠不关心。 You should take care that your code is nicely formatted. You should choose a set of simple rules that govern the format of your code, and then you should consistently apply those rules. If you are working on a team, then the team should agree to a single set of formatting rules and all members should comply. It helps to have an automated tool that can apply those formatting rules for you. > 当有人查看底层代码实现时,我们希望他们为其整洁、一致及所感知到的对细节的关注而震惊。我们希望他们高高扬起眉毛,一路看下去。我们希望他们感受到那些为之劳作的专业人士们。但若他们看到的只是一堆像是由酒醉的水手写出的鬼画符,那他们多半会得出结论,认为项目其他任何部分也同样对细节漠不关心。 ## 5.1 THE PURPOSE OF FORMATTING 格式的目的 First of all, let’s be clear. Code formatting is important. It is too important to ignore and it is too important to treat religiously. Code formatting is about communication, and communication is the professional developer’s first order of business. > 先明确一下,代码格式很重要。代码格式不可忽略,必须严肃对待。代码格式关乎沟通,而沟通是专业开发者的头等大事。 Perhaps you thought that “getting it working” was the first order of business for a professional developer. I hope by now, however, that this book has disabused you of that idea. The functionality that you create today has a good chance of changing in the next release, but the readability of your code will have a profound effect on all the changes that will ever be made. The coding style and readability set precedents that continue to affect maintainability and extensibility long after the original code has been changed beyond recognition. Your style and discipline survives, even though your code does not. > 或许你认为“让代码能工作”才是专业开发者的头等大事。然而,我希望本书能让你抛掉那种想法。你今天编写的功能,极有可能在下一版本中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。原始代码修改之后很久,其代码风格和可读性仍会影响到可维护性和扩展性。即便代码已不复存在,你的风格和律条仍存活下来。 So what are the formatting issues that help us to communicate best? > 那么,哪些代码格式相关方面能帮我们最好地沟通呢? ## 5.2 VERTICAL FORMATTING 垂直格式 Let’s start with vertical size. How big should a source file be? In Java, file size is closely related to class size. We’ll talk about class size when we talk about classes. For the moment let’s just consider file size. > 从垂直尺寸开始吧。源代码文件该有多大?在 Java 中,文件尺寸与类尺寸极其相关。讨论类时再说类的尺寸。现在先考虑文件尺寸。 How big are most Java source files? It turns out that there is a huge range of sizes and some remarkable differences in style. Figure 5-1 shows some of those differences. > 多数 Java 源代码文件有多大?事实说明,尺寸各各不同,长度殊异,如图 5-1 所示。 Seven different projects are depicted. Junit, FitNesse, testNG, Time and Money, JDepend, Ant, and Tomcat. The lines through the boxes show the minimum and maximum file lengths in each project. The box shows approximately one-third (one standard deviation1) of the files. The middle of the box is the mean. So the average file size in the FitNesse project is about 65 lines, and about one-third of the files are between 40 and 100+ lines. The largest file in FitNesse is about 400 lines and the smallest is 6 lines. Note that this is a log scale, so the small difference in vertical position implies a very large difference in absolute size. > 图 5-1 中涉及 7 个不同项目:Junit、FitNesse、testNG、Time and Money、JDepend、Ant 和 Tomcat。贯穿方块的直线两端显示这些项目中最小和最大的文件长度。方块表示在平均值以上或以下的大约三分之一文件(一个标准偏差[1])的长度。方块中间位置就是平均数。所以 FitNesse 项目的文件平均尺寸是 65 行,而上面三分之一在 40 ~ 100 行及 100 行以上之间。FitNesse 中最大的文件大约 400 行,最小是 6 行。这是个对数标尺,所以较小的垂直位置差异意味着文件绝对尺寸的较大差异。 1. The box shows sigma/2 above and below the mean. Yes, I know that the file length distribution is not normal, and so the standard deviation is not mathematically precise. But we’re not trying for precision here. We’re just trying to get a feel. Figure 5-1 File length distributions LOG scale (box height = sigma) ![](figures/ch5/5_2fig_martin.jpg) Junit, FitNesse, and Time and Money are composed of relatively small files. None are over 500 lines and most of those files are less than 200 lines. Tomcat and Ant, on the other hand, have some files that are several thousand lines long and close to half are over 200 lines. > Junit、FitNesse 和 Time and Money 由相对较小的文件组成。没有一个超过 500 行,多数都小于 200 行。Tomcat 和 Ant 则有些文件达到数千行,将近一半文件长于 200 行。 What does that mean to us? It appears to be possible to build significant systems (FitNesse is close to 50,000 lines) out of files that are typically 200 lines long, with an upper limit of 500. Although this should not be a hard and fast rule, it should be considered very desirable. Small files are usually easier to understand than large files are. > 对我们来说,这意味着什么?意味着有可能用大多数为 200 行、最长 500 行的单个文件构造出色的系统(FitNesse 总长约 50000 行)。尽管这并非不可违背的原则,也应该乐于接受。短文件通常比长文件易于理解。 ### 5.2.1 The Newspaper Metaphor 向报纸学习 Think of a well-written newspaper article. You read it vertically. At the top you expect a headline that will tell you what the story is about and allows you to decide whether it is something you want to read. The first paragraph gives you a synopsis of the whole story, hiding all the details while giving you the broad-brush concepts. As you continue downward, the details increase until you have all the dates, names, quotes, claims, and other minutia. > 想想看写得很好的报纸文章。你从上到下阅读。在顶部,你期望有个头条,告诉你故事主题,好让你决定是否要读下去。第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事细节。接着读下去,细节渐次增加,直至你了解所有的日期、名字、引语、说法及其他细节。 We would like a source file to be like a newspaper article. The name should be simple but explanatory. The name, by itself, should be sufficient to tell us whether we are in the right module or not. The topmost parts of the source file should provide the high-level concepts and algorithms. Detail should increase as we move downward, until at the end we find the lowest level functions and details in the source file. > 源文件也要像报纸文章那样。名称应当简单且一目了然。名称本身应该足够告诉我们是否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。 A newspaper is composed of many articles; most are very small. Some are a bit larger. Very few contain as much text as a page can hold. This makes the newspaper usable. If the newspaper were just one long story containing a disorganized agglomeration of facts, dates, and names, then we simply would not read it. > 报纸由许多篇文章组成;多数短小精悍。有些稍微长点儿。很少有占满一整页的。这样做,报纸才可用。假若一份报纸只登载一篇长故事,其中充斥毫无组织的事实、日期、名字等,没人会去读它。 ### 5.2.2 Vertical Openness Between Concepts 概念间垂直方向上的区隔 Nearly all code is read left to right and top to bottom. Each line represents an expression or a clause, and each group of lines represents a complete thought. Those thoughts should be separated from each other with blank lines. > 几乎所有的代码都是从上往下读,从左往右读。每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。这些思路用空白行区隔开来。 Consider, for example, Listing 5-1. There are blank lines that separate the package declaration, the import(s), and each of the functions. This extremely simple rule has a profound effect on the visual layout of the code. Each blank line is a visual cue that identifies a new and separate concept. As you scan down the listing, your eye is drawn to the first line that follows a blank line. > 以代码清单 5-1 为例。在封包声明、导入声明和每个函数之间,都有空白行隔开。这条极其简单的规则极大地影响到代码的视觉外观。每个空白行都是一条线索,标识出新的独立概念。往下读代码时,你的目光总会停留于空白行之后那一行。 Listing 5-1 BoldWidget.java > 代码清单 5-1 BoldWidget.java ```java package fitnesse.wikitext.widgets; import java.util.regex.*; public class BoldWidget extends ParentWidget { public static final String REGEXP = “’’’.+?’’’”; private static final Pattern pattern = Pattern.compile(“’’’(.+?)’’’”, Pattern.MULTILINE + Pattern.DOTALL ); public BoldWidget(ParentWidget parent, String text) throws Exception { super(parent); Matcher match = pattern.matcher(text); match.find(); addChildWidgets(match.group(1)); } public String render() throws Exception { StringBuffer html = new StringBuffer(“”); html.append(childHtml()).append(“”); return html.toString(); } } ``` Taking those blank lines out, as in Listing 5-2, has a remarkably obscuring effect on the readability of the code. > 如代码清单 5-2 所示,抽掉这些空白行,代码可读性减弱了不少。 Listing 5-2 BoldWidget.java > 代码清单 5-2 BoldWidget.java ```java package fitnesse.wikitext.widgets; import java.util.regex.*; public class BoldWidget extends ParentWidget { public static final String REGEXP = “’’’.+?’’’”; private static final Pattern pattern = Pattern.compile(“’’’(.+?)’’’”, Pattern.MULTILINE + Pattern.DOTALL); public BoldWidget(ParentWidget parent, String text) throws Exception { super(parent); Matcher match = pattern.matcher(text); match.find(); addChildWidgets(match.group(1));} public String render() throws Exception { StringBuffer html = new StringBuffer(“”); html.append(childHtml()).append(“”); return html.toString(); } } ``` This effect is even more pronounced when you unfocus your eyes. In the first example the different groupings of lines pop out at you, whereas the second example looks like a muddle. The difference between these two listings is a bit of vertical openness. > 在你不特意注视时,后果就更严重了。在第一个例子中,代码组会跳到你眼中,而第二个例子就像一堆乱麻。两段代码的区别,展示了垂直方向上区隔的作用。 ### 5.2.3 Vertical Density 垂直方向上的靠近 If openness separates concepts, then vertical density implies close association. So lines of code that are tightly related should appear vertically dense. Notice how the useless comments in Listing 5-3 break the close association of the two instance variables. > 如果说空白行隔开了概念,靠近的代码行则暗示了它们之间的紧密关系。所以,紧密相关的代码应该互相靠近。注意代码清单 5-3 中的注释是如何割断两个实体变量间的联系的。 Listing 5-3 > 代码清单 5-3 ```java public class ReporterConfig { /** * The class name of the reporter listener */ private String m_className; /** * The properties of the reporter listener */ private List m_properties = new ArrayList(); public void addProperty(Property property) { m_properties.add(property); } ``` Listing 5-4 is much easier to read. It fits in an “eye-full,” or at least it does for me. I can look at it and see that this is a class with two variables and a method, without having to move my head or eyes much. The previous listing forces me to use much more eye and head motion to achieve the same level of comprehension. > 代码清单 5-4 更易于阅读。它刚好“一览无遗”,至少对我来说是这样。我一眼就能看到,这是个有两个变量和一个方法的类。看上面的代码时,我不得不更多地移动头部和眼球,才能获得相同的理解度。 Listing 5-4 > 代码清单 5-4 ```java public class ReporterConfig { private String m_className; private List m_properties = new ArrayList(); public void addProperty(Property property) { m_properties.add(property); } ``` ### 5.2.4 Vertical Distance 垂直距离 Have you ever chased your tail through a class, hopping from one function to the next, scrolling up and down the source file, trying to divine how the functions relate and operate, only to get lost in a rat’s nest of confusion? Have you ever hunted up the chain of inheritance for the definition of a variable or function? This is frustrating because you are trying to understand what the system does, but you are spending your time and mental energy on trying to locate and remember where the pieces are. > 你是否曾经在某个类中摸索,从一个函数跳到另一个函数,上下求索,想要弄清楚这些函数如何操作、如何互相相关,最后却被搞糊涂了?你是否曾经苦苦追索某个变量或函数的继承链条?这让人沮丧,因为你想要理解系统做什么,但却花时间和精力在找到和记住那些代码碎片在哪里。 Concepts that are closely related should be kept vertically close to each other [G10]. Clearly this rule doesn’t work for concepts that belong in separate files. But then closely related concepts should not be separated into different files unless you have a very good reason. Indeed, this is one of the reasons that protected variables should be avoided. > 关系密切的概念应该互相靠近[G10]。显然,这条规则并不适用于分布在不同文件中的概念。除非有很好的理由,否则就不要把关系密切的概念放到不同的文件中。实际上,这也是避免使用 protected 变量的理由之一。 For those concepts that are so closely related that they belong in the same source file, their vertical separation should be a measure of how important each is to the understandability of the other. We want to avoid forcing our readers to hop around through our source files and classes. > 对于那些关系密切、放置于同一源文件中的概念,它们之间的区隔应该成为对相互的易懂度有多重要的衡量标准。应避免迫使读者在源文件和类中跳来跳去。 Variable Declarations. Variables should be declared as close to their usage as possible. Because our functions are very short, local variables should appear a the top of each function, as in this longish function from Junit4.3.1. > 变量声明。变量声明应尽可能靠近其使用位置。因为函数很短,本地变量应该在函数的顶部出现,就像 Junit4.3.1 中这个稍长的函数中那样。 ```java private static void readPreferences() { InputStream is= null; try { is= new FileInputStream(getPreferencesFile()); setPreferences(new Properties(getPreferences())); getPreferences().load(is); } catch (IOException e) { try { if (is != null) is.close(); } catch (IOException e1) { } } } ``` Control variables for loops should usually be declared within the loop statement, as in this cute little function from the same source. > 对于那些关系密切、放置于同一源文件中的概念,它们之间的区隔应该成为对相互的易懂度有多重要的衡量标准。应避免迫使读者在源文件和类中跳来跳去。 ```java public int countTestCases() { int count= 0; for (Test each : tests) count += each.countTestCases(); return count; } ``` In rare cases a variable might be declared at the top of a block or just before a loop in a long-ish function. You can see such a variable in this snippet from the midst of a very long function in TestNG. > 偶尔,在较长的函数中,变量也可能在某个代码块顶部,或在循环之前声明。你可以在以下摘自 TestNG 中一个长函数的代码片段中找到类似的变量。 ```java … for (XmlTest test : m_suite.getTests()) { TestRunner tr = m_runnerFactory.newTestRunner(this, test); tr.addListener(m_textReporter); m_testRunners.add(tr); invoker = tr.getInvoker(); for (ITestNGMethod m : tr.getBeforeSuiteMethods()) { beforeSuiteMethods.put(m.getMethod(), m); } for (ITestNGMethod m : tr.getAfterSuiteMethods()) { afterSuiteMethods.put(m.getMethod(), m); } } … ``` Instance variables, on the other hand, should be declared at the top of the class. This should not increase the vertical distance of these variables, because in a well-designed class, they are used by many, if not all, of the methods of the class. > 实体变量应该在类的顶部声明。这应该不会增加变量的垂直距离,因为在设计良好的类中,它们如果不是被该类的所有方法也是被大多数方法所用。 There have been many debates over where instance variables should go. In C++ we commonly practiced the so-called scissors rule, which put all the instance variables at the bottom. The common convention in Java, however, is to put them all at the top of the class. I see no reason to follow any other convention. The important thing is for the instance variables to be declared in one well-known place. Everybody should know where to go to see the declarations. > 关于实体变量应该放在哪里,争论不断。在 C++中,通常会采用所谓“剪刀原则”(scissors rule),所有实体变量都放在底部。而在 Java 中,惯例是放在类的顶部。没理由去遵循其他惯例。重点是在谁都知道的地方声明实体变量。大家都应该知道在哪儿能看到这些声明。 Consider, for example, the strange case of the TestSuite class in JUnit 4.3.1. I have greatly attenuated this class to make the point. If you look about halfway down the listing, you will see two instance variables declared there. It would be hard to hide them in a better place. Someone reading this code would have to stumble across the declarations by accident (as I did). > 例如 JUnit 4.3.1 中的这个奇怪情形。我极力删减了这个类,好说明问题。如果你看到代码清单大致一半的位置,会看到在那里声明了两个实体变量。如果放在更好的位置,它们就会更明显。而现在,读代码者只能在无意中看到这些声明(就像我一样)。 ```java public class TestSuite implements Test { static public Test createTest(Class theClass, String name) { … } public static Constructor getTestConstructor(Class theClass) throws NoSuchMethodException { … } public static Test warning(final String message) { … } private static String exceptionToString(Throwable t) { … } private String fName; private Vector fTests= new Vector(10); public TestSuite() { } public TestSuite(final Class theClass) { … } public TestSuite(Class theClass, String name) { … } … … … … … } ``` Dependent Functions. If one function calls another, they should be vertically close, and the caller should be above the callee, if at all possible. This gives the program a natural flow. If the convention is followed reliably, readers will be able to trust that function definitions will follow shortly after their use. Consider, for example, the snippet from FitNesse in Listing 5-5. Notice how the topmost function calls those below it and how they in turn call those below them. This makes it easy to find the called functions and greatly enhances the readability of the whole module. > 相关函数。若某个函数调用了另外一个,就应该把它们放到一起,而且调用者应该尽可能放在被调用者上面。这样,程序就有个自然的顺序。若坚定地遵循这条约定,读者将能够确信函数声明总会在其调用后很快出现。以源自 FitNesse 的代码清单 5-5 为例。注意顶部的函数是如何调用其下的函数,而这些被调用的函数又是如何调用更下面的函数的。这样就能轻易找到被调用的函数,极大地增强了整个模块的可读性。 Listing 5-5 WikiPageResponder.java > 代码清单 5-5 WikiPageResponder.java ```java public class WikiPageResponder implements SecureResponder { protected WikiPage page; protected PageData pageData; protected String pageTitle; protected Request request; protected PageCrawler crawler; public Response makeResponse(FitNesseContext context, Request request) throws Exception { String pageName = getPageNameOrDefault(request, “FrontPage”); loadPage(pageName, context); if (page == null) return notFoundResponse(context, request); else return makePageResponse(context); } private String getPageNameOrDefault(Request request, String defaultPageName) { String pageName = request.getResource(); if (StringUtil.isBlank(pageName)) pageName = defaultPageName; return pageName; } protected void loadPage(String resource, FitNesseContext context) throws Exception { WikiPagePath path = PathParser.parse(resource); crawler = context.root.getPageCrawler(); crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler()); page = crawler.getPage(context.root, path); if (page != null) pageData = page.getData(); } private Response notFoundResponse(FitNesseContext context, Request request) throws Exception { return new NotFoundResponder().makeResponse(context, request); } private SimpleResponse makePageResponse(FitNesseContext context) throws Exception { pageTitle = PathParser.render(crawler.getFullPath(page)); String html = makeHtml(context); SimpleResponse response = new SimpleResponse(); response.setMaxAge(0); response.setContent(html); return response; } … ``` As an aside, this snippet provides a nice example of keeping constants at the appropriate level [G35]. The “FrontPage” constant could have been buried in the getPageNameOrDefault function, but that would have hidden a well-known and expected constant in an inappropriately low-level function. It was better to pass that constant down from the place where it makes sense to know it to the place that actually uses it. > 说句题外话,以上代码片段也是把常量保持在恰当级别的好例子[G35]。FrontPage 常量可以埋在 getPageNameOrDefault 函数中,但那样就会把一个众人皆知的常量埋藏到位于不太合适的底层函数中。更好的做法是把它放在易于找到的位置,然后再传递到真实使用的位置。 Conceptual Affinity. Certain bits of code want to be near other bits. They have a certain conceptual affinity. The stronger that affinity, the less vertical distance there should be between them. > 概念相关。概念相关的代码应该放到一起。相关性越强,彼此之间的距离就该越短。 As we have seen, this affinity might be based on a direct dependence, such as one function calling another, or a function using a variable. But there are other possible causes of affinity. Affinity might be caused because a group of functions perform a similar operation. Consider this snippet of code from Junit 4.3.1: > 如上所述,相关性应建立在直接依赖的基础上,如函数间调用,或函数使用某个变量。但也有其他相关性的可能。相关性可能来自于执行相似操作的一组函数。请看以下来自 Junit 4.3.1 的代码片段: ![](figures/ch5/5_3fig_martin.jpg) ```java public class Assert { static public void assertTrue(String message, boolean condition) { if (!condition) fail(message); } static public void assertTrue(boolean condition) { assertTrue(null, condition); } static public void assertFalse(String message, boolean condition) { assertTrue(message, !condition); } static public void assertFalse(boolean condition) { assertFalse(null, condition); } … ``` These functions have a strong conceptual affinity because they share a common naming scheme and perform variations of the same basic task. The fact that they call each other is secondary. Even if they didn’t, they would still want to be close together. 这些函数有着极强的概念相关性,因为他们拥有共同的命名模式,执行同一基础任务的不同变种。互相调用是第二位的。即便没有互相调用,也应该放在一起。 ### 5.2.5 Vertical Ordering 垂直顺序 In general we want function call dependencies to point in the downward direction. That is, a function that is called should be below a function that does the calling.2 This creates a nice flow down the source code module from high level to low level. > 一般而言,我们想自上向下展示函数调用依赖顺序。也就是说,被调用的函数应该放在执行调用的函数下面[2]。这样就建立了一种自顶向下贯穿源代码模块的良好信息流。 2. This is the exact opposite of languages like Pascal, C, and C++ that enforce functions to be defined, or at least declared, before they are used. As in newspaper articles, we expect the most important concepts to come first, and we expect them to be expressed with the least amount of polluting detail. We expect the low-level details to come last. This allows us to skim source files, getting the gist from the first few functions, without having to immerse ourselves in the details. Listing 5-5 is organized this way. Perhaps even better examples are Listing 15-5 on page 263, and Listing 3-7 on page 50. > 像报纸文章一般,我们指望最重要的概念先出来,指望以包括最少细节的方式表述它们。我们指望底层细节最后出来。这样,我们就能扫过源代码文件,自最前面的几个函数获知要旨,而不至于沉溺到细节中。代码清单 5-5 就是如此组织的。或许,更好的例子是代码清单 15-5,及代码清单 3-7。 ## 5.3 HORIZONTAL FORMATTING 横向格式 How wide should a line be? To answer that, let’s look at how wide lines are in typical programs. Again, we examine the seven different projects. Figure 5-2 shows the distribution of line lengths of all seven projects. The regularity is impressive, especially right around 45 characters. Indeed, every size from 20 to 60 represents about 1 percent of the total number of lines. That’s 40 percent! Perhaps another 30 percent are less than 10 characters wide. Remember this is a log scale, so the linear appearance of the drop-off above 80 characters is really very significant. Programmers clearly prefer short lines. > 一行代码应该有多宽?要回答这个问题,来看看典型的程序中代码行的宽度。我们再一次检验 7 个不同项目。图 5-2 展示了这 7 个项目的代码行宽度分布情况。其中展现的规律性令人印象深刻,45 个字符左右的宽度分布尤为如此。其实,20 ~ 60 的每个尺寸,都代表全部代码行数的 1%。也就是总共 40%!或许其余 30%的代码行短于 10 个字符。记住,这是个对数标尺,所以图中长于 80 个字符部分的线性下降在实际情况中会极其可观。程序员们显然更喜爱短代码行。 Figure 5-2 Java line width distribution ![](figures/ch5/5_4fig_martin.jpg) This suggests that we should strive to keep our lines short. The old Hollerith limit of 80 is a bit arbitrary, and I’m not opposed to lines edging out to 100 or even 120. But beyond that is probably just careless. > 这说明,应该尽力保持代码行短小。死守 80 个字符的上限有点僵化,而且我也并不反对代码行长度达到 100 个字符或 120 个字符。再多的话,大抵就是肆意妄为了。 I used to follow the rule that you should never have to scroll to the right. But monitors are too wide for that nowadays, and younger programmers can shrink the font so small that they can get 200 characters across the screen. Don’t do that. I personally set my limit at 120. > 我一向遵循无需拖动滚动条到右边的原则。但近年来显示器越来越宽,而年轻程序员又能将显示字符缩小到如此程度,屏幕上甚至能容纳 200 个字符的宽度。别那么做。我个人的上限是 120 个字符。 ### 5.3.1 Horizontal Openness and Density 水平方向上的区隔与靠近 We use horizontal white space to associate things that are strongly related and disassociate things that are more weakly related. Consider the following function: > 我们使用空格字符将彼此紧密相关的事物连接到一起,也用空格字符把相关性较弱的事物分隔开。请看以下函数: ```java private void measureLine(String line) { lineCount++; int lineSize = line.length(); totalChars += lineSize; lineWidthHistogram.addLine(lineSize, lineCount); recordWidestLine(lineSize); } ``` I surrounded the assignment operators with white space to accentuate them. Assignment statements have two distinct and major elements: the left side and the right side. The spaces make that separation obvious. > 我在赋值操作符周围加上空格字符,以此达到强调目的。赋值语句有两个确定而重要的要素:左边和右边。空格字符加强了分隔效果。 On the other hand, I didn’t put spaces between the function names and the opening parenthesis. This is because the function and its arguments are closely related. Separating them makes them appear disjoined instead of conjoined. I separate arguments within the function call parenthesis to accentuate the comma and show that the arguments are separate. > 另一方面,我不在函数名和左圆括号之间加空格。这是因为函数与其参数密切相关,如果隔开,就会显得互无关系。我把函数调用括号中的参数一一隔开,强调逗号,表示参数是互相分离的。 Another use for white space is to accentuate the precedence of operators. > 空格字符的另一种用法是强调其前面的运算符。 ```java public class Quadratic { public static double root1(double a, double b, double c) { double determinant = determinant(a, b, c); return (-b + Math.sqrt(determinant)) / (2*a); } public static double root2(int a, int b, int c) { double determinant = determinant(a, b, c); return (-b - Math.sqrt(determinant)) / (2*a); } private static double determinant(double a, double b, double c) { return b*b - 4*a*c; } } ``` Notice how nicely the equations read. The factors have no white space between them because they are high precedence. The terms are separated by white space because addition and subtraction are lower precedence. > 看看这些等式读起来多舒服。乘法因子之间没加空格,因为它们具有较高优先级。加减法运算项之间用空格隔开,因为加法和减法优先级较低。 Unfortunately, most tools for reformatting code are blind to the precedence of operators and impose the same spacing throughout. So subtle spacings like those shown above tend to get lost after you reformat the code. > 不幸的是,多数代码格式化工具都会漠视运算符优先级,从头到尾采用同样的空格方式。在重新格式化代码后,以上这些微妙的空格用法就消失殆尽了。 ### 5.3.2 Horizontal Alignment 水平对齐 When I was an assembly language programmer,3 I used horizontal alignment to accentuate certain structures. When I started coding in C, C++, and eventually Java, I continued to try to line up all the variable names in a set of declarations, or all the rvalues in a set of assignment statements. My code might have looked like this: > 当我还是个汇编语言程序员时[3],使用水平对齐来强调某些程序结构。开始用 C、C++编码,最终转向 Java 后,我继续尽力对齐一组声明中的变量名,或一组赋值语句中的右值。我的代码看起来大概是这样: 3. Who am I kidding? I still am an assembly language programmer. You can take the boy away from the metal, but you can’t take the metal out of the boy! ```java public class FitNesseExpediter implements ResponseSender { private Socket socket; private InputStream input; private OutputStream output; private Request request; private Response response; private FitNesseContext context; protected long requestParsingTimeLimit; private long requestProgress; private long requestParsingDeadline; private boolean hasError; public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception { this.context = context; socket = s; input = s.getInputStream(); output = s.getOutputStream(); requestParsingTimeLimit = 10000; } ``` I have found, however, that this kind of alignment is not useful. The alignment seems to emphasize the wrong things and leads my eye away from the true intent. For example, in the list of declarations above you are tempted to read down the list of variable names without looking at their types. Likewise, in the list of assignment statements you are tempted to look down the list of rvalues without ever seeing the assignment operator. To make matters worse, automatic reformatting tools usually eliminate this kind of alignment. > 我发现这种对齐方式没什么用。对齐,像是在强调不重要的东西,把我的目光从真正的意义上拉开。例如,在上面的声明列表中,你会从上到下阅读变量名,而忽视了它们的类型。同样,在赋值语句代码清单中,你也会从上到下阅读右值,而对赋值运算符视而不见。更麻烦的是,代码自动格式化工具通常会把这类对齐消除掉。 So, in the end, I don’t do this kind of thing anymore. Nowadays I prefer unaligned declarations and assignments, as shown below, because they point out an important deficiency. If I have long lists that need to be aligned, the problem is the length of the lists, not the lack of alignment. The length of the list of declarations in FitNesseExpediter below suggests that this class should be split up. > 所以,我最终放弃了这种做法。如今,我更喜欢用不对齐的声明和赋值,如下所示,因为它们指出了重点。如果有较长的列表需要做对齐处理,那问题就是在列表的长度上而不是对齐上。下例 FitNesseExpediter 类中声明列表的长度说明该类应该被拆分了。 ```java public class FitNesseExpediter implements ResponseSender { private Socket socket; private InputStream input; private OutputStream output; private Request request; private Response response; private FitNesseContext context; protected long requestParsingTimeLimit; private long requestProgress; private long requestParsingDeadline; private boolean hasError; public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception { this.context = context; socket = s; input = s.getInputStream(); output = s.getOutputStream(); requestParsingTimeLimit = 10000; } ``` ### 5.3.3 Indentation 缩进 A source file is a hierarchy rather like an outline. There is information that pertains to the file as a whole, to the individual classes within the file, to the methods within the classes, to the blocks within the methods, and recursively to the blocks within the blocks. Each level of this hierarchy is a scope into which names can be declared and in which declarations and executable statements are interpreted. > 源文件是一种继承结构,而不是一种大纲结构。其中的信息涉及整个文件、文件中每个类、类中的方法、方法中的代码块,也涉及代码块中的代码块。这种继承结构中的每一层级都圈出一个范围,名称可以在其中声明,而声明和执行语句也可以在其中解释。 To make this hierarchy of scopes visible, we indent the lines of source code in proportion to their position in the hiearchy. Statements at the level of the file, such as most class declarations, are not indented at all. Methods within a class are indented one level to the right of the class. Implementations of those methods are implemented one level to the right of the method declaration. Block implementations are implemented one level to the right of their containing block, and so on. > 要让这种范围式继承结构可见,我们依源代码行在继承结构中的位置对源代码行做缩进处理。在文件顶层的语句,例如大多数的类声明,根本不缩进。类中的方法相对该类缩进一个层级。方法的实现相对方法声明缩进一个层级。代码块的实现相对于其容器代码块缩进一个层级,以此类推。 Programmers rely heavily on this indentation scheme. They visually line up lines on the left to see what scope they appear in. This allows them to quickly hop over scopes, such as implementations of if or while statements, that are not relevant to their current situation. They scan the left for new method declarations, new variables, and even new classes. Without indentation, programs would be virtually unreadable by humans. > 程序员相当依赖这种缩进模式。他们从代码行左边查看自己在什么范围中工作。这让他们能快速跳过与当前关注的情形无关的范围,例如 if 或 while 语句的实现之类。他们的眼光扫过左边,查找新的方法声明、新变量,甚至新类。没有缩进的话,程序就会变得无法阅读。 Consider the following programs that are syntactically and semantically identical: > 试看以下在语法和语义上等价的两个程序: ```java public class FitNesseServer implements SocketServer { private FitNesseContext context; public FitNesseServer(FitNesseContext context) { this.context = context; } public void serve(Socket s) { serve(s, 10000); } public void serve(Socket s, long requestTimeout) { try { FitNesseExpediter sender = new FitNesseExpediter(s, context); sender.setRequestParsingTimeLimit(requestTimeout); sender.start(); } catch(Exception e) { e.printStackTrace(); } } } ----- public class FitNesseServer implements SocketServer { private FitNesseContext context; public FitNesseServer(FitNesseContext context) { this.context = context; } public void serve(Socket s) { serve(s, 10000); } public void serve(Socket s, long requestTimeout) { try { FitNesseExpediter sender = new FitNesseExpediter(s, context); sender.setRequestParsingTimeLimit(requestTimeout); sender.start(); } catch (Exception e) { e.printStackTrace(); } } } ``` Your eye can rapidly discern the structure of the indented file. You can almost instantly spot the variables, constructors, accessors, and methods. It takes just a few seconds to realize that this is some kind of simple front end to a socket, with a time-out. The unindented version, however, is virtually impenetrable without intense study. > 你能很快地洞悉有缩进的那个文件的结构。你几乎能立即就辨别出那些变量、构造器、存取器和方法。只需要几秒钟就能了解这是一个套接字的简单前端,其中包括了超时设定。而未缩进的版本则不经过一番折腾就无法明白。 Breaking Indentation. It is sometimes tempting to break the indentation rule for short if statements, short while loops, or short functions. Whenever I have succumbed to this temptation, I have almost always gone back and put the indentation back in. So I avoid collapsing scopes down to one line like this: > 违反缩进规则。有时,会忍不住想要在短小的 if 语句、while 循环或小函数中违反缩进规则。一旦这么做了,我多数时候还是会回头加上缩进。这样就避免了出现以下这种范围层级坍塌到一行的情况: ```java public class CommentWidget extends TextWidget { public static final String REGEXP = “^#[^\r\n]*(?:(?:\r\n)|\n|\r)?”; public CommentWidget(ParentWidget parent, String text){super(parent, text);} public String render() throws Exception {return “”; } } ``` I prefer to expand and indent the scopes instead, like this: > 我更喜欢扩展和缩进范围,就像这样: ```java public class CommentWidget extends TextWidget { public static final String REGEXP = “^#[^\r\n]*(?:(?:\r\n)|\n|\r)?” public CommentWidget(ParentWidget parent, String text) { super(parent, text); } public String render() throws Exception { return “”; } } ``` ### 5.3.4 Dummy Scopes 空范围 Sometimes the body of a while or for statement is a dummy, as shown below. I don’t like these kinds of structures and try to avoid them. When I can’t avoid them, I make sure that the dummy body is properly indented and surrounded by braces. I can’t tell you how many times I’ve been fooled by a semicolon silently sitting at the end of a while loop on the same line. Unless you make that semicolon visible by indenting it on it’s own line, it’s just too hard to see. > 有时,while 或 for 语句的语句体为空,如下所示。我不喜欢这种结构,尽量不使用。如果无法避免,就确保空范围体的缩进,用括号包围起来。我无法告诉你,我曾经多少次被静静安坐在与 while 循环语句同一行末尾的分号所欺骗。除非你把那个分号放到另一行再加以缩进,否则就很难看到它。 ```java while (dis.read(buf, 0, readBufferSize) != -1) ; ``` ## 5.4 TEAM RULES 团队规则 The title of this section is a play on words. Every programmer has his own favorite formatting rules, but if he works in a team, then the team rules. > 每个程序员都有自己喜欢的格式规则,但如果在一个团队中工作,就是团队说了算[4]。 A team of developers should agree upon a single formatting style, and then every member of that team should use that style. We want the software to have a consistent style. We don’t want it to appear to have been written by a bunch of disagreeing individuals. > 一组开发者应当认同一种格式风格,每个成员都应该采用那种风格。我们想要让软件拥有一以贯之的风格。我们不想让它显得是由一大票意见相左的个人所写成。 ![](figures/ch5/5_5fig_martin.jpg) When I started the FitNesse project back in 2002, I sat down with the team to work out a coding style. This took about 10 minutes. We decided where we’d put our braces, what our indent size would be, how we would name classes, variables, and methods, and so forth. Then we encoded those rules into the code formatter of our IDE and have stuck with them ever since. These were not the rules that I prefer; they were rules decided by the team. As a member of that team I followed them when writing code in the FitNesse project. > 002 年启动 FitNesse 项目时,我和开发团队一起制订了一套编码风格。这只花了我们 10 分钟时间。我们决定了在什么地方放置括号,缩进几个字符,如何命名类、变量和方法,如此等等。然后,我们把这些规则编写进 IDE 的代码格式功能,接着就一直沿用。这些规则并非全是我喜爱的;但它们是团队决定了的规则。作为团队一员,在为 FitNesse 项目编写代码时,我遵循这些规则。 Remember, a good software system is composed of a set of documents that read nicely. They need to have a consistent and smooth style. The reader needs to be able to trust that the formatting gestures he or she has seen in one source file will mean the same thing in others. The last thing we want to do is add more complexity to the source code by writing it in a jumble of different individual styles. > 记住,好的软件系统是由一系列读起来不错的代码文件组成的。它们需要拥有一致和顺畅的风格。读者要能确信,他们在一个源文件中看到的格式风格在其他文件中也是同样的用法。绝对不要用各种不同的风格来编写源代码,这样会增加其复杂度。 ## 5.5 UNCLE BOB’S FORMATTING RULES 鲍勃大叔的格式规则 The rules I use personally are very simple and are illustrated by the code in Listing 5-6. Consider this an example of how code makes the best coding standard document. > 我个人使用的规则相当简单,如代码清单 5-6 所示。可以把这段代码看作是展示如何把代码写成最好的编码标准文档的范例。 Listing 5-6 CodeAnalyzer.java > 代码清单 5-6 CodeAnalyzer.java ```java public int getWidestLineNumber() { return widestLineNumber; } public LineWidthHistogram getLineWidthHistogram() { return lineWidthHistogram; } public double getMeanLineWidth() { return (double)totalChars/lineCount; } public int getMedianLineWidth() { Integer[] sortedWidths = getSortedWidths(); int cumulativeLineCount = 0; for (int width : sortedWidths) { cumulativeLineCount += lineCountForWidth(width); if (cumulativeLineCount > lineCount/2) return width; } throw new Error(“Cannot get here”); } private int lineCountForWidth(int width) { return lineWidthHistogram.getLinesforWidth(width).size(); } private Integer[] getSortedWidths() { Set widths = lineWidthHistogram.getWidths(); Integer[] sortedWidths = (widths.toArray(new Integer[0])); Arrays.sort(sortedWidths); return sortedWidths; } } ``` ================================================ FILE: docs/ch6.md ================================================ # 第 6 章 Objects and Data Structures 对象和数据结构 ![](figures/ch6/6_1fig_martin.jpg) There is a reason that we keep our variables private. We don’t want anyone else to depend on them. We want to keep the freedom to change their type or implementation on a whim or an impulse. Why, then, do so many programmers automatically add getters and setters to their objects, exposing their private variables as if they were public? > 将变量设置为私有(private)有一个理由:我们不想其他人依赖这些变量。我们还想在心血来潮时能自由修改其类型或实现。那么,为什么还是有那么多程序员给对象自动添加赋值器和取值器,将私有变量公之于众、如同它们根本就是公共变量一般呢? ## 6.1 DATA ABSTRACTION 数据抽象 Consider the difference between Listing 6-1 and Listing 6-2. Both represent the data of a point on the Cartesian plane. And yet one exposes its implementation and the other completely hides it. > 看看代码清单 6-1 和代码清单 6-2 之间的区别。每段代码都表示笛卡儿平面上的一个点。不过,其中之一曝露了其实现,而另一个则完全隐藏了其实现。 Listing 6-1 Concrete Point > 代码清单 6-1 具象点 ```java public class Point { public double x; public double y; } ``` Listing 6-2 Abstract Point > 代码清单 6-2 抽象点 ```java public interface Point { double getX(); double getY(); void setCartesian(double x, double y); double getR(); double getTheta(); void setPolar(double r, double theta); } ``` The beautiful thing about Listing 6-2 is that there is no way you can tell whether the implementation is in rectangular or polar coordinates. It might be neither! And yet the interface still unmistakably represents a data structure. > 代码清单 6-2 的漂亮之处在于,你不知道该实现会是在矩形坐标系中还是在极坐标系中。可能两个都不是!然而,该接口还是明白无误地呈现了一种数据结构。 But it represents more than just a data structure. The methods enforce an access policy. You can read the individual coordinates independently, but you must set the coordinates together as an atomic operation. > 不过它呈现的还不止是一个数据结构。那些方法固定了一套存取策略。你可以单独读取某个坐标,但必须通过一次原子操作设定所有坐标。 Listing 6-1, on the other hand, is very clearly implemented in rectangular coordinates, and it forces us to manipulate those coordinates independently. This exposes implementation. Indeed, it would expose implementation even if the variables were private and we were using single variable getters and setters. > 而代码清单 6-1 则非常清楚地是在矩形坐标系中实现,并要求我们单个操作那些坐标。这就曝露了实现。实际上,即便变量都是私有,而且我们也通过变量取值器和赋值器使用变量,其实现仍然曝露了。 Hiding implementation is not just a matter of putting a layer of functions between the variables. Hiding implementation is about abstractions! A class does not simply push its variables out through getters and setters. Rather it exposes abstract interfaces that allow its users to manipulate the essence of the data, without having to know its implementation. > 隐藏实现并非只是在变量之间放上一个函数层那么简单。隐藏实现关乎抽象!类并不简单地用取值器和赋值器将其变量推向外间,而是曝露抽象接口,以便用户无需了解数据的实现就能操作数据本体。 Consider Listing 6-3 and Listing 6-4. The first uses concrete terms to communicate the fuel level of a vehicle, whereas the second does so with the abstraction of percentage. In the concrete case you can be pretty sure that these are just accessors of variables. In the abstract case you have no clue at all about the form of the data. > 看看代码清单 6-3 和代码清单 6-4。前者使用具象手段与机动车的燃料层通信,而后者则采用百分比抽象。你能确定前者里面都是些变量存取器,而却无法得知后者中的数据形态。 Listing 6-3 Concrete Vehicle > 代码清单 6-3 具象机动车 ```java public interface Vehicle { double getFuelTankCapacityInGallons(); double getGallonsOfGasoline(); } ``` Listing 6-4 Abstract Vehicle > 代码清单 6-4 抽象机动车 ```java public interface Vehicle { double getPercentFuelRemaining(); } ``` In both of the above cases the second option is preferable. We do not want to expose the details of our data. Rather we want to express our data in abstract terms. This is not merely accomplished by using interfaces and/or getters and setters. Serious thought needs to be put into the best way to represent the data that an object contains. The worst option is to blithely add getters and setters. > 以上两段代码以后者为佳。我们不愿曝露数据细节,更愿意以抽象形态表述数据。这并不只是用接口和/或赋值器、取值器就万事大吉。要以最好的方式呈现某个对象包含的数据,需要做严肃的思考。傻乐着乱加取值器和赋值器,是最坏的选择。 ## 6.2 DATA/OBJECT ANTI-SYMMETRY 数据、对象的反对称性 These two examples show the difference between objects and data structures. Objects hide their data behind abstractions and expose functions that operate on that data. Data structure expose their data and have no meaningful functions. Go back and read that again. Notice the complimentary nature of the two definitions. They are virtual opposites. This difference may seem trivial, but it has far-reaching implications. > 这两个例子展示了对象与数据结构之间的差异。对象把数据隐藏于抽象之后,曝露操作数据的函数。数据结构曝露其数据,没有提供有意义的函数。回过头再读一遍。留意这两种定义的本质。它们是对立的。这种差异貌似微小,但却有深远的含义。 Consider, for example, the procedural shape example in Listing 6-5. The Geometry class operates on the three shape classes. The shape classes are simple data structures without any behavior. All the behavior is in the Geometry class. > 例如,代码清单 6-5 中的过程式代码形状范例。Geometry 类操作三个形状类。形状类都是简单的数据结构,没有任何行为。所有行为都在 Geometry 类中。 Listing 6-5 Procedural Shape > 代码清单 6-5 过程式形状代码 ```java public class Square { public Point topLeft; public double side; } public class Rectangle { public Point topLeft; public double height; public double width; } public class Circle { public Point center; public double radius; } public class Geometry { public final double PI = 3.141592653589793; public double area(Object shape) throws NoSuchShapeException { if (shape instanceof Square) { Square s = (Square)shape; return s.side * s.side; } else if (shape instanceof Rectangle) { Rectangle r = (Rectangle)shape; return r.height * r.width; } else if (shape instanceof Circle) { Circle c = (Circle)shape; return PI * c.radius * c.radius; } throw new NoSuchShapeException(); } } ``` Object-oriented programmers might wrinkle their noses at this and complain that it is procedural—and they’d be right. But the sneer may not be warranted. Consider what would happen if a perimeter() function were added to Geometry. The shape classes would be unaffected! Any other classes that depended upon the shapes would also be unaffected! On the other hand, if I add a new shape, I must change all the functions in Geometry to deal with it. Again, read that over. Notice that the two conditions are diametrically opposed. > 面向对象程序员可能会对此嗤之以鼻,抱怨说这是过程式代码——他们大概是对的,不过这种嘲笑并不完全正确。想想看,如果给 Geometry 类添加一个 primeter() 函数会怎样。那些形状类根本不会因此而受影响!另一方面,如果添加一个新形状,就得修改 Geometry 中的所有函数来处理它。再读一遍代码。注意,这两种情形也是直接对立的。 Now consider the object-oriented solution in Listing 6-6. Here the area() method is polymorphic. No Geometry class is necessary. So if I add a new shape, none of the existing functions are affected, but if I add a new function all of the shapes must be changed!1 > 现在来看看代码清单 6-6 中的面向对象方案。这里,area() 方法是多态的。不需要有 Geometry 类。所以,如果添加一个新形状,现有的函数一个也不会受到影响,而当添加新函数时所有的形状都得做修改[1]! 1. There are ways around this that are well known to experienced object-oriented designers: VISITOR, or dual-dispatch, for example. But these techniques carry costs of their own and generally return the structure to that of a procedural program. Listing 6-6 Polymorphic Shapes > 代码清单 6-6 多态式形状 ```java public class Square implements Shape { private Point topLeft; private double side; public double area() { return side*side; } } public class Rectangle implements Shape { private Point topLeft; private double height; private double width; public double area() { return height * width; } } public class Circle implements Shape { private Point center; private double radius; public final double PI = 3.141592653589793; public double area() { return PI * radius * radius; } } ``` Again, we see the complimentary nature of these two definitions; they are virtual opposites! This exposes the fundamental dichotomy between objects and data structures: > 我们再次看到这两种定义的本质;它们是截然对立的。这说明了对象与数据结构之间的二分原理: Procedural code (code using data structures) makes it easy to add new functions without changing the existing data structures. OO code, on the other hand, makes it easy to add new classes without changing existing functions. > 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。 The complement is also true: > 反过来讲也说得通: Procedural code makes it hard to add new data structures because all the functions must change. OO code makes it hard to add new functions because all the classes must change. > 过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。 So, the things that are hard for OO are easy for procedures, and the things that are hard for procedures are easy for OO! > 所以,对于面向对象较难的事,对于过程式代码却较容易,反之亦然! In any complex system there are going to be times when we want to add new data types rather than new functions. For these cases objects and OO are most appropriate. On the other hand, there will also be times when we’ll want to add new functions as opposed to data types. In that case procedural code and data structures will be more appropriate. > 在任何一个复杂系统中,都会有需要添加新数据类型而不是新函数的时候。这时,对象和面向对象就比较适合。另一方面,也会有想要添加新函数而不是数据类型的时候。在这种情况下,过程式代码和数据结构更合适。 Mature programmers know that the idea that everything is an object is a myth. Sometimes you really do want simple data structures with procedures operating on them. ## 6.3 THE LAW OF DEMETER 得墨忒耳律 There is a well-known heuristic called the Law of Demeter2 that says a module should not know about the innards of the objects it manipulates. As we saw in the last section, objects hide their data and expose operations. This means that an object should not expose its internal structure through accessors because to do so is to expose, rather than to hide, its internal structure. > 著名的得墨忒耳律(The Law of Demeter)[2]认为,模块不应了解它所操作对象的内部情形。如上节所见,对象隐藏数据,曝露操作。这意味着对象不应通过存取器曝露其内部结构,因为这样更像是曝露而非隐藏其内部结构。 2. http://en.wikipedia.org/wiki/Law_of_Demeter More precisely, the Law of Demeter says that a method f of a class C should only call the methods of these: > 更准确地说,得墨忒耳律认为,类 C 的方法 f 只应该调用以下对象的方法: - C - An object created by f - An object passed as an argument to f - An object held in an instance variable of C --- > - C > - 由 f 创建的对象; > - 作为参数传递给 f 的对象; > - 由 C 的实体变量持有的对象。 The method should not invoke methods on objects that are returned by any of the allowed functions. In other words, talk to friends, not to strangers. > 方法不应调用由任何函数返回的对象的方法。换言之,只跟朋友谈话,不与陌生人谈话。 The following code3 appears to violate the Law of Demeter (among other things) because it calls the getScratchDir() function on the return value of getOptions() and then calls getAbsolutePath() on the return value of getScratchDir(). > 下列代码[3]违反了得墨忒耳律(除了违反其他规则之外),因为它调用了 getOptions( )返回值的 getScratchDir( )函数,又调用了 getScratchDir( )返回值的 getAbsolutePath( )方法。 3. Found somewhere in the apache framework. ```java final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); ``` ### 6.3.1 Train Wrecks 火车失事 This kind of code is often called a train wreck because it look like a bunch of coupled train cars. Chains of calls like this are generally considered to be sloppy style and should be avoided [G36]. It is usually best to split them up as follows: > 这类代码常被称作火车失事,因为它看起来就像是一列火车。这类连串的调用通常被认为是肮脏的风格,应该避免[G36]。最好做类似如下的切分: ```java Options opts = ctxt.getOptions(); File scratchDir = opts.getScratchDir(); final String outputDir = scratchDir.getAbsolutePath(); ``` Are these two snippets of code violations of the Law of Demeter? Certainly the containing module knows that the ctxt object contains options, which contain a scratch directory, which has an absolute path. That’s a lot of knowledge for one function to know. The calling function knows how to navigate through a lot of different objects. > 上列代码是否违反了得墨忒耳律呢?当然,模块知道 ctxt 对象包含有多个选项,每个选项中都有一个临时目录,而每个临时目录都有一个绝对路径。对于一个函数,这些知识真够丰富的。调用函数懂得如何在一大堆不同对象间浏览。 ![](figures/ch6/6_2fig_martin.jpg) Whether this is a violation of Demeter depends on whether or not ctxt, Options, and ScratchDir are objects or data structures. If they are objects, then their internal structure should be hidden rather than exposed, and so knowledge of their innards is a clear violation of the Law of Demeter. On the other hand, if ctxt, Options, and ScratchDir are just data structures with no behavior, then they naturally expose their internal structure, and so Demeter does not apply. > 这些代码是否违反得墨忒耳律,取决于 ctxt、Options 和 ScratchDir 是对象还是数据结构。如果是对象,则它们的内部结构应当隐藏而不曝露,而有关其内部细节的知识就明显违反了得墨忒耳律。如果 ctxt、Options 和 ScratchDir 只是数据结构,没有任何行为,则它们自然会曝露其内部结构,得墨忒耳律也就不适用了。 The use of accessor functions confuses the issue. If the code had been written as follows, then we probably wouldn’t be asking about Demeter violations. > 属性访问器函数的使用把问题搞复杂了。如果像下面这样写代码,我们大概就不会提及对得墨忒耳律的违反。 ```java final String outputDir = ctxt.options.scratchDir.absolutePath; ``` This issue would be a lot less confusing if data structures simply had public variables and no functions, whereas objects had private variables and public functions. However, there are frameworks and standards (e.g., “beans”) that demand that even simple data structures have accessors and mutators. > 如果数据结构只简单地拥有公共变量,没有函数,而对象则拥有私有变量和公共函数,这个问题就不那么混淆。然而,有些框架和标准甚至要求最简单的数据结构都要有访问器和改值器。 ### 6.3.2 Hybrids 混杂 This confusion sometimes leads to unfortunate hybrid structures that are half object and half data structure. They have functions that do significant things, and they also have either public variables or public accessors and mutators that, for all intents and purposes, make the private variables public, tempting other external functions to use those variables the way a procedural program would use a data structure.4 > 这种混淆有时会不幸导致混合结构,一半是对象,一半是数据结构。这种结构拥有执行操作的函数,也有公共变量或公共访问器及改值器。无论出于怎样的初衷,公共访问器及改值器都把私有变量公开化,诱导外部函数以过程式程序使用数据结构的方式使用这些变量[4]。 4. This is sometimes called Feature Envy from [Refactoring]. Such hybrids make it hard to add new functions but also make it hard to add new data structures. They are the worst of both worlds. Avoid creating them. They are indicative of a muddled design whose authors are unsure of—or worse, ignorant of—whether they need protection from functions or types. > 此类混杂增加了添加新函数的难度,也增加了添加新数据结构的难度,两面不讨好。应避免创造这种结构。它们的出现,展示了一种乱七八糟的设计,其作者不确定——或者更糟糕,完全无视——他们是否需要函数或类型的保护。 ### 6.3.3 Hiding Structure 隐藏结构 What if ctxt, options, and scratchDir are objects with real behavior? Then, because objects are supposed to hide their internal structure, we should not be able to navigate through them. How then would we get the absolute path of the scratch directory? > 假使 ctxt、Options 和 ScratchDir 是拥有真实行为的对象又怎样呢?由于对象应隐藏其内部结构,我们就不该能够看到内部结构。这样一来,如何才能取得临时目录的绝对路径呢? ```java ctxt.getAbsolutePathOfScratchDirectoryOption(); ``` or > 或者 ```java ctx.getScratchDirectoryOption().getAbsolutePath() ``` The first option could lead to an explosion of methods in the ctxt object. The second presumes that getScratchDirectoryOption() returns a data structure, not an object. Neither option feels good. > 第一种方案可能导致 ctxt 对象中方法的曝露。第二种方案是在假设 getScratchDirectoryOption()返回一个数据结构而非对象。两种方案感觉都不好。 If ctxt is an object, we should be telling it to do something; we should not be asking it about its internals. So why did we want the absolute path of the scratch directory? What were we going to do with it? Consider this code from (many lines farther down in) the same module: > 如果 ctxt 是个对象,就应该要求它做点什么,不该要求它给出内部情形。那我们为何还要得到临时目录的绝对路径呢?我们要它做什么?来看看同一模块(许多行之后)的这段代码: ```java String outFile = outputDir + “/” + className.replace('.', '/') + “.class”; FileOutputStream fout = new FileOutputStream(outFile); BufferedOutputStream bos = new BufferedOutputStream(fout); ``` The admixture of different levels of detail [G34][g6] is a bit troubling. Dots, slashes, file extensions, and File objects should not be so carelessly mixed together, and mixed with the enclosing code. Ignoring that, however, we see that the intent of getting the absolute path of the scratch directory was to create a scratch file of a given name. > 这种不同层级细节的混杂([G34][g36])有点麻烦。句点、斜杠、文件扩展名和 File 对象不该如此随便地混杂到一起。不过,撇开这些毛病,我们发现,取得临时目录绝对路径的初衷是为了创建指定名称的临时文件。 So, what if we told the ctxt object to do this? > 所以,直接让 ctxt 对象来做这事如何? ```java BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName); ``` That seems like a reasonable thing for an object to do! This allows ctxt to hide its internals and prevents the current function from having to violate the Law of Demeter by navigating through objects it shouldn’t know about. > 这下看起来像是个对象做的事了!ctxt 隐藏了其内部结构,防止当前函数因浏览它不该知道的对象而违反得墨忒耳律。 ## 6.4 DATA TRANSFER OBJECTS 数据传送对象 The quintessential form of a data structure is a class with public variables and no functions. This is sometimes called a data transfer object, or DTO. DTOs are very useful structures, especially when communicating with databases or parsing messages from sockets, and so on. They often become the first in a series of translation stages that convert raw data in a database into objects in the application code. > 最为精练的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时被称为数据传送对象,或 DTO(Data Transfer Objects)。 DTO 是非常有用的结构,尤其是在与数据库通信、或解析套接字传递的消息之类场景中。在应用程序代码里一系列将原始数据转换为数据库的翻译过程中,它们往往是排头兵。 Somewhat more common is the “bean” form shown in Listing 6-7. Beans have private variables manipulated by getters and setters. The quasi-encapsulation of beans seems to make some OO purists feel better but usually provides no other benefit. > 更常见的是如代码清单 6-7 所示的“豆”(bean)结构。豆结构拥有由赋值器和取值器操作的私有变量。对豆结构的半封装会让某些 OO 纯化论者感觉舒服些,不过通常没有其他好处。 Listing 6-7 address.java > 代码清单 6-7 address.java ```java public class Address { private String street; private String streetExtra; private String city; private String state; private String zip; public Address(String street, String streetExtra, String city, String state, String zip) { this.street = street; this.streetExtra = streetExtra; this.city = city; this.state = state; this.zip = zip; } public String getStreet() { return street; } public String getStreetExtra() { return streetExtra; } public String getCity() { return city; } public String getState() { return state; } public String getZip() { return zip; } } ``` ### Active Record Active Records are special forms of DTOs. They are data structures with public (or bean-accessed) variables; but they typically have navigational methods like save and find. Typically these Active Records are direct translations from database tables, or other data sources. > Active Record 是一种特殊的 DTO 形式。它们是拥有公共(或可豆式访问的)变量的数据结构,但通常也会拥有类似 save 和 find 这样的可浏览方法。Active Record 一般是对数据库表或其他数据源的直接翻译。 Unfortunately we often find that developers try to treat these data structures as though they were objects by putting business rule methods in them. This is awkward because it creates a hybrid between a data structure and an object. > 我们不幸经常发现开发者往这类数据结构中塞进业务规则方法,把这类数据结构当成对象来用。这是不智的行为,因为它导致了数据结构和对象的混杂体。 The solution, of course, is to treat the Active Record as a data structure and to create separate objects that contain the business rules and that hide their internal data (which are probably just instances of the Active Record). > 当然,解决方案就是把 Active Record 当做数据结构,并创建包含业务规则、隐藏内部数据(可能就是 Active Record 的实体)的独立对象。 ## 6.5 CONCLUSION 小结 Objects expose behavior and hide data. This makes it easy to add new kinds of objects without changing existing behaviors. It also makes it hard to add new behaviors to existing objects. Data structures expose data and have no significant behavior. This makes it easy to add new behaviors to existing data structures but makes it hard to add new data structures to existing functions. > 对象曝露行为,隐藏数据。便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为。数据结构曝露数据,没有明显的行为。便于向既有数据结构添加新行为,同时也难以向既有函数添加新数据结构。 In any given system we will sometimes want the flexibility to add new data types, and so we prefer objects for that part of the system. Other times we will want the flexibility to add new behaviors, and so in that part of the system we prefer data types and procedures. Good software developers understand these issues without prejudice and choose the approach that is best for the job at hand. > 在任何系统中,我们有时会希望能够灵活地添加新数据类型,所以更喜欢在这部分使用对象。另外一些时候,我们希望能灵活地添加新行为,这时我们更喜欢使用数据类型和过程。优秀的软件开发者不带成见地了解这种情形,并依据手边工作的性质选择其中一种手段。 ================================================ FILE: docs/ch7.md ================================================ # 第 7 章 Error Handling 错误处理 by Michael Feathers ![](figures/ch7/103fig01.jpg) It might seem odd to have a section about error handling in a book about clean code. Error handling is just one of those things that we all have to do when we program. Input can be abnormal and devices can fail. In short, things can go wrong, and when they do, we as programmers are responsible for making sure that our code does what it needs to do. > 在一本有关整洁代码的书中,居然有讨论错误处理的章节,看起来有些突兀。错误处理只不过是编程时必须要做的事之一。输入可能出现异常,设备可能失效。简言之,可能会出错,当错误发生时,程序员就有责任确保代码照常工作。 The connection to clean code, however, should be clear. Many code bases are completely dominated by error handling. When I say dominated, I don’t mean that error handling is all that they do. I mean that it is nearly impossible to see what the code does because of all of the scattered error handling. Error handling is important, but if it obscures logic, it’s wrong. > 然而,应该弄清楚错误处理与整洁代码的关系。许多程序完全由错误处理所占据。所谓占据,并不是说错误处理就是全部。我的意思是几乎无法看明白代码所做的事,因为到处都是凌乱的错误处理代码。错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。 In this chapter I’ll outline a number of techniques and considerations that you can use to write code that is both clean and robust—code that handles errors with grace and style. > 在本章中,我将概要列出编写既整洁又强固的代码——雅致地处理错误代码的一些技巧和思路。 ## 7.1 USE EXCEPTIONS RATHER THAN RETURN CODES 使用异常而非返回码 Back in the distant past there were many languages that didn’t have exceptions. In those languages the techniques for handling and reporting errors were limited. You either set an error flag or returned an error code that the caller could check. The code in Listing 7-1 illustrates these approaches. > 在很久以前,许多语言都不支持异常。这些语言处理和汇报错误的手段都有限。你要么设置一个错误标识,要么返回给调用者检查的错误码。代码清单 7-1 中的代码展示了这些手段。 Listing 7-1 DeviceController.java > 代码清单 7-1 DeviceController.java ```java public class DeviceController { … public void sendShutDown() { DeviceHandle handle = getHandle(DEV1); // Check the state of the device if (handle != DeviceHandle.INVALID) { // Save the device status to the record field retrieveDeviceRecord(handle); // If not suspended, shut down if (record.getStatus() != DEVICE_SUSPENDED) { pauseDevice(handle); clearDeviceWorkQueue(handle); closeDevice(handle); } else { logger.log("Device suspended. Unable to shut down"); } } else { logger.log("Invalid handle for: " + DEV1.toString()); } } … } ``` The problem with these approaches is that they clutter the caller. The caller must check for errors immediately after the call. Unfortunately, it’s easy to forget. For this reason it is better to throw an exception when you encounter an error. The calling code is cleaner. Its logic is not obscured by error handling. > 这类手段的问题在于,它们搞乱了调用者代码。调用者必须在调用之后即刻检查错误。不幸的是,这个步骤很容易被遗忘。所以,遇到错误时,最好抛出一个异常。调用代码很整洁,其逻辑不会被错误处理搞乱。 Listing 7-2 shows the code after we’ve chosen to throw exceptions in methods that can detect errors. > 代码清单 7-2 展示了在方法中遇到错误时抛出异常的情形。 Listing 7-2 DeviceController.java (with exceptions) > 代码清单 7-2 展示了在方法中遇到错误时抛出异常的情形。 ```java public class DeviceController { … public void sendShutDown() { try { tryToShutDown(); } catch (DeviceShutDownError e) { logger.log(e); } } private void tryToShutDown() throws DeviceShutDownError { DeviceHandle handle = getHandle(DEV1); DeviceRecord record = retrieveDeviceRecord(handle); pauseDevice(handle); clearDeviceWorkQueue(handle); closeDevice(handle); } private DeviceHandle getHandle(DeviceID id) { … throw new DeviceShutDownError(“Invalid handle for: ” + id.toString()); … } … } ``` Notice how much cleaner it is. This isn’t just a matter of aesthetics. The code is better because two concerns that were tangled, the algorithm for device shutdown and error handling, are now separated. You can look at each of those concerns and understand them independently. > 注意这段代码整洁了很多。这不仅关乎美观。这段代码更好,因为之前纠结的两个元素设备关闭算法和错误处理现在被隔离了。你可以查看其中任一元素,分别理解它。 ## 7.2 WRITE YOUR TRY-CATCH-FINALLY STATEMENT FIRST 先写 Try-Catch-Finally 语句 One of the most interesting things about exceptions is that they define a scope within your program. When you execute code in the try portion of a try-catch-finally statement, you are stating that execution can abort at any point and then resume at the catch. > 异常的妙处之一是,它们在程序中定义了一个范围。执行 try-catchfinally 语句中 try 部分的代码时,你是在表明可随时取消执行,并在 catch 语句中接续。 In a way, try blocks are like transactions. Your catch has to leave your program in a consistent state, no matter what happens in the try. For this reason it is good practice to start with a try-catch-finally statement when you are writing code that could throw exceptions. This helps you define what the user of that code should expect, no matter what goes wrong with the code that is executed in the try. > 在某种意义上,try 代码块就像是事务。catch 代码块将程序维持在一种持续状态,无论 try 代码块中发生了什么均如此。所以,在编写可能抛出异常的代码时,最好先写出 try-catch-finally 语句。这能帮你定义代码的用户应该期待什么,无论 try 代码块中执行的代码出什么错都一样。 Let’s look at an example. We need to write some code that accesses a file and reads some serialized objects. > 来看个例子。我们要编写访问某个文件并读出一些序列化对象的代码。 We start with a unit test that shows that we’ll get an exception when the file doesn’t exist: > 先写一个单元测试,其中显示当文件不存在时将得到一个异常: ```java @Test(expected = StorageException.class) public void retrieveSectionShouldThrowOnInvalidFileName() { sectionStore.retrieveSection(“invalid - file”); } ``` The test drives us to create this stub: > 该测试驱动我们创建以下占位代码: ```java public List retrieveSection(String sectionName) { // dummy return until we have a real implementation return new ArrayList(); } ``` Our test fails because it doesn’t throw an exception. Next, we change our implementation so that it attempts to access an invalid file. This operation throws an exception: > 测试失败了,因为以上代码并未抛出异常。下一步,修改实现代码,尝试访问非法文件。该操作抛出一个异常: ```java public List retrieveSection(String sectionName) { try { FileInputStream stream = new FileInputStream(sectionName) } catch (Exception e) { throw new StorageException(“retrieval error”, e); } return new ArrayList(); } ``` Our test passes now because we’ve caught the exception. At this point, we can refactor. We can narrow the type of the exception we catch to match the type that is actually thrown from the FileInputStream constructor: FileNotFoundException: > 这次测试通过了,因为我们捕获了异常。此时,我们可以重构了。我们可以缩小异常类型的范围,使之符合 FileInputStream 构造器真正抛出的异常,即 FileNotFoundException: ```java public List retrieveSection(String sectionName) { try { FileInputStream stream = new FileInputStream(sectionName); stream.close(); } catch (FileNotFoundException e) { throw new StorageException(“retrieval error”, e); } return new ArrayList(); } ``` Now that we’ve defined the scope with a try-catch structure, we can use TDD to build up the rest of the logic that we need. That logic will be added between the creation of the FileInputStream and the close, and can pretend that nothing goes wrong. > 如此一来,我们就用 try-catch 结构定义了一个范围,可以继续用测试驱动(TDD)方法构建剩余的代码逻辑。这些代码逻辑将在 FileInputStream 和 close 之间添加,装作一切正常的样子。 Try to write tests that force exceptions, and then add behavior to your handler to satisfy your tests. This will cause you to build the transaction scope of the try block first and will help you maintain the transaction nature of that scope. > 尝试编写强行抛出异常的测试,再往处理器中添加行为,使之满足测试要求。结果就是你要先构造 try 代码块的事务范围,而且也会帮助你维护好该范围的事务特征。 ## 7.3 USE UNCHECKED EXCEPTIONS 使用不可控异常 The debate is over. For years Java programmers have debated over the benefits and liabilities of checked exceptions. When checked exceptions were introduced in the first version of Java, they seemed like a great idea. The signature of every method would list all of the exceptions that it could pass to its caller. Moreover, these exceptions were part of the type of the method. Your code literally wouldn’t compile if the signature didn’t match what your code could do. > 辩论业已结束。多年来,Java 程序员们一直在争论可控异常(checked exception)的利与弊。Java 的第一个版本中引入可控异常时,看似一个极好的点子。每个方法的签名都列出它可能传递给调用者的异常。而且,这些异常就是方法类型的一部分。如果签名与代码实际所做之事不符,代码在字面上就无法编译。 At the time, we thought that checked exceptions were a great idea; and yes, they can yield some benefit. However, it is clear now that they aren’t necessary for the production of robust software. C# doesn’t have checked exceptions, and despite valiant attempts, C++ doesn’t either. Neither do Python or Ruby. Yet it is possible to write robust software in all of these languages. Because that is the case, we have to decide—really—whether checked exceptions are worth their price. > 那时,我们认为可控异常是个绝妙的主意;而且,它也有所裨益。然而,现在已经很清楚,对于强固软件的生产,它并非必需。C#不支持可控异常。尽管做过勇敢的尝试,C++最后也不支持可控异常。Python 和 Ruby 同样如此。不过,用这些语言也有可能写出强固的软件。我们得决定——的确如此——可控异常是否值回票价。 What price? The price of checked exceptions is an Open/Closed Principle1 violation. If you throw a checked exception from a method in your code and the catch is three levels above, you must declare that exception in the signature of each method between you and the catch. This means that a change at a low level of the software can force signature changes on many higher levels. The changed modules must be rebuilt and redeployed, even though nothing they care about changed. > 代价是什么?可控异常的代价就是违反开放/闭合原则[1]。如果你在方法中抛出可控异常,而 catch 语句在三个层级之上,你就得在 catch 语句和抛出异常处之间的每个方法签名中声明该异常。这意味着对软件中较低层级的修改,都将波及较高层级的签名。修改好的模块必须重新构建、发布,即便它们自身所关注的任何东西都没改动过。 1. [Martin]. Consider the calling hierarchy of a large system. Functions at the top call functions below them, which call more functions below them, ad infinitum. Now let’s say one of the lowest level functions is modified in such a way that it must throw an exception. If that exception is checked, then the function signature must add a throws clause. But this means that every function that calls our modified function must also be modified either to catch the new exception or to append the appropriate throws clause to its signature. Ad infinitum. The net result is a cascade of changes that work their way from the lowest levels of the software to the highest! Encapsulation is broken because all functions in the path of a throw must know about details of that low-level exception. Given that the purpose of exceptions is to allow you to handle errors at a distance, it is a shame that checked exceptions break encapsulation in this way. > 以某个大型系统的调用层级为例。顶端函数调用它们之下的函数,逐级向下。假设某个位于最底层级的函数被修改为抛出一个异常。如果该异常是可控的,则函数签名就要添加 throw 子句。这意味着每个调用该函数的函数都要修改,捕获新异常,或在其签名中添加合适的 throw 子句。以此类推。最终得到的就是一个从软件最底端贯穿到最高端的修改链!封装被打破了,因为在抛出路径中的每个函数都要去了解下一层级的异常细节。既然异常旨在让你能在较远处处理错误,可控异常以这种方式破坏封装简直就是一种耻辱。 Checked exceptions can sometimes be useful if you are writing a critical library: You must catch them. But in general application development the dependency costs outweigh the benefits. > 如果你在编写一套关键代码库,则可控异常有时也会有用:你必须捕获异常。但对于一般的应用开发,其依赖成本要高于收益。 ## 7.4 PROVIDE CONTEXT WITH EXCEPTIONS 给出异常发生的环境说明 Each exception that you throw should provide enough context to determine the source and location of an error. In Java, you can get a stack trace from any exception; however, a stack trace can’t tell you the intent of the operation that failed. > 你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所。在 Java 中,你可以从任何异常里得到堆栈踪迹(stack trace);然而,堆栈踪迹却无法告诉你该失败操作的初衷。 Create informative error messages and pass them along with your exceptions. Mention the operation that failed and the type of failure. If you are logging in your application, pass along enough information to be able to log the error in your catch. > 应创建信息充分的错误消息,并和异常一起传递出去。在消息中,包括失败的操作和失败类型。如果你的应用程序有日志系统,传递足够的信息给 catch 块,并记录下来。 ## 7.5 DEFINE EXCEPTION CLASSES IN TERMS OF A CALLER’S NEEDS 依调用者需要定义异常类 There are many ways to classify errors. We can classify them by their source: Did they come from one component or another? Or their type: Are they device failures, network failures, or programming errors? However, when we define exception classes in an application, our most important concern should be how they are caught. > 对错误分类有很多方式。可以依其来源分类:是来自组件还是其他地方?或依其类型分类:是设备错误、网络错误还是编程错误?不过,当我们在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获。 Let’s look at an example of poor exception classification. Here is a try-catch-finally statement for a third-party library call. It covers all of the exceptions that the calls can throw: > 来看一个不太好的异常分类例子。下面的 try-catch-finally 语句是对某个第三方代码库的调用。它覆盖了该调用可能抛出的所有异常: ```java ACMEPort port = new ACMEPort(12); try { port.open(); } catch (DeviceResponseException e) { reportPortError(e); logger.log(“Device response exception”, e); } catch (ATM1212UnlockedException e) { reportPortError(e); logger.log(“Unlock exception”, e); } catch (GMXError e) { reportPortError(e); logger.log(“Device response exception”); } finally { … } ``` That statement contains a lot of duplication, and we shouldn’t be surprised. In most exception handling situations, the work that we do is relatively standard regardless of the actual cause. We have to record an error and make sure that we can proceed. > 语句包含了一大堆重复代码,这并不出奇。在大多数异常处理中,不管真实原因如何,我们总是做相对标准的处理。我们得记录错误,确保能继续工作。 In this case, because we know that the work that we are doing is roughly the same regardless of the exception, we can simplify our code considerably by wrapping the API that we are calling and making sure that it returns a common exception type: > 在本例中,既然知道我们所做的事不外如此,就可以通过打包调用 API、确保它返回通用异常类型,从而简化代码。 ```java LocalPort port = new LocalPort(12); try { port.open(); } catch (PortDeviceFailure e) { reportError(e); logger.log(e.getMessage(), e); } finally { … } ``` Our LocalPort class is just a simple wrapper that catches and translates exceptions thrown by the ACMEPort class: > 在本例中,既然知道我们所做的事不外如此,就可以通过打包调用 API、确保它返回通用异常类型,从而简化代码。 ```java public class LocalPort { private ACMEPort innerPort; public LocalPort(int portNumber) { innerPort = new ACMEPort(portNumber); } public void open() { try { innerPort.open(); } catch (DeviceResponseException e) { throw new PortDeviceFailure(e); } catch (ATM1212UnlockedException e) { throw new PortDeviceFailure(e); } catch (GMXError e) { throw new PortDeviceFailure(e); } } … } ``` Wrappers like the one we defined for ACMEPort can be very useful. In fact, wrapping third-party APIs is a best practice. When you wrap a third-party API, you minimize your dependencies upon it: You can choose to move to a different library in the future without much penalty. Wrapping also makes it easier to mock out third-party calls when you are testing your own code. > 类似我们为 ACMEPort 定义的这种打包类非常有用。实际上,将第三方 API 打包是个良好的实践手段。当你打包一个第三方 API,你就降低了对它的依赖:未来你可以不太痛苦地改用其他代码库。在你测试自己的代码时,打包也有助于模拟第三方调用。 One final advantage of wrapping is that you aren’t tied to a particular vendor’s API design choices. You can define an API that you feel comfortable with. In the preceding example, we defined a single exception type for port device failure and found that we could write much cleaner code. > 打包的好处还在于你不必绑死在某个特定厂商的 API 设计上。你可以定义自己感觉舒服的 API。在上例中,我们为 port 设备错误定义了一个异常类型,然后发现这样能写出更整洁的代码。 Often a single exception class is fine for a particular area of code. The information sent with the exception can distinguish the errors. Use different classes only if there are times when you want to catch one exception and allow the other one to pass through. > 对于代码的某个特定区域,单一异常类通常可行。伴随异常发送出来的信息能够区分不同错误。如果你想要捕获某个异常,并且放过其他异常,就使用不同的异常类。 ## 7.6 DEFINE THE NORMAL FLOW 定义常规流程 If you follow the advice in the preceding sections, you’ll end up with a good amount of separation between your business logic and your error handling. The bulk of your code will start to look like a clean unadorned algorithm. However, the process of doing this pushes error detection to the edges of your program. You wrap external APIs so that you can throw your own exceptions, and you define a handler above your code so that you can deal with any aborted computation. Most of the time this is a great approach, but there are some times when you may not want to abort. > 如果你遵循前文提及的建议,在业务逻辑和错误处理代码之间就会有良好的区隔。大量代码会开始变得像是整洁而简朴的算法。然而,这样做却把错误检测推到了程序的边缘地带。你打包了外部 API 以抛出自己的异常,你在代码的顶端定义了一个处理器来应付任何失败了的运算。在大多数时候,这种手段很棒,不过有时你也许不愿这么做。 ![](figures/ch7/103fig02.jpg) Let’s take a look at an example. Here is some awkward code that sums expenses in a billing application: > 来看一个例子。下面的笨代码来自某个记账应用的开支总计模块: ```java try { MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal(); } catch(MealExpensesNotFound e) { m_total += getMealPerDiem(); } ``` In this business, if meals are expensed, they become part of the total. If they aren’t, the employee gets a meal per diem amount for that day. The exception clutters the logic. Wouldn’t it be better if we didn’t have to deal with the special case? If we didn’t, our code would look much simpler. It would look like this: > 业务逻辑是,如果消耗了餐食,则计入总额中。如果没有消耗,则员工得到当日餐食补贴。异常打断了业务逻辑。如果不去处理特殊情况会不会好一些?那样的话代码看起来会更简洁。就像这样: ```java MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal(); ``` Can we make the code that simple? It turns out that we can. We can change the ExpenseReportDAO so that it always returns a MealExpense object. If there are no meal expenses, it returns a MealExpense object that returns the per diem as its total: > 其总是返回 MealExpense 对象。如果没有餐食消耗,就返回一个返回餐食补贴的 MealExpense 对象。 ```java public class PerDiemMealExpenses implements MealExpenses { public int getTotal() { // return the per diem default } } ``` This is called the SPECIAL CASE PATTERN [Fowler]. You create a class or configure an object so that it handles a special case for you. When you do, the client code doesn’t have to deal with exceptional behavior. That behavior is encapsulated in the special case object. > 这种手法叫做特例模式(SPECIAL CASE PATTERN,[Fowler])。创建一个类或配置一个对象,用来处理特例。你来处理特例,客户代码就不用应付异常行为了。异常行为被封装到特例对象中。 ## 7.7 DON’T RETURN NULL 别返回 null 值 I think that any discussion about error handling should include mention of the things we do that invite errors. The first on the list is returning null. I can’t begin to count the number of applications I’ve seen in which nearly every other line was a check for null. Here is some example code: > 我认为,要讨论错误处理,就一定要提及那些容易引发错误的做法。第一项就是返回 null 值。我不想去计算曾经见过多少几乎每行代码都在检查 null 值的应用程序。下面就是个例子: ```java public void registerItem(Item item) { if (item != null) { ItemRegistry registry = peristentStore.getItemRegistry(); if (registry != null) { Item existing = registry.getItem(item.getID()); if (existing.getBillingPeriod().hasRetailOwner()) { existing.register(item); } } } } ``` If you work in a code base with code like this, it might not look all that bad to you, but it is bad! When we return null, we are essentially creating work for ourselves and foisting problems upon our callers. All it takes is one missing null check to send an application spinning out of control. > 这种代码看似不坏,其实糟透了!返回 null 值,基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没检查 null 值,应用程序就会失控。 Did you notice the fact that there wasn’t a null check in the second line of that nested if statement? What would have happened at runtime if persistentStore were null? We would have had a NullPointerException at runtime, and either someone is catching NullPointerException at the top level or they are not. Either way it’s bad. What exactly should you do in response to a NullPointerException thrown from the depths of your application? > 你有没有注意到,嵌套 if 语句的第二行没有检查 null 值?如果在运行时 persistentStore 为 null 会发生什么事?我们会在运行时得到一个 NullPointerException 异常,也许有人在代码顶端捕获这个异常,也可能没有捕获。两种情况都很糟糕。对于从应用程序深处抛出的 NullPointerException 异常,你到底该作何反应呢? It’s easy to say that the problem with the code above is that it is missing a null check, but in actuality, the problem is that it has too many. If you are tempted to return null from a method, consider throwing an exception or returning a SPECIAL CASE object instead. If you are calling a null-returning method from a third-party API, consider wrapping that method with a method that either throws an exception or returns a special case object. > 可以敷衍说上列代码的问题是少做了一次 null 值检查,其实问题多多。如果你打算在方法中返回 null 值,不如抛出异常,或是返回特例对象。如果你在调用某个第三方 API 中可能返回 null 值的方法,可以考虑用新方法打包这个方法,在新方法中抛出异常或返回特例对象。 In many cases, special case objects are an easy remedy. Imagine that you have code like this: > 在许多情况下,特例对象都是爽口良药。设想有这么一段代码: ```java List employees = getEmployees(); if (employees != null) { for(Employee e : employees) { totalPay += e.getPay(); } } ``` Right now, getEmployees can return null, but does it have to? If we change getEmployee so that it returns an empty list, we can clean up the code: > 现在,getExployees 可能返回 null,但是否一定要这么做呢?如果修改 getEmployee,返回空列表,就能使代码整洁起来: ```java List employees = getEmployees(); for(Employee e : employees) { totalPay += e.getPay(); } ``` Fortunately, Java has Collections.emptyList(), and it returns a predefined immutable list that we can use for this purpose: > 所幸 Java 有 Collections.emptyList( )方法,该方法返回一个预定义不可变列表,可用于这种目的: ```java public List getEmployees() { if( .. there are no employees .. ) return Collections.emptyList(); } ``` If you code this way, you will minimize the chance of NullPointerExceptions and your code will be cleaner. > 这样编码,就能尽量避免 NullPointerException 的出现,代码也就更整洁了。 ## 7.8 DON’T PASS NULL 别传递 null 值 Returning null from methods is bad, but passing null into methods is worse. Unless you are working with an API which expects you to pass null, you should avoid passing null in your code whenever possible. > 在方法中返回 null 值是糟糕的做法,但将 null 值传递给其他方法就更糟糕了。除非 API 要求你向它传递 null 值,否则就要尽可能避免传递 null 值。 Let’s look at an example to see why. Here is a simple method which calculates a metric for two points: > 举例说明原因。用下面这个简单的方法计算两点的投射: ```java public class MetricsCalculator { public double xProjection(Point p1, Point p2) { return (p2.x – p1.x) * 1.5; } … } ``` What happens when someone passes null as an argument? > 如果有人传入 null 值会怎样? ```java calculator.xProjection(null, new Point(12, 13)); ``` We’ll get a NullPointerException, of course. > 当然,我们会得到一个 NullPointerException 异常。 How can we fix it? We could create a new exception type and throw it: > 如何修正?可以创建一个新异常类型并抛出: ```java public class MetricsCalculator { public double xProjection(Point p1, Point p2) { if (p1 == null || p2 == null) { throw InvalidArgumentException( “Invalid argument for MetricsCalculator.xProjection”); } return (p2.x – p1.x) * 1.5; } } ``` Is this better? It might be a little better than a null pointer exception, but remember, we have to define a handler for InvalidArgumentException. What should the handler do? Is there any good course of action? > 这样做好些吗?可能比 null 指针异常好一些,但要记住,我们还得为 InvalidArgumentException 异常定义处理器。这个处理器该做什么?还有更好的做法吗? There is another alternative. We could use a set of assertions: > 还有替代方案。可以使用一组断言 ```java public class MetricsCalculator { public double xProjection(Point p1, Point p2) { assert p1 != null : “p1 should not be null”; assert p2 != null : “p2 should not be null”; return (p2.x – p1.x) * 1.5; } } ``` It’s good documentation, but it doesn’t solve the problem. If someone passes null, we’ll still have a runtime error. > 看上去很美,但仍未解决问题。如果有人传入 null 值,还是会得到运行时错误。 In most programming languages there is no good way to deal with a null that is passed by a caller accidentally. Because this is the case, the rational approach is to forbid passing null by default. When you do, you can code with the knowledge that a null in an argument list is an indication of a problem, and end up with far fewer careless mistakes. > 在大多数编程语言中,没有良好的方法能对付由调用者意外传入的 null 值。事已如此,恰当的做法就是禁止传入 null 值。这样,你在编码的时候,就会时时记住参数列表中的 null 值意味着出问题了,从而大量避免这种无心之失。 ## 7.9 CONCLUSION 小结 Clean code is readable, but it must also be robust. These are not conflicting goals. We can write robust clean code if we see error handling as a separate concern, something that is viewable independently of our main logic. To the degree that we are able to do that, we can reason about it independently, and we can make great strides in the maintainability of our code. > 整洁代码是可读的,但也要强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码。做到这一步,我们就能单独处理它,也极大地提升了代码的可维护性。 ================================================ FILE: docs/ch8.md ================================================ # 第 8 章 Boundaries 边界 by James Grenning ![](figures/ch8/113fig01.jpg) We seldom control all the software in our systems. Sometimes we buy third-party packages or use open source. Other times we depend on teams in our own company to produce components or subsystems for us. Somehow we must cleanly integrate this foreign code with our own. In this chapter we look at practices and techniques to keep the boundaries of our software clean. > 我们很少控制系统中的全部软件。有时我们购买第三方程序包或使用开放源代码,有时我们依靠公司中其他团队打造组件或子系统。不管是哪种情况,我们都得将外来代码干净利落地整合进自己的代码中。本章将介绍一些保持软件边界整洁的实践手段和技巧。 ## 8.1 USING THIRD-PARTY CODE 使用第三方代码 There is a natural tension between the provider of an interface and the user of an interface. Providers of third-party packages and frameworks strive for broad applicability so they can work in many environments and appeal to a wide audience. Users, on the other hand, want an interface that is focused on their particular needs. This tension can cause problems at the boundaries of our systems. > 在接口提供者和使用者之间,存在与生俱来的张力。第三方程序包和框架提供者追求普适性,这样就能在多个环境中工作,吸引广泛的用户。而使用者则想要集中满足特定需求的接口。这种张力会导致系统边界上出现问题。 Let’s look at java.util.Map as an example. As you can see by examining Figure 8-1, Maps have a very broad interface with plenty of capabilities. Certainly this power and flexibility is useful, but it can also be a liability. For instance, our application might build up a Map and pass it around. Our intention might be that none of the recipients of our Map delete anything in the map. But right there at the top of the list is the clear() method. Any user of the Map has the power to clear it. Or maybe our design convention is that only particular types of objects can be stored in the Map, but Maps do not reliably constrain the types of objects placed within them. Any determined user can add items of any type to any Map. > 以 java.util.Map 为例。如你在表 8-1 中所见,Map 有着广阔的接口和丰富的功能。当然,这种力量和灵活性很有用,但也要付出代价。比如,应用程序可能构造一个 Map 对象并传递它。我们的初衷可能是 Map 对象的所有接收者都不要删除映射图中的任何东西。但表 8-1 的顶端却正好有一个 clear( )方法。Map 的任何使用者都能清除映射图。或许设计惯例是 Map 中只能保存特定的类型,但 Map 并不会可靠地约束存于其中的对象的类型。使用者可随意往 Map 中塞入任何类型的条目。 Figure 8-1 The methods of Map ![](figures/ch8/114fig01.jpg) If our application needs a Map of Sensors, you might find the sensors set up like this: > 如果你的应用程序需要一个包容 Sensor 类对象的 Map 映射图,大概会是这样: ```java Map sensors = new HashMap(); ``` Then, when some other part of the code needs to access the sensor, you see this code: > 当代码的其他部分需要访问这些 sensor,就会有这行代码: ```java Sensor s = (Sensor)sensors.get(sensorId ); ``` We don’t just see it once, but over and over again throughout the code. The client of this code carries the responsibility of getting an Object from the Map and casting it to the right type. This works, but it’s not clean code. Also, this code does not tell its story as well as it could. The readability of this code can be greatly improved by using generics, as shown below: > 这行代码一再出现。代码的调用端承担了从 Map 中取得对象并将其转换为正确类型的职责。行倒是行,却并非整洁的代码。而且,这行代码并未说明自己的用途。通过对泛型的使用,这段代码可读性可以大大提高,如下所示: ```java Map sensors = new HashMap(); … Sensor s = sensors.get(sensorId ); ``` However, this doesn’t solve the problem that `Map` provides more capability than we need or want. > 不过,`Map` 提供了超出所需/所愿的功能的问题,仍未得到解决。 Passing an instance of `Map` liberally around the system means that there will be a lot of places to fix if the interface to Map ever changes. You might think such a change to be unlikely, but remember that it changed when generics support was added in Java 5. Indeed, we’ve seen systems that are inhibited from using generics because of the sheer magnitude of changes needed to make up for the liberal use of Maps. > 在系统中不受限制地传递`Map`的实体,意味着当到 Map 的接口被修改时,有许多地方都要跟着改。你或许会认为这样的改动不太可能发生,不过,当 Java 5 加入对泛型的支持时,的确发生了改动。我们也的确见到一些系统因为要做大量改动才能自由使用 Map 类,而无法使用泛型。 A cleaner way to use Map might look like the following. No user of Sensors would care one bit if generics were used or not. That choice has become (and always should be) an implementation detail. > 使用 Map 的更整洁的方式大致如下。Sensors 的用户不必关心是否用了泛型,那将是(也该是)实现细节才关心的。 ```java public class Sensors { private Map sensors = new HashMap(); public Sensor getById(String id) { return (Sensor) sensors.get(id); } //snip } ``` The interface at the boundary (Map) is hidden. It is able to evolve with very little impact on the rest of the application. The use of generics is no longer a big issue because the casting and type management is handled inside the Sensors class. > 边界上的接口(Map)是隐藏的。它能随来自应用程序其他部分的极小的影响而变动。对泛型的使用不再是个大问题,因为转换和类型管理是在 Sensors 类内部处理的。 This interface is also tailored and constrained to meet the needs of the application. It results in code that is easier to understand and harder to misuse. The Sensors class can enforce design and business rules. > 该接口也经过仔细修整和归置以适应应用程序的需要。结果就是得到易于理解、难以被误用的代码。Sensors 类推动了设计和业务的规则。我们并不建议总是以这种方式封装 Map 的使用。 We are not suggesting that every use of Map be encapsulated in this form. Rather, we are advising you not to pass Maps (or any other interface at a boundary) around your system. If you use a boundary interface like Map, keep it inside the class, or close family of classes, where it is used. Avoid returning it from, or accepting it as an argument to, public APIs. > 我们建议不要将 Map(或在边界上的其他接口)在系统中传递。如果你使用类似 Map 这样的边界接口,就把它保留在类或近亲类中。避免从公共 API 中返回边界接口,或将边界接口作为参数传递给公共 API。 ## 8.2 EXPLORING AND LEARNING BOUNDARIES 浏览和学习边界 Third-party code helps us get more functionality delivered in less time. Where do we start when we want to utilize some third-party package? It’s not our job to test the third-party code, but it may be in our best interest to write tests for the third-party code we use. > 第三方代码帮助我们在更少时间内发布更丰富的功能。在利用第三方程序包时,该从何处入手呢?我们没有测试第三方代码的职责,但为要使用的第三方代码编写测试,可能最符合我们的利益。 Suppose it is not clear how to use our third-party library. We might spend a day or two (or more) reading the documentation and deciding how we are going to use it. Then we might write our code to use the third-party code and see whether it does what we think. We would not be surprised to find ourselves bogged down in long debugging sessions trying to figure out whether the bugs we are experiencing are in our code or theirs. > 设想第三方代码库的使用方法并不清楚。我们可能会花上一两天(或者更多)时间阅读文档,决定如何使用。然后,我们会编写使用第三方代码的代码,看看是否如我们所愿地工作。陷入长时间的调试、找出在我们或他们代码中的缺陷,这可不是什么稀罕事。 Learning the third-party code is hard. Integrating the third-party code is hard too. Doing both at the same time is doubly hard. What if we took a different approach? Instead of experimenting and trying out the new stuff in our production code, we could write some tests to explore our understanding of the third-party code. Jim Newkirk calls such tests learning tests.1 > 学习第三方代码很难。整合第三方代码也很难。同时做这两件事难上加难。如果我们采用不同的做法呢?不要在生产代码中试验新东西,而是编写测试来遍览和理解第三方代码。Jim Newkirk 把这叫做学习性测试(learning tests)1。 1. [BeckTDD], pp. 136–137. In learning tests we call the third-party API, as we expect to use it in our application. We’re essentially doing controlled experiments that check our understanding of that API. The tests focus on what we want out of the API. > 在学习性测试中,我们如在应用中那样调用第三方代码。我们基本上是在通过核对试验来检测自己对那个 API 的理解程度。测试聚焦于我们想从 API 得到的东西。 ## 8.3 LEARNING LOG4J 学习 log4j Let’s say we want to use the apache log4j package rather than our own custom-built logger. We download it and open the introductory documentation page. Without too much reading we write our first test case, expecting it to write “hello” to the console. > 比如,我们想使用 apache log4j 包来代替自定义的日志代码。我们下载了 log4j,打开介绍文档页。无需看太久,就编写了第一个测试用例,希望它能向控制台输出 hello 字样。 ```java @Test public void testLogCreate() { Logger logger = Logger.getLogger(“MyLogger”); logger.info(“hello”); } ``` When we run it, the logger produces an error that tells us we need something called an Appender. After a little more reading we find that there is a ConsoleAppender. So we create a ConsoleAppender and see whether we have unlocked the secrets of logging to the console. > 运行,logger 发生了一个错误,告诉我们需要用 Appender。再多读一点文档,我们发现有个 ConsoleAppender。于是我们创建了一个 ConsoleAppender,再看是否能解开向控制台输出日志的秘诀。 ```java @Test public void testLogAddAppender() { Logger logger = Logger.getLogger(“MyLogger”); ConsoleAppender appender = new ConsoleAppender(); logger.addAppender(appender); logger.info(“hello”); } ``` This time we find that the Appender has no output stream. Odd—it seems logical that it’d have one. After a little help from Google, we try the following: > 比如,我们想使用 apache log4j 包来代替自定义的日志代码。我们下载了 log4j,打开介绍文档页。无需看太久,就编写了第一个测试用例,希望它能向控制台输出 hello 字样。 ```java @Test public void testLogAddAppender() { Logger logger = Logger.getLogger(“MyLogger”); logger.removeAllAppenders(); logger.addAppender(new ConsoleAppender( new PatternLayout(“%p %t %m%n”), ConsoleAppender.SYSTEM_OUT)); logger.info(“hello”); } ``` That worked; a log message that includes “hello” came out on the console! It seems odd that we have to tell the ConsoleAppender that it writes to the console. > 这回行了;hello 字样的日志信息出现在控制台上!必须告知 ConsoleAppender,让它往控制台写字,看起来有点奇怪。 Interestingly enough, when we remove the ConsoleAppender.SystemOut argument, we see that “hello” is still printed. But when we take out the PatternLayout, it once again complains about the lack of an output stream. This is very strange behavior. > 很有趣,当我们移除 ConsoleAppender.SystemOut 参数时,那个 hello 字样仍然输出到屏幕上。但如果取走 PatternLayout,就会出现关于没有输出流的错误信息。这实在太古怪了。 Looking a little more carefully at the documentation, we see that the default ConsoleAppender constructor is “unconfigured,” which does not seem too obvious or useful. This feels like a bug, or at least an inconsistency, in log4j. > 再仔细看看文档,我们看到默认的 ConsoleAppender 构造器是“未配置”的,这看起来并不明显或没什么用,反而像是 log4j 的一个缺陷,或者至少是前后不太一致。 A bit more googling, reading, and testing, and we eventually wind up with Listing 8-1. We’ve discovered a great deal about the way that log4j works, and we’ve encoded that knowledge into a set of simple unit tests. > 再搜索、阅读、测试,最终我们得到代码清单 8-1。我们极大地发掘了 log4j 的工作方式,也将得到的知识融入了一系列简单的单元测试中。 Listing 8-1 LogTest.java > 代码清单 8-1 LogTest.java ```java public class LogTest { private Logger logger; @Before public void initialize() { logger = Logger.getLogger(“logger”); logger.removeAllAppenders(); Logger.getRootLogger().removeAllAppenders(); } @Test public void basicLogger() { BasicConfigurator.configure(); logger.info(“basicLogger”); } @Test public void addAppenderWithStream() { logger.addAppender(new ConsoleAppender( new PatternLayout(“%p %t %m%n”), ConsoleAppender.SYSTEM_OUT)); logger.info(“addAppenderWithStream”); } @Test public void addAppenderWithoutStream() { logger.addAppender(new ConsoleAppender( new PatternLayout(“%p %t %m%n”))); logger.info(“addAppenderWithoutStream”); } } ``` Now we know how to get a simple console logger initialized, and we can encapsulate that knowledge into our own logger class so that the rest of our application is isolated from the log4j boundary interface. > 现在我们知道如何初始化一个简单的控制台日志器,也能把这些知识封装到自己的日志类中,好将应用程序的其他部分与 log4j 的边界接口隔离开来。 ## 8.4 LEARNING TESTS ARE BETTER THAN FREE 学习性测试的好处不只是免费 The learning tests end up costing nothing. We had to learn the API anyway, and writing those tests was an easy and isolated way to get that knowledge. The learning tests were precise experiments that helped increase our understanding. > 学习性测试毫无成本。无论如何我们都得学习要使用的 API,而编写测试则是获得这些知识的容易而不会影响其他工作的途径。学习性测试是一种精确试验,帮助我们增进对 API 的理解。 Not only are learning tests free, they have a positive return on investment. When there are new releases of the third-party package, we run the learning tests to see whether there are behavioral differences. > 学习性测试不光免费,还在投资上有正面的回报。当第三方程序包发布了新版本,我们可以运行学习性测试,看看程序包的行为有没有改变。 Learning tests verify that the third-party packages we are using work the way we expect them to. Once integrated, there are no guarantees that the third-party code will stay compatible with our needs. The original authors will have pressures to change their code to meet new needs of their own. They will fix bugs and add new capabilities. With each release comes new risk. If the third-party package changes in some way incompatible with our tests, we will find out right away. > 学习性测试确保第三方程序包按照我们想要的方式工作。一旦整合进来,就不能保证第三方代码总与我们的需要兼容。原作者不得不修改代码来满足他们自己的新需要。他们会修正缺陷、添加新功能。风险伴随新版本而来。如果第三方程序包的修改与测试不兼容,我们也能马上发现。 Whether you need the learning provided by the learning tests or not, a clean boundary should be supported by a set of outbound tests that exercise the interface the same way the production code does. Without these boundary tests to ease the migration, we might be tempted to stay with the old version longer than we should. > 无论你是否需要通过学习性测试来学习,总要有一系列与生产代码中调用方式一致的输出测试来支持整洁的边界。不使用这些边界测试来减轻迁移的劳力,我们可能会超出应有时限,长久地绑在旧版本上面。 ## 8.5 USING CODE THAT DOES NOT YET EXIST 使用尚不存在的代码 There is another kind of boundary, one that separates the known from the unknown. There are often places in the code where our knowledge seems to drop off the edge. Sometimes what is on the other side of the boundary is unknowable (at least right now). Sometimes we choose to look no farther than the boundary. > 还有另一种边界,那种将已知和未知分隔开的边界。在代码中总有许多地方是我们的知识未及之处。有时,边界那边就是未知的(至少目前未知)。有时,我们并不往边界那边看过去。 A number of years back I was part of a team developing software for a radio communications system. There was a subsystem, the “Transmitter,” that we knew little about, and the people responsible for the subsystem had not gotten to the point of defining their interface. We did not want to be blocked, so we started our work far away from the unknown part of the code. > 好多年以前,我曾在一个开发无线通信系统软件的团队中工作。该系统有个子系统 Transmitter(发送机)。我们对 Transmitter 知之甚少,而该子系统的开发者还没有对接口进行定义。我们不想受这种事阻碍,就从距未知那部分代码很远处开始工作。 We had a pretty good idea of where our world ended and the new world began. As we worked, we sometimes bumped up against this boundary. Though mists and clouds of ignorance obscured our view beyond the boundary, our work made us aware of what we wanted the boundary interface to be. We wanted to tell the transmitter something like this: > 对于我们的世界如何结束、新世界如何开始,我们有许多好主意。工作时,我们偶尔会跨越那道边界。尽管云雾遮挡了我们看向边界那边的视线,我们还是从工作中了解到我们想要的边界接口是什么样的。我们想要告知发送机一些事: Key the transmitter on the provided frequency and emit an analog representation of the data coming from this stream. > 对于我们的世界如何结束、新世界如何开始,我们有许多好主意。工作时,我们偶尔会跨越那道边界。 We had no idea how that would be done because the API had not been designed yet. So we decided to work out the details later. > 尽管云雾遮挡了我们看向边界那边的视线,我们还是从工作中了解到我们想要的边界接口是什么样的。我们想要告知发送机一些事: To keep from being blocked, we defined our own interface. We called it something catchy, like Transmitter. We gave it a method called transmit that took a frequency and a data stream. This was the interface we wished we had. > 为了不受阻碍,我们定义了自己使用的接口。我们给它取了个好记的名字,比如 Transmitter。我们给它写了个名为 transmit 的方法,获取频率参数和数据流。这就是我们希望得到的接口。 One good thing about writing the interface we wish we had is that it’s under our control. This helps keep client code more readable and focused on what it is trying to accomplish. > 编写我们想得到的接口,好处之一是它在我们控制之下。这有助于保持客户代码更可读,且集中于它该完成的工作。 In Figure 8-2, you can see that we insulated the CommunicationsController classes from the transmitter API (which was out of our control and undefined). By using our own application specific interface, we kept our CommunicationsController code clean and expressive. Once the transmitter API was defined, we wrote the TransmitterAdapter to bridge the gap. The ADAPTER2 encapsulated the interaction with the API and provides a single place to change when the API evolves. > 在图 8-2 中可以看到,我们将 CommunicationsController 类从发送器 API(该 API 不受我们控制,而且还没定义)中隔离出来。通过使用符合应用程序的接口,CommunicationsController 代码整洁且足以表达其意图。一旦发送器 API 被定义出来,我们就编写 TransmitterAdapter 来跨接。ADAPTER[1]封装了与 API 的互动,也提供了一个当 API 发生变动时唯一需要改动的地方。 2. See the Adapter pattern in [GOF]. Figure 8-2 Predicting the transmitter ![](figures/ch8/119fig01.jpg) This design also gives us a very convenient seam3 in the code for testing. Using a suitable FakeTransmitter, we can test the CommunicationsController classes. We can also create boundary tests once we have the TransmitterAPI that make sure we are using the API correctly. > 这套设计方案为测试提供了一种极为方便的接缝[2]。使用适当的 FakeTransmitter,我们就能测试 CommunicationsController 类。在拿到 TransmitterAPI 时,我们也能创建确保正确使用 API 的边界测试。 3. See more about seams in [WELC]. ## 8.6 CLEAN BOUNDARIES 整洁的边界 Interesting things happen at boundaries. Change is one of those things. Good software designs accommodate change without huge investments and rework. When we use code that is out of our control, special care must be taken to protect our investment and make sure future change is not too costly. > 边界上会发生有趣的事。改动是其中之一。有良好的软件设计,无需巨大投入和重写即可进行修改。在使用我们控制不了的代码时,必须加倍小心保护投资,确保未来的修改不至于代价太大。 Code at the boundaries needs clear separation and tests that define expectations. We should avoid letting too much of our code know about the third-party particulars. It’s better to depend on something you control than on something you don’t control, lest it end up controlling you. > 边界上的代码需要清晰的分割和定义了期望的测试。应该避免我们的代码过多地了解第三方代码中的特定信息。依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受它控制。 We manage third-party boundaries by having very few places in the code that refer to them. We may wrap them as we did with Map, or we may use an ADAPTER to convert from our perfect interface to the provided interface. Either way our code speaks to us better, promotes internally consistent usage across the boundary, and has fewer maintenance points when the third-party code changes. > 我们通过代码中少数几处引用第三方边界接口的位置来管理第三方边界。可以像我们对待 Map 那样包装它们,也可以使用 ADAPTER 模式将我们的接口转换为第三方提供的接口。采用这两种方式,代码都能更好地与我们沟通,在边界两边推动内部一致的用法,当第三方代码有改动时修改点也会更少。 ================================================ FILE: docs/ch9.md ================================================ # 第 9 章 Unit Tests ![](figures/ch9/9_1fig_martin.jpg) Our profession has come a long way in the last ten years. In 1997 no one had heard of Test Driven Development. For the vast majority of us, unit tests were short bits of throw-away code that we wrote to make sure our programs “worked.” We would painstakingly write our classes and methods, and then we would concoct some ad hoc code to test them. Typically this would involve some kind of simple driver program that would allow us to manually interact with the program we had written. I remember writing a C++ program for an embedded real-time system back in the mid-90s. The program was a simple timer with the following signature: ```cpp void Timer::ScheduleCommand(Command* theCommand, int milliseconds) ``` The idea was simple; the execute method of the Command would be executed in a new thread after the specified number of milliseconds. The problem was, how to test it. I cobbled together a simple driver program that listened to the keyboard. Every time a character was typed, it would schedule a command that would type the same character five seconds later. Then I tapped out a rhythmic melody on the keyboard and waited for that melody to replay on the screen five seconds later. “I … want-a-girl … just … like-the-girl-who-marr … ied … dear … old … dad.” I actually sang that melody while typing the “.” key, and then I sang it again as the dots appeared on the screen. That was my test! Once I saw it work and demonstrated it to my colleagues, I threw the test code away. As I said, our profession has come a long way. Nowadays I would write a test that made sure that every nook and cranny of that code worked as I expected it to. I would isolate my code from the operating system rather than just calling the standard timing functions. I would mock out those timing functions so that I had absolute control over the time. I would schedule commands that set boolean flags, and then I would step the time forward, watching those flags and ensuring that they went from false to true just as I changed the time to the right value. Once I got a suite of tests to pass, I would make sure that those tests were convenient to run for anyone else who needed to work with the code. I would ensure that the tests and the code were checked in together into the same source package. Yes, we’ve come a long way; but we have farther to go. The Agile and TDD movements have encouraged many programmers to write automated unit tests, and more are joining their ranks every day. But in the mad rush to add testing to our discipline, many programmers have missed some of the more subtle, and important, points of writing good tests. THE THREE LAWS OF TDD By now everyone knows that TDD asks us to write unit tests first, before we write production code. But that rule is just the tip of the iceberg. Consider the following three laws:1 1. Professionalism and Test-Driven Development, Robert C. Martin, Object Mentor, IEEE Software, May/June 2007 (Vol. 24, No. 3) pp. 32–36 http://doi.ieeecomputersociety.org/10.1109/MS.2007.85 First Law You may not write production code until you have written a failing unit test. Second Law You may not write more of a unit test than is sufficient to fail, and not compiling is failing. Third Law You may not write more production code than is sufficient to pass the currently failing test. These three laws lock you into a cycle that is perhaps thirty seconds long. The tests and the production code are written together, with the tests just a few seconds ahead of the production code. If we work this way, we will write dozens of tests every day, hundreds of tests every month, and thousands of tests every year. If we work this way, those tests will cover virtually all of our production code. The sheer bulk of those tests, which can rival the size of the production code itself, can present a daunting management problem. KEEPING TESTS CLEAN Some years back I was asked to coach a team who had explicitly decided that their test code should not be maintained to the same standards of quality as their production code. They gave each other license to break the rules in their unit tests. “Quick and dirty” was the watchword. Their variables did not have to be well named, their test functions did not need to be short and descriptive. Their test code did not need to be well designed and thoughtfully partitioned. So long as the test code worked, and so long as it covered the production code, it was good enough. Some of you reading this might sympathize with that decision. Perhaps, long in the past, you wrote tests of the kind that I wrote for that Timer class. It’s a huge step from writing that kind of throw-away test, to writing a suite of automated unit tests. So, like the team I was coaching, you might decide that having dirty tests is better than having no tests. What this team did not realize was that having dirty tests is equivalent to, if not worse than, having no tests. The problem is that tests must change as the production code evolves. The dirtier the tests, the harder they are to change. The more tangled the test code, the more likely it is that you will spend more time cramming new tests into the suite than it takes to write the new production code. As you modify the production code, old tests start to fail, and the mess in the test code makes it hard to get those tests to pass again. So the tests become viewed as an ever-increasing liability. From release to release the cost of maintaining my team’s test suite rose. Eventually it became the single biggest complaint among the developers. When managers asked why their estimates were getting so large, the developers blamed the tests. In the end they were forced to discard the test suite entirely. But, without a test suite they lost the ability to make sure that changes to their code base worked as expected. Without a test suite they could not ensure that changes to one part of their system did not break other parts of their system. So their defect rate began to rise. As the number of unintended defects rose, they started to fear making changes. They stopped cleaning their production code because they feared the changes would do more harm than good. Their production code began to rot. In the end they were left with no tests, tangled and bug-riddled production code, frustrated customers, and the feeling that their testing effort had failed them. In a way they were right. Their testing effort had failed them. But it was their decision to allow the tests to be messy that was the seed of that failure. Had they kept their tests clean, their testing effort would not have failed. I can say this with some certainty because I have participated in, and coached, many teams who have been successful with clean unit tests. The moral of the story is simple: Test code is just as important as production code. It is not a second-class citizen. It requires thought, design, and care. It must be kept as clean as production code. Tests Enable the -ilities If you don’t keep your tests clean, you will lose them. And without them, you lose the very thing that keeps your production code flexible. Yes, you read that correctly. It is unit tests that keep our code flexible, maintainable, and reusable. The reason is simple. If you have tests, you do not fear making changes to the code! Without tests every change is a possible bug. No matter how flexible your architecture is, no matter how nicely partitioned your design, without tests you will be reluctant to make changes because of the fear that you will introduce undetected bugs. But with tests that fear virtually disappears. The higher your test coverage, the less your fear. You can make changes with near impunity to code that has a less than stellar architecture and a tangled and opaque design. Indeed, you can improve that architecture and design without fear! So having an automated suite of unit tests that cover the production code is the key to keeping your design and architecture as clean as possible. Tests enable all the -ilities, because tests enable change. So if your tests are dirty, then your ability to change your code is hampered, and you begin to lose the ability to improve the structure of that code. The dirtier your tests, the dirtier your code becomes. Eventually you lose the tests, and your code rots. CLEAN TESTS What makes a clean test? Three things. Readability, readability, and readability. Readability is perhaps even more important in unit tests than it is in production code. What makes tests readable? The same thing that makes all code readable: clarity, simplicity, and density of expression. In a test you want to say a lot with as few expressions as possible. Consider the code from FitNesse in Listing 9-1. These three tests are difficult to understand and can certainly be improved. First, there is a terrible amount of duplicate code [G5] in the repeated calls to addPage and assertSubString. More importantly, this code is just loaded with details that interfere with the expressiveness of the test. Listing 9-1 SerializedPageResponderTest.java ```java public void testGetPageHieratchyAsXml() throws Exception { crawler.addPage(root, PathParser.parse(“PageOne”)); crawler.addPage(root, PathParser.parse(“PageOne.ChildOne”)); crawler.addPage(root, PathParser.parse(“PageTwo”)); request.setResource(“root”); request.addInput(“type”, “pages”); Responder responder = new SerializedPageResponder(); SimpleResponse response = (SimpleResponse) responder.makeResponse( new FitNesseContext(root), request); String xml = response.getContent(); assertEquals(“text/xml”, response.getContentType()); assertSubString(“PageOne”, xml); assertSubString(“PageTwo”, xml); assertSubString(“ChildOne”, xml); } public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks() throws Exception { WikiPage pageOne = crawler.addPage(root, PathParser.parse(“PageOne”)); crawler.addPage(root, PathParser.parse(“PageOne.ChildOne”)); crawler.addPage(root, PathParser.parse(“PageTwo”)); PageData data = pageOne.getData(); WikiPageProperties properties = data.getProperties(); WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME); symLinks.set(“SymPage”, ”PageTwo”); pageOne.commit(data); request.setResource(“root”); request.addInput(“type”, ”pages”); Responder responder = new SerializedPageResponder(); SimpleResponse response = (SimpleResponse) responder.makeResponse( new FitNesseContext(root), request); String xml = response.getContent(); assertEquals(“text/xml”, response.getContentType()); assertSubString(“PageOne”, xml); assertSubString(“PageTwo”, xml); assertSubString(“ChildOne”, xml); assertNotSubString(“SymPage”, xml); } public void testGetDataAsHtml() throws Exception { crawler.addPage(root, PathParser.parse(“TestPageOne”), ”test page”); request.setResource(“TestPageOne”); request.addInput(“type”, ”data”); Responder responder = new SerializedPageResponder(); SimpleResponse response = (SimpleResponse) responder.makeResponse( new FitNesseContext(root), request); String xml = response.getContent(); assertEquals(“text/xml”, response.getContentType()); assertSubString(“test page”, xml); assertSubString(“PageOne”, “PageTwo”, “ChildOne” ); } public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception { WikiPage page = makePage(“PageOne”); makePages(“PageOne.ChildOne”, “PageTwo”); addLinkTo(page, “PageTwo”, “SymPage”); submitRequest(“root”, “type:pages”); assertResponseIsXML(); assertResponseContains( “PageOne”, “PageTwo”, “ChildOne” ); assertResponseDoesNotContain(“SymPage”); } public void testGetDataAsXml() throws Exception { makePageWithContent(“TestPageOne”, “test page”); submitRequest(“TestPageOne”, “type:data”); assertResponseIsXML(); assertResponseContains(“test page”, “PageOne”, “PageTwo”, “ChildOne” ); } ``` Notice that I have changed the names of the functions to use the common given-when-then5 convention. This makes the tests even easier to read. Unfortunately, splitting the tests as shown results in a lot of duplicate code. 5. [RSpec]. We can eliminate the duplication by using the TEMPLATE METHOD6 pattern and putting the given/when parts in the base class, and the then parts in different derivatives. Or we could create a completely separate test class and put the given and when parts in the @Before function, and the when parts in each @Test function. But this seems like too much mechanism for such a minor issue. In the end, I prefer the multiple asserts in Listing 9-2. 6. [GOF]. I think the single assert rule is a good guideline.7 I usually try to create a domain-specific testing language that supports it, as in Listing 9-5. But I am not afraid to put more than one assert in a test. I think the best thing we can say is that the number of asserts in a test ought to be minimized. 7. “Keep to the code!” Single Concept per Test Perhaps a better rule is that we want to test a single concept in each test function. We don’t want long test functions that go testing one miscellaneous thing after another. Listing 9-8 is an example of such a test. This test should be split up into three independent tests because it tests three independent things. Merging them all together into the same function forces the reader to figure out why each section is there and what is being tested by that section. Listing 9-8 ```java /** * Miscellaneous tests for the addMonths() method. */ public void testAddMonths() { SerialDate d1 = SerialDate.createInstance(31, 5, 2004); SerialDate d2 = SerialDate.addMonths(1, d1); assertEquals(30, d2.getDayOfMonth()); assertEquals(6, d2.getMonth()); assertEquals(2004, d2.getYYYY()); SerialDate d3 = SerialDate.addMonths(2, d1); assertEquals(31, d3.getDayOfMonth()); assertEquals(7, d3.getMonth()); assertEquals(2004, d3.getYYYY()); SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1)); assertEquals(30, d4.getDayOfMonth()); assertEquals(7, d4.getMonth()); assertEquals(2004, d4.getYYYY()); } ``` The three test functions probably ought to be like this: - Given the last day of a month with 31 days (like May): 1. When you add one month, such that the last day of that month is the 30th (like June), then the date should be the 30th of that month, not the 31st. 2. When you add two months to that date, such that the final month has 31 days, then the date should be the 31st. - Given the last day of a month with 30 days in it (like June): 1. When you add one month such that the last day of that month has 31 days, then the date should be the 30th, not the 31st. Stated like this, you can see that there is a general rule hiding amidst the miscellaneous tests. When you increment the month, the date can be no greater than the last day of the month. This implies that incrementing the month on February 28th should yield March 28th. That test is missing and would be a useful test to write. So it’s not the multiple asserts in each section of Listing 9-8 that causes the problem. Rather it is the fact that there is more than one concept being tested. So probably the best rule is that you should minimize the number of asserts per concept and test just one concept per test function. F.I.R.S.T.8 8. Object Mentor Training Materials. Clean tests follow five other rules that form the above acronym: Fast Tests should be fast. They should run quickly. When tests run slow, you won’t want to run them frequently. If you don’t run them frequently, you won’t find problems early enough to fix them easily. You won’t feel as free to clean up the code. Eventually the code will begin to rot. Independent Tests should not depend on each other. One test should not set up the conditions for the next test. You should be able to run each test independently and run the tests in any order you like. When tests depend on each other, then the first one to fail causes a cascade of downstream failures, making diagnosis difficult and hiding downstream defects. Repeatable Tests should be repeatable in any environment. You should be able to run the tests in the production environment, in the QA environment, and on your laptop while riding home on the train without a network. If your tests aren’t repeatable in any environment, then you’ll always have an excuse for why they fail. You’ll also find yourself unable to run the tests when the environment isn’t available. Self-Validating The tests should have a boolean output. Either they pass or fail. You should not have to read through a log file to tell whether the tests pass. You should not have to manually compare two different text files to see whether the tests pass. If the tests aren’t self-validating, then failure can become subjective and running the tests can require a long manual evaluation. Timely The tests need to be written in a timely fashion. Unit tests should be written just before the production code that makes them pass. If you write tests after the production code, then you may find the production code to be hard to test. You may decide that some production code is too hard to test. You may not design the production code to be testable. CONCLUSION We have barely scratched the surface of this topic. Indeed, I think an entire book could be written about clean tests. Tests are as important to the health of a project as the production code is. Perhaps they are even more important, because tests preserve and enhance the flexibility, maintainability, and reusability of the production code. So keep your tests constantly clean. Work to make them expressive and succinct. Invent testing APIs that act as domain-specific language that helps you write the tests. If you let the tests rot, then your code will rot too. Keep your tests clean. ================================================ FILE: gitee-deploy.sh ================================================ #!/usr/bin/env sh # abort on errors set -e # build yarn docs:build # navigate into the build output directory cd docs/.vuepress/dist # if you are deploying to a custom domain echo 'http://gdut_yy.gitee.io/doc-cleancode/' > CNAME git init git add -A git commit -m 'deploy' # if you are deploying to https://.github.io git push -f git@gitee.com:gdut_yy/doc-cleancode.git master # if you are deploying to https://.github.io/ # git push -f git@github.com:/.git master:gh-pages cd - ================================================ FILE: package.json ================================================ { "scripts": { "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs" } }