12.07.2015 Views

Clean Code Args

Clean Code Args

Clean Code Args

SHOW MORE
SHOW LESS
  • No tags were found...

You also want an ePaper? Increase the reach of your titles

YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.

2! Have you ever been significantly impeded bybad code?


! Have you ever been significantly impeded bybad code?! Why did you write it?3


What do we do about bad code?! The Grand Redesign in the sky?! Incremental Improvement5


The Grand Redesign in the Sky! The Design Degrades! The Developers Revolt! Management Concedes! A TIGER TEAM is selected! A long, long race.6


Incremental Improvement! Always check code in better than youchecked it out.! Never let the sun set on bad code.! Test First!! Provides the necessary flexibility7


I won’t apologize for showing code.! <strong>Code</strong> is our medium.! We should revel in reading it.8


Too many people apologize for code.! They think it’s something to be gotten rid of.! MDA, etc.9


You can’t get rid of code.! Because code is detail.! And you can’t get rid of detail.! You can change it’s form.10


There are better languages! To be sure!! Ruby, Smalltalk, prolog.! These are all very dense languages.11


Even a pictorial language! Will have to capture details.! And so it will still be code.12


Face it.! <strong>Code</strong> is here to stay.! So let’s look at some.13


<strong>Clean</strong> <strong>Code</strong>: <strong>Args</strong>! An exploration of a mess that got cleaned.! It’s a comand line argument parser.! History:! The Dave Astels Challenge.14


Main: An example of how it’s used.public static void main(String[] args) {try {<strong>Args</strong> arg = new <strong>Args</strong>("l,p#,d*", args);boolean logging = arg.getBoolean('l');int port = arg.getInt('p');String directory = arg.getString('d');executeApplication(logging, port, directory);} catch (<strong>Args</strong>Exception e) {System.out.printf("Argument error: %s\n", e.errorMessage());}}>java MyProgram -l -p80 -d/home/logs15


Here’s what <strong>Args</strong> looked like! For just boolean arguments.16


<strong>Args</strong>public class <strong>Args</strong> {private String schema;private String[] args;private boolean valid;private Set unexpectedArguments =new TreeSet();private Map boolean<strong>Args</strong> =new HashMap();private int numberOfArguments = 0;public <strong>Args</strong>(String schema, String[] args) {this.schema = schema;this.args = args;valid = parse();}public boolean isValid() {return valid;}17


parse()private boolean parse() {if (schema.length() == 0 && args.length == 0)return true;parseSchema();parseArguments();return unexpectedArguments.size() == 0;}18


parseSchema()private boolean parseSchema() {for (String element : schema.split(",")) {parseSchemaElement(element);}return true;}19


parseSchemaElement()private void parseSchemaElement(String element) {if (element.length() == 1) {parseBooleanSchemaElement(element);}}private void parseBooleanSchemaElement(String element) {char c = element.charAt(0);if (Character.isLetter(c)) {boolean<strong>Args</strong>.put(c, false);}}20


parseArguments()private boolean parseArguments() {for (String arg : args)parseArgument(arg);return true;}private void parseArgument(String arg) {if (arg.startsWith("-"))parseElements(arg);}private void parseElements(String arg) {for (int i = 1; i < arg.length(); i++)parseElement(arg.charAt(i));}21


parseElement()private void parseElement(char argChar) {if (isBoolean(argChar)) {numberOfArguments++;setBooleanArg(argChar, true);} elseunexpectedArguments.add(argChar);}private boolean isBoolean(char argChar) {return boolean<strong>Args</strong>.containsKey(argChar);}private void setBooleanArg(char argChar, boolean value) {boolean<strong>Args</strong>.put(argChar, value);}22


getBoolean()public boolean getBoolean(char arg) {return boolean<strong>Args</strong>.get(arg);}23


Miscpublic int cardinality() {return numberOfArguments;}public String usage() {if (schema.length() > 0)return "-["+schema+"]";elsereturn "";}public String errorMessage() {if (unexpectedArguments.size() > 0) {return unexpectedArgumentMessage();} elsereturn "";}24


Miscprivate String unexpectedArgumentMessage() {StringBuffer message = new StringBuffer("Argument(s) -");for (char c : unexpectedArguments) {message.append(c);}message.append(" unexpected.");}return message.toString();25


This isn’t too bad.! Functions are small and obvious.! <strong>Code</strong> is relatively clean.! The hashmaps are a bit of a “trick” but theyaren’t hard to figure out.26


The Wrath of KhanThis code is about to “Grow”.


We need to add integers and strings! Adding these two argument types turned outto be much more complicated than theprevious code might have led us to believe.28


<strong>Args</strong>public class <strong>Args</strong> {private String schema;private String[] args;private boolean valid = true;private Set unexpectedArguments =new TreeSet();private Map boolean<strong>Args</strong> =new HashMap();private Map string<strong>Args</strong> =new HashMap();private Map int<strong>Args</strong> =new HashMap();private Set argsFound = new HashSet();private int currentArgument;private char errorArgumentId = '\0';private String errorParameter = "TILT";private Error<strong>Code</strong> error<strong>Code</strong> = Error<strong>Code</strong>.OK;29private enum Error<strong>Code</strong> {OK, MISSING_STRING, MISSING_INTEGER,INVALID_INTEGER, UNEXPECTED_ARGUMENT}


<strong>Args</strong> Constructorpublic <strong>Args</strong>(String schema, String[] args) throws ParseException {this.schema = schema;this.args = args;valid = parse();}private boolean parse() throws ParseException {if (schema.length() == 0 && args.length == 0)return true;parseSchema();try {parseArguments();} catch (<strong>Args</strong>Exception e) {}return valid;}30


parseSchema()private boolean parseSchema() throws ParseException {for (String element : schema.split(",")) {if (element.length() > 0) {String trimmedElement = element.trim();parseSchemaElement(trimmedElement);}}return true;}31


parseSchema()private boolean parseSchema() throws ParseException {for (String element : schema.split(",")) {if (element.length() > 0) {String trimmedElement = element.trim();parseSchemaElement(trimmedElement);}}return true;}32


validateSchemaElementId()private void validateSchemaElementId(char elementId)throws ParseException {if (!Character.isLetter(elementId)) {throw new ParseException("Bad character:" + elementId +"in <strong>Args</strong> format: " + schema, 0);}}34


parsexxxSchemaElement()private void parseBooleanSchemaElement(char elementId) {boolean<strong>Args</strong>.put(elementId, false);}private void parseIntegerSchemaElement(char elementId) {int<strong>Args</strong>.put(elementId, 0);}private void parseStringSchemaElement(char elementId) {string<strong>Args</strong>.put(elementId, "");}36


There is a symmetry here.! But nothing is holding that symmetry togetherexcept convention.! It’s just a bunch of functions with similarnames.! And if/else statements calling thosefunctions.! And a lot of duplication!! The original design pattern did not scale.37


parseArguments()private boolean parseArguments() throws <strong>Args</strong>Exception {for (currentArgument = 0;currentArgument < args.length;currentArgument++){String arg = args[currentArgument];parseArgument(arg);}return true;}38private void parseArgument(String arg)throws <strong>Args</strong>Exception {if (arg.startsWith("-"))parseElements(arg);}


parseElements()private void parseElements(String arg) throws <strong>Args</strong>Exception {for (int i = 1; i < arg.length(); i++)parseElement(arg.charAt(i));}private void parseElement(char argChar) throws <strong>Args</strong>Exception {if (setArgument(argChar))argsFound.add(argChar);else {unexpectedArguments.add(argChar);error<strong>Code</strong> = Error<strong>Code</strong>.UNEXPECTED_ARGUMENT;valid = false;}}39


setArgument()private boolean setArgument(char argChar) throws <strong>Args</strong>Exception {if (isBooleanArg(argChar))setBooleanArg(argChar, true);else if (isStringArg(argChar))setStringArg(argChar);else if (isIntArg(argChar))setIntArg(argChar);elsereturn false;}return true;40


xxxIntArg()private boolean isIntArg(char argChar){return int<strong>Args</strong>.containsKey(argChar);}private void setIntArg(char argChar) throws <strong>Args</strong>Exception {currentArgument++;String parameter = null;try {parameter = args[currentArgument];int<strong>Args</strong>.put(argChar, new Integer(parameter));} catch (ArrayIndexOutOfBoundsException e) {valid = false;errorArgumentId = argChar;error<strong>Code</strong> = Error<strong>Code</strong>.MISSING_INTEGER;throw new <strong>Args</strong>Exception();} catch (NumberFormatException e) {valid = false;errorArgumentId = argChar;errorParameter = parameter;error<strong>Code</strong> = Error<strong>Code</strong>.INVALID_INTEGER;throw new <strong>Args</strong>Exception();}}41


xxxStringArg()private void setStringArg(char argChar) throws <strong>Args</strong>Exception {currentArgument++;try {string<strong>Args</strong>.put(argChar, args[currentArgument]);} catch (ArrayIndexOutOfBoundsException e) {valid = false;errorArgumentId = argChar;error<strong>Code</strong> = Error<strong>Code</strong>.MISSING_STRING;throw new <strong>Args</strong>Exception();}}private boolean isStringArg(char argChar) {return string<strong>Args</strong>.containsKey(argChar);}42


xxxBooleanArg()private void setBooleanArg(char argChar, boolean value) {boolean<strong>Args</strong>.put(argChar, value);}private boolean isBooleanArg(char argChar) {return boolean<strong>Args</strong>.containsKey(argChar);}43


MISCpublic int cardinality() {return argsFound.size();}public String usage() {if (schema.length() > 0)return "-[" + schema + "]";elsereturn "";}44


MISCpublic String errorMessage() throws Exception {switch (error<strong>Code</strong>) {case OK:throw new Exception("TILT: Should not get here.");case UNEXPECTED_ARGUMENT:return unexpectedArgumentMessage();case MISSING_STRING:return String.format("Could not find string parameter for -%c.",errorArgumentId);case INVALID_INTEGER:return String.format("Argument -%c expects an integer but was '%s'.",errorArgumentId, errorParameter);case MISSING_INTEGER:return String.format("Could not find integer parameter for -%c.",errorArgumentId);}return "";}45


MISCprivate String unexpectedArgumentMessage() {StringBuffer message = new StringBuffer("Argument(s) -");for (char c : unexpectedArguments) {message.append(c);}message.append(" unexpected.");}return message.toString();private boolean falseIfNull(Boolean b) {return b != null && b;}private int zeroIfNull(Integer i) {return i == null ? 0 : i;}private String blankIfNull(String s) {return s == null ? "" : s;}46


MISCpublic String getString(char arg) {return blankIfNull(string<strong>Args</strong>.get(arg));}public int getInt(char arg) {return zeroIfNull(int<strong>Args</strong>.get(arg));}public boolean getBoolean(char arg) {return falseIfNull(boolean<strong>Args</strong>.get(arg));}public boolean has(char arg) {return argsFound.contains(arg);}public boolean isValid() {return valid;}private class <strong>Args</strong>Exception extends Exception {}47


So, this is pretty Yukky.! To make matters worse, we still have to adddoubles and string arrays to it!! The sheer number of variables is daunting.! Odd strings like “TILT”.! The hashsets, the try-catch blocks.! All add up to a:48


So, this is pretty Yukky.! To make matters worse, we still have to adddoubles and string arrays to it!! The sheer number of variables is daunting.! Odd strings like “TILT”.! The hashsets, the try-catch blocks.! All add up to a:! Festering Pile.49


But it didn’t start out that way.! It started out pretty clean.! The mess built up over time.! The initial structure didn’t scale well.! The more it grew, the worse it got.! Eventually I had to stop.50


On Incrementalism! One of the best ways to ruin a program:! Make massive changes in the name ofimprovement.! It’s hard to get the program working again.! TDD: Keep the system running at all times!! I am not allowed to make a change thatbreaks the system.! Every tiny change I make must keep thesystem working51


So I started to Refactor.! Fortunately I had tests!! Very comprehensive tests.! Adding the String and Integer types! Made it clear that each new types forced changes inthree places:! The schema element needs to be parsed.! The arguments need to be parsed.! Each argument needs a getxxx method.! Many different types? All with similar functions?! That sounds like a class!! And so the Argument Marshaler was born.52


ArgumentMarshalerprivate class ArgumentMarshaler {private boolean booleanValue = false;public void setBoolean(boolean value) {booleanValue = value;}}public boolean getBoolean() {return booleanValue;}private class BooleanArgumentMarshaler extends ArgumentMarshaler {}private class StringArgumentMarshaler extends ArgumentMarshaler {}}private class IntegerArgumentMarshaler extends ArgumentMarshaler {}53


All tests still passed. (ATP)! This obviously didn’t break anything.54


This couldn’t possibly break anything?private Map boolean<strong>Args</strong> =new HashMap();This broke a few statements, which I quickly fixed.private void parseBooleanSchemaElement(char elementId) {boolean<strong>Args</strong>.put(elementId, new BooleanArgumentMarshaler());}private void setBooleanArg(char argChar, boolean value) {boolean<strong>Args</strong>.get(argChar).setBoolean(value);}public boolean getBoolean(char arg) {return falseIfNull(boolean<strong>Args</strong>.get(arg).getBoolean());}55


But the tests failed.! Because:public boolean getBoolean(char arg) {return falseIfNull(boolean<strong>Args</strong>.get(arg).getBoolean());}! The .get can return null.! And falseIfNull is no longer needed.! So I made the following changes:56


Incrementally get the tests to pass.57I deleted the FalseIfNull function and removed the call from getBooleanpublic boolean getBoolean(char arg) {return boolean<strong>Args</strong>.get(arg).getBoolean();}TESTNext I split the function into two lines and put the ArgumentMarshaller into itsown variable.public boolean getBoolean(char arg) {<strong>Args</strong>.ArgumentMarshaler am = boolean<strong>Args</strong>.get(arg);return am.getBoolean();}TESTAnd then I put in the null detection logic.public boolean getBoolean(char arg) {<strong>Args</strong>.ArgumentMarshaler am = boolean<strong>Args</strong>.get(arg);return am != null && am.getBoolean();}TEST


Granularity! This was the granularity!! Even if we go a bit faster in this presentation:! Never forget that all the changes I made,were made at this level of granularity!! The steps were tiny!58


Refactoring String Arguments! Similar to boolean arguments.! Must change hashmap, parse, set, and getfunctions.! Only surprise is that I am loading all theimplementation into the ArgumentMarshalerbase class.! All in good time, dearie; all in good time.59


Refactoring String arguments.private Map string<strong>Args</strong> =new HashMap();----private void parseStringSchemaElement(char elementId) {string<strong>Args</strong>.put(elementId, new StringArgumentMarshaler());}----private void setStringArg(char argChar) throws <strong>Args</strong>Exception {currentArgument++;try {string<strong>Args</strong>.get(argChar).setString(args[currentArgument]);} catch (ArrayIndexOutOfBoundsException e) {valid = false;errorArgumentId = argChar;error<strong>Code</strong> = Error<strong>Code</strong>.MISSING_STRING;throw new <strong>Args</strong>Exception();}}60


Refactoring String arguments.public String getString(char arg) {<strong>Args</strong>.ArgumentMarshaler am = string<strong>Args</strong>.get(arg);return am == null ? "" : am.getString();}private class ArgumentMarshaler {private boolean booleanValue = false;private String stringValue;}61public void setBoolean(boolean value) {booleanValue = value;}public boolean getBoolean() {return booleanValue;}public void setString(String s) {stringValue = s;}public String getString() {return stringValue == null ? "" : stringValue;}


Granularity Reminder! These changes were made one tiny step at atime.! All tests were kept running.! When a test broke I stopped and got itrunning again before continuing.62


Refactoring Strategy! By now it should be clear.! All the marshaling behavior gets moved intoArgumentMarshaler base class.! Once the rest of the app depends onArgumentMarshaller I’ll push the behavior intoderivatives.! So let’s do integers.63


Refactoring Integer arguments.private Map int<strong>Args</strong> =new HashMap();----private void parseIntegerSchemaElement(char elementId) {int<strong>Args</strong>.put(elementId, new IntegerArgumentMarshaler());}----public int getInt(char arg) {<strong>Args</strong>.ArgumentMarshaler am = int<strong>Args</strong>.get(arg);return am == null ? 0 : am.getInteger();}64


Refactoring Integer arguments.private void setIntArg(char argChar) throws <strong>Args</strong>Exception {currentArgument++;String parameter = null;try {parameter = args[currentArgument];int<strong>Args</strong>.get(argChar).setInteger(Integer.parseInt(parameter));} catch (ArrayIndexOutOfBoundsException e) {valid = false;errorArgumentId = argChar;error<strong>Code</strong> = Error<strong>Code</strong>.MISSING_INTEGER;throw new <strong>Args</strong>Exception();} catch (NumberFormatException e) {valid = false;errorArgumentId = argChar;errorParameter = parameter;error<strong>Code</strong> = Error<strong>Code</strong>.INVALID_INTEGER;throw new <strong>Args</strong>Exception();}}65


Refactoring Integer arguments.private class ArgumentMarshaler {private boolean booleanValue = false;private String stringValue;private int integerValue;public void setBoolean(boolean value) {booleanValue = value;}public boolean getBoolean() {return booleanValue;}public void setString(String s) {stringValue = s;}public String getString() {return stringValue == null ? "" : stringValue;}public void setInteger(int i) {integerValue = i;}66}public int getInteger() {return integerValue;}


All the Marshaling is encapsulated.! Now all the marshalling is inArgumentMarshaler.! So it’s time to push the behavior down intothe derivatives.! We’ll start, as always, with the booleans.67


Creating BooleanArgumentMarshalerprivate abstract class ArgumentMarshaler {protected boolean booleanValue = false;private String stringValue;private int integerValue;68public void setBoolean(boolean value) {. . .}public boolean getBoolean() {. . .}public void setString(String s) {. . .}public String getString() {. . .}public void setInteger(int i) {. . .}public int getInteger() {. . .}public abstract void set(String s);}----private class BooleanArgumentMarshaler extends ArgumentMarshaler {public void set(String s) {booleanValue = true;}}


Creating BooleanArgumentMarshalerprivate void setBooleanArg(char argChar, boolean value) {boolean<strong>Args</strong>.get(argChar).set("true");}The tests still pass.And the ‘set’ function for boolean arguments isDeployed to the BooleanArgumentMarshaler69


The Boolean Get Function.! Polymorphically deploying ‘get’ functions isalways tricky because of the return typeissue.70


The Boolean Get Functionpublic boolean getBoolean(char arg) {<strong>Args</strong>.ArgumentMarshaler am = boolean<strong>Args</strong>.get(arg);return am != null && (Boolean)am.get();}----private abstract class ArgumentMarshaler {protected boolean booleanValue = false;...}public abstract Object get();71private class BooleanArgumentMarshaler extends ArgumentMarshaler {public void set(String s) {booleanValue = true;}}public Object get() {return booleanValue;}


Both ‘get’ and ‘set’ work for boolean! So now I can do the same for integers andstrings.! Then I can remove all the cruft inArgumentMarshaller that was supporting theargument types.! Remember, all this was done in tiny granuleswhile keeping the tests running at all times.72


<strong>Clean</strong>ing ArgumentMarshallerprivate abstract class ArgumentMarshaler {public abstract void set(String s) throws <strong>Args</strong>Exception;public abstract Object get();}private class BooleanArgumentMarshaler extends ArgumentMarshaler {private boolean booleanValue = false;public void set(String s) {booleanValue = true;}public Object get() {return booleanValue;}}private class StringArgumentMarshaler extends ArgumentMarshaler {private String stringValue = "";public void set(String s) {stringValue = s; }public Object get() {return stringValue;}}73


<strong>Clean</strong>ing ArgumentMarshallerprivate class IntegerArgumentMarshaler extends ArgumentMarshaler {private int intValue = 0;public void set(String s) throws <strong>Args</strong>Exception {try {intValue = Integer.parseInt(s);}catch (NumberFormatException e) {throw new <strong>Args</strong>Exception();}}public Object get() {return intValue;}}74


And now we can clean <strong>Args</strong>.public class <strong>Args</strong> {private Map marshalers;private Set argsFound;private ListIterator currentArgument;public <strong>Args</strong>(String schema, String[] args) throws <strong>Args</strong>Exception {marshalers = new HashMap();argsFound = new HashSet();}parseSchema(schema);parseArgumentStrings(Arrays.asList(args));private void parseSchema(String schema) throws <strong>Args</strong>Exception {for (String element : schema.split(","))if (element.length() > 0)parseSchemaElement(element.trim());}75


And now we can clean <strong>Args</strong>.private void parseSchemaElement(String element) throws <strong>Args</strong>Exception {char elementId = element.charAt(0);String elementTail = element.substring(1);validateSchemaElementId(elementId);if (elementTail.length() == 0)marshalers.put(elementId, new BooleanArgumentMarshaler());else if (elementTail.equals("*"))marshalers.put(elementId, new StringArgumentMarshaler());else if (elementTail.equals("#"))marshalers.put(elementId, new IntegerArgumentMarshaler());elsethrow new <strong>Args</strong>Exception(INVALID_ARGUMENT_FORMAT,elementId, elementTail);}private void validateSchemaElementId(char elementId)throws <strong>Args</strong>Exception {if (!Character.isLetter(elementId))throw new <strong>Args</strong>Exception(INVALID_ARGUMENT_NAME, elementId, null);}76


And now we can clean <strong>Args</strong>.private void parseArgumentStrings(List argsList)throws <strong>Args</strong>Exception{for (currentArgument = argsList.listIterator();currentArgument.hasNext();){String argString = currentArgument.next();if (argString.startsWith("-")) {parseArgumentCharacters(argString.substring(1));} else {currentArgument.previous();break;}}}private void parseArgumentCharacters(String argChars)throws <strong>Args</strong>Exception {for (int i = 0; i < argChars.length(); i++)parseArgumentCharacter(argChars.charAt(i));}77


And now we can clean <strong>Args</strong>.private void parseArgumentCharacter(char argChar) throws <strong>Args</strong>Exception {ArgumentMarshaler m = marshalers.get(argChar);if (m == null) {throw new <strong>Args</strong>Exception(UNEXPECTED_ARGUMENT, argChar, null);} else {argsFound.add(argChar);try {m.set(currentArgument);} catch (<strong>Args</strong>Exception e) {e.setErrorArgumentId(argChar);throw e;}}}public boolean has(char arg) {return argsFound.contains(arg);}public int nextArgument() {return currentArgument.nextIndex();}78


And now we can clean <strong>Args</strong>.public boolean getBoolean(char arg) {return BooleanArgumentMarshaler.getValue(marshalers.get(arg));}public String getString(char arg) {return StringArgumentMarshaler.getValue(marshalers.get(arg));}public int getInt(char arg) {return IntegerArgumentMarshaler.getValue(marshalers.get(arg));}79


Was this worth it?! Bad code gets harder and harder to clean astime goes by.! If you want clean code, you have to clean itas soon as it gets messy.! What about time to market?! The “Dinner” parable.! Have you ever been impeded???80


Bad code.! Nothing has a more profound and long-termdegrading effect than bad code.! Bad schedules can be redone.! Bad requirements can be redefined.! Bad team dynamic can be resolved.! But bad code rots and ferments.! It becomes an inexorable weight that dragsthe team down.81


Professional Behavior! The “Green Band”.! Professionals write tests -- first.! Professionals clean their code.! Professionals know that the only way to gofast! Is to go well.82


The “<strong>Clean</strong> <strong>Code</strong>” project.! Articles:! The “<strong>Args</strong>” article.! The “<strong>Clean</strong> <strong>Code</strong>” book.83


Contact Information! Robert C. Martinunclebob@objectmentor.com! Website:www.objectmentor.com! FitNesse:www.fitnesse.org84

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!