بهبود تدریجی
Case Study of a Command-Line Argument Parser
این فصل یک مطالعهٔ موردی در زمینهٔ بهبود تدریجی است. در ابتدا ماژولی را میبینید که شروع خوبی داشت، اما به خوبی مقیاسپذیر نبود. سپس خواهید دید که این ماژول چگونه refactor و تمیز شد.
بیشتر ما گاهی مجبور شدهایم که آرگومانهای خط فرمان (command-line arguments) را پردازش کنیم. اگر ابزار مناسبی در دسترس نداشته باشیم، معمولاً آرایهای از رشتهها را که به تابع main
پاس داده میشود، پیمایش میکنیم. ابزارهای خوبی از منابع مختلف برای این کار وجود دارد، اما هیچکدام دقیقاً آن کاری را نمیکنند که من میخواهم. بنابراین، تصمیم گرفتم ابزار خودم را بنویسم. اسمش را گذاشتم: Args
.
استفاده از Args
بسیار ساده است. کافی است شیئی از کلاس Args
را با آرگومانهای ورودی و یک رشتهٔ قالب (format string) بسازید و سپس از طریق آن شیء، مقادیر آرگومانها را استخراج کنید. به مثال سادهٔ زیر توجه کنید:
Listing 14-1
public static void main (String[] args)
{
try
{
Args arg = new Args ("l,p#,d*", args);
boolean logging = arg.getBoolean ('l');
int port = arg.getInt ('p');
String directory = arg.getString ('d');
executeApplication (logging, port, directory);
}
catch (ArgsException e)
{
System.out.printf ("Argument error: %s\n", e.errorMessage ());
}
}
میبینید که استفاده از این ابزار چقدر ساده است. فقط کافی است یک نمونه از کلاس Args
با دو پارامتر ایجاد کنیم. پارامتر اول، یک رشتهٔ قالب (یا شِما – schema) است: "l,p#,d*"
که سه آرگومان خط فرمان را تعریف میکند. اولین آرگومان -l یک آرگومان بولی (boolean
) است. دومی -p یک آرگومان عددی (integer
) است. سومی -d یک آرگومان رشتهای (string
) است.
پارامتر دوم سازندهٔ Args
همان آرایهای از آرگومانهای خط فرمان است که به تابع main
پاس داده شده.
اگر سازنده بدون پرتاب (throw
) کردن یک استثنای ArgsException
اجرا شود، یعنی ورودی خط فرمان بهدرستی تجزیه شده و شیء Args آمادهٔ پرسوجو است. متدهایی مثل getBoolean،
getInteger
و getString
به ما امکان میدهند مقدار آرگومانها را بر اساس نامشان دریافت کنیم.
اگر مشکلی در رشتهٔ قالب یا خود آرگومانهای خط فرمان وجود داشته باشد، یک استثنای (exception) از نوع ArgsException
پرتاب میشود. با استفاده از متد errorMessage در این استثنا، میتوان توضیح دقیقی از خطا دریافت کرد.
پیادهسازی Args
Listing 14-2 پیادهسازی کلاس Args
را نشان میدهد. لطفاً آن را با دقت مطالعه کنید. من برای سبک و ساختار آن تلاش زیادی کردهام و امیدوارم ارزش الگوبرداری داشته باشد.
Listing 14-2
package com.objectmentor.utilities.args;
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
import java.util.*;
public class Args
{
private Map<Character, ArgumentMarshaler> marshalers;
private Set<Character> argsFound;
private ListIterator<String> currentArgument;
public Args (String schema, String[] args) throws ArgsException
{
marshalers = new HashMap<Character, ArgumentMarshaler> ();
argsFound = new HashSet<Character> ();
parseSchema (schema);
parseArgumentStrings (Arrays.asList (args));
}
private void
parseSchema (String schema) throws ArgsException
{
for (String element : schema.split (","))
if (element.length () > 0)
parseSchemaElement (element.trim ());
}
private void
parseSchemaElement (String element) throws ArgsException
{
char elementId = element.charAt (0);
String elementTail = element.substring (1);
validateSchemaElementId (elementId);
if (elementTail.length () == 0)
marshalers.put (elementId, new BooleanArgumentMarshaler ());
else if (elementTail.equals ("*"))
marshalers.put (elementId, new StringArgumentMarshaler ());
else if (elementTail.equals ("#"))
marshalers.put (elementId, new IntegerArgumentMarshaler ());
else if (elementTail.equals ("##"))
marshalers.put (elementId, new DoubleArgumentMarshaler ());
else if (elementTail.equals ("[*]"))
marshalers.put (elementId, new StringArrayArgumentMarshaler ());
else
throw new ArgsException (INVALID_ARGUMENT_FORMAT, elementId,
elementTail);
}
private void
validateSchemaElementId (char elementId) throws ArgsException
{
if (!Character.isLetter (elementId))
throw new ArgsException (INVALID_ARGUMENT_NAME, elementId, null);
}
private void
parseArgumentStrings (List<String> argsList) throws ArgsException
{
for (currentArgument = argsList.listIterator ();
currentArgument.hasNext ();)
{
String argString = currentArgument.next ();
if (argString.startsWith ("-"))
{
parseArgumentCharacters (argString.substring (1));
}
else
{
currentArgument.previous ();
break;
}
}
}
private void
parseArgumentCharacters (String argChars) throws ArgsException
{
for (int i = 0; i < argChars.length (); i++)
parseArgumentCharacter (argChars.charAt (i));
}
private void
parseArgumentCharacter (char argChar) throws ArgsException
{
ArgumentMarshaler m = marshalers.get (argChar);
if (m == null)
{
throw new ArgsException (UNEXPECTED_ARGUMENT, argChar, null);
}
else
{
argsFound.add (argChar);
try
{
m.set (currentArgument);
}
catch (ArgsException e)
{
e.setErrorArgumentId (argChar);
throw e;
}
}
}
public boolean
has (char arg)
{
return argsFound.contains (arg);
}
public int
nextArgument ()
{
return currentArgument.nextIndex ();
}
public boolean
getBoolean (char arg)
{
return BooleanArgumentMarshaler.getValue (marshalers.get (arg));
}
public String
getString (char arg)
{
return StringArgumentMarshaler.getValue (marshalers.get (arg));
}
public int
getInt (char arg)
{
return IntegerArgumentMarshaler.getValue (marshalers.get (arg));
}
public double
getDouble (char arg)
{
return DoubleArgumentMarshaler.getValue (marshalers.get (arg));
}
public String[]
getStringArray (char arg)
{
return StringArrayArgumentMarshaler.getValue (marshalers.get (arg));
}
}
توجه کنید که میتوانید این کد را از بالا به پایین بخوانید، بدون اینکه مجبور شوید زیاد بین بخشهای مختلف جابهجا شوید یا به جلو بپرید. تنها چیزی که شاید نیاز داشته باشید جلوتر ببینید، تعریف ArgumentMarshaler
است که عمداً آن را حذف کردهام. اگر این کد را با دقت خوانده باشید، باید متوجه شده باشید که رابط (interface) ArgumentMarshaler
چیست و پیادهسازیهای مختلف آن چه کاری انجام میدهند.
الان چند نمونه از آنها را به شما نشان میدهم.
Listing 14-3 - ArgumentMarshaler.java
public interface ArgumentMarshaler
{
void set (Iterator<String> currentArgument) throws ArgsException;
}
Listing 14-4 - BooleanArgumentMarshaler.java
public class BooleanArgumentMarshaler implements ArgumentMarshaler
{
private boolean booleanValue = false;
public void
set (Iterator<String> currentArgument) throws ArgsException
{
booleanValue = true;
}
public static boolean
getValue (ArgumentMarshaler am)
{
if (am != null && am instanceof BooleanArgumentMarshaler)
return ((BooleanArgumentMarshaler)am).booleanValue;
else
return false;
}
}
Listing 14-5 - StringArgumentMarshaler.java
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
public class StringArgumentMarshaler implements ArgumentMarshaler
{
private String stringValue = "";
public void
set (Iterator<String> currentArgument) throws ArgsException
{
try
{
stringValue = currentArgument.next ();
}
catch (NoSuchElementException e)
{
throw new ArgsException (MISSING_STRING);
}
}
public static String
getValue (ArgumentMarshaler am)
{
if (am != null && am instanceof StringArgumentMarshaler)
return ((StringArgumentMarshaler)am).stringValue;
else
return "";
}
}
Listing 14-6 - IntegerArgumentMarshaler.java
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
public class IntegerArgumentMarshaler implements ArgumentMarshaler
{
private int intValue = 0;
public void
set (Iterator<String> currentArgument) throws ArgsException
{
String parameter = null;
try
{
parameter = currentArgument.next ();
intValue = Integer.parseInt (parameter);
}
catch (NoSuchElementException e)
{
throw new ArgsException (MISSING_INTEGER);
}
catch (NumberFormatException e)
{
throw new ArgsException (INVALID_INTEGER, parameter);
}
}
public static int
getValue (ArgumentMarshaler am)
{
if (am != null && am instanceof IntegerArgumentMarshaler)
return ((IntegerArgumentMarshaler)am).intValue;
else
return 0;
}
}
سایر مشتقهای ArgumentMarshaler
هم به همین سبک برای double
ها و آرایههای رشتهای (String[])
پیادهسازی شدهاند و فقط باعث شلوغی این فصل میشوند. آنها را بهعنوان تمرین به شما واگذار میکنم.
ممکن است یک نکتهٔ دیگر ذهن شما را مشغول کرده باشد: تعریف ثابتهای مربوط به کد خطا (error code constants). این موارد در کلاس ArgsException
تعریف شدهاند (Listing 14-7).
Listing 14-7 - ArgsException.java
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
public class ArgsException extends Exception
{
private char errorArgumentId = '\0';
private String errorParameter = null;
private ErrorCode errorCode = OK;
public ArgsException () {}
public ArgsException (String message) { super (message); }
public ArgsException (ErrorCode errorCode) { this.errorCode = errorCode; }
public ArgsException (ErrorCode errorCode, String errorParameter)
{
this.errorCode = errorCode;
this.errorParameter = errorParameter;
}
public ArgsException (ErrorCode errorCode, char errorArgumentId,
String errorParameter)
{
this.errorCode = errorCode;
this.errorParameter = errorParameter;
this.errorArgumentId = errorArgumentId;
}
public char
getErrorArgumentId ()
{
return errorArgumentId;
}
public void
setErrorArgumentId (char errorArgumentId)
{
this.errorArgumentId = errorArgumentId;
}
public String
getErrorParameter ()
{
return errorParameter;
}
public void
setErrorParameter (String errorParameter)
{
this.errorParameter = errorParameter;
}
public ErrorCode
getErrorCode ()
{
return errorCode;
}
public void
setErrorCode (ErrorCode errorCode)
{
this.errorCode = errorCode;
}
public String
errorMessage ()
{
switch (errorCode)
{
case OK:
return "TILT: Should not get here.";
case UNEXPECTED_ARGUMENT:
return String.format ("Argument -%c unexpected.",
errorArgumentId);
case MISSING_STRING:
return String.format (
"Could not find string parameter for -%c.",
errorArgumentId);
case INVALID_INTEGER:
return String.format (
"Argument -%c expects an integer but was '%s'.",
errorArgumentId, errorParameter);
case MISSING_INTEGER:
return String.format (
"Could not find integer parameter for -%c.",
errorArgumentId);
case INVALID_DOUBLE:
return String.format (
"Argument -%c expects a double but was '%s'.",
errorArgumentId, errorParameter);
case MISSING_DOUBLE:
return String.format (
"Could not find double parameter for -%c.",
errorArgumentId);
case INVALID_ARGUMENT_NAME:
return String.format ("'%c' is not a valid argument name.",
errorArgumentId);
case INVALID_ARGUMENT_FORMAT:
return String.format ("'%s' is not a valid argument format.",
errorParameter);
}
return "";
}
public enum ErrorCode
{
OK,
INVALID_ARGUMENT_FORMAT,
UNEXPECTED_ARGUMENT,
INVALID_ARGUMENT_NAME,
MISSING_STRING,
MISSING_INTEGER,
INVALID_INTEGER,
MISSING_DOUBLE,
INVALID_DOUBLE
}
}
جالبه که ببینیم برای پیادهسازی جزئیات چنین مفهوم سادهای، اینهمه کد لازم شده. یکی از دلایلش اینه که ما از زبانی استفاده میکنیم که بهطور خاص پرحرفه! جاوا، بهعنوان یک زبان ایستا از نظر نوعدهی (statically typed)، مجبورمون میکنه که برای راضی کردن سیستم نوعدهی، کد زیادی بنویسیم. در زبانهایی مثل Ruby،
Python
یا Smalltalk،
این برنامه بسیار جمعوجورتر میشد.
لطفاً یک بار دیگه این کد رو با دقت بخونید. به نامگذاریها، اندازهٔ توابع، و قالببندی کد توجه ویژهای داشته باشید. اگر یک برنامهنویس با تجربه باشید، ممکنه به بعضی از سبکها یا ساختارهای استفادهشده انتقادهایی داشته باشید. اما در کل، امیدوارم به این نتیجه برسید که این برنامه بهخوبی نوشته شده و ساختار تمیزی دارد.
برای مثال، باید کاملاً مشخص باشه که چطور میتونید یک نوع آرگومان جدید، مثل تاریخ (Date) یا عدد مختلط (Complex Number) اضافه کنید و این کار چقدر ساده خواهد بود. در واقع، برای این کار فقط باید:
یک زیرکلاس جدید از ArgumentMarshaler
تعریف کنید،
یک تابع getXXX
جدید بنویسید،
و یک case جدید به تابع parseSchemaElement
اضافه کنید.
احتمالاً باید یک ArgsException
.ErrorCode
جدید هم اضافه بشه و یک پیام خطای جدید هم برایش تعریف بشه.
چطور این کار رو انجام دادم؟
بذارید خیالتون رو راحت کنم: من این برنامه رو بهصورت یکجا، از اول تا آخر، توی همین فرم نهایی ننوشتم. مهمتر از اون، انتظار ندارم که شما هم بتونید برنامههای تمیز و شستهرفتهای رو در یکبار نوشتن تولید کنید.
اگر از چند دهه گذشته چیزی یاد گرفته باشیم، اینه که برنامهنویسی بیشتر یک مهارته تا یک علم. برای نوشتن کد تمیز، باید اول کد کثیف بنویسید و بعد اون رو تمیز کنید.
این نباید براتون تعجبآور باشه. ما این حقیقت رو توی مدرسه یاد گرفتیم، وقتی معلمهامون (معمولاً بینتیجه!) سعی میکردن ما رو قانع کنن که پیشنویسهای اولیه بنویسیم.
فرآیندش، همونطور که میگفتن، این بود که باید یک پیشنویس بنویسیم، بعد نسخهٔ دوم، و بعد چند نسخهٔ بعدی، تا برسیم به نسخهٔ نهایی.
اونها سعی داشتن بهمون بگن که نوشتن یک متن تمیز، فرایندی از پالایش گامبهگام (successive refinement) است.
بیشتر برنامهنویسهای تازهکار (مثل بیشتر دانشآموزها) خیلی خوب این توصیه رو رعایت نمیکنن. اونها فکر میکنن هدف اصلی اینه که برنامه "کار کنه". وقتی برنامه "کار کرد"، میرن سراغ کار بعدی و برنامه رو همونجوری که بالاخره به کار افتاده، رها میکنن.
اما بیشتر برنامهنویسهای باتجربه میدونن که این کار، خودکشی حرفهایه.
Args: پیشنویس اولیه
Listing 14-8 نسخهای اولیه از کلاس Args
رو نشون میده. این نسخه "کار میکنه"، ولی بههمریخته است.
Listing 14-8 - Args.java (first draft)
import java.text.ParseException;
import java.util.*;
public class Args
{
private String schema;
private String[] args;
private boolean valid = true;
private Set<Character> unexpectedArguments = new TreeSet<Character> ();
private Map<Character, Boolean> booleanArgs
= new HashMap<Character, Boolean> ();
private Map<Character, String> stringArgs
= new HashMap<Character, String> ();
private Map<Character, Integer> intArgs
= new HashMap<Character, Integer> ();
private Set<Character> argsFound = new HashSet<Character> ();
private int currentArgument;
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
private enum ErrorCode
{
OK,
MISSING_STRING,
MISSING_INTEGER,
INVALID_INTEGER,
UNEXPECTED_ARGUMENT
}
public Args (String schema, String[] args) throws ParseException
{
this.schema = schema;
this.args = args;
valid = parse ();
}
private boolean
parse () throws ParseException
{
if (schema.length () == 0 && args.length == 0)
return true;
parseSchema ();
try
{
parseArguments ();
}
catch (ArgsException e)
{
}
return valid;
}
private boolean
parseSchema () throws ParseException
{
for (String element : schema.split (","))
{
if (element.length () > 0)
{
String trimmedElement = element.trim ();
parseSchemaElement (trimmedElement);
}
}
return true;
}
private void
parseSchemaElement (String element) throws ParseException
{
char elementId = element.charAt (0);
String elementTail = element.substring (1);
validateSchemaElementId (elementId);
if (isBooleanSchemaElement (elementTail))
parseBooleanSchemaElement (elementId);
else if (isStringSchemaElement (elementTail))
parseStringSchemaElement (elementId);
else if (isIntegerSchemaElement (elementTail))
{
parseIntegerSchemaElement (elementId);
}
else
{
throw new ParseException (
String.format ("Argument: %c has invalid format: %s.",
elementId, elementTail),
0);
}
}
private void
validateSchemaElementId (char elementId) throws ParseException
{
if (!Character.isLetter (elementId))
{
throw new ParseException ("Bad character:" + elementId
+ "in Args format: " + schema,
0);
}
}
private void
parseBooleanSchemaElement (char elementId)
{
booleanArgs.put (elementId, false);
}
private void
parseIntegerSchemaElement (char elementId)
{
intArgs.put (elementId, 0);
}
private void
parseStringSchemaElement (char elementId)
{
stringArgs.put (elementId, "");
}
private boolean
isStringSchemaElement (String elementTail)
{
return elementTail.equals ("*");
}
private boolean
isBooleanSchemaElement (String elementTail)
{
return elementTail.length () == 0;
}
private boolean
isIntegerSchemaElement (String elementTail)
{
return elementTail.equals ("#");
}
private boolean
parseArguments () throws ArgsException
{
for (currentArgument = 0; currentArgument < args.length;
currentArgument++)
{
String arg = args[currentArgument];
parseArgument (arg);
}
return true;
}
private void
parseArgument (String arg) throws ArgsException
{
if (arg.startsWith ("-"))
parseElements (arg);
}
private void
parseElements (String arg) throws ArgsException
{
for (int i = 1; i < arg.length (); i++)
parseElement (arg.charAt (i));
}
private void
parseElement (char argChar) throws ArgsException
{
if (setArgument (argChar))
argsFound.add (argChar);
else
{
unexpectedArguments.add (argChar);
errorCode = ErrorCode.UNEXPECTED_ARGUMENT;
valid = false;
}
}
private boolean
setArgument (char argChar) throws ArgsException
{
if (isBooleanArg (argChar))
setBooleanArg (argChar, true);
else if (isStringArg (argChar))
setStringArg (argChar);
else if (isIntArg (argChar))
setIntArg (argChar);
else
return false;
return true;
}
private boolean
isIntArg (char argChar)
{
return intArgs.containsKey (argChar);
}
private void
setIntArg (char argChar) throws ArgsException
{
currentArgument++;
String parameter = null;
try
{
parameter = args[currentArgument];
intArgs.put (argChar, new Integer (parameter));
}
catch (ArrayIndexOutOfBoundsException e)
{
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException ();
}
catch (NumberFormatException e)
{
valid = false;
errorArgumentId = argChar;
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw new ArgsException ();
}
}
private void
setStringArg (char argChar) throws ArgsException
{
currentArgument++;
try
{
stringArgs.put (argChar, args[currentArgument]);
}
catch (ArrayIndexOutOfBoundsException e)
{
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException ();
}
}
private boolean
isStringArg (char argChar)
{
return stringArgs.containsKey (argChar);
}
private void
setBooleanArg (char argChar, boolean value)
{
booleanArgs.put (argChar, value);
}
private boolean
isBooleanArg (char argChar)
{
return booleanArgs.containsKey (argChar);
}
public int
cardinality ()
{
return argsFound.size ();
}
public String
usage ()
{
if (schema.length () > 0)
return "-[" + schema + "]";
else
return "";
}
public String
errorMessage () throws Exception
{
switch (errorCode)
{
case OK:
throw new Exception ("TILT: Should not get here.");
case UNEXPECTED_ARGUMENT:
return unexpectedArgumentMessage ();
case MISSING_STRING:
return String.format (
"Could not find string parameter for -%c.",
errorArgumentId);
case INVALID_INTEGER:
return String.format (
"Argument -%c expects an integer but was '%s'.",
errorArgumentId, errorParameter);
case MISSING_INTEGER:
return String.format (
"Could not find integer parameter for -%c.",
errorArgumentId);
}
return "";
}
private String
unexpectedArgumentMessage ()
{
StringBuffer message = new StringBuffer ("Argument(s) -");
for (char c : unexpectedArguments)
{
message.append (c);
}
message.append (" unexpected.");
return message.toString ();
}
private boolean
falseIfNull (Boolean b)
{
return b != null && b;
}
private int
zeroIfNull (Integer i)
{
return i == null ? 0 : i;
}
private String
blankIfNull (String s)
{
return s == null ? "" : s;
}
public String
getString (char arg)
{
return blankIfNull (stringArgs.get (arg));
}
public int
getInt (char arg)
{
return zeroIfNull (intArgs.get (arg));
}
public boolean
getBoolean (char arg)
{
return falseIfNull (booleanArgs.get (arg));
}
public boolean
has (char arg)
{
return argsFound.contains (arg);
}
public boolean
isValid ()
{
return valid;
}
private class ArgsException extends Exception
{
}
}
امیدوارم واکنش اولیه شما به این حجم کد این باشه که "خوشحالم که اون رو به همین شکل رها نکرده!". اگر این احساس رو دارید، پس به یاد داشته باشید که دیگران هم همینطور در مورد کدی که شما بهصورت پیشنویس رها میکنید، احساس خواهند کرد.
در واقع، شاید first draft
شاید مهربانانهترین چیزی باشه که میتونید در مورد این کد بگید. این کد قطعاً در حال پیشرفت است. تعداد زیاد متغیرهای نمونه (instance variables) نگرانکننده است. رشتههای عجیب مثل TILT
، HashSet ها و TreeSet ها، و بلاکهای try-catch-catch همه با هم جمع شدهاند و تبدیل به یک پشتهٔ عفونتزای (festering pile) شدهاند.
من قصد نداشتم که یک پشتهٔ عفونتزا بنویسم. در واقع، تلاش میکردم که همه چیز رو بهطور نسبتاً سازماندهیشده نگه دارم. احتمالاً میتوانید این رو از انتخاب نام توابع و متغیرها و این حقیقت که برنامه ساختار نسبتاً خامی دارد، متوجه بشید. اما، واضح است که من اجازه دادم مشکل از کنترل من خارج بشه.
این بهمریختگی بهتدریج ساخته شد. نسخههای قبلی اصلاً به این اندازه بد نبودند. برای مثال، Listing 14-9 نسخهای از کد رو نشون میده که تنها آرگومانهای بولی کار میکردند.
Listing 14-9 - Args.java (Boolean only)
package com.objectmentor.utilities.getopts;
import java.util.*;
public class Args
{
private String schema;
private String[] args;
private boolean valid;
private Set<Character> unexpectedArguments = new TreeSet<Character> ();
private Map<Character, Boolean> booleanArgs
= new HashMap<Character, Boolean> ();
private int numberOfArguments = 0;
public Args (String schema, String[] args)
{
this.schema = schema;
this.args = args;
valid = parse ();
}
public boolean
isValid ()
{
return valid;
}
private boolean
parse ()
{
if (schema.length () == 0 && args.length == 0)
return true;
parseSchema ();
parseArguments ();
return unexpectedArguments.size () == 0;
}
private boolean
parseSchema ()
{
for (String element : schema.split (","))
{
parseSchemaElement (element);
}
return true;
}
private void
parseSchemaElement (String element)
{
if (element.length () == 1)
{
parseBooleanSchemaElement (element);
}
}
private void
parseBooleanSchemaElement (String element)
{
char c = element.charAt (0);
if (Character.isLetter (c))
{
booleanArgs.put (c, false);
}
}
private boolean
parseArguments ()
{
for (String arg : args)
parseArgument (arg);
return true;
}
private void
parseArgument (String arg)
{
if (arg.startsWith ("-"))
parseElements (arg);
}
private void
parseElements (String arg)
{
for (int i = 1; i < arg.length (); i++)
parseElement (arg.charAt (i));
}
private void
parseElement (char argChar)
{
if (isBoolean (argChar))
{
numberOfArguments++;
setBooleanArg (argChar, true);
}
else
unexpectedArguments.add (argChar);
}
private void
setBooleanArg (char argChar, boolean value)
{
booleanArgs.put (argChar, value);
}
private boolean
isBoolean (char argChar)
{
return booleanArgs.containsKey (argChar);
}
public int
cardinality ()
{
return numberOfArguments;
}
public String
usage ()
{
if (schema.length () > 0)
return "-[" + schema + "]";
else
return "";
}
public String
errorMessage ()
{
if (unexpectedArguments.size () > 0)
{
return unexpectedArgumentMessage ();
}
else
return "";
}
private String
unexpectedArgumentMessage ()
{
StringBuffer message = new StringBuffer ("Argument(s) -");
for (char c : unexpectedArguments)
{
message.append (c);
}
message.append (" unexpected.");
return message.toString ();
}
public boolean
getBoolean (char arg)
{
return booleanArgs.get (arg);
}
}
با اینکه میتوانید ایرادات زیادی در این کد پیدا کنید، اما واقعاً آنقدرها هم بد نیست. کد فشرده و ساده است و فهمیدنش راحت. با این حال، در دل این کد، بهراحتی میتوان بذرهای پشتهٔ عفونتزای بعدی را مشاهده کرد. خیلی واضح است که چطور این کد به آن بهمریختگی تبدیل شد.
توجه کنید که آن بهمریختگی تنها دو نوع آرگومان بیشتر از این کد دارد: رشته (String
) و عدد (integer
). اضافه کردن تنها دو نوع آرگومان دیگر تاثیر منفی عظیمی روی کد داشت. این تغییر کد را از چیزی که میتوانست بهطور نسبتاً قابل نگهداری باشد، به چیزی تبدیل کرد که من انتظار دارم به زودی پر از باگ و مشکلات مختلف بشود.
من این دو نوع آرگومان را بهطور تدریجی اضافه کردم. اول، آرگومان رشته را اضافه کردم که این نتیجه را داد:
Listing 14-10 - Args.java (Boolean and String)
package com.objectmentor.utilities.getopts;
import java.text.ParseException;
import java.util.*;
public class Args
{
private String schema;
private String[] args;
private boolean valid = true;
private Set<Character> unexpectedArguments = new TreeSet<Character> ();
private Map<Character, Boolean> booleanArgs
= new HashMap<Character, Boolean> ();
private Map<Character, String> stringArgs
= new HashMap<Character, String> ();
private Set<Character> argsFound = new HashSet<Character> ();
private int currentArgument;
private char errorArgument = '\0';
enum ErrorCode
{
OK,
MISSING_STRING
}
private ErrorCode errorCode = ErrorCode.OK;
public Args (String schema, String[] args) throws ParseException
{
this.schema = schema;
this.args = args;
valid = parse ();
}
private boolean
parse () throws ParseException
{
if (schema.length () == 0 && args.length == 0)
return true;
parseSchema ();
parseArguments ();
return valid;
}
private boolean
parseSchema () throws ParseException
{
for (String element : schema.split (","))
{
if (element.length () > 0)
{
String trimmedElement = element.trim ();
parseSchemaElement (trimmedElement);
}
}
return true;
}
private void
parseSchemaElement (String element) throws ParseException
{
char elementId = element.charAt (0);
String elementTail = element.substring (1);
validateSchemaElementId (elementId);
if (isBooleanSchemaElement (elementTail))
parseBooleanSchemaElement (elementId);
else if (isStringSchemaElement (elementTail))
parseStringSchemaElement (elementId);
}
private void
validateSchemaElementId (char elementId) throws ParseException
{
if (!Character.isLetter (elementId))
{
throw new ParseException ("Bad character:" + elementId
+ "in Args format: " + schema,
0);
}
}
private void
parseStringSchemaElement (char elementId)
{
stringArgs.put (elementId, "");
}
private boolean
isStringSchemaElement (String elementTail)
{
return elementTail.equals ("*");
}
private boolean
isBooleanSchemaElement (String elementTail)
{
return elementTail.length () == 0;
}
private void
parseBooleanSchemaElement (char elementId)
{
booleanArgs.put (elementId, false);
}
private boolean
parseArguments ()
{
for (currentArgument = 0; currentArgument < args.length;
currentArgument++)
{
String arg = args[currentArgument];
parseArgument (arg);
}
return true;
}
private void
parseArgument (String arg)
{
if (arg.startsWith ("-"))
parseElements (arg);
}
private void
parseElements (String arg)
{
for (int i = 1; i < arg.length (); i++)
parseElement (arg.charAt (i));
}
private void
parseElement (char argChar)
{
if (setArgument (argChar))
argsFound.add (argChar);
else
{
unexpectedArguments.add (argChar);
valid = false;
}
}
private boolean
setArgument (char argChar)
{
boolean set = true;
if (isBoolean (argChar))
setBooleanArg (argChar, true);
else if (isString (argChar))
setStringArg (argChar, "");
else
set = false;
return set;
}
private void
setStringArg (char argChar, String s)
{
currentArgument++;
try
{
stringArgs.put (argChar, args[currentArgument]);
}
catch (ArrayIndexOutOfBoundsException e)
{
valid = false;
errorArgument = argChar;
errorCode = ErrorCode.MISSING_STRING;
}
}
private boolean
isString (char argChar)
{
return stringArgs.containsKey (argChar);
}
private void
setBooleanArg (char argChar, boolean value)
{
booleanArgs.put (argChar, value);
}
private boolean
isBoolean (char argChar)
{
return booleanArgs.containsKey (argChar);
}
public int
cardinality ()
{
return argsFound.size ();
}
public String
usage ()
{
if (schema.length () > 0)
return "-[" + schema + "]";
else
return "";
}
public String
errorMessage () throws Exception
{
if (unexpectedArguments.size () > 0)
{
return unexpectedArgumentMessage ();
}
else
switch (errorCode)
{
case MISSING_STRING:
return String.format (
"Could not find string parameter for -%c.",
errorArgument);
case OK:
throw new Exception ("TILT: Should not get here.");
}
return "";
}
private String
unexpectedArgumentMessage ()
{
StringBuffer message = new StringBuffer ("Argument(s) -");
for (char c : unexpectedArguments)
{
message.append (c);
}
message.append (" unexpected.");
return message.toString ();
}
public boolean
getBoolean (char arg)
{
return falseIfNull (booleanArgs.get (arg));
}
private boolean
falseIfNull (Boolean b)
{
return b == null ? false : b;
}
public String
getString (char arg)
{
return blankIfNull (stringArgs.get (arg));
}
private String
blankIfNull (String s)
{
return s == null ? "" : s;
}
public boolean
has (char arg)
{
return argsFound.contains (arg);
}
public boolean
isValid ()
{
return valid;
}
}
میبینید که اوضاع کمکم داره از کنترل خارج میشه. هنوز فاجعهبار نیست، اما بهمریختگی داره کمکم رشد میکنه. فعلاً فقط یه توده است، اما هنوز عفونتزده و فاسد نشده. اضافه شدن نوع آرگومان عددی بود که این توده رو واقعاً وارد مرحلهٔ تخمیر و فساد کرد.
پس متوقف شدم
حداقل دو نوع آرگومان دیگه هم داشتم که میخواستم اضافه کنم، اما مشخص بود که اوضاع رو خیلی بدتر خواهند کرد. اگر بیمهابا جلو میرفتم، احتمالاً میتونستم کاری کنم که درست کار کنن، اما پشت سرم یه آشوب بزرگی باقی میذاشتم که دیگه قابل اصلاح نبود. اگر قرار بود ساختار این کد واقعاً قابل نگهداری بمونه، الان وقت اصلاحش بود.
پس افزودن قابلیتها رو متوقف کردم و شروع به بازآرایی (Refactoring) کردم. چون تازه آرگومانهای رشتهای و عددی رو اضافه کرده بودم، خوب میدونستم که هر نوع آرگومان جدید به کدهای تازهای در سه نقطهٔ اصلی نیاز داره:
ابتدا باید یه راهی برای تحلیل عنصر اسکیمای اون آرگومان تعریف میشد تا بتونم HashMap
مخصوص اون نوع رو انتخاب کنم.
بعد، اون نوع آرگومان باید در آرگومانهای خط فرمان تجزیه (parse) میشد و به نوع واقعی خودش تبدیل میشد.
در نهایت، یه متد getXXX
مخصوص لازم بود که مقدار اون آرگومان رو به عنوان نوع واقعی خودش به کاربر برگردونه.
چندین نوع مختلف که همشون متدهایی مشابه دارن — این یعنی وقتشه که یه کلاس جدید تعریف کنیم. و اینطوری بود که مفهوم ArgumentMarshaler
شکل گرفت.
دربارهٔ رویکرد تدریجی (Incrementalism)
یکی از بدترین کارهایی که میتونید با یه برنامه بکنید اینه که در نام اصلاح و بهبود، تغییرات عظیم ساختاری در اون ایجاد کنید. بعضی برنامهها هیچوقت از این «بهبودها» جان سالم به در نمیبرن. مشکل اینجاست که خیلی سخته برنامهای رو که قبل از این تغییرات بهدرستی کار میکرد، دوباره دقیقاً به همون وضعیت عملکردی برسونید.
برای جلوگیری از این موضوع، من از روش توسعه مبتنی بر آزمون (TDD
) استفاده میکنم. یکی از اصول اصلی در این روش اینه که سیستم همیشه باید کار کنه. بهعبارت دیگه، در TDD
من اجازه ندارم تغییری در سیستم بدم که باعث خراب شدنش بشه. هر تغییری که اعمال میکنم باید سیستم رو در همون وضعیت قبلی نگه داره.
برای رسیدن به این هدف، باید یه مجموعه تست خودکار داشته باشم که هر لحظه بتونم اجراش کنم تا مطمئن شم که رفتار سیستم تغییر نکرده. برای کلاس Args،
من همزمان با ساختن همون پشتهٔ عفونتزا، یه مجموعه تست واحد (unit test) و تست پذیرش (acceptance test) ایجاد کرده بودم. تستهای واحد رو با استفاده از JUnit در جاوا نوشته بودم. تستهای پذیرش هم به صورت صفحات ویکی در FitNesse
نوشته شده بودن.
هر زمان که میخواستم، میتونستم این تستها رو اجرا کنم و اگر همهشون پاس میشدن، با اطمینان میگفتم که سیستم همونطوری که انتظار دارم کار میکنه.
پس من جلو رفتم و تعداد زیادی تغییر خیلی کوچک اعمال کردم. هر تغییر، ساختار سیستم رو به سمت مفهوم ArgumentMarshaler
هدایت میکرد. و در عین حال، هر تغییر سیستم رو سالم نگه میداشت.
اولین کاری که کردم، اضافه کردن اسکلت اولیهٔ کلاس ArgumentMarshaler
به انتهای همون پشتهٔ عفونتزا بود.
Listing 14-11 - ArgumentMarshaller appended to Args.java
private class ArgumentMarshaler
{
private boolean booleanValue = false;
public void setBoolean(boolean value) { booleanValue = value; }
public boolean getBoolean() { return booleanValue; }
}
private class BooleanArgumentMarshaler extends ArgumentMarshaler
{}
private class StringArgumentMarshaler extends ArgumentMarshaler
{}
private class IntegerArgumentMarshaler extends ArgumentMarshaler
{}
}
بدیهیه که این تغییر قرار نبود چیزی رو خراب کنه. پس قدم بعدی رو برداشتم و سادهترین تغییری رو اعمال کردم که میتونستم، طوری که کمترین آسیب ممکن رو وارد کنه. من HashMap
مربوط به آرگومانهای بولی (Boolean
) رو تغییر دادم تا به جای نگهداری مقدار نهایی، یک ArgumentMarshaler
نگهداری کنه.
private Map<Character, ArgumentMarshaler> booleanArgs =
new HashMap<Character, ArgumentMarshaler>();
این تغییر چند خط از کد رو خراب کرد، که من سریعاً اصلاحشون کردم.
...
private void parseBooleanSchemaElement(char elementId)
{
booleanArgs.put(elementId, new BooleanArgumentMarshaler());
}
...
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.get(argChar).setBoolean(value);
}
...
public boolean getBoolean(char arg) {
return falseIfNull(booleanArgs.get(arg).getBoolean());
}
دقت کنید که این تغییرات دقیقاً در همون بخشهایی اعمال شدن که قبلاً هم بهشون اشاره کردم: تحلیل (parse
)، تنظیم (set
) و دریافت (get
) مقدار برای نوع آرگومان.
با وجود اینکه این تغییر خیلی کوچک بود، ولی متأسفانه باعث شد بعضی از تستها fail بشن.
اگر به متد getBoolean
دقت کنید، میبینید که اگه این تابع رو با آرگومانی مثل 'y' صدا بزنید، ولی هیچ آرگومان 'y'ای وجود نداشته باشه، اونوقت booleanArgs.get('y')
مقدار null
برمیگردونه و این باعث میشه که تابع، یه استثنای NullPointerException
بندازه.
قبلاً برای جلوگیری از این اتفاق از تابعی به نام falseIfNull
استفاده میکردم، ولی با تغییری که دادم، دیگه این تابع کارایی نداشت.
رویکرد تدریجی (Incrementalism) ایجاب میکرد که اول همین مشکل رو برطرف کنم، قبل از اینکه تغییرات دیگهای اعمال کنم.
خوشبختانه رفع این مشکل خیلی سخت نبود. فقط کافی بود بررسی null بودن رو جابهجا کنم — دیگه لازم نبود بررسی کنم که مقدار بولی null هست یا نه؛ باید بررسی میکردم که آیا ArgumentMarshaler
موجود هست یا نه.
اول از همه، فراخوانی تابع falseIfNull
در getBoolean
رو حذف کردم چون دیگه بیفایده بود، و بعد خود تابع falseIfNull
رو هم کاملاً پاک کردم.
تستها هنوز هم به همون شکل fail میشدن، و این یعنی با اطمینان میتونستم بگم که اشکال جدیدی ایجاد نکردم.
public boolean getBoolean(char arg)
{
return booleanArgs.get(arg).getBoolean();
}
در مرحلهٔ بعد، تابع رو به دو خط تقسیم کردم و نمونهٔ ArgumentMarshaler
رو داخل یک متغیر جداگانه به نام argumentMarshaller
قرار دادم.
از اسم طولانی این متغیر خوشم نیومد؛ خیلی تکراری و شلوغکننده بود و باعث میشد تابع ظاهر نامرتبی داشته باشه.
پس اسمش رو کوتاه کردم و گذاشتم am
[یادداشت N5].
public boolean getBoolean(char arg)
{
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am.getBoolean();
}
و بعدش منطق تشخیص مقدار null
رو اضافه کردم.
public boolean getBoolean(char arg)
{
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && am.getBoolean();
}
آرگومانهای رشتهای (String Arguments)
اضافه کردن آرگومانهای String
خیلی شبیه به اضافه کردن آرگومانهای boolean
بود.
باید HashMap
مربوطه رو تغییر میدادم و مطمئن میشدم که توابع parse،
set
و get
بهدرستی کار میکنن.
نباید چیز شگفتانگیزی در ادامه وجود داشته باشه، مگر اینکه متوجه بشید که من بخش زیادی از پیادهسازی marshalling
رو در کلاس پایهٔ ArgumentMarshaller
قرار دادم،
بهجای اینکه این منطق رو بین کلاسهای مشتقشده پخش کنم.
private Map<Character, ArgumentMarshaler> stringArgs
= new HashMap<Character, ArgumentMarshaler> ();
...
private void parseStringSchemaElement (char elementId)
{
stringArgs.put (elementId, new StringArgumentMarshaler ());
}
...
private void setStringArg (char argChar) throws ArgsException
{
currentArgument++;
try
{
stringArgs.get (argChar).setString (args[currentArgument]);
}
catch (ArrayIndexOutOfBoundsException e)
{
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException ();
}
}
...
public String getString (char arg)
{
Args.ArgumentMarshaler am = stringArgs.get (arg);
return am == null ? "" : am.getString ();
}
...
private class ArgumentMarshaler
{
private boolean booleanValue = false;
private String stringValue;
public void setBoolean (boolean value)
{
booleanValue = value;
}
public boolean getBoolean ()
{
return booleanValue;
}
public void setString (String s)
{
stringValue = s;
}
public String getString ()
{
return stringValue == null ? "" : stringValue;
}
}
باز هم، این تغییرات یکییکی و بهصورت تدریجی انجام شدن، بهطوری که تستها همچنان قابل اجرا بودن—حتی اگر همهشون پاس نمیکردن.
هر وقت یکی از تستها fail
میشد، اول اونو اصلاح میکردم و دوباره به حالت پاسشده درمیآوردم، بعد سراغ تغییر بعدی میرفتم.
تا اینجا احتمالاً متوجه هدف کلی من شدهاید:
میخواستم تمام رفتارهای فعلی marshalling
رو وارد کلاس پایهی ArgumentMarshaler
کنم،
و بعدش بهتدریج این رفتارها رو به کلاسهای مشتقشده انتقال بدم.
این کار باعث میشه برنامه در طول فرایند تغییر ساختار، همچنان کار کنه و نشکنه.
گام بعدی هم مشخص بود: باید قابلیت پردازش آرگومانهای عدد صحیح (int
) رو به ArgumentMarshaler
منتقل میکردم.
و باز هم، مثل قبل، هیچ شگفتی خاصی در این مرحله وجود نداشت.
private Map<Character, ArgumentMarshaler> intArgs
= new HashMap<Character, ArgumentMarshaler> ();
private void parseIntegerSchemaElement (char elementId)
{
intArgs.put (elementId, new IntegerArgumentMarshaler ());
}
...
private void setIntArg (char argChar) throws ArgsException
{
currentArgument++;
String parameter = null;
try
{
parameter = args[currentArgument];
intArgs.get (argChar).setInteger (Integer.parseInt (parameter));
}
catch (ArrayIndexOutOfBoundsException e)
{
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException ();
}
catch (NumberFormatException e)
{
valid = false;
errorArgumentId = argChar;
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw new ArgsException ();
}
}
...
public int getInt (char arg)
{
Args.ArgumentMarshaler am = intArgs.get (arg);
return am == null ? 0 : am.getInteger ();
}
...
private class ArgumentMarshaler
{
private boolean booleanValue = false;
private String stringValue;
private int integerValue;
public void setBoolean (boolean value)
{
booleanValue = value;
}
public boolean getBoolean ()
{
return booleanValue;
}
public void setString (String s)
{
stringValue = s;
}
public String getString ()
{
return stringValue == null ? "" : stringValue;
}
public void setInteger (int i)
{
integerValue = i;
}
public int getInteger ()
{
return integerValue;
}
}
وقتی که تمام منطق marshalling
به کلاس پایهی ArgumentMarshaler
منتقل شد، شروع کردم به انتقال عملکردها به کلاسهای مشتقشده.
اولین قدم، منتقل کردن تابع setBoolean
به کلاس BooleanArgumentMarshaller
بود و مطمئن شدم که این تابع بهدرستی فراخوانی میشه.
برای این کار، یک متد انتزاعی (abstract
) به نام set
ایجاد کردم.
این کار پایهای بود برای اینکه هر کلاس مشتقشده، رفتار مخصوص به خودش رو در متد set
پیادهسازی کنه.
private abstract class ArgumentMarshaler {
protected boolean booleanValue = false;
private String stringValue;
private int integerValue;
public void setBoolean(boolean value) {
booleanValue = value;
}
public boolean getBoolean() {
return booleanValue;
}
public void setString(String s) {
stringValue = s;
}
public String getString() {
return stringValue == null ? "" : stringValue;
}
public void setInteger(int i) {
integerValue = i;
}
public int getInteger() {
return integerValue;
}
public abstract void set(String s);
}
سپس متد set
رو در کلاس BooleanArgumentMarshaller
پیادهسازی کردم:
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
public void set(String s) {
booleanValue = true;
}
}
و در نهایت، فراخوانی تابع setBoolean
رو با فراخوانی set
جایگزین کردم:
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.get(argChar).set("true");
}
تمام تستها همچنان پاس میشدن، و چون این تغییر باعث شد که متد set حالا در BooleanArgumentMarshaller
پیادهسازی شده باشه، متد setBoolean
رو از کلاس پایهی ArgumentMarshaler
حذف کردم.
به این نکته دقت کن که متد انتزاعی set
یک آرگومان از نوع String
دریافت میکنه،
اما پیادهسازی اون در BooleanArgumentMarshaller
اصلاً از این آرگومان استفاده نمیکنه.
من این آرگومان رو عمداً اضافه کردم، چون میدونستم که کلاسهای StringArgumentMarshaller
و IntegerArgumentMarshaller
به اون نیاز خواهند داشت.
در ادامه، تصمیم گرفتم تابع get
رو هم به BooleanArgumentMarshaler
منتقل کنم.
انتقال توابع get
معمولاً کمی ناخوشایند هست، چون باید مقدار بازگشتی از نوع Object
باشه
و در این مورد، باید به Boolean
تبدیل (cast
) بشه:
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && (Boolean) am.get();
}
برای اینکه کد کامپایل بشه، ابتدا متد get
رو به صورت پیشفرض در ArgumentMarshaler
اضافه کردم:
private abstract class ArgumentMarshaler {
...
public Object get() {
return null;
}
}
کد کامپایل شد اما تستها شکست خوردن. برای درست کردن تستها،
متد get
رو abstract
کردم و در BooleanArgumentMarshaler
پیادهسازیاش کردم:
private abstract class ArgumentMarshaler {
...
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;
}
}
و حالا دوباره همهی تستها پاس میشن.
با این کار، متدهای set
و get
بهطور کامل به کلاس BooleanArgumentMarshaler
منتقل شدن.
این اجازه رو بهم داد که:
متد قدیمی getBoolean
رو از ArgumentMarshaler
حذف کنم
متغیر booleanValue
رو از کلاس پایه حذف و در BooleanArgumentMarshaler
به صورت private
منتقل کنم
بعد از این، دقیقاً همین الگو رو برای آرگومانهای String
هم پیادهسازی کردم:
تابعهای set
و get
رو منتقل کردم، تابعهای بلااستفاده رو حذف کردم، و متغیرها رو جابجا کردم.
private void setStringArg(char argChar) throws ArgsException {
currentArgument++;
try {
stringArgs.get(argChar).set(args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
...
public String getString(char arg) {
Args.ArgumentMarshaler am = stringArgs.get(arg);
return am == null ? "" : (String) am.get();
}
...
private abstract class ArgumentMarshaler {
private int integerValue;
public void setInteger(int i) {
integerValue = i;
}
public int getInteger() {
return integerValue;
}
public abstract void set(String s);
public abstract Object get();
}
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
private boolean booleanValue = false;
public void set(String s) {
booleanValue = true;
}
public Object get() {
return booleanValue;
}
}
private class StringArgumentMarshaler extends ArgumentMarshaler {
private String stringValue = "";
public void set(String s) {
stringValue = s;
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
public void set(String s) {}
public Object get() {
return null;
}
}
در نهایت، من همین فرایند را برای اعداد صحیح (integers
) تکرار کردم. این کار کمی پیچیدهتر بود زیرا اعداد صحیح نیاز به تجزیه (parsing
) داشتند و عملیات تجزیه میتواند استثنا (exception
) ایجاد کند. اما نتیجه بهتر شد زیرا کل مفهوم NumberFormatException
در داخل کلاس IntegerArgumentMarshaler
پنهان شد.
private boolean isIntArg(char argChar) {
return intArgs.containsKey(argChar);
}
private void setIntArg(char argChar) throws ArgsException {
currentArgument++;
String parameter = null;
try {
parameter = args[currentArgument];
intArgs.get(argChar).set(parameter);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
... private void setBooleanArg(char argChar) {
try {
booleanArgs.get(argChar).set("true");
} catch (ArgsException e) {
}
}
... public int getInt(char arg) {
Args.ArgumentMarshaler am = intArgs.get(arg);
return am == null ? 0 : (Integer) am.get();
}
... private abstract class ArgumentMarshaler {
public abstract void set(String s) throws ArgsException;
public abstract Object get();
}
... private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}
مطمئناً، تستها همچنان موفق بودند. سپس، من سه نقشه مختلف (Map
) که در ابتدای الگوریتم وجود داشتند را حذف کردم. این کار سیستم را به طور کلی جنریکتر کرد. اما نمیتوانستم آنها را فقط با حذف کردن از سیستم پاک کنم زیرا این کار باعث خراب شدن سیستم میشد. در عوض، من یک نقشه جدید برای ArgumentMarshaler
اضافه کردم و سپس به طور تدریجی، هر یک از متدها را تغییر دادم تا از این نقشه جدید به جای سه نقشه اصلی استفاده کنند.
public class Args {
... private Map<Character, ArgumentMarshaler> booleanArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> stringArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> intArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
... private void parseBooleanSchemaElement(char elementId) {
ArgumentMarshaler m = new BooleanArgumentMarshaler();
booleanArgs.put(elementId, m);
marshalers.put(elementId, m);
}
private void parseIntegerSchemaElement(char elementId) {
ArgumentMarshaler m = new IntegerArgumentMarshaler();
intArgs.put(elementId, m);
marshalers.put(elementId, m);
}
private void parseStringSchemaElement(char elementId) {
ArgumentMarshaler m = new StringArgumentMarshaler();
stringArgs.put(elementId, m);
marshalers.put(elementId, m);
}
}
مطمئناً، تمام تستها همچنان موفق بودند. سپس، من متد isBooleanArg
را از این حالت تغییر دادم:
private boolean isBooleanArg(char argChar) {
return booleanArgs.containsKey(argChar);
}
private boolean isBooleanArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof BooleanArgumentMarshaler;
}
تستها همچنان موفق بودند. بنابراین، من همین تغییر را برای متدهای isIntArg
و isStringArg
نیز اعمال کردم.
private boolean isIntArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof IntegerArgumentMarshaler;
}
private boolean isStringArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof StringArgumentMarshaler;
}
تستها همچنان موفق بودند. بنابراین، من تمام تماسهای تکراری به marshalers.get
را به شکل زیر حذف کردم:
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (isBooleanArg(m))
setBooleanArg(argChar);
else if (isStringArg(m))
setStringArg(argChar);
else if (isIntArg(m))
setIntArg(argChar);
else
return false;
return true;
}
private boolean isIntArg(ArgumentMarshaler m) {
return m instanceof IntegerArgumentMarshaler;
}
private boolean isStringArg(ArgumentMarshaler m) {
return m instanceof StringArgumentMarshaler;
}
private boolean isBooleanArg(ArgumentMarshaler m) {
return m instanceof BooleanArgumentMarshaler;
}
این کار هیچ دلیلی برای وجود متدهای سهگانه isxxxArg
باقی نگذاشت. بنابراین، من آنها را به صورت درونخطی (inline) تغییر دادم:
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(argChar);
else if (m instanceof StringArgumentMarshaler)
setStringArg(argChar);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(argChar);
else
return false;
return true;
}
سپس، من شروع به استفاده از نقشه marshalers
در توابع set کردم و استفاده از سه نقشه دیگر را متوقف کردم. من ابتدا با مقادیر بولی (boolean
) شروع کردم.
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(argChar);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(argChar);
else
return false;
return true;
}
...
private void setBooleanArg(ArgumentMarshaler m) {
try {
m.set("true"); // was: booleanArgs.get(argChar).set("true");
}
catch (ArgsException e) {}
}
تستها همچنان موفق بودند، بنابراین همین کار را برای رشتهها (Strings
) و اعداد صحیح (Integers
) انجام دادم. این کار به من اجازه داد تا برخی از کدهای پیچیده مدیریت استثناها را درون تابع setArgument
ادغام کنم.
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
else
return false;
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
private void setIntArg(ArgumentMarshaler m) throws ArgsException {
currentArgument++;
String parameter = null;
try {
parameter = args[currentArgument];
m.set(parameter);
} catch (ArrayIndexOutOfBoundsException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
private void setStringArg(ArgumentMarshaler m) throws ArgsException {
currentArgument++;
try {
m.set(args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
من نزدیک بودم که بتوانم سه نقشه قدیمی را حذف کنم. اولین کاری که باید میکردم این بود که تابع getBoolean
را از این حالت تغییر دهم:
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && (Boolean) am.get();
}
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
boolean b = false;
try {
b = am != null && (Boolean) am.get();
} catch (ClassCastException e) {
b = false;
}
return b;
}
این تغییر آخر ممکن است شگفتانگیز به نظر برسد. چرا ناگهان تصمیم گرفتم که با ClassCastException
برخورد کنم؟ دلیل این است که من یک مجموعه از تستهای واحد و یک مجموعه جداگانه از تستهای پذیرش که در FitNesse
نوشته شدهاند، دارم. مشخص شد که تستهای FitNesse
تضمین میکنند که اگر از getBoolean
برای یک آرگومان غیر بولی استفاده کنید، مقدار false
دریافت خواهید کرد، اما تستهای واحد این کار را انجام نمیدهند. تا به این نقطه، من تنها تستهای واحد را اجرا کرده بودم.
این تغییر آخر به من این امکان را داد که استفاده دیگری از نقشه بولی را حذف کنم:
private void parseBooleanSchemaElement(char elementId) {
ArgumentMarshaler m = new BooleanArgumentMarshaler();
//booleanArgs.put(elementId, m);
marshalers.put(elementId, m);
}
و حالا میتوانیم نقشهی Boolean
را حذف کنیم.
public class Args {
...
//private Map<Character, ArgumentMarshaler> booleanArgs =
//new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> stringArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> intArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
...
سپس آرگومانهای رشتهای و عدد صحیح را به همان شیوه منتقل کردم و کمی پاکسازی در بخش boolean
ها انجام دادم.
private void parseBooleanSchemaElement(char elementId) {
marshalers.put(elementId, new BooleanArgumentMarshaler());
}
private void parseIntegerSchemaElement(char elementId) {
marshalers.put(elementId, new IntegerArgumentMarshaler());
}
private void parseStringSchemaElement(char elementId) {
marshalers.put(elementId, new StringArgumentMarshaler());
}
...
public String getString(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? "" : (String) am.get();
} catch (ClassCastException e) {
return "";
}
}
public int getInt(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Integer) am.get();
} catch (Exception e) {
return 0;
}
}
...
public class Args {
...
//private Map<Character, ArgumentMarshaler> stringArgs =
//new HashMap<Character, ArgumentMarshaler>();
//private Map<Character, ArgumentMarshaler> intArgs =
//new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
...
}
سپس سه متد parse
را درونخطی (inline) کردم چون دیگر کار خاصی انجام نمیدادند.
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (isBooleanSchemaElement(elementTail))
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (isStringSchemaElement(elementTail))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (isIntegerSchemaElement(elementTail)) {
marshalers.put(elementId, new IntegerArgumentMarshaler());
} else {
throw new ParseException(
String.format(
"Argument: %c has invalid format: %s.", elementId, elementTail),
0);
}
}
خُب، حالا بیایید دوباره به تصویر کلی نگاه کنیم. Listing 14-12 نسخهی فعلی کلاس Args
را نشان میدهد.
Listing 14-12 - Args.java (After first refactoring)
package com.objectmentor.utilities.getopts;
import java.text.ParseException;
import java.util.*;
public class Args {
private String schema;
private String[] args;
private boolean valid = true;
private Set<Character> unexpectedArguments = new TreeSet<Character>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
private Set<Character> argsFound = new HashSet<Character>();
private int currentArgument;
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
private enum ErrorCode {
OK,
MISSING_STRING,
MISSING_INTEGER,
INVALID_INTEGER,
UNEXPECTED_ARGUMENT
}
public Args(String schema, String[] args) throws ParseException {
this.schema = schema;
this.args = args;
valid = parse();
}
private boolean parse() throws ParseException {
if (schema.length() == 0 && args.length == 0)
return true;
parseSchema();
try {
parseArguments();
} catch (ArgsException e) {
}
return valid;
}
private boolean parseSchema() throws ParseException {
for (String element : schema.split(",")) {
if (element.length() > 0) {
String trimmedElement = element.trim();
parseSchemaElement(trimmedElement);
}
}
return true;
}
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (isBooleanSchemaElement(elementTail))
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (isStringSchemaElement(elementTail))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (isIntegerSchemaElement(elementTail)) {
marshalers.put(elementId, new IntegerArgumentMarshaler());
} else {
throw new ParseException(
String.format("Argument: %c has invalid format: %s.", elementId,
elementTail),
0);
}
}
private void validateSchemaElementId(char elementId) throws ParseException {
if (!Character.isLetter(elementId)) {
throw new ParseException(
"Bad character:" + elementId + "in Args format: " + schema, 0);
}
}
private boolean isStringSchemaElement(String elementTail) {
return elementTail.equals("*");
}
private boolean isBooleanSchemaElement(String elementTail) {
return elementTail.length() == 0;
}
private boolean isIntegerSchemaElement(String elementTail) {
return elementTail.equals("#");
}
private boolean parseArguments() throws ArgsException {
for (currentArgument = 0; currentArgument < args.length;
currentArgument++) {
String arg = args[currentArgument];
parseArgument(arg);
}
return true;
}
private void parseArgument(String arg) throws ArgsException {
if (arg.startsWith("-"))
parseElements(arg);
}
private void parseElements(String arg) throws ArgsException {
for (int i = 1; i < arg.length(); i++) parseElement(arg.charAt(i));
}
private void parseElement(char argChar) throws ArgsException {
if (setArgument(argChar))
argsFound.add(argChar);
else {
unexpectedArguments.add(argChar);
errorCode = ErrorCode.UNEXPECTED_ARGUMENT;
valid = false;
}
}
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
else
return false;
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
private void setIntArg(ArgumentMarshaler m) throws ArgsException {
currentArgument++;
String parameter = null;
try {
parameter = args[currentArgument];
m.set(parameter);
} catch (ArrayIndexOutOfBoundsException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
private void setStringArg(ArgumentMarshaler m) throws ArgsException {
currentArgument++;
try {
m.set(args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
private void setBooleanArg(ArgumentMarshaler m) {
try {
m.set("true");
} catch (ArgsException e) {
}
}
public int cardinality() {
return argsFound.size();
}
public String usage() {
if (schema.length() > 0)
return "-[" + schema + "]";
else
return "";
}
public String errorMessage() throws Exception {
switch (errorCode) {
case OK:
throw new Exception("TILT: Should not get here.");
case UNEXPECTED_ARGUMENT:
return unexpectedArgumentMessage();
case MISSING_STRING:
return String.format("Could not find string parameter for -%c.",
errorArgumentId);
case INVALID_INTEGER:
return String.format(
"Argument -%c expects an integer but was '%s'.",
errorArgumentId, errorParameter);
case MISSING_INTEGER:
return String.format(
"Could not find integer parameter for -%c.",
errorArgumentId);
}
return "";
}
private String unexpectedArgumentMessage() {
StringBuffer message = new StringBuffer("Argument(s) -");
for (char c : unexpectedArguments) {
message.append(c);
}
message.append(" unexpected.");
return message.toString();
}
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
boolean b = false;
try {
b = am != null && (Boolean) am.get();
} catch (ClassCastException e) {
b = false;
}
return b;
}
public String getString(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? "" : (String) am.get();
} catch (ClassCastException e) {
return "";
}
}
public int getInt(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Integer) am.get();
} catch (Exception e) {
return 0;
}
}
public boolean has(char arg) {
return argsFound.contains(arg);
}
public boolean isValid() {
return valid;
}
private class ArgsException extends Exception {}
private abstract class ArgumentMarshaler {
public abstract void set(String s) throws ArgsException;
public abstract Object get();
}
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
private boolean booleanValue = false;
public void set(String s) {
booleanValue = true;
}
public Object get() {
return booleanValue;
}
}
private class StringArgumentMarshaler extends ArgumentMarshaler {
private String stringValue = "";
public void set(String s) {
stringValue = s;
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}
}
بعد از تمام این کارها، نتیجه کمی ناامیدکننده است. ساختار کمی بهتر شده، اما هنوز تمام آن متغیرها در بالای کلاس هستند؛ هنوز یک بررسی نوع وحشتناک در setArgument
داریم؛ و تمام آن توابع set
واقعاً زشت هستند. تازه به پردازش خطاها هم اشارهای نکردهایم. هنوز کار زیادی پیش رو داریم.
واقعاً دوست دارم از شر آن بررسی نوع در setArgument
خلاص شوم [G23]
. چیزی که میخواهم، یک فراخوانی واحد به ArgumentMarshaler.set
است. این یعنی باید توابع setIntArg،
setStringArg
و setBooleanArg
را به کلاسهای مشتقشدهی ArgumentMarshaler
منتقل کنم. اما یک مشکل وجود دارد.
اگر دقیقتر به setIntArg
نگاه کنید، متوجه میشوید که از دو متغیر نمونهای استفاده میکند: args
و currentArg
. برای اینکه بتوانم setIntArg
را به داخل IntegerArgumentMarshaler
منتقل کنم، باید هر دو را بهعنوان آرگومان به تابع پاس بدهم. این کار ناپسند است [F1]
. ترجیح میدهم فقط یک آرگومان پاس بدهم نه دو تا. خوشبختانه، یک راهحل ساده وجود دارد. میتوانیم آرایهی args
را به یک لیست تبدیل کنیم و یک Iterator
را به توابع set
پاس بدهیم.
کاری که انجام دادم طی ده مرحله بود و پس از هر مرحله تمام تستها را پاس میکردم. اما من فقط نتیجه را به شما نشان میدهم. شما باید بتوانید بیشتر مراحل کوچک را خودتان حدس بزنید.
public class Args {
private String schema;
//private String[] args;
private boolean valid = true;
private Set<Character> unexpectedArguments = new TreeSet<Character>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
private Set<Character> argsFound = new HashSet<Character>();
private Iterator<String> currentArgument;
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
private List<String> argsList;
private enum ErrorCode {
OK,
MISSING_STRING,
MISSING_INTEGER,
INVALID_INTEGER,
UNEXPECTED_ARGUMENT
}
public Args(String schema, String[] args) throws ParseException {
this.schema = schema;
argsList = Arrays.asList(args);
valid = parse();
}
private boolean parse() throws ParseException {
if (schema.length() == 0 && argsList.size() == 0)
return true;
parseSchema();
try {
parseArguments();
} catch (ArgsException e) {
}
return valid;
}
-- -private boolean parseArguments() throws ArgsException {
for (currentArgument = argsList.iterator();
currentArgument.hasNext();) {
String arg = currentArgument.next();
parseArgument(arg);
}
return true;
}
-- -private void setIntArg(ArgumentMarshaler m) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
m.set(parameter);
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
private void setStringArg(ArgumentMarshaler m) throws ArgsException {
try {
m.set(currentArgument.next());
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
}
اینها تغییرات سادهای بودند که باعث شدند تمام تستها همچنان پاس شوند. حالا میتوانیم شروع به انتقال توابع set
به کلاسهای مشتقشدهی مناسب کنیم. ابتدا باید تغییر زیر را در تابع setArgument
اعمال کنم:
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
//else
//return false;
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
این تغییر مهم است چون میخواهیم زنجیره if-else
را بهطور کامل حذف کنیم. بنابراین، لازم بود شرط خطا را از آن خارج کنیم.
حالا میتوانیم شروع به انتقال توابع set کنیم. تابع setBooleanArg
ساده است، پس ابتدا آن را آماده میکنیم. هدف ما این است که تابع setBooleanArg
را بهگونهای تغییر دهیم که فقط عملیات را به کلاس BooleanArgumentMarshaler
واگذار کند.
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m, currentArgument);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
---
private void setBooleanArg(ArgumentMarshaler m,
Iterator<String> currentArgument) throws ArgsException {
//try {
m.set("true");
// catch (ArgsException e) {
// }
}
مگر همین پردازش استثنا را تازه اضافه نکرده بودیم؟ اضافه کردن چیزهایی برای اینکه بعداً آنها را حذف کنیم، در فرایند بازآرایی (Refactoring) بسیار رایج است. کوچکی گامها و نیاز به حفظ عملکرد صحیح برنامه باعث میشود که چیزها را زیاد جابهجا کنید. بازآرایی خیلی شبیه حل کردن مکعب روبیک است. برای رسیدن به یک هدف بزرگ، گامهای کوچکی زیادی لازم است. هر گام، امکان اجرای گام بعدی را فراهم میکند.
چرا آن iterator
را ارسال کردیم وقتی که setBooleanArg
قطعاً به آن نیازی ندارد؟ چون setIntArg
و setStringArg
نیاز خواهند داشت! و از آنجایی که میخواهم هر سه تابع را از طریق یک متد انتزاعی در ArgumentMarshaller
پیادهسازی کنم، باید آن را به setBooleanArg
نیز ارسال کنم.
پس حالا تابع setBooleanArg
بیاستفاده شده است. اگر تابعی به نام set
در کلاس ArgumentMarshaler
وجود داشت، میتوانستیم مستقیماً آن را فراخوانی کنیم. بنابراین، زمان آن رسیده که این تابع را اضافه کنیم! اولین گام این است که یک متد انتزاعی (abstract
) جدید به کلاس ArgumentMarshaler
اضافه کنیم.
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();
}
مطمئناً این تغییر باعث شکست در همه مشتقات (derivatives
) میشود. پس بیایید متد جدید را در هر کدام پیادهسازی کنیم.
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
private boolean booleanValue = false;
public void set(Iterator<String> currentArgument) throws ArgsException {
// booleanValue = true;
}
public void set(String s) {
booleanValue = true;
}
public Object get() {
return booleanValue;
}
}
private class StringArgumentMarshaler extends ArgumentMarshaler {
private String stringValue = "";
public void set(Iterator<String> currentArgument) throws ArgsException {}
public void set(String s) {
stringValue = s;
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {}
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}
و حالا میتوانیم setBooleanArg
را حذف کنیم!
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
m.set(currentArgument);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
تمام تستها پاس شدند و تابع set
به درستی به BooleanArgumentMarshaler
منتقل شد! حالا میتوانیم همین کار را برای رشتهها و اعداد صحیح انجام دهیم.
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
m.set(currentArgument);
else if (m instanceof StringArgumentMarshaler)
m.set(currentArgument);
else if (m instanceof IntegerArgumentMarshaler)
m.set(currentArgument);
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
---
private class StringArgumentMarshaler extends ArgumentMarshaler {
private String stringValue = "";
public void set(Iterator<String> currentArgument) throws ArgsException {
try {
stringValue = currentArgument.next();
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
public void set(String s) {}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
set(parameter);
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
m.set(currentArgument);
return true;
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
}
حالا میتوانیم برخی از توابع اضافی در IntegerArgumentMarshaler
را حذف کرده و کمی آن را تمیز کنیم.
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0 public void set(Iterator<String> currentArgument)
throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
intValue = Integer.parseInt(parameter);
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}
ما همچنین میتوانیم ArgumentMarshaler
را به یک اینترفیس تبدیل کنیم.
private interface ArgumentMarshaler {
void set(Iterator<String> currentArgument) throws ArgsException;
Object get();
}
حال ببینیم اضافه کردن یک نوع آرگومان جدید به ساختارمان چقدر آسان است. این کار باید تغییرات بسیار کمی را نیاز داشته باشد و این تغییرات باید مجزا باشند. ابتدا با افزودن یک تست جدید شروع میکنیم تا بررسی کنیم که آرگومان نوع double
به درستی کار میکند.
public void testSimpleDoublePresent() throws Exception {
Args args = new Args("x##", new String[] {"-x","42.3"});
assertTrue(args.isValid());
assertEquals(1, args.cardinality());
assertTrue(args.has('x'));
assertEquals(42.3, args.getDouble('x'), .001);
}
حالا کد تجزیه اسکیمای خود را تمیز میکنیم و تشخیص ##
برای نوع آرگومان double
را اضافه میکنیم.
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (elementTail.length() == 0)
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (elementTail.equals("*"))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (elementTail.equals("#"))
marshalers.put(elementId, new IntegerArgumentMarshaler());
else if (elementTail.equals("##"))
marshalers.put(elementId, new DoubleArgumentMarshaler());
else
throw new ParseException(
String.format(
"Argument: %c has invalid format: %s.", elementId, elementTail),
0);
}
سپس کلاس DoubleArgumentMarshaler
را مینویسیم.
private class DoubleArgumentMarshaler implements ArgumentMarshaler {
private double doubleValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
doubleValue = Double.parseDouble(parameter);
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_DOUBLE;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_DOUBLE;
throw new ArgsException();
}
}
public Object get() {
return doubleValue;
}
}
این ما را مجبور میکند تا یک ErrorCode
جدید اضافه کنیم.
private enum ErrorCode {
OK,
MISSING_STRING,
MISSING_INTEGER,
INVALID_INTEGER,
UNEXPECTED_ARGUMENT,
MISSING_DOUBLE,
INVALID_DOUBLE
}
و ما نیاز به متد getDouble
داریم.
public double getDouble(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Double) am.get();
} catch (Exception e) {
return 0.0;
}
}
و همه تستها موفقیتآمیز بودند! این کار نسبتاً بیدردسر بود. حالا بیایید مطمئن شویم که پردازش خطا به درستی کار میکند. تست بعدی بررسی میکند که آیا در صورتی که یک رشته غیرقابل تجزیه به یک آرگومان ##
داده شود، خطا اعلام میشود یا خیر.
public void testInvalidDouble() throws Exception {
Args args = new Args("x##", new String[] {"-x", "Forty two"});
assertFalse(args.isValid());
assertEquals(0, args.cardinality());
assertFalse(args.has('x'));
assertEquals(0, args.getInt('x'));
assertEquals("Argument -x expects a double but was 'Forty two'.",
args.errorMessage());
}
-- -public String errorMessage() throws Exception {
switch (errorCode) {
case OK:
throw new Exception("TILT: Should not get here.");
case UNEXPECTED_ARGUMENT:
return unexpectedArgumentMessage();
case MISSING_STRING:
return String.format(
"Could not find string parameter for -%c.", errorArgumentId);
case INVALID_INTEGER:
return String.format(
"Argument -%c expects an integer but was '%s'.",
errorArgumentId, errorParameter);
case MISSING_INTEGER:
return String.format(
"Could not find integer parameter for -%c.", errorArgumentId);
case INVALID_DOUBLE:
return String.format("Argument -%c expects a double but was '%s'.",
errorArgumentId, errorParameter);
case MISSING_DOUBLE:
return String.format(
"Could not find double parameter for -%c.", errorArgumentId);
}
return "";
}
و تستها موفقیتآمیز بودند. تست بعدی بررسی میکند که آیا آرگومان double
گمشده بهدرستی تشخیص داده میشود
public void testMissingDouble() throws Exception {
Args args = new Args("x##", new String[] {"-x"});
assertFalse(args.isValid());
assertEquals(0, args.cardinality());
assertFalse(args.has('x'));
assertEquals(0.0, args.getDouble('x'), 0.01);
assertEquals(
"Could not find double parameter for -x.", args.errorMessage());
}
این تست همانطور که انتظار داشتیم موفقیتآمیز بود. ما آن را صرفاً برای تکمیل کار نوشتیم.
کد مربوط به استثنا کمی نامرتب است و واقعاً در کلاس Args
جایگاهی ندارد. همچنین ما ParseException
را پرتاب میکنیم، در حالی که این استثنا به ما تعلق ندارد. بنابراین، بیایید تمام استثناها را در یک کلاس واحد به نام ArgsException
ترکیب کنیم و آن را به یک ماژول اختصاصی منتقل کنیم.
public class ArgsException extends Exception {
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
public ArgsException() {}
public ArgsException(String message) {
super(message);
}
public enum ErrorCode {
OK,
MISSING_STRING,
MISSING_INTEGER,
INVALID_INTEGER,
UNEXPECTED_ARGUMENT,
MISSING_DOUBLE,
INVALID_DOUBLE
}
}
-- -public class Args {
... private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ArgsException.ErrorCode errorCode = ArgsException.ErrorCode.OK;
private List<String> argsList;
public Args(String schema, String[] args) throws ArgsException {
this.schema = schema;
argsList = Arrays.asList(args);
valid = parse();
}
private boolean parse() throws ArgsException {
if (schema.length() == 0 && argsList.size() == 0)
return true;
parseSchema();
try {
parseArguments();
} catch (ArgsException e) {
}
return valid;
}
private boolean parseSchema() throws ArgsException {
...
}
private void parseSchemaElement(String element) throws ArgsException {
... else throw new ArgsException(String.format(
"Argument: %c has invalid format: %s.", elementId, elementTail));
}
private void validateSchemaElementId(char elementId) throws ArgsException {
if (!Character.isLetter(elementId)) {
throw new ArgsException(
"Bad character:" + elementId + "in Args format: " + schema);
}
}
... private void parseElement(char argChar) throws ArgsException {
if (setArgument(argChar))
argsFound.add(argChar);
else {
unexpectedArguments.add(argChar);
errorCode = ArgsException.ErrorCode.UNEXPECTED_ARGUMENT;
valid = false;
}
}
... private class StringArgumentMarshaler implements ArgumentMarshaler {
private String stringValue = "";
public void set(Iterator<String> currentArgument) throws ArgsException {
try {
stringValue = currentArgument.next();
} catch (NoSuchElementException e) {
errorCode = ArgsException.ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler implements ArgumentMarshaler {
private int intValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
intValue = Integer.parseInt(parameter);
} catch (NoSuchElementException e) {
errorCode = ArgsException.ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ArgsException.ErrorCode.INVALID_INTEGER;
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}
private class DoubleArgumentMarshaler implements ArgumentMarshaler {
private double doubleValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
doubleValue = Double.parseDouble(parameter);
} catch (NoSuchElementException e) {
errorCode = ArgsException.ErrorCode.MISSING_DOUBLE;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ArgsException.ErrorCode.INVALID_DOUBLE;
throw new ArgsException();
}
}
public Object get() {
return doubleValue;
}
}
}
این عالی است! اکنون تنها استثنایی که توسط Args
پرتاب میشود، ArgsException
است. انتقال ArgsException
به یک ماژول اختصاصی به ما این امکان را میدهد که بسیاری از کدهای پراکنده مربوط به مدیریت خطا را در آن ماژول جای دهیم و Args
را از این موارد جدا کنیم.
این کار نهتنها مکانی طبیعی و مناسب برای نگهداری این کدها فراهم میکند، بلکه باعث مرتبتر شدن ماژول Args
در آینده میشود.
اکنون ما بهطور کامل کدهای مربوط به مدیریت خطا و استثنا را از ماژول Args
تفکیک کردهایم. (به فهرست 14-13 تا 14-16 مراجعه کنید.) این تغییرات طی حدود 30 گام کوچک انجام شدهاند، در حالی که تمامی تستها در هر مرحله موفقیتآمیز باقی ماندهاند.
Listing 14-13 - ArgsTest.java
package com.objectmentor.utilities.args;
import junit.framework.TestCase;
public class ArgsTest extends TestCase {
public void testCreateWithNoSchemaOrArguments() throws Exception {
Args args = new Args("", new String[0]);
assertEquals(0, args.cardinality());
}
public void testWithNoSchemaButWithOneArgument() throws Exception {
try {
new Args("", new String[] {"-x"});
fail();
} catch (ArgsException e) {
assertEquals(
ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
public void testWithNoSchemaButWithMultipleArguments() throws Exception {
try {
new Args("", new String[] {"-x", "-y"});
fail();
} catch (ArgsException e) {
assertEquals(
ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
public void testNonLetterSchema() throws Exception {
try {
new Args("*", new String[] {});
fail("Args constructor should have thrown exception");
} catch (ArgsException e) {
assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME,
e.getErrorCode());
assertEquals('*', e.getErrorArgumentId());
}
}
public void testInvalidArgumentFormat() throws Exception {
try {
new Args("f~", new String[] {});
fail("Args constructor should have throws exception");
} catch (ArgsException e) {
assertEquals(
ArgsException.ErrorCode.INVALID_FORMAT, e.getErrorCode());
assertEquals('f', e.getErrorArgumentId());
}
}
public void testSimpleBooleanPresent() throws Exception {
Args args = new Args("x", new String[] {"-x"});
assertEquals(1, args.cardinality());
assertEquals(true, args.getBoolean('x'));
}
public void testSimpleStringPresent() throws Exception {
Args args = new Args("x*", new String[] {"-x", "param"});
assertEquals(1, args.cardinality());
assertTrue(args.has('x'));
assertEquals("param", args.getString('x'));
}
public void testMissingStringArgument() throws Exception {
try {
new Args("x*", new String[] {"-x"});
fail();
} catch (ArgsException e) {
assertEquals(
ArgsException.ErrorCode.MISSING_STRING, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
public void testSpacesInFormat() throws Exception {
Args args = new Args("x, y", new String[] {"-xy"});
assertEquals(2, args.cardinality());
assertTrue(args.has('x'));
assertTrue(args.has('y'));
}
public void testSimpleIntPresent() throws Exception {
Args args = new Args("x#", new String[] {"-x", "42"});
assertEquals(1, args.cardinality());
assertTrue(args.has('x'));
assertEquals(42, args.getInt('x'));
}
public void testInvalidInteger() throws Exception {
try {
new Args("x#", new String[] {"-x", "Forty two"});
fail();
} catch (ArgsException e) {
assertEquals(
ArgsException.ErrorCode.INVALID_INTEGER, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
assertEquals("Forty two", e.getErrorParameter());
}
}
public void testMissingInteger() throws Exception {
try {
new Args("x#", new String[] {"-x"});
fail();
} catch (ArgsException e) {
assertEquals(
ArgsException.ErrorCode.MISSING_INTEGER, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
public void testSimpleDoublePresent() throws Exception {
Args args = new Args("x##", new String[] {"-x", "42.3"});
assertEquals(1, args.cardinality());
assertTrue(args.has('x'));
assertEquals(42.3, args.getDouble('x'), .001);
}
public void testInvalidDouble() throws Exception {
try {
new Args("x##", new String[] {"-x", "Forty two"});
fail();
} catch (ArgsException e) {
assertEquals(
ArgsException.ErrorCode.INVALID_DOUBLE, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
assertEquals("Forty two", e.getErrorParameter());
}
}
public void testMissingDouble() throws Exception {
try {
new Args("x##", new String[] {"-x"});
fail();
} catch (ArgsException e) {
assertEquals(
ArgsException.ErrorCode.MISSING_DOUBLE, e.getErrorCode());
assertEquals('x', e.getErrorArgumentId());
}
}
}
Listing 14-14 - ArgsExceptionTest.java
public class ArgsExceptionTest extends TestCase {
public void testUnexpectedMessage() throws Exception {
ArgsException e = new ArgsException(
ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, 'x', null);
assertEquals("Argument -x unexpected.", e.errorMessage());
}
public void testMissingStringMessage() throws Exception {
ArgsException e = new ArgsException(
ArgsException.ErrorCode.MISSING_STRING, 'x', null);
assertEquals(
"Could not find string parameter for -x.", e.errorMessage());
}
public void testInvalidIntegerMessage() throws Exception {
ArgsException e = new ArgsException(
ArgsException.ErrorCode.INVALID_INTEGER, 'x', "Forty two");
assertEquals("Argument -x expects an integer but was 'Forty two'.",
e.errorMessage());
}
public void testMissingIntegerMessage() throws Exception {
ArgsException e = new ArgsException(
ArgsException.ErrorCode.MISSING_INTEGER, 'x', null);
assertEquals(
"Could not find integer parameter for -x.", e.errorMessage());
}
public void testInvalidDoubleMessage() throws Exception {
ArgsException e = new ArgsException(
ArgsException.ErrorCode.INVALID_DOUBLE, 'x', "Forty two");
assertEquals("Argument -x expects a double but was 'Forty two'.",
e.errorMessage());
}
public void testMissingDoubleMessage() throws Exception {
ArgsException e = new ArgsException(
ArgsException.ErrorCode.MISSING_DOUBLE, 'x', null);
assertEquals(
"Could not find double parameter for -x.", e.errorMessage());
}
}
Listing 14-15 - ArgsException.java
public class ArgsException extends Exception {
private char errorArgumentId = '\0';
private String errorParameter = "TILT";
private ErrorCode errorCode = ErrorCode.OK;
public ArgsException() {}
public ArgsException(String message) {
super(message);
}
public ArgsException(ErrorCode errorCode) {
this.errorCode = errorCode;
}
public ArgsException(ErrorCode errorCode, String errorParameter) {
this.errorCode = errorCode;
this.errorParameter = errorParameter;
}
public ArgsException(
ErrorCode errorCode, char errorArgumentId, String errorParameter) {
this.errorCode = errorCode;
this.errorParameter = errorParameter;
this.errorArgumentId = errorArgumentId;
}
public char getErrorArgumentId() {
return errorArgumentId;
}
public void setErrorArgumentId(char errorArgumentId) {
this.errorArgumentId = errorArgumentId;
}
public String getErrorParameter() {
return errorParameter;
}
public void setErrorParameter(String errorParameter) {
this.errorParameter = errorParameter;
}
public ErrorCode getErrorCode() {
return errorCode;
}
public void setErrorCode(ErrorCode errorCode) {
this.errorCode = errorCode;
}
public String errorMessage() throws Exception {
switch (errorCode) {
case OK:
throw new Exception("TILT: Should not get here.");
case UNEXPECTED_ARGUMENT:
return String.format(
"Argument -%c unexpected.", errorArgumentId);
case MISSING_STRING:
return String.format("Could not find string parameter for -%c.",
errorArgumentId);
case INVALID_INTEGER:
return String.format(
"Argument -%c expects an integer but was '%s'.",
errorArgumentId, errorParameter);
case MISSING_INTEGER:
return String.format(
"Could not find integer parameter for -%c.",
errorArgumentId);
case INVALID_DOUBLE:
return String.format(
"Argument -%c expects a double but was '%s'.",
errorArgumentId, errorParameter);
case MISSING_DOUBLE:
return String.format("Could not find double parameter for -%c.",
errorArgumentId);
}
return "";
}
public enum ErrorCode {
OK,
INVALID_FORMAT,
UNEXPECTED_ARGUMENT,
INVALID_ARGUMENT_NAME,
MISSING_STRING,
MISSING_INTEGER,
INVALID_INTEGER,
MISSING_DOUBLE,
INVALID_DOUBLE
}
}
Listing 14-16 - Args.java
public class Args {
private String schema;
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
private Set<Character> argsFound = new HashSet<Character>();
private Iterator<String> currentArgument;
private List<String> argsList;
public Args(String schema, String[] args) throws ArgsException {
this.schema = schema;
argsList = Arrays.asList(args);
parse();
}
private void parse() throws ArgsException {
parseSchema();
parseArguments();
}
private boolean parseSchema() throws ArgsException {
for (String element : schema.split(",")) {
if (element.length() > 0) {
parseSchemaElement(element.trim());
}
}
return true;
}
private void parseSchemaElement(String element) throws ArgsException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (elementTail.length() == 0)
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (elementTail.equals("*"))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (elementTail.equals("#"))
marshalers.put(elementId, new IntegerArgumentMarshaler());
else if (elementTail.equals("##"))
marshalers.put(elementId, new DoubleArgumentMarshaler());
else
throw new ArgsException(
ArgsException.ErrorCode.INVALID_FORMAT, elementId, elementTail);
}
private void validateSchemaElementId(char elementId) throws ArgsException {
if (!Character.isLetter(elementId)) {
throw new ArgsException(
ArgsException.ErrorCode.INVALID_ARGUMENT_NAME, elementId, null);
}
}
private void parseArguments() throws ArgsException {
for (currentArgument = argsList.iterator();
currentArgument.hasNext();) {
String arg = currentArgument.next();
parseArgument(arg);
}
}
private void parseArgument(String arg) throws ArgsException {
if (arg.startsWith("-"))
parseElements(arg);
}
private void parseElements(String arg) throws ArgsException {
for (int i = 1; i < arg.length(); i++) parseElement(arg.charAt(i));
}
private void parseElement(char argChar) throws ArgsException {
if (setArgument(argChar))
argsFound.add(argChar);
else {
throw new ArgsException(
ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, argChar, null);
}
}
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
m.set(currentArgument);
return true;
} catch (ArgsException e) {
e.setErrorArgumentId(argChar);
throw e;
}
}
public int cardinality() {
return argsFound.size();
}
public String usage() {
if (schema.length() > 0)
return "-[" + schema + "]";
else
return "";
}
public boolean getBoolean(char arg) {
ArgumentMarshaler am = marshalers.get(arg);
boolean b = false;
try {
b = am != null && (Boolean) am.get();
} catch (ClassCastException e) {
b = false;
}
return b;
}
public String getString(char arg) {
ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? "" : (String) am.get();
} catch (ClassCastException e) {
return "";
}
}
public int getInt(char arg) {
ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Integer) am.get();
} catch (Exception e) {
return 0;
}
}
public double getDouble(char arg) {
ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Double) am.get();
} catch (Exception e) {
return 0.0;
}
}
public boolean has(char arg) {
return argsFound.contains(arg);
}
}
کلاس Args
بیشتر تغییراتش مربوط به حذف بخشهایی از کد بود. بخش زیادی از کد فقط از داخل Args
منتقل شد و به ArgsException
اضافه گردید. عالی است! همچنین تمام ArgumentMarshallerها
به فایلهای جداگانه منتقل شدند. خیلی بهتر!
بخش زیادی از طراحی خوب نرمافزار، در واقع در مورد بخشبندی است—ایجاد مکانهای مناسب برای قرار دادن انواع مختلف کد. این تفکیک مسئولیتها باعث میشود کد بسیار سادهتر قابل فهم و نگهداری باشد.
نکته جالب توجه، متد errorMessage
در ArgsException
است. به وضوح، قرار دادن کد فرمت کردن پیام خطا در داخل Args
نقض اصل SRP
بود. Args
باید در مورد پردازش آرگومانها باشد، نه فرمت پیامهای خطا. اما آیا واقعاً منطقی است که کد فرمت پیام خطا را در داخل ArgsException
قرار دهیم؟ به طور صریح، این یک مصالحه است. کاربرانی که از پیامهای خطا که توسط ArgsException
فراهم شده است راضی نیستند، باید پیامهای خطای خودشان را بنویسند. اما راحتی در داشتن پیامهای خطای آماده، بیاهمیت نیست.
تا به حال باید واضح شده باشد که ما تقریباً به راهحل نهایی که در ابتدای این فصل آورده شده، نزدیک شدهایم. من باقیمانده تغییرات را به شما میسپارم تا به عنوان تمرین انجام دهید.
نتیجهگیری
کافی نیست که کد کار کند. کد کارا اغلب به شدت خراب است. برنامهنویسهایی که فقط به کارکردن کد اکتفا میکنند، به طور غیرحرفهای عمل میکنند. آنها ممکن است بترسند که وقت کافی برای بهبود ساختار و طراحی کد خود نداشته باشند، اما من با این نظر مخالفم. هیچ چیز به اندازه کد بد تأثیر مخرب و بلندمدتی بر روی یک پروژه توسعه نمیگذارد. برنامهنویسی که فقط به کد کارا اکتفا کند، در نهایت به یک کد خراب تبدیل میشود که بر پروژه تسلط پیدا میکند.
البته که کد بد قابل تمیز کردن است، اما این کار بسیار پرهزینه است. وقتی کد خراب میشود، ماژولها به هم میپیوندند و وابستگیهای پنهان و درهمتنیدهای ایجاد میکنند. پیدا کردن و شکستن این وابستگیهای قدیمی کار زمانبر و سختی است. از طرف دیگر، نگهداری کد تمیز نسبتاً راحت است. اگر صبح یک ماژول را خراب کردید، میتوانید بعدازظهر آن را تمیز کنید. بهتر از آن، اگر پنج دقیقه پیش یک ماژول را خراب کردهاید، بسیار آسان است که همین الان آن را تمیز کنید.
بنابراین راهحل این است که کد خود را به طور مداوم تمیز و ساده نگه دارید. هرگز اجازه ندهید که فساد کد آغاز شود.