mirror of
https://github.com/glen9527/Clean-Code-zh.git
synced 2025-12-18 03:04:21 +08:00
标题 & 代码高亮
This commit is contained in:
248
docs/ch14.md
248
docs/ch14.md
@@ -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. I’ll show a few of them to you now (Listing 14-3 through Listing 14-6).
|
||||
|
||||
|
||||
Listing 14-3 ArgumentMarshaler.java
|
||||
|
||||
```java
|
||||
public interface ArgumentMarshaler {
|
||||
void set(Iterator<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. I’ll leave them to you as an exercise.
|
||||
|
||||
One other bit of information might be troubling you: the definition of the error code constants. They are in the ArgsException class (Listing 14-7).
|
||||
|
||||
|
||||
Listing 14-6 IntegerArgumentMarshaler.java
|
||||
|
||||
```java
|
||||
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
|
||||
|
||||
public class IntegerArgumentMarshaler implements ArgumentMarshaler {
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
```
|
||||
It’s remarkable how much code is required to flesh out the details of this simple concept. One of the reasons for this is that we are using a particularly wordy language. Java, being a statically typed language, requires a lot of words in order to satisfy the type system. In a language like Ruby, Python, or Smalltalk, this program is much smaller.1
|
||||
|
||||
1. I recently rewrote this module in Ruby. It was 1/7th the size and had a subtly better structure.
|
||||
@@ -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 “I’m certainly glad he didn’t leave it like that!” If you feel like this, then remember that’s how other people are going to feel about code that you leave in rough-draft form.
|
||||
|
||||
Actually “rough draft” is probably the kindest thing you can say about this code. It’s clearly a work in progress. The sheer number of instance variables is daunting. The odd strings like “TILT,” the HashSets and TreeSets, and the try-catch-catch blocks all add up to a festering pile.
|
||||
@@ -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, it’s really not that bad. It’s compact and simple and easy to understand. However, within this code it is easy to see the seeds of the later festering pile. It’s quite clear how this grew into the latter mess.
|
||||
|
||||
Notice that the latter mess has only two more argument types than this: String and integer. The addition of just two more argument types had a massively negative impact on the code. It converted it from something that would have been reasonably maintainable into something that I would expect to become riddled with bugs and warts.
|
||||
@@ -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. It’s still not horrible, but the mess is certainly starting to grow. It’s a pile, but it’s not festering quite yet. It took the addition of the integer argument type to get this pile really fermenting and festering.
|
||||
|
||||
So I Stopped
|
||||
@@ -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 wasn’t going to break anything. So then I made the simplest modification I could, one that would break as little as possible. I changed the HashMap for the Boolean arguments to take an ArgumentMarshaler.
|
||||
|
||||
```java
|
||||
private Map<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, you’ll see that if you call it with 'y,' but there is no y argument, then booleanArgs.get('y') will return null, and the function will throw a NullPointerException. The falseIfNull function had been used to protect against this, but the change I made caused that function to become irrelevant.
|
||||
|
||||
Incrementalism demanded that I get this working quickly before making any other changes. Indeed, the fix was not too difficult. I just had to move the check for null. It was no longer the boolean being null that I needed to check; it was the ArgumentMarshaller.
|
||||
|
||||
First, I removed the falseIfNull call in the getBoolean function. It was useless now, so I also eliminated the function itself. The tests still failed in the same way, so I was confident that I hadn’t introduced any new errors.
|
||||
|
||||
```java
|
||||
public boolean getBoolean(char arg) {
|
||||
return booleanArgs.get(arg).getBoolean();
|
||||
}
|
||||
|
||||
```
|
||||
Next, I split the function into two lines and put the ArgumentMarshaller into its own variable named argumentMarshaller. I didn’t care for the long variable name; it was badly redundant and cluttered up the function. So I shortened it to am [N5].
|
||||
|
||||
```java
|
||||
public boolean getBoolean(char arg) {
|
||||
Args.ArgumentMarshaler am = booleanArgs.get(arg);
|
||||
return am.getBoolean();
|
||||
}
|
||||
|
||||
```
|
||||
And then I put in the null detection logic.
|
||||
|
||||
```java
|
||||
public boolean getBoolean(char arg) {
|
||||
Args.ArgumentMarshaler am = booleanArgs.get(arg);
|
||||
return am != null && am.getBoolean();
|
||||
}
|
||||
|
||||
```
|
||||
STRING ARGUMENTS
|
||||
Addin_g String arguments was very similar to adding boolean arguments. I had to change the HashMap and get the parse, set, and get functions working. There shouldn’t be any surprises in what follows except, perhaps, that I seem to be putting all the marshalling implementation in the ArgumentMarshaller base class instead of distributing it to the derivatives.
|
||||
|
||||
```java
|
||||
private Map<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, I’m going to start pushing that behavior down into the derivatives. This will allow me to keep everything running while I gradually change the shape of this program.
|
||||
|
||||
The obvious next step was to move the int argument functionality into the ArgumentMarshaler. Again, there weren’t any surprises.
|
||||
|
||||
```java
|
||||
private Map<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 couldn’t get rid of them just by deleting them because that would break the system. Instead, I added a new Map for the ArgumentMarshaler and then one by one changed the methods to use it instead of the three original maps.
|
||||
|
||||
```java
|
||||
public class Args {
|
||||
…
|
||||
private Map<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 didn’t 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 didn’t do much anymore:
|
||||
“Argument: %c has invalid format: %s.”, elementId, elementTail), 0);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
Okay, so now let’s look at the whole picture again. Listing 14-12 shows the current form of the Args class.
|
||||
|
||||
|
||||
Listing 14-12 Args.java (After first refactoring)
|
||||
|
||||
```java
|
||||
package com.objectmentor.utilities.getopts;
|
||||
|
||||
|
||||
@@ -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; there’s still a horrible type-case in setArgument; and all those set functions are really ugly. Not to mention all the error processing. We still have a lot of work ahead of us.
|
||||
|
||||
I’d really like to get rid of that type-case up in setArgument [G23]. What I’d like in setArgument is a single call to ArgumentMarshaler.set. This means I need to push setIntArg, setStringArg, and setBooleanArg down into the appropriate ArgumentMarshaler derivatives. But there is a problem.
|
||||
|
||||
If you look closely at setIntArg, you’ll notice that it uses two instance variables: args and currentArg. To move setIntArg down into BooleanArgumentMarshaler, I’ll have to pass both args and currentArgs as function arguments. That’s dirty [F1]. I’d rather pass one argument instead of two. Fortunately, there is a simple solution. We can convert the args array into a list and pass an Iterator down to the set functions. The following took me ten steps, passing all the tests after each. But I’ll just show you the result. You should be able to figure out what most of the tiny little steps were.
|
||||
|
||||
```java
|
||||
public class Args {
|
||||
private String schema;
|
||||
private String[] args;
|
||||
@@ -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 we’ll prepare that one first. Our goal is to change the setBooleanArg function to simply forward to the BooleanArgumentMarshaler.
|
||||
|
||||
```java
|
||||
private boolean setArgument(char argChar) throws ArgsException {
|
||||
ArgumentMarshaler m = marshalers.get(argChar);
|
||||
if (m == null)
|
||||
@@ -2050,22 +2050,22 @@ Now we can start to move the set functions. The setBooleanArg function is trivia
|
||||
catch (ArgsException e) {
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
Didn’t we just put that exception processing in? Putting things in so you can take them out again is pretty common in refactoring. The smallness of the steps and the need to keep the tests running means that you move things around a lot. Refactoring is a lot like solving a Rubik’s cube. There are lots of little steps required to achieve a large goal. Each step enables the next.
|
||||
|
||||
Why did we pass that iterator when setBooleanArg certainly doesn’t need it? Because setIntArg and setStringArg will! And because I want to deploy all three of these functions through an abstract method in ArgumentMarshaller, I need to pass it to setBooleanArg.
|
||||
|
||||
So now setBooleanArg is useless. If there were a set function in ArgumentMarshaler, we could call it directly. So it’s time to make that function! The first step is to add the new abstract method to ArgumentMarshaler.
|
||||
|
||||
```java
|
||||
private abstract class ArgumentMarshaler {
|
||||
public abstract void set(Iterator<String> currentArgument)
|
||||
throws ArgsException;
|
||||
public abstract void set(String s) throws ArgsException;
|
||||
public abstract Object get();
|
||||
}
|
||||
|
||||
```
|
||||
Of course this breaks all the derivatives. So let’s implement the new method in each.
|
||||
|
||||
```java
|
||||
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
|
||||
private boolean booleanValue = false;
|
||||
|
||||
@@ -2109,9 +2109,9 @@ Of course this breaks all the derivatives. So let’s 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 let’s see how easy it is to add a new argument type to our structure. It should require very few changes, and those changes should be isolated. First, we begin by adding a new test case to check that the double argument works correctly.
|
||||
|
||||
```java
|
||||
public void testSimpleDoublePresent() throws Exception {
|
||||
Args args = new Args(”x##”, new String[] {”-x”,”42.3”});
|
||||
assertTrue(args.isValid());
|
||||
@@ -2261,9 +2261,9 @@ So now let’s 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 let’s make sure all the error processing works correctly. The next test case checks that an error is declared if an unparseable string is fed to a ## argument.
|
||||
|
||||
```java
|
||||
public void testInvalidDouble() throws Exception {
|
||||
Args args = new Args(”x##”, new String[] {”-x”,”Forty two”});
|
||||
assertFalse(args.isValid());
|
||||
@@ -2359,9 +2359,9 @@ And all the tests pass! That was pretty painless. So now let’s 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 doesn’t really belong in the Args class. We are also throwing out ParseException, which doesn’t really belong to us. So let’s merge all the exceptions into a single ArgsException class and move it into its own module.
|
||||
|
||||
```java
|
||||
public class ArgsException extends Exception {
|
||||
private char errorArgumentId = ’\0’;
|
||||
private String errorParameter = ”TILT”;
|
||||
@@ -2505,14 +2505,14 @@ The exception code is pretty ugly and doesn’t 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.
|
||||
|
||||
Reference in New Issue
Block a user