标题 & 代码高亮

This commit is contained in:
gdut-yy
2019-12-28 22:27:33 +08:00
parent 67f3f67fac
commit 893fb65d8c
17 changed files with 684 additions and 682 deletions

View File

@@ -1,4 +1,4 @@
Clean Code
# 第 1 章 Clean Code
Image
Image

View File

@@ -1,4 +1,4 @@
Classes
# 第 10 章 Classes
with Jeff Langr
Image
@@ -24,7 +24,7 @@ Listing 10-1 outlines a class, SuperDashboard, that exposes about 70 public meth
Listing 10-1 Too Many Responsibilities
```java
public class SuperDashboard extends JFrame implements MetaDataUser
public String getCustomizerLanguagePath()
public void setSystemConfigPath(String systemConfigPath)
@@ -98,12 +98,12 @@ Listing 10-1 Too Many Responsibilities
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)
@@ -111,7 +111,7 @@ Listing 10-2 Small Enough?
public int getMinorVersionNumber()
public int getBuildNumber()
}
```
Five methods isnt 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 its 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.
@@ -129,13 +129,13 @@ Trying to identify responsibilities (reasons to change) often helps us recognize
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. Its 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.
@@ -159,7 +159,7 @@ Consider the implementation of a Stack in Listing 10-4. This is a very cohesive
Listing 10-4 Stack.java A cohesive class.
```java
public class Stack {
private int topOfStack = 0;
List<Integer> elements = new LinkedList<Integer>();
@@ -181,7 +181,7 @@ Listing 10-4 Stack.java A cohesive class.
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
@@ -199,7 +199,7 @@ As a demonstration of what I mean, lets use a time-honored example taken from
Listing 10-5 PrintPrimes.java
```java
package literatePrimes;
public class PrintPrimes {
@@ -270,14 +270,14 @@ Listing 10-5 PrintPrimes.java
}
}
}
```
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 {
@@ -296,10 +296,10 @@ Listing 10-6 PrimePrinter.java (refactored)
}
}
```
Listing 10-7 RowColumnPagePrinter.java
```java
package literatePrimes;
import java.io.PrintStream;
@@ -369,10 +369,10 @@ Listing 10-7 RowColumnPagePrinter.java
this.printStream = printStream;
}
}
```
Listing 10-8 PrimeGenerator.java
```java
package literatePrimes;
import java.util.ArrayList;
@@ -443,7 +443,7 @@ Listing 10-8 PrimeGenerator.java
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.
@@ -463,7 +463,7 @@ The Sql class in Listing 10-9 is used to generate properly formed SQL strings gi
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)
@@ -477,7 +477,7 @@ Listing 10-9 A class that must be opened for change
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.
@@ -488,7 +488,7 @@ What if we considered a solution like that in Listing 10-10? Each public interfa
Listing 10-10 A set of closed classes
```java
abstract public class Sql {
public Sql(String table, Column[] columns)
abstract public String generate();
@@ -543,7 +543,7 @@ Listing 10-10 A set of closed classes
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 its 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.
@@ -560,13 +560,13 @@ Needs will change, therefore code will change. We learned in OO 101 that there a
Dependencies upon concrete details create challenges for testing our system. If were building a Portfolio class and it depends upon an external TokyoStockExchange API to derive the portfolios value, our test cases are impacted by the volatility of such a lookup. Its 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) {
@@ -574,9 +574,9 @@ We design TokyoStockExchange to implement this interface. We also make sure that
}
// …
}
```
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;
@@ -594,7 +594,7 @@ Now our test can create a testable implementation of the StockExchange interface
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.

View File

@@ -1,4 +1,4 @@
Systems
# 第 11 章 Systems
by Dr. Kevin Dean Wampler
Image
@@ -22,13 +22,13 @@ Software systems should separate the startup process, when the application objec
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 dont 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 dont 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 cant compile without resolving these dependencies, even if we never actually use an object of this type at runtime!
@@ -73,9 +73,9 @@ A powerful mechanism for separating construction from use is Dependency Injectio
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 doesnt 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.
@@ -111,7 +111,7 @@ First, you had to define a local (in process) or remote (separate JVM) interface
Listing 11-1 An EJB2 local interface for a Bank EJB
```java
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;
@@ -131,12 +131,12 @@ Listing 11-1 An EJB2 local interface for a Bank EJB
void setAccounts(Collection accounts) throws EJBException;
void addAccount(AccountDTO accountDTO) throws EJBException;
}
```
I have shown several attributes for the Banks 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.*;
@@ -176,7 +176,7 @@ Listing 11-2 The corresponding EJB2 Entity Bean Implementation
public void ejbStore() {}
public void ejbRemove() {}
}
```
I havent 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.
@@ -215,7 +215,7 @@ Listing 11-3 shows the skeleton for a JDK proxy to provide persistence support f
Listing 11-3 JDK Proxy Example
```java
// Bank.java (suppressing package names…)
import java.utils.*;
@@ -277,7 +277,7 @@ Listing 11-3 JDK Proxy Example
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.
@@ -303,7 +303,7 @@ Listing 11-4 shows a typical fragment of a Spring V2.5 configuration file, app.x
Listing 11-4 Spring 2.X configuration file
```xml
<beans>
<bean id=”appDataSource”
@@ -322,7 +322,7 @@ Listing 11-4 Spring 2.X configuration file
p:dataAccessObject-ref=”bankDataAccessObject”/>
</beans>
```
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.)
@@ -335,11 +335,11 @@ The client believes it is invoking getAccounts() on a Bank object, but it is act
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
@@ -355,7 +355,7 @@ Listing 11-5 shows our Bank object rewritten in EJB316.
Listing 11-5 An EBJ3 Bank EJB
```java
package com.example.banking.model;
import javax.persistence.*;
import java.util.ArrayList;
@@ -401,7 +401,7 @@ Listing 11-5 An EBJ3 Bank EJB
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 wont change frequently, many teams may choose to keep the annotations, but with far fewer harmful drawbacks compared to the EJB2 invasiveness.

View File

@@ -1,4 +1,4 @@
Emergence
# 第 12 章 Emergence
by Jeff Langr
Image
@@ -40,18 +40,18 @@ During this refactoring step, we can apply anything from the entire body of know
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)
@@ -72,10 +72,10 @@ Creating a clean system requires the will to eliminate duplication, even in just
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)
@@ -93,11 +93,11 @@ To keep this system clean, we should eliminate the small amount of duplication b
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
@@ -117,11 +117,11 @@ The TEMPLATE METHOD2 pattern is a common technique for removing higher-level dup
// …
}
}
```
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();
@@ -146,7 +146,7 @@ We can eliminate the obvious duplication by applying the TEMPLATE METHOD pattern
// EU specific logic
}
}
```
The subclasses fill in the “hole” in the accrueVacation algorithm, supplying the only bits of information that are not duplicated.
EXPRESSIVE

View File

@@ -1,4 +1,4 @@
Concurrency
# 第 13 章 Concurrency
by Brett L. Schuchert
Image
@@ -56,7 +56,7 @@ Here are a few more balanced sound bites regarding writing concurrent software:
CHALLENGES
What makes concurrent programming so difficult? Consider the following trivial class:
```java
public class X {
private int lastIdUsed;
@@ -64,7 +64,7 @@ What makes concurrent programming so difficult? Consider the following trivial c
return ++lastIdUsed;
}
}
```
Lets 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.
@@ -295,7 +295,7 @@ 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 youre 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();
@@ -305,7 +305,7 @@ Here is an example of doing just that:
}
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.
@@ -326,14 +326,14 @@ Clearly, if we divide our system up into POJOs that know nothing of threading an
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();
@@ -345,7 +345,7 @@ You can add calls to this in various places within your code:
}
return null;
}
```
Now you use a simple aspect that randomly selects among doing nothing, sleeping, or yielding.

View File

@@ -1,4 +1,4 @@
Successive Refinement
# 第 14 章 Successive Refinement
Case Study of a Command-Line Argument Parser
Image
@@ -11,7 +11,7 @@ Args is very simple to use. You simply construct the Args class with the input a
Listing 14-1 Simple use of Args
```java
public static void main(String[] args) {
try {
Args arg = new Args(l,p#,d*, args);
@@ -23,7 +23,7 @@ Listing 14-1 Simple use of Args
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.
@@ -35,7 +35,7 @@ Listing 14-2 is the implementation of the Args class. Please read it very carefu
Listing 14-2 Args.java
```java
package com.objectmentor.utilities.args;
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
@@ -137,19 +137,19 @@ Listing 14-2 Args.java
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. Ill 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<String> currentArgument) throws ArgsException;
}
```
Listing 14-4 BooleanArgumentMarshaler.java
```java
public class BooleanArgumentMarshaler implements ArgumentMarshaler {
private boolean booleanValue = false;
@@ -164,10 +164,10 @@ public class BooleanArgumentMarshaler implements ArgumentMarshaler {
return false;
}
}
```
Listing 14-5 StringArgumentMarshaler.java
```java
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
public class StringArgumentMarshaler implements ArgumentMarshaler {
@@ -188,14 +188,14 @@ public class StringArgumentMarshaler implements ArgumentMarshaler {
return ””;
}
}
```
The other ArgumentMarshaler derivatives simply replicate this pattern for doubles and String arrays and would serve to clutter this chapter. Ill 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 {
@@ -220,10 +220,10 @@ Listing 14-6 IntegerArgumentMarshaler.java
return 0;
}
}
```
Listing 14-7 ArgsException.java
```java
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
public class ArgsException extends Exception {
@@ -312,7 +312,7 @@ public class ArgsException extends Exception {
MISSING_INTEGER, INVALID_INTEGER,
MISSING_DOUBLE, INVALID_DOUBLE}
}
```
Its 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.
@@ -333,7 +333,7 @@ Listing 14-8 shows an earlier version of the Args class. It “works.” And it
Listing 14-8 Args.java (first draft)
```java
import java.text.ParseException;
import java.util.*;
@@ -597,7 +597,7 @@ public class Args {
private class ArgsException extends Exception {
}
}
```
I hope your initial reaction to this mass of code is “Im certainly glad he didnt leave it like that!” If you feel like this, then remember thats 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. Its 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.
@@ -608,7 +608,7 @@ The mess built gradually. Earlier versions had not been nearly so nasty. For exa
Listing 14-9 Args.java (Boolean only)
```java
package com.objectmentor.utilities.getopts;
import java.util.*;
@@ -722,7 +722,7 @@ public class Args {
return booleanArgs.get(arg);
}
}
```
Although you can find plenty to complain about in this code, its really not that bad. Its compact and simple and easy to understand. However, within this code it is easy to see the seeds of the later festering pile. Its 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.
@@ -731,7 +731,7 @@ I added the two argument types incrementally. First, I added the String argument
Listing 14-10 Args.java (Boolean and String)
```java
package com.objectmentor.utilities.getopts;
import java.text.ParseException;
@@ -934,7 +934,7 @@ Listing 14-10 Args.java (Boolean and String)
return valid;
}
}
```
You can see that this is starting to get out of hand. Its still not horrible, but the mess is certainly starting to grow. Its a pile, but its not festering quite yet. It took the addition of the integer argument type to get this pile really fermenting and festering.
So I Stopped
@@ -956,7 +956,7 @@ So I proceeded to make a large number of very tiny changes. Each change moved th
Listing 14-11 ArgumentMarshaller appended to Args.java
```java
private class ArgumentMarshaler }
private boolean booleanValue = false;
@@ -977,14 +977,14 @@ Listing 14-11 ArgumentMarshaller appended to Args.java
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
}
}
```
Clearly, this wasnt 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<Character, ArgumentMarshaler> booleanArgs =
new HashMap<Character, ArgumentMarshaler>();
This broke a few statements, which I quickly fixed.
```java
private void parseBooleanSchemaElement(char elementId) {
booleanArgs.put(elementId, new BooleanArgumentMarshaler());
@@ -997,34 +997,34 @@ This broke a few statements, which I quickly fixed.
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, youll 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 hadnt 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 didnt 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 shouldnt 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<Character, ArgumentMarshaler> stringArgs =
new HashMap<Character, ArgumentMarshaler>();
@@ -1068,13 +1068,13 @@ Addin_g String arguments was very similar to adding boolean arguments. I had to
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, Im 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 werent any surprises.
```java
private Map<Character, ArgumentMarshaler> intArgs =
new HashMap<Character, ArgumentMarshaler>();
@@ -1134,9 +1134,9 @@ The obvious next step was to move the int argument functionality into the Argume
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;
@@ -1167,34 +1167,34 @@ With all the marshalling moved to the ArgumentMarshaler, I started pushing funct
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 {
@@ -1202,9 +1202,9 @@ Just to get this to compile, I added the get function to the ArgumentMarshaler.
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;
@@ -1221,11 +1221,11 @@ This compiled and obviously failed the tests. Getting the tests working again wa
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 {
@@ -1293,9 +1293,9 @@ I did the same pattern of changes for Strings. I deployed both set and get, dele
}
}
}
```
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 {
@@ -1350,9 +1350,9 @@ Finally, I repeated the process for integers. This was just a little more compli
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 couldnt 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<Character, ArgumentMarshaler> booleanArgs =
@@ -1381,22 +1381,22 @@ Of course, the tests continued to pass. Next, I got rid of the three different m
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;
@@ -1406,9 +1406,9 @@ The tests still passed. So I made the same change to isIntArg and isStringArg.
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))
@@ -1434,9 +1434,9 @@ The tests still passed. So I eliminated all the duplicate calls to marshalers.ge
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)
@@ -1450,9 +1450,9 @@ This left no good reason for the three isxxxArg methods. So I inlined them:
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)
@@ -1472,9 +1472,9 @@ Next, I started using the marshalers map in the set functions, breaking the use
} 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 {
@@ -1519,16 +1519,16 @@ The tests still passed, so I did the same with Strings and Integers. This allowe
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;
@@ -1540,21 +1540,21 @@ to this:
}
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<Character, ArgumentMarshaler> booleanArgs
@@ -1566,9 +1566,9 @@ And now we can delete the boolean map.
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
```
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());
}
@@ -1607,9 +1607,9 @@ Next, I migrated the String and Integer arguments in the same manner and did a l
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
```
Next, I inlined the three parse methods because they didnt do much anymore:
```java
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
@@ -1625,12 +1625,12 @@ Next, I inlined the three parse methods because they didnt do much anymore:
Argument: %c has invalid format: %s., elementId, elementTail), 0);
}
}
```
Okay, so now lets 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;
@@ -1922,13 +1922,13 @@ Listing 14-12 Args.java (After first refactoring)
}
}
}
```
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; theres 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.
Id really like to get rid of that type-case up in setArgument [G23]. What Id 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, youll notice that it uses two instance variables: args and currentArg. To move setIntArg down into BooleanArgumentMarshaler, Ill have to pass both args and currentArgs as function arguments. Thats dirty [F1]. Id 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 Ill 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;
@@ -1995,9 +1995,9 @@ public class Args {
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)
@@ -2018,11 +2018,11 @@ These were simple changes that kept all the tests passing. Now we can start movi
}
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 well 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)
@@ -2050,22 +2050,22 @@ Now we can start to move the set functions. The setBooleanArg function is trivia
catch (ArgsException e) {
}
}
```
Didnt 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 Rubiks 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 doesnt 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 its 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<String> currentArgument)
throws ArgsException;
public abstract void set(String s) throws ArgsException;
public abstract Object get();
}
```
Of course this breaks all the derivatives. So lets implement the new method in each.
```java
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
private boolean booleanValue = false;
@@ -2109,9 +2109,9 @@ Of course this breaks all the derivatives. So lets implement the new method i
return intValue;
}
}
```
And now we can eliminate setBooleanArg!
```java
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
@@ -2131,9 +2131,9 @@ And now we can eliminate setBooleanArg!
}
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)
@@ -2203,9 +2203,9 @@ The tests all pass, and the set function is deploying to BooleanArgumentMarshale
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)
@@ -2219,9 +2219,9 @@ And so the coup de grace: The type-case can be removed! Touche!
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
@@ -2244,16 +2244,16 @@ Now we can get rid of some crufty functions in IntegerArgumentMarshaler and clea
return intValue;
}
}
```
We can also turn ArgumentMarshaler into an interface.
```java
private interface ArgumentMarshaler {
void set(Iterator<String> currentArgument) throws ArgsException;
Object get();
}
So now lets 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());
@@ -2261,9 +2261,9 @@ So now lets see how easy it is to add a new argument type to our structure. I
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);
@@ -2280,9 +2280,9 @@ Now we clean up the schema parsing code and add the ## detection for the double
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;
@@ -2305,15 +2305,15 @@ Next, we write the DoubleArgumentMarshaler class.
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 {
@@ -2322,9 +2322,9 @@ And we need a getDouble function.
return 0.0;
}
}
```
And all the tests pass! That was pretty painless. So now lets 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());
@@ -2359,9 +2359,9 @@ And all the tests pass! That was pretty painless. So now lets make sure all t
}
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());
@@ -2371,11 +2371,11 @@ And the tests pass. The next test makes sure we detect a missing double argument
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 doesnt really belong in the Args class. We are also throwing out ParseException, which doesnt really belong to us. So lets 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;
@@ -2505,14 +2505,14 @@ The exception code is pretty ugly and doesnt really belong in the Args class.
}
}
}
```
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;
@@ -2656,10 +2656,10 @@ Listing 14-13 ArgsTest.java
}
}
}
```
Listing 14-14 ArgsExceptionTest.java
```java
public class ArgsExceptionTest extends TestCase {
public void testUnexpectedMessage() throws Exception {
ArgsException e =
@@ -2702,10 +2702,10 @@ Listing 14-14 ArgsExceptionTest.java
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;
@@ -2787,10 +2787,10 @@ Listing 14-15 ArgsException.java
MISSING_INTEGER, INVALID_INTEGER,
MISSING_DOUBLE, INVALID_DOUBLE}
}
```
Listing 14-16 Args.java
```java
public class Args {
private String schema;
private Map<Character, ArgumentMarshaler> marshalers =
@@ -2937,7 +2937,7 @@ Listing 14-16 Args.java
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.

View File

@@ -1,4 +1,4 @@
JUnit Internals
# 第 15 章 JUnit Internals
Image
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 well critique an example drawn from the JUnit framework.
@@ -14,7 +14,7 @@ I could explain it further, but the test cases do a better job. So take a look a
Listing 15-1 ComparisonCompactorTest.java
```java
package junit.tests.framework;
import junit.framework.ComparisonCompactor;
import junit.framework.TestCase;
@@ -116,14 +116,14 @@ Listing 15-1 ComparisonCompactorTest.java
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 youll find it to be nicely partitioned, reasonably expressive, and simple in structure. Once you are done, then well pick the nits together.
Listing 15-2 ComparisonCompactor.java (Original)
```java
package junit.framework;
public class ComparisonCompactor {
@@ -204,12 +204,12 @@ Listing 15-2 ComparisonCompactor.java (Original)
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;
@@ -257,21 +257,21 @@ Listing 15-3 ComparisonCompator.java (defactored)
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 dont care for is the f prefix for the member variables [N6]. Todays environments make this kind of scope encoding redundant. So lets eliminate all the fs.
```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);
@@ -281,9 +281,9 @@ Next, we have an unencapsulated conditional at the beginning of the compact func
String actual = compactString(this.actual);
return Assert.format(message, expected, actual);
}
```
This conditional should be encapsulated to make our intent clear. So lets extract a method that explains it.
```java
public String compact(String message) {
if (shouldNotCompact())
return Assert.format(message, expected, actual);
@@ -296,13 +296,13 @@ This conditional should be encapsulated to make our intent clear. So lets ext
private boolean shouldNotCompact() {
return expected == null || actual == null || areStringsEqual();
}
```
I dont 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? Dont 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 lets turn that if statement on its head and invert the sense of the conditional.
```java
public String compact(String message) {
if (canBeCompacted()) {
findCommonPrefix();
@@ -317,13 +317,13 @@ Negatives are slightly harder to understand than positives [G29]. So lets tur
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 lets split it up as follows:
```java
private String compactExpected;
private String compactActual;
@@ -343,9 +343,9 @@ The body of the if statement is where the true compacting of the expected and ac
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
```
Notice that this required us to promote compactExpected and compactActual to member variables. I dont like the way that the last two lines of the new function return variables, but the first two dont. They arent 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();
@@ -371,11 +371,11 @@ Notice that this required us to promote compactExpected and compactActual to mem
}
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, lets have findCommonSuffix take the prefixIndex as an argument.
```java
private void compactExpectedAndActual() {
prefixIndex = findCommonPrefix();
suffixIndex = findCommonSuffix(prefixIndex);
@@ -392,9 +392,9 @@ Careful inspection of findCommonSuffix exposes a hidden temporal coupling [G31];
}
return expected.length() - expectedSuffix;
}
```
Im 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 theres no indication that the parameter is really needed. So lets take a different tack.
```java
private void compactExpectedAndActual() {
findCommonPrefixAndSuffix();
compactExpected = compactString(expected);
@@ -421,9 +421,9 @@ Im not really happy with this. The passing of the prefixIndex argument is a b
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. Lets clean it up now.
```java
private void findCommonPrefixAndSuffix() {
findCommonPrefix();
int suffixLength = 1;
@@ -440,12 +440,12 @@ We put findCommonPrefix and findCommonSuffix back the way they were, changing th
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 lets fix that. The result is in Listing 15-4.
Listing 15-4 ComparisonCompactor.java (interim)
```java
public class ComparisonCompactor {
private int suffixLength;
@@ -489,7 +489,7 @@ Listing 15-4 ComparisonCompactor.java (interim)
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:
@@ -499,7 +499,7 @@ There is a problem however. As I was eliminating the +1s, I noticed the followin
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 didnt 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 lets comment them out and run the tests. They passed! So lets restructure compactString to eliminate the extraneous if statements and make the function much simpler [G9].
```java
private String compactString(String source) {
return
computeCommonPrefix() +
@@ -508,12 +508,12 @@ This calls into question both if statements in compactString! It looks as though
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, Ill just show you the result in Listing 15-5.
Listing 15-5 ComparisonCompactor.java (final)
```java
package junit.framework;
public class ComparisonCompactor {
@@ -616,7 +616,7 @@ Listing 15-5 ComparisonCompactor.java (final)
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.

View File

@@ -1,4 +1,4 @@
Refactoring SerialDate
# 第 16 章 Refactoring SerialDate
Image
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.
@@ -29,7 +29,7 @@ The first few commented-out tests (lines 23-63) were a bit of conceit on my part
I left the tests at line 32 and line 45 commented out because its not clear to me that the “tues” and “thurs” abbreviations ought to be supported.
The tests on line 153 and line 154 dont 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++) {
@@ -43,11 +43,11 @@ The tests on line 153 and line 154 dont pass. Clearly, they should [G2]. We c
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].
@@ -55,7 +55,7 @@ The testGetNearestDayOfWeek unit test (line 329), which tests the getNearestDayO
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;
@@ -63,7 +63,7 @@ The right algorithm is shown below:
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 its time to make it “right.”
@@ -75,7 +75,7 @@ Starting at line 1, we see a ream of comments with license information, copyrigh
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, its hard to keep things straight. For example, the nice positioning of line 71 and line 72 are lost when the Javadoc is generated, and yet who wants to see <ul> and <li> in the source code? A better strategy might be to just surround the whole comment with <pre> so that the formatting that is apparent in the source code is preserved within the Javadoc.1
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, its hard to keep things straight. For example, the nice positioning of line 71 and line 72 are lost when the Javadoc is generated, and yet who wants to see `<ul>` and `<li>` in the source code? A better strategy might be to just surround the whole comment with `<pre>` so that the formatting that is apparent in the source code is preserved within the Javadoc.1
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.
@@ -92,7 +92,7 @@ Unfortunately, there are already too many classes in the Java library named Date
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 its a bad idea [J2]. MonthConstants should really be an enum.
```java
public abstract class DayDate implements Comparable,
Serializable {
public static enum Month {
@@ -122,7 +122,7 @@ I understand why DayDate inherits from Comparable and Serializable. But why does
}
public final int index;
}
```
Changing MonthConstants to this enum forces quite a few changes to the DayDate class and all its 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 wont be readable anymore and will result in an InvalidClassException. If you dont 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, Id much rather debug an InvalidClassException than the odd behavior that would ensue if I forgot to change the serialVersionUID. So Im going to delete the variable—at least for the time being.2
@@ -132,10 +132,10 @@ Next, we have line 91, serialVersionUID. This variable is used to control the se
I find the comment on line 93 redundant. Redundant comments are just places to collect lies and misinformation [C2]. So Im 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
```
Its 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].
@@ -149,7 +149,7 @@ What we need to do is provide this information without polluting DayDate itself.
Its 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) {
@@ -186,7 +186,7 @@ Its generally a bad idea for base classes to know about their derivatives. To
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.
@@ -194,7 +194,7 @@ This factory class replaces the createInstance methods with makeDate methods, wh
5. Ibid.
The SpreadsheetDateFactory looks like this.
```java
public class SpreadsheetDateFactory extends DayDateFactory {
public DayDate _makeDate(int ordinal) {
return new SpreadsheetDate(ordinal);
@@ -225,7 +225,7 @@ The SpreadsheetDateFactory looks like this.
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]. Weve seen this pattern before, so I wont repeat it here. Youll see it in the final listings.
@@ -245,7 +245,7 @@ What settles the argument for me is that to be consistent [G11], we should make
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 162205). 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;
@@ -254,7 +254,7 @@ Next, we see three sets of constants that can be turned into enums (lines 162
this.index = index;
}
}
```
The second set of constants (lines 177187) 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 189205) 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.
@@ -278,7 +278,7 @@ I didnt care for the duplicate if statements [G5] inside the for loop (line 2
It occurred to me that this method does not really belong in DayDate. Its 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 272286) into the Day enumeration and called it toString.
```java
public enum Day {
MONDAY(Calendar.MONDAY),
TUESDAY(Calendar.TUESDAY),
@@ -324,27 +324,27 @@ I also moved the next function, weekdayCodeToString (lines 272286) into the D
return dateSymbols.getWeekdays()[index];
}
}
```
There are two getMonths functions (lines 288316). 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 326346) was made irrelevant by the Month enum, so I deleted it [G9].
The monthCodeToQuarter function (lines 356375) 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 377426). 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];
}
@@ -352,9 +352,9 @@ The next two methods are named monthCodeToString (lines 377426). Again, we se
public String toShortString() {
return dateFormatSymbols.getShortMonths()[index - 1];
}
```
The next method is stringToMonthCode (lines 428472). 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())
@@ -372,37 +372,37 @@ The next method is stringToMonthCode (lines 428472). I renamed it, moved it i
return s.equalsIgnoreCase(toString()) ||
s.equalsIgnoreCase(toShortString());
}
```
The isLeapYear method (lines 495517) 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 519536) doesnt really belong in DayDate. Nobody calls it except for two methods in SpreadsheetDate. So I pushed it down [G6].
The lastDayOfMonth function (lines 538560) 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 562576). 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 578602). 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;
@@ -413,40 +413,40 @@ The same goes for addMonths (lines 578602). It should be an instance method [
int resultDay = Math.min(getDayOfMonth(), lastDayOfResultMonth);
return DayDateFactory.makeDate(resultDay, resultMonth, resultYear);
}
```
The addYears function (lines 604626) 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 doesnt 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 628660) 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 9971008).
```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 662693).
```java
public DayDate getFollowingDayOfWeek(Day targetDayOfWeek) {
int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;
if (offsetToTarget <= 0)
@@ -454,9 +454,9 @@ The exact same analysis and result occurred for getFollowingDayOfWeek (lines 662
offsetToTarget += 7;
return plusDays(offsetToTarget);
}
```
The next function is getNearestDayOfWeek (lines 695726), which we corrected back on page 270. But the changes I made back then arent 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;
@@ -467,16 +467,16 @@ The next function is getNearestDayOfWeek (lines 695726), which we corrected b
else
return plusDays(offsetToFutureTarget);
}
```
The getEndOfCurrentMonth method (lines 728740) 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 742761) 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 411415, 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.…
@@ -496,13 +496,13 @@ If you look carefully (line 247, Listing B-5, page 382), youll see that the a
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 902913). 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 werent any tests for this method, so I wrote them.
@@ -510,7 +510,7 @@ The next method is compare (lines 902913). Again, this method is inappropriat
The next six functions (lines 915980) are all abstract methods that should be implemented in DayDate. So I pulled them all up from SpreadsheetDate.
The last function, isInRange (lines 982995) 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) {
@@ -541,7 +541,7 @@ The last function, isInRange (lines 982995) also needs to be pulled up and re
int right = Math.max(d1.getOrdinalDay(), d2.getOrdinalDay());
return interval.isIn(getOrdinalDay(), left, right);
}
```
That brings us to the end of DayDate. So now well 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].

View File

@@ -1,4 +1,4 @@
Smells and Heuristics
# 第 17 章 Smells and Heuristics
Image
Image
@@ -22,11 +22,11 @@ A comment that has gotten old, irrelevant, and incorrect is obsolete. Comments g
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
@@ -34,7 +34,7 @@ Another example is a Javadoc that says nothing more than (or even less than) the
*/
public SellResponse beginSellItem(SellRequest sellRequest)
throws ManagedComponentException
```
Comments should say things that the code cannot say for itself.
C4: Poorly Written Comment
@@ -50,11 +50,11 @@ When you see commented-out code, delete it! Dont worry, the source code contr
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.
@@ -82,9 +82,9 @@ Following “The Principle of Least Surprise,”2 any function or class should i
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.
@@ -128,7 +128,7 @@ For example, constants, variables, or utility functions that pertain only to the
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 dont 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;
@@ -137,13 +137,13 @@ Consider the following code:
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.
@@ -193,7 +193,7 @@ G14: Feature Envy
This is one of Martin Fowlers 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();
@@ -205,11 +205,11 @@ This is one of Martin Fowlers code smells.6 The methods of a class should be
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 ;
@@ -225,14 +225,14 @@ All else being equal, we want to eliminate Feature Envy because it exposes the i
employee.getTenthsWorked()%10);
}
}
```
Clearly, the reportHours method envies the HourlyEmployee class. On the other hand, we dont 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();
@@ -243,9 +243,9 @@ There is hardly anything more abominable than a dangling false argument at the e
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. Its 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();
}
@@ -260,19 +260,19 @@ You call this function with a true if overtime is paid as time and a half, and w
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 authors 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, its also virtually impenetrable. It is worth taking the time to make the intent of our code visible to our readers.
G17: Misplaced Responsibility
@@ -290,9 +290,9 @@ 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 wed 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 doesnt operate on any particular object and gets all its data from its arguments. However, there is a reasonable chance that well 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 youll want it to behave polymorphically.
@@ -305,7 +305,7 @@ Kent Beck wrote about this in his great book Smalltalk Best Practice Patterns8 a
9. [Beck07].
Consider this example from FitNesse:
```java
Matcher match = headerPattern.matcher(line);
if(match.find())
{
@@ -313,7 +313,7 @@ Consider this example from FitNesse:
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.
@@ -322,9 +322,9 @@ G20: Function Names Should Say What They Do
Image
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 cant 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.
@@ -351,7 +351,7 @@ For example, imagine that you are writing a function that prints a plain text re
Listing 17-1 HourlyReporter.java
```java
public class HourlyReporter {
private HourlyReportFormatter formatter;
private List<LineItem> page;
@@ -392,7 +392,7 @@ Listing 17-1 HourlyReporter.java
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.
@@ -423,27 +423,27 @@ This is probably one of the oldest rules in software development. I remember rea
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 dont 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 dont 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 dont 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.
@@ -462,27 +462,27 @@ 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()) {
@@ -491,9 +491,9 @@ For example:
}
}
}
```
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);
@@ -508,12 +508,12 @@ This bit of code does three things. It loops over all the employees, checks to s
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<Spline> splines;
@@ -525,9 +525,9 @@ Temporal couplings are often necessary, but you should not hide the coupling. St
}
}
```
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<Spline> splines;
@@ -539,7 +539,7 @@ The order of the three functions is important. You must saturate the gradient be
}
}
```
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 youd be right. But that extra syntactic complexity exposes the true temporal complexity of the situation.
@@ -548,7 +548,7 @@ Note that I left the instance variables in place. I presume that they are needed
G32: Dont 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 {
@@ -556,32 +556,32 @@ Have a reason for the way you structure your code, and make sure that reason is
}
```
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. Dont let them leak all over the code. We dont 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(<hr);
@@ -591,13 +591,13 @@ The statements within a function should all be written at the same level of abst
return html.toString();
}
```
A moments study and you can see whats 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);
@@ -611,13 +611,13 @@ I refactored this bit of code as follows. Note that I changed the name of the si
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 <hr> instead of <hr/>.) 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 its 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);
@@ -626,12 +626,12 @@ Separating levels of abstraction is one of the most important functions of refac
}
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);
@@ -646,11 +646,11 @@ If you have a constant such as a default or configuration value that is known an
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 dont 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
@@ -663,15 +663,15 @@ This is sometimes called the Law of Demeter. The Pragmatic Programmers call it
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. Youd 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 dont 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.
@@ -682,7 +682,7 @@ Wildcard imports can sometimes cause name conflicts and ambiguities. Two classes
J2: Dont 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;
@@ -696,24 +696,24 @@ I have seen this several times and it always makes me grimace. A programmer puts
}
}
```
Where did the constants TENTHS_PER_WEEK and OVERTIME_RATE come from? They might have come from class Employee; so lets 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! Dont 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 {
@@ -729,12 +729,12 @@ This is a hideous practice! The constants are hidden at the top of the inheritan
}
}
```
J3: Constants versus Enums
Now that enums have been added to the language (Java 5), use them! Dont 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.
Whats 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;
@@ -772,7 +772,7 @@ Whats more, study the syntax for enums carefully. They can have methods and f
public abstract double rate();
}
```
NAMES
N1: Choose Descriptive Names
Dont 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.
@@ -780,7 +780,7 @@ Dont be too quick to choose a name. Make sure the name is descriptive. Rememb
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 its just a hodge-podge of symbols and magic numbers.
```java
public int x() {
int q = 0;
int z = 0;
@@ -801,9 +801,9 @@ Consider the code below. What does it do? If I show you the code with well-chose
}
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;
@@ -821,18 +821,18 @@ Here is the code the way it should be written. This snippet is actually less com
}
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 Cunninghams quote on page 11.
```java
private boolean isStrike(int frame) {
return rolls[frame] == 10;
}
```
N2: Choose Names at the Appropriate Level of Abstraction
Dont 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();
@@ -840,9 +840,9 @@ Dont pick names that communicate implementation; choose names the reflect the
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 arent 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();
@@ -850,7 +850,7 @@ At first this looks fine. The functions all seem appropriate. Indeed, for many a
char recv();
String getConnectedLocator();
}
```
Now the names dont 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
@@ -864,7 +864,7 @@ Teams will often invent their own standard system of names for a particular proj
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)
@@ -875,7 +875,7 @@ Choose names that make the workings of a function or variable unambiguous. Consi
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 its only called from one place in the module, so its explanatory value outweighs the length.
@@ -884,13 +884,13 @@ 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<n; i++)
g.roll(pins);
}
```
This is perfectly clear and would be obfuscated if the variable i were replaced with something annoying like rollCount. On the other hand, variables and functions with short names lose their meaning over long distances. So the longer the scope of the name, the longer and more precise the name should be.
N6: Avoid Encodings
@@ -898,14 +898,14 @@ Names should not be encoded with type or scope information. Prefixes such as m_
N7: Names Should Describe Side-Effects
Names should describe everything that a function, variable, or class is or does. Dont hide side effects with a name. Dont use a simple verb to describe a function that does more than just that simple action. For example, consider this code from TestNG:
```java
public ObjectOutputStream getOos() throws IOException {
if (m_oos == null) {
m_oos = new ObjectOutputStream(m_socket.getOutputStream());
}
return m_oos;
}
```
This function does a bit more than get an “oos”; it creates the “oos” if it hasnt been created already. Thus, a better name might be createOrReturnOos.
TESTS

View File

@@ -1,4 +1,4 @@
Meaningful Names
# 第 2 章 Meaningful Names
by Tim Ottinger
Image
@@ -9,18 +9,18 @@ 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:
```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<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
@@ -28,7 +28,7 @@ Choosing names that reveal intent can make it much easier to understand and chan
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 arent even any fancy classes or polymorphic methods, just a list of arrays (or so it seems).
The problem isnt 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:
@@ -44,7 +44,7 @@ The problem isnt the simplicity of the code but the implicity of the code (to
The answers to these questions are not present in the code sample, but they could have been. Say that were working in a mine sweeper game. We find that the board is a list of cells called theList. Lets rename that to 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:
```java
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
@@ -52,11 +52,11 @@ Each cell on the board is represented by a simple array. We further find that th
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:
```java
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
@@ -64,7 +64,7 @@ We can go further and write a simple class for cells instead of using an array o
flaggedCells.add(cell);
return flaggedCells;
}
```
With these simple name changes, its not difficult to understand whats going on. This is the power of choosing good names.
AVOID DISINFORMATION
@@ -79,13 +79,13 @@ Beware of using names which vary in small ways. How long does it take to spot th
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.
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.
```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.
MAKE MEANINGFUL DISTINCTIONS
@@ -98,13 +98,13 @@ Programmers create problems for themselves when they write code solely to satisf
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 authors intention. Consider:
```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.
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.
@@ -116,11 +116,11 @@ Note that there is nothing wrong with using prefix conventions like a and the so
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 customers payment history?
There is an application we know of where this is illustrated. weve changed the names to protect the guilty, but heres 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.
@@ -131,23 +131,23 @@ Humans are good at words. A significant part of our brains is dedicated to the c
If you cant pronounce it, you cant 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.
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
```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 tomorrows date! How can that be?”
USE SEARCHABLE NAMES
@@ -158,13 +158,13 @@ One might easily grep for MAX_CLASSES_PER_STUDENT, but the number 7 could be mor
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.
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
```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;
@@ -173,7 +173,7 @@ to
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.
AVOID ENCODINGS
@@ -187,13 +187,13 @@ HN was considered to be pretty important back in the Windows C API, when everyth
In modern languages we have much richer type systems, and the compilers remember and enforce the types. Whats more, there is a trend toward smaller classes and shorter functions so that people can usually see the point of declaration of each variable theyre using.
Java programmers dont 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
PhoneNumber phoneString;
// name not changed when type changed!
```
Member Prefixes
You also dont need to prefix member variables with m_ anymore. Your classes and functions should be small enough that you dont need them. And you should be using an editing environment that highlights or colorizes members to make them distinct.
```java
public class Part {
private String m_dsc; // The textual description
void setName(String name) {
@@ -207,7 +207,7 @@ You also dont need to prefix member variables with m_ anymore. Your classes a
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.
Interfaces and Implementations
@@ -229,19 +229,19 @@ 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
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.
DONT BE CUTE
@@ -292,7 +292,7 @@ Consider the method in Listing 2-1. Do the variables need a more meaningful cont
Listing 2-1 Variables with unclear context.
```java
private void printGuessStatistics(char candidate, int count) { String number;
String verb;
String pluralModifier;
@@ -314,12 +314,12 @@ Listing 2-1 Variables with unclear context.
);
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.)
Listing 2-2 Variables have a context.
```java
public class GuessStatisticsMessage {
private String number;
private String verb;
@@ -360,7 +360,7 @@ Listing 2-2 Variables have a context.
pluralModifier = "s";
}
}
```
DONT 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?

View File

@@ -1,4 +1,4 @@
Functions
# 第 3 章 Functions
Image
Image
@@ -11,7 +11,7 @@ Consider the code in Listing 3-1. Its hard to find a long function in FitNess
Listing 3-1 HtmlUtil.java (FitNesse 20070619)
```java
public static String testableHtml(
PageData pageData,
boolean includeSuiteSetup
@@ -76,14 +76,14 @@ Listing 3-1 HtmlUtil.java (FitNesse 20070619)
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
```
Do you understand the function after three minutes of study? Probably not. Theres 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.
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.
Listing 3-2 HtmlUtil.java (refactored)
```java
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite
) throws Exception {
@@ -99,7 +99,7 @@ Listing 3-2 HtmlUtil.java (refactored)
return pageData.getHtml();
}
```
Unless you are a student of FitNesse, you probably dont 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 its pretty well obscured by Listing 3-1.
2. An open-source unit-testing tool for Java. www.junit.org
@@ -119,7 +119,7 @@ How short should your functions be? They should usually be shorter than Listing
Listing 3-3 HtmlUtil.java (re-refactored)
```java
public static String renderPageWith
SetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
@@ -127,7 +127,7 @@ Listing 3-3 HtmlUtil.java (re-refactored)
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.
@@ -198,7 +198,7 @@ Consider Listing 3-4. It shows just one of the operations that might depend on t
Listing 3-4 Payroll.java
```java
public Money calculatePay(Employee e)
throws InvalidEmployeeType {
switch (e.type) {
@@ -212,7 +212,7 @@ Listing 3-4 Payroll.java
throw new InvalidEmployeeType(e.type);
}
}
```
There are several problems with this function. First, its 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
7. a. http://en.wikipedia.org/wiki/Single_responsibility_principle
@@ -222,13 +222,13 @@ 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.
@@ -239,7 +239,7 @@ My general rule for switch statements is that they can be tolerated if they appe
Listing 3-5 Employee and Factory
```java
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
@@ -265,7 +265,7 @@ Listing 3-5 Employee and Factory
}
}
}
```
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 Wards 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.
@@ -322,27 +322,27 @@ On the other hand, here is a triad that is not quite so insidious: assertEquals(
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 its 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.
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.
```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);
```
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.”
@@ -355,7 +355,7 @@ Consider, for example, the seemingly innocuous function in Listing 3-6. This fun
Listing 3-6 UserValidator.java
```java
public class UserValidator {
private Cryptographer cryptographer;
@@ -373,53 +373,53 @@ Listing 3-6 UserValidator.java
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.
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.”
Output Arguments
Arguments are most naturally interpreted as inputs to a function. If you have been programming for more than a few years, Im sure youve 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 doesnt take long to look at the function signature and see:
```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. Its 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
```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.
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:
```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”? Its hard to infer the meaning from the call because its not clear whether the word “set” is a verb or an adjective.
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 doesnt 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.
```java
if (attributeExists(username)) {
setAttribute(username, unclebob);
}
```
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.
```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){
@@ -434,9 +434,9 @@ This does not suffer from verb/adjective confusion but does lead to deeply neste
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);
@@ -445,10 +445,10 @@ On the other hand, if you use exceptions instead of returned error codes, then t
catch (Exception e) {
logger.log(e.getMessage());
}
```
Extract Try/Catch Blocks
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.
```java
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
@@ -467,7 +467,7 @@ Try/catch blocks are ugly in their own right. They confuse the structure of the
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.
Error Handling Is One Thing
@@ -475,7 +475,7 @@ Functions should do one thing. Error handing is one thing. Thus, a function that
The Error.java Dependency Magnet
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,
@@ -485,7 +485,7 @@ Returning error codes usually implies that there is some class or enum in which
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 dont 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.
11. Those who felt that they could get away without recompiling and redeploying have been found—and dealt with.
@@ -533,7 +533,7 @@ This chapter has been about the mechanics of writing functions well. If you foll
SETUPTEARDOWNINCLUDER
Listing 3-7 SetupTeardownIncluder.java
```java
package fitnesse.html;
import fitnesse.responders.run.SuiteResponder;
@@ -643,4 +643,5 @@ Listing 3-7 SetupTeardownIncluder.java
.append(pagePathName)
.append("\n");
}
}
}
```

View File

@@ -1,4 +1,4 @@
Comments
# 第 4 章 Comments
Image
Image
@@ -17,7 +17,7 @@ So when you find yourself in a position where you need to write a comment, think
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 cant 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 dont always follow them—cant 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+
@@ -27,7 +27,7 @@ Code changes and evolves. Chunks of it move from here to there. Those chunks bif
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 its explanatory comment.
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.
@@ -43,15 +43,15 @@ Clear and expressive code with few comments is far superior to cluttered and com
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 its simply a matter of creating a function that says the same thing as the comment you want to write.
GOOD COMMENTS
@@ -61,31 +61,31 @@ 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.
```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.
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.
Heres a case thats 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.
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)
@@ -97,9 +97,9 @@ Sometimes a comment goes beyond just useful information about the implementation
}
return 1; // we are greater because we are the right type.
}
```
Heres an even better example. You might not agree with the programmers 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});
@@ -119,10 +119,10 @@ Heres an even better example. You might not agree with the programmers sol
}
assertEquals(false, failFlag.get());
}
```
Clarification
Sometimes it is just helpful to translate the meaning of some obscure argument or return value into something thats 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");
@@ -142,14 +142,14 @@ Sometimes it is just helpful to translate the meaning of some obscure argument o
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 its 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.
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:
Image
```java
// Don't run unless you
// have some time to kill.
public void _testWithReallyBigFile()
@@ -162,11 +162,11 @@ Image
assertSubString("Content-Length: 1000000000", responseString);
assertTrue(bytesSent > 1000000000);
}
```
Nowadays, of course, wed 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.
Heres another, more poignant example:
```java
public static
SimpleDateFormat makeStandardHttpDateFormat()
{
@@ -176,33 +176,33 @@ Heres another, more poignant example:
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.
TODO Comments
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 functions future should be.
```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 cant 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.
Nowadays, most good IDEs provide special gestures and features to locate all the TODO comments, so its not likely that they will get lost. Still, you dont want your code to be littered with TODOs. So scan through them regularly and eliminate the ones you can.
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()));
```
Javadocs in Public APIs
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.
@@ -215,7 +215,7 @@ 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:
```java
public void loadProperties()
{
try
@@ -231,7 +231,7 @@ Here, for example, is a case I found in FitNesse, where a comment might indeed h
// 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?
Our only recourse is to examine the code in other parts of the system to find out whats 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.
@@ -241,7 +241,7 @@ Listing 4-1 shows a simple function with a header comment that is completely red
Listing 4-1 waitForClose
```java
// Utility method that returns when this.closed
is true. Throws an exception
// if the timeout is reached.
@@ -255,14 +255,14 @@ Listing 4-1 waitForClose
throw new Exception("MockResponseSender could not be closed");
}
}
```
What purpose does this comment serve? Its 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 dont need to look under the hood.
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)
```java
public abstract class ContainerBase
implements Container, Lifecycle, Pipeline,
MBeanRegistration, Serializable {
@@ -355,7 +355,7 @@ Listing 4-2 ContainerBase.java (Tomcat)
* is associated.
*/
protected DirContext resources = null;
```
Misleading Comments
Sometimes, with all the best intentions, a programmer makes a statement in his comments that isnt precise enough to be accurate. Consider for another moment the badly redundant but also subtly misleading comment we saw in Listing 4-1.
@@ -370,7 +370,7 @@ For example, required javadocs for every function lead to abominations such as L
Listing 4-3
```java
/**
*
* @param title The title of the CD
@@ -387,10 +387,10 @@ Listing 4-3
cd.duration = duration;
cdList.add(cd);
}
```
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
@@ -410,25 +410,25 @@ Sometimes people add a comment to the start of a module every time they edit it.
* 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 didnt 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.
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 theres this paragon of redundancy:
```java
/**
* Returns the day of the month.
*
@@ -437,7 +437,7 @@ And then theres this paragon of redundancy:
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.
@@ -446,7 +446,7 @@ The first comment in Listing 4-4 seems appropriate.2 It explains why the catch b
Listing 4-4 startSending
```java
private void startSending()
{
try
@@ -470,12 +470,12 @@ Listing 4-4 startSending
}
}
}
```
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.
Listing 4-5 startSending (refactored)
```java
private void startSending()
{
try
@@ -503,12 +503,12 @@ Listing 4-5 startSending (refactored)
{
}
}
```
Replace the temptation to create noise with the determination to clean your code. Youll find it makes you a better and happier programmer.
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.
```java
/** The name. */
private String name;
@@ -520,29 +520,29 @@ Javadocs can also be noisy. What purpose do the following Javadocs (from a well-
/** The version. */
private String info;
```
Read these comments again more carefully. Do you see the cut-paste error? If authors arent paying attention when comments are written (or pasted), why should readers be expected to profit from them?
Dont 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 <mod> 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.
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 dont see banners very often. So use them very sparingly, and only when the benefit is significant. If you overuse banners, theyll fall into the background noise and be ignored.
@@ -552,7 +552,7 @@ Sometimes programmers will put special comments on closing braces, as in Listing
Listing 4-6 wc.java
```java
public class wc {
public static void main(String[] args) {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
@@ -576,7 +576,7 @@ Listing 4-6 wc.java
} //catch
} //main
}
```
Attributions and Bylines
/* Added by Rick */
@@ -586,17 +586,17 @@ Again, the source code control system is a better place for this kind of informa
Commented-Out Code
Few practices are as odious as commenting-out code. Dont 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 wont have the courage to delete it. Theyll 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:
```java
this.bytePos = writeBytes(pngIdBytes, 0);
//hdrPos = bytePos;
writeHeader();
@@ -611,14 +611,14 @@ Consider this from apache commons:
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 weve had good source code control systems for a very long time now. Those systems will remember the code for us. We dont have to comment it out any more. Just delete the code. We wont lose it. Promise.
HTML Comments
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.
```java
/**
* Task to run fit tests.
* This task runs fitnesse tests and publishes the results.
@@ -640,10 +640,10 @@ HTML in source code comments is an abomination, as you can tell by reading the c
* classpathref=&quot;classpath&quot; /&gt;
* </pre>
*/
```
Nonlocal Information
If you must write a comment, then make sure it describes the code it appears near. Dont 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.
```java
/**
* Port on which fitnesse would run. Defaults to 8082.
*
@@ -653,10 +653,10 @@ If you must write a comment, then make sure it describes the code it appears nea
{
this.fitnessePort = fitnessePort;
}
```
Too Much Information
Dont 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.
```java
/*
RFC 2045 - Multipurpose Internet Mail Extensions (MIME)
Part One: Format of Internet Message Bodies
@@ -672,18 +672,18 @@ Dont put interesting historical discussions or irrelevant descriptions of det
the first 8-bit byte, and the eighth bit will be the low-order bit in
the first 8-bit byte, and so on.
*/
```
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 youd 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:
```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.
Function Headers
@@ -699,7 +699,7 @@ What I find fascinating about this module is that there was a time when many of
Listing 4-7 GeneratePrimes.java
```java
/**
* This class Generates prime numbers up to a user specified
* maximum. The algorithm used is the Sieve of Eratosthenes.
@@ -774,12 +774,12 @@ Listing 4-7 GeneratePrimes.java
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.
Listing 4-8 PrimeGenerator.java (refactored)
```java
/**
* This class Generates prime numbers up to a user specified
* maximum. The algorithm used is the Sieve of Eratosthenes.
@@ -863,7 +863,7 @@ Listing 4-8 PrimeGenerator.java (refactored)
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 Im inclined to leave it.
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 Im saving?

View File

@@ -1,4 +1,4 @@
Formatting
# 第 5 章 Formatting
Image
Image
@@ -46,7 +46,7 @@ Consider, for example, Listing 5-1. There are blank lines that separate the pack
Listing 5-1 BoldWidget.java
```java
package fitnesse.wikitext.widgets;
import java.util.regex.*;
@@ -71,12 +71,12 @@ Listing 5-1 BoldWidget.java
}
}
```
Taking those blank lines out, as in Listing 5-2, has a remarkably obscuring effect on the readability of the code.
Listing 5-2 BoldWidget.java
```java
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
@@ -94,7 +94,7 @@ Listing 5-2 BoldWidget.java
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.
Vertical Density
@@ -102,7 +102,7 @@ If openness separates concepts, then vertical density implies close association.
Listing 5-3
```java
public class ReporterConfig {
/**
@@ -118,12 +118,12 @@ Listing 5-3
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.
Listing 5-4
```java
public class ReporterConfig {
private String m_className;
private List<Property> m_properties = new ArrayList<Property>();
@@ -131,7 +131,7 @@ Listing 5-4
public void addProperty(Property property) {
m_properties.add(property);
}
```
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 rats 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.
@@ -140,7 +140,7 @@ Concepts that are closely related should be kept vertically close to each other
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.
```java
private static void readPreferences() {
InputStream is= null;
try {
@@ -155,9 +155,9 @@ Variable Declarations. Variables should be declared as close to their usage as p
}
}
}
```
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)
@@ -166,7 +166,7 @@ Control variables for loops should usually be declared within the loop statement
}
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.
```java
for (XmlTest test : m_suite.getTests()) {
TestRunner tr = m_runnerFactory.newTestRunner(this, test);
@@ -184,13 +184,13 @@ In rare cases a variable might be declared at the top of a block or just before
}
}
```
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.
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).
```java
public class TestSuite implements Test {
static public Test createTest(Class<? extends TestCase> theClass,
String name) {
@@ -227,12 +227,12 @@ Consider, for example, the strange case of the TestSuite class in JUnit 4.3.1. I
}
}
```
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.
Listing 5-5 WikiPageResponder.java
```java
public class WikiPageResponder implements SecureResponder {
protected WikiPage page;
protected PageData pageData;
@@ -285,7 +285,7 @@ Listing 5-5 WikiPageResponder.java
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.
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.
@@ -293,7 +293,7 @@ Conceptual Affinity. Certain bits of code want to be near other bits. They have
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:
Image
```java
public class Assert {
static public void assertTrue(String message, boolean condition) {
if (!condition)
@@ -312,7 +312,7 @@ Image
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 didnt, they would still want to be close together.
Vertical Ordering
@@ -336,7 +336,7 @@ I used to follow the rule that you should never have to scroll to the right. But
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();
@@ -344,13 +344,13 @@ We use horizontal white space to associate things that are strongly related and
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 didnt 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);
@@ -366,7 +366,7 @@ Another use for white space is to accentuate the precedence of operators.
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.
@@ -375,7 +375,7 @@ 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. Who am I kidding? I still am an assembly language programmer. You can take the boy away from the metal, but you cant take the metal out of the boy!
```java
public class FitNesseExpediter implements ResponseSender
{
private Socket socket;
@@ -398,11 +398,11 @@ When I was an assembly language programmer,3 I used horizontal alignment to acce
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 dont 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.
```java
public class FitNesseExpediter implements ResponseSender
{
private Socket socket;
@@ -425,7 +425,7 @@ So, in the end, I dont do this kind of thing anymore. Nowadays I prefer unali
output = s.getOutputStream();
requestParsingTimeLimit = 10000;
}
```
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.
@@ -434,7 +434,7 @@ To make this hierarchy of scopes visible, we indent the lines of source code in
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.
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
@@ -469,11 +469,11 @@ Consider the following programs that are syntactically and semantically identica
}
}
}
```
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:
```java
public class CommentWidget extends TextWidget
{
public static final String REGEXP = ^#[^\r\n]*(?:(?:\r\n)|\n|\r)?;
@@ -481,9 +481,9 @@ Breaking Indentation. It is sometimes tempting to break the indentation rule for
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)?
@@ -495,12 +495,12 @@ I prefer to expand and indent the scopes instead, like this:
return “”;
}
}
```
Dummy Scopes
Sometimes the body of a while or for statement is a dummy, as shown below. I dont like these kinds of structures and try to avoid them. When I cant avoid them, I make sure that the dummy body is properly indented and surrounded by braces. I cant tell you how many times Ive 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 its own line, its just too hard to see.
```java
while (dis.read(buf, 0, readBufferSize) != -1) ;
```
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.
@@ -517,7 +517,7 @@ The rules I use personally are very simple and are illustrated by the code in Li
Listing 5-6 CodeAnalyzer.java
```java
public int getWidestLineNumber() {
return widestLineNumber;
}
@@ -551,4 +551,5 @@ Listing 5-6 CodeAnalyzer.java
Arrays.sort(sortedWidths);
return sortedWidths;
}
}
}
```

View File

@@ -1,4 +1,4 @@
Objects and Data Structures
# 第 6 章 Objects and Data Structures
Image
Image
@@ -10,15 +10,15 @@ Consider the difference between Listing 6-1 and Listing 6-2. Both represent the
Listing 6-1 Concrete Point
```java
public class Point {
public double x;
public double y;
}
```
Listing 6-2 Abstract Point
```java
public interface Point {
double getX();
double getY();
@@ -27,7 +27,7 @@ Listing 6-2 Abstract Point
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.
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.
@@ -40,19 +40,19 @@ Consider Listing 6-3 and Listing 6-4. The first uses concrete terms to communica
Listing 6-3 Concrete Vehicle
```java
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}
```
Listing 6-4 Abstract Vehicle
```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.
DATA/OBJECT ANTI-SYMMETRY
@@ -62,7 +62,7 @@ Consider, for example, the procedural shape example in Listing 6-5. The Geometry
Listing 6-5 Procedural Shape
```java
public class Square {
public Point topLeft;
public double side;
@@ -100,7 +100,7 @@ Listing 6-5 Procedural Shape
throw new NoSuchShapeException();
}
}
```
Object-oriented programmers might wrinkle their noses at this and complain that it is procedural—and theyd 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.
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
@@ -109,7 +109,7 @@ Now consider the object-oriented solution in Listing 6-6. Here the area() method
Listing 6-6 Polymorphic Shapes
```java
public class Square implements Shape {
private Point topLeft;
private double side;
@@ -138,7 +138,7 @@ Listing 6-6 Polymorphic Shapes
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.
@@ -173,16 +173,16 @@ The method should not invoke methods on objects that are returned by any of the
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. Found somewhere in the apache framework.
```java
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
```
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:
```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. Thats a lot of knowledge for one function to know. The calling function knows how to navigate through a lot of different objects.
Image
@@ -190,9 +190,9 @@ Image
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.
The use of accessor functions confuses the issue. If the code had been written as follows, then we probably wouldnt 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.
Hybrids
@@ -204,27 +204,27 @@ Such hybrids make it hard to add new functions but also make it hard to add new
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?
```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.
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:
```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.
So, what if we told the ctxt object to do this?
```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 shouldnt know about.
DATA TRANSFER OBJECTS
@@ -234,7 +234,7 @@ Somewhat more common is the “bean” form shown in Listing 6-7. Beans have pri
Listing 6-7 address.java
```java
public class Address {
private String street;
private String streetExtra;
@@ -271,7 +271,7 @@ Listing 6-7 address.java
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.

View File

@@ -1,4 +1,4 @@
Error Handling
# 第 7 章 Error Handling
Image
Image
@@ -18,7 +18,7 @@ Back in the distant past there were many languages that didnt have exceptions
Listing 7-1 DeviceController.java
```java
public class DeviceController {
public void sendShutDown() {
@@ -41,14 +41,14 @@ Listing 7-1 DeviceController.java
}
}
```
The problem with these approaches is that they clutter the caller. The caller must check for errors immediately after the call. Unfortunately, its 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 weve chosen to throw exceptions in methods that can detect errors.
Listing 7-2 DeviceController.java (with exceptions)
```java
public class DeviceController {
@@ -77,7 +77,7 @@ Listing 7-2 DeviceController.java (with exceptions)
}
```
Notice how much cleaner it is. This isnt 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.
WRITE YOUR TRY-CATCH-FINALLY STATEMENT FIRST
@@ -88,21 +88,21 @@ In a way, try blocks are like transactions. Your catch has to leave your program
Lets 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 well get an exception when the file doesnt exist:
```java
@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
sectionStore.retrieveSection(invalid - file);
}
```
The test drives us to create this stub:
```java
public List<RecordedGrip> retrieveSection(String sectionName) {
// dummy return until we have a real implementation
return new ArrayList<RecordedGrip>();
}
```
Our test fails because it doesnt 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<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName)
@@ -111,9 +111,9 @@ Our test fails because it doesnt throw an exception. Next, we change our impl
}
return new ArrayList<RecordedGrip>();
}
```
Our test passes now because weve 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:
```java
public List<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName);
@@ -123,7 +123,7 @@ Our test passes now because weve caught the exception. At this point, we can
}
return new ArrayList<RecordedGrip>();
}
```
Now that weve 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 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.
@@ -150,7 +150,7 @@ DEFINE EXCEPTION CLASSES IN TERMS OF A CALLERS 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.
Lets 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:
```java
ACMEPort port = new ACMEPort(12);
try {
@@ -167,11 +167,11 @@ Lets look at an example of poor exception classification. Here is a try-catch
} finally {
}
```
That statement contains a lot of duplication, and we shouldnt 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:
```java
LocalPort port = new LocalPort(12);
try {
port.open();
@@ -181,9 +181,9 @@ In this case, because we know that the work that we are doing is roughly the sam
} finally {
}
```
Our LocalPort class is just a simple wrapper that catches and translates exceptions thrown by the ACMEPort class:
```java
public class LocalPort {
private ACMEPort innerPort;
@@ -204,7 +204,7 @@ Our LocalPort class is just a simple wrapper that catches and translates excepti
}
}
```
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.
One final advantage of wrapping is that you arent tied to a particular vendors 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.
@@ -217,32 +217,32 @@ If you follow the advice in the preceding sections, youll end up with a good
Image
Lets 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 arent, the employee gets a meal per diem amount for that day. The exception clutters the logic. Wouldnt it be better if we didnt have to deal with the special case? If we didnt, 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:
```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 doesnt have to deal with exceptional behavior. That behavior is encapsulated in the special case object.
DONT RETURN 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 cant begin to count the number of applications Ive seen in which nearly every other line was a check for null. Here is some example code:
```java
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = peristentStore.getItemRegistry();
@@ -254,7 +254,7 @@ I think that any discussion about error handling should include mention of the t
}
}
}
```
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.
Did you notice the fact that there wasnt 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 its bad. What exactly should you do in response to a NullPointerException thrown from the depths of your application?
@@ -262,35 +262,35 @@ Did you notice the fact that there wasnt a null check in the second line of t
Its 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.
In many cases, special case objects are an easy remedy. Imagine that you have code like this:
```java
List<Employee> 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:
```java
List<Employee> 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
public List<Employee> 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.
DONT PASS 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.
Lets 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) {
@@ -298,15 +298,15 @@ Lets look at an example to see why. Here is a simple method which calculates
}
}
```
What happens when someone passes null as an argument?
```java
calculator.xProjection(null, new Point(12, 13));
Well get a NullPointerException, of course.
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) {
@@ -317,11 +317,11 @@ How can we fix it? We could create a new exception type and throw it:
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?
There is another alternative. We could use a set of assertions:
```java
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
@@ -330,7 +330,7 @@ There is another alternative. We could use a set of assertions:
return (p2.x p1.x) * 1.5;
}
}
```
Its good documentation, but it doesnt solve the problem. If someone passes null, well still have a runtime error.
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.

View File

@@ -1,4 +1,4 @@
Boundaries
# 第 8 章 Boundaries
Image
by James Grenning
@@ -18,25 +18,25 @@ Figure 8-1 The methods of Map
Image
If our application needs a Map of Sensors, you might find the sensors set up like this:
```java
Map sensors = new HashMap();
```
Then, when some other part of the code needs to access the sensor, you see this code:
```java
Sensor s = (Sensor)sensors.get(sensorId );
```
We dont 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 its 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:
```java
Map<Sensor> sensors = new HashMap<Sensor>();
Sensor s = sensors.get(sensorId );
```
However, this doesnt solve the problem that `Map<Sensor>` provides more capability than we need or want.
However, this doesnt solve the problem that Map<Sensor> provides more capability than we need or want.
Passing an instance of Map<Sensor> 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, weve 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.
Passing an instance of `Map<Sensor>` 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, weve 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.
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.
```java
public class Sensors {
private Map sensors = new HashMap();
@@ -46,7 +46,7 @@ A cleaner way to use Map might look like the following. No user of Sensors would
//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.
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.
@@ -66,15 +66,15 @@ In learning tests we call the third-party API, as we expect to use it in our app
LEARNING LOG4J
Lets 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.
```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.
```java
@Test
public void testLogAddAppender() {
Logger logger = Logger.getLogger(MyLogger);
@@ -82,9 +82,9 @@ When we run it, the logger produces an error that tells us we need something cal
logger.addAppender(appender);
logger.info(hello);
}
```
This time we find that the Appender has no output stream. Odd—it seems logical that itd have one. After a little help from Google, we try the following:
```java
@Test
public void testLogAddAppender() {
Logger logger = Logger.getLogger(MyLogger);
@@ -94,7 +94,7 @@ This time we find that the Appender has no output stream. Odd—it seems logical
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.
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.
@@ -105,7 +105,7 @@ A bit more googling, reading, and testing, and we eventually wind up with Listin
Listing 8-1 LogTest.java
```java
public class LogTest {
private Logger logger;
@@ -134,7 +134,7 @@ Listing 8-1 LogTest.java
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.
LEARNING TESTS ARE BETTER THAN FREE

View File

@@ -1,4 +1,4 @@
Unit Tests
# 第 9 章 Unit Tests
Image
Image
@@ -8,9 +8,9 @@ Image
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.
@@ -75,7 +75,7 @@ Consider the code from FitNesse in Listing 9-1. These three tests are difficult
Listing 9-1 SerializedPageResponderTest.java
```java
public void testGetPageHieratchyAsXml() throws Exception
{
@@ -140,7 +140,7 @@ Listing 9-1 SerializedPageResponderTest.java
assertSubString(test page, xml);
assertSubString(<Test, xml);
}
```
For example, look at the PathParser calls. They transform strings into PagePath instances used by the crawlers. This transformation is completely irrelevant to the test at hand and serves only to obfuscate the intent. The details surrounding the creation of the responder and the gathering and casting of the response are also just noise. Then theres the ham-handed way that the request URL is built from a resource and an argument. (I helped write this code, so I feel free to roundly criticize it.)
In the end, this code was not designed to be read. The poor reader is inundated with a swarm of details that must be understood before the tests make any real sense.
@@ -149,7 +149,7 @@ Now consider the improved tests in Listing 9-2. These tests do the exact same th
Listing 9-2 SerializedPageResponderTest.java (refactored)
```java
public void testGetPageHierarchyAsXml() throws Exception {
makePages(PageOne, PageOne.ChildOne, PageTwo);
@@ -185,7 +185,7 @@ Listing 9-2 SerializedPageResponderTest.java (refactored)
assertResponseIsXML();
assertResponseContains(test page, <Test);
}
```
The BUILD-OPERATE-CHECK2 pattern is made obvious by the structure of these tests. Each of the tests is clearly split into three parts. The first part builds up the test data, the second part operates on that test data, and the third part checks that the operation yielded the expected results.
2. http://fitnesse.org/FitNesse.AcceptanceTestPatterns
@@ -204,7 +204,7 @@ Consider the test in Listing 9-3. I wrote this test as part of an environment co
Listing 9-3 EnvironmentControllerTest.java
```java
@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
hw.setTemp(WAY_TOO_COLD);
@@ -215,7 +215,7 @@ Listing 9-3 EnvironmentControllerTest.java
assertFalse(hw.hiTempAlarm());
assertTrue(hw.loTempAlarm());
}
```
There are, of course, lots of details here. For example, what is that tic function all about? In fact, Id rather you not worry about that while reading this test. Id rather you just worry about whether you agree that the end state of the system is consistent with the temperature being “way too cold.”
Notice, as you read the test, that your eye needs to bounce back and forth between the name of the state being checked, and the sense of the state being checked. You see heaterState, and then your eyes glissade left to assertTrue. You see coolerState and your eyes must track left to assertFalse. This is tedious and unreliable. It makes the test hard to read.
@@ -224,13 +224,13 @@ I improved the reading of this test greatly by transforming it into Listing 9-4.
Listing 9-4 EnvironmentControllerTest.java (refactored)
```java
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals(HBchL, hw.getState());
}
```
Of course I hid the detail of the tic function by creating a wayTooCold function. But the thing to note is the strange string in the assertEquals. Upper case means “on,” lower case means “off,” and the letters are always in the following order: {heater, blower, cooler, hi-temp-alarm, lo-temp-alarm}.
Even though this is close to a violation of the rule about mental mapping,3 it seems appropriate in this case. Notice, once you know the meaning, your eyes glide across that string and you can quickly interpret the results. Reading the test becomes almost a pleasure. Just take a look at Listing 9-5 and see how easy it is to understand these tests.
@@ -239,7 +239,7 @@ Even though this is close to a violation of the rule about mental mapping,3 it s
Listing 9-5 EnvironmentControllerTest.java (bigger selection)
```java
@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
tooHot();
@@ -262,12 +262,12 @@ Listing 9-5 EnvironmentControllerTest.java (bigger selection)
wayTooCold();
assertEquals(HBchL, hw.getState());
}
```
The getState function is shown in Listing 9-6. Notice that this is not very efficient code. To make it efficient, I probably should have used a StringBuffer.
Listing 9-6 MockControlHardware.java
```java
public String getState() {
String state = ””;
state += heater ? H : h;
@@ -277,7 +277,7 @@ Listing 9-6 MockControlHardware.java
state += loTempAlarm ? L : l;
return state;
}
```
StringBuffers are a bit ugly. Even in production code I will avoid them if the cost is small; and you could argue that the cost of the code in Listing 9-6 is very small. However, this application is clearly an embedded real-time system, and it is likely that computer and memory resources are very constrained. The test environment, however, is not likely to be constrained at all.
That is the nature of the dual standard. There are things that you might never do in a production environment that are perfectly fine in a test environment. Usually they involve issues of memory or CPU efficiency. But they never involve issues of cleanliness.
@@ -291,7 +291,7 @@ But what about Listing 9-2? It seems unreasonable that we could somehow easily m
Listing 9-7 SerializedPageResponderTest.java (Single Assert)
```java
public void testGetPageHierarchyAsXml() throws Exception {
givenPages(PageOne, PageOne.ChildOne, PageTwo);
@@ -308,7 +308,7 @@ Listing 9-7 SerializedPageResponderTest.java (Single Assert)
<name>PageOne</name>, <name>PageTwo</name>, <name>ChildOne</name>
);
}
```
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].
@@ -326,7 +326,7 @@ Perhaps a better rule is that we want to test a single concept in each test func
Listing 9-8
```java
/**
* Miscellaneous tests for the addMonths() method.
*/
@@ -349,7 +349,7 @@ Listing 9-8
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):