Uncle Bob has written a article about craftsmanship, professionalism, and refactoring (Discussion).
However, after reading this wonderful article (If you have not read it, please start now), I found that Im very uncomfortable with the "Switch-Case" (or if - elseif - elseif - else) statements, just like:
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);
}And
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 "";
}
Though those code is clean, but everytime if we wanna add a new type of Argument, we have to add new if-else statement in "parseSchemaElement" method, and add new case statement in "errorMessage". If your Args have to support more than 10 types argument, your if-else(switch-case) statements will be huge and ugly.
Bad smell.
So, can we avoid the Switch-Case statement in our system? Fortunately, the answer is yes: Just use the loop statement instead of it.
Let us see a new version of Args library: First, the Args Class:
namespace ArgsUtility
  {
public class Args
 {
//ArgumentName:ArgumentMashaler
private Dictionary<char, ArgumentMarshalerBase> _myArgumentMashalers = new Dictionary<char, ArgumentMarshalerBase>();
//ArgumentName:ArgumentValue
private Dictionary<char, string> _myArgumentValues = new Dictionary<char,string>();

public Args(string schema, string[] argumens)
 {
this.ParseSchema(schema);
this.ParseArgumentStrings(argumens);
}

private void ParseSchema(string schema)
 {
if (string.IsNullOrEmpty(schema))
 {
throw new Exception("Null Schema");
}

foreach (string s in schema.Split(','))
 {
string schemaNameAndSymbol = s.Trim();
ValidateSchemaElement(schemaNameAndSymbol);

char argumentName = schemaNameAndSymbol[0];
char argumentTypeSymbol = schemaNameAndSymbol[1];

ArgumentMarshalerBase amb = SupportedArguments.Instance.GetArgumentMarshaler(argumentTypeSymbol);
this._myArgumentMashalers.Add(argumentName, amb);
}
}

private void ValidateSchemaElement(string s)
 {
//each schema element should like this: 'x*', only two char
if (s.Length != 2)
 {
throw new Exception("Bad Schema: " + s);
}
}

private void ParseArgumentStrings(string[] values)
 {
IEnumerator ie = values.GetEnumerator();

//ugly code, i think, but all tests can pass, so let me ignore it at this time.
while (ie.MoveNext())
 {
if (ie.Current.ToString().StartsWith("-"))
 {
char argumentName = ie.Current.ToString()[1];
if (ie.MoveNext())
 {
string argumentValue = ie.Current.ToString();
this._myArgumentValues.Add(argumentName, argumentValue);
}
}
}
}

public object this[char argumentName]
 {
get
 {
ArgumentMarshalerBase amb;
if (this._myArgumentMashalers.ContainsKey(argumentName))
 {
amb = this._myArgumentMashalers[argumentName];
}
else
 {
throw new Exception(string.Format("Argument {0} unexpected.", argumentName));
}

string argumentValue;
if (this._myArgumentValues.ContainsKey(argumentName))
 {
argumentValue = this._myArgumentValues[argumentName];
}
else
 {
throw new Exception(string.Format("Can't find {0} parameter for {1}.", amb.ArgumentType, argumentName));
}

return amb.ParseInputValue(argumentValue);
}
}
}
}
Look, in new version of "ParseSchema" method, the if-else statements were gone, and a static method which named "SupportedArguments.Instance.GetArgumentMarshaler(argumentTypeSymbol);" instead of them.
So, let see how "GetArgumentMarshaler(string argumentTypeSymbol)" works:
namespace ArgsUtility.ArgumentMarshaler
  {
class SupportedArguments
 {
private List<ArgumentMarshalerBase> _commonArgs = new List<ArgumentMarshalerBase>();
readonly public static SupportedArguments Instance = new SupportedArguments();

private SupportedArguments()
 {
this._commonArgs.Add(new BooleanArgumentMarshaler());
this._commonArgs.Add(new IntegerArgumentMarshaler());
this._commonArgs.Add(new DoubleArgumentMashaler());
this._commonArgs.Add(new StringArgumentMashaler());
//todo: add more argumentMarshalers
}

public ArgumentMarshalerBase GetArgumentMarshaler(char typeSymbol)
 {
foreach (ArgumentMarshalerBase amb in this._commonArgs)
 {
if (amb.IsSymbolMatched(typeSymbol))
 {
return amb;
}
}
throw new Exception(string.Format("Unknow Argument Symbol:{0}.",typeSymbol));
}

}
}
"GetArgumentMashaler" method check all supported argumentMashalers in _commonArgs collection, check them one by one and find the correctly Argument Mashaler.
And the ArgumentMarshalerBase is:
namespace ArgsUtility.ArgumentMarshaler
  {
abstract class ArgumentMarshalerBase
 {
private char _schemaSymbol;
public ArgumentMarshalerBase(char schemaSymbol)
 {
this._schemaSymbol = schemaSymbol;
}

public bool IsSymbolMatched(char targetSymbol)
 {
return targetSymbol == this._schemaSymbol;
}

public object ParseInputValue(string value)
 {
try
 {
return this.DoParse(value);
}
catch (FormatException ex)
 {
//catch all format problems
throw new Exception(string.Format("'{0}' is an invalid {1} format.", value, this.ArgumentType),ex);
}

}

abstract protected object DoParse(string value);
abstract public string ArgumentType
 {
get;
}
}
}
The BooleanArgumentMarshaler implement as below:
namespace ArgsUtility.ArgumentMarshaler
  {
class BooleanArgumentMarshaler:ArgumentMarshalerBase
 {
public BooleanArgumentMarshaler()
: base('?')
 {
}

protected override object DoParse(string value)
 {
return Boolean.Parse(value);
}

public override string ArgumentType
 {
get
 {
return "Boolean";
}
}
}
}Pretty simply,huh? Now, if we wanna add new type of argument, we just need: 1. Write a new class which implement abstract class: ArgumentMashalerBase 2. In the new class constructor, set the a symbol for it. This symbol will be used in schema string. E.g. the symbol of boolean is "?" and symbol "#" is for integer type argument. You can will see the new argument will works well.
[Test]
public void TestComplexArguments()
 {
 /**//* this schema includes 7 arguments:
* Integer Arguments: i, j
* Boolean Arguments: b
* Double Arguments: d, x
* String Arguments: m, n
*/
string schema = "i#,b?,j#,d&,m*,n*,x&";

//the argument values
 string[] values = new string[] {
"-i", "32",
"-d", "33.4",
"-m", "string 1",
"-b", "false",
"-j", "53",
"-x", "11.2",
"-n", "string 2"};

Args args = new Args(schema, values);

Assert.AreEqual(32, (int)args['i']);
Assert.AreEqual(33.4, (double)args['d']);
Assert.AreEqual("string 2", (string)args['n']);
Assert.AreEqual("string 1", (string)args['m']);
Assert.AreEqual(53, (int)args['j']);
Assert.IsFalse((bool)args['b']);
Assert.AreEqual(11.2, (double)args['x']);
}
 Is that all? NO! Please, do not forget add new test cases for your new argumet ;) Full source code and unit test are HERE. Any suggest is welcome. linkcd 2006/02/21 Update 2006/02/22 Uncle Bob said: "As for the argument about if/else and switch, that's a religious
argument IMHO. Switches and if/else chains may be badly abused, and may
never be necessary, but sometimes they are pleasant to read. My rule
for them is that they should never appear more than once."
My Reply: "But Uncle Bob, EVERYTIME if we wanna add a new argument type, we
have to modify the if-else statement in Args class, and switch-case
statement in exception class. We are hit by SAME bullet again and
again. I think it violates "Open Close Principle". Yes, i agree the
Switch statement are pleasant to read, but if our users ALWAYS want us
to support new argument type, we should better avoid if-else/switch
statements."
Uncle Bob reply: "You make a good point. The two switch statements, while not identical, are related; and this violates my rule. On the other hand there is no benefit to turning the switch statement in ArgsException into a table, since it would still need to be modified under the same conditions. I could eliminate the ArgsException switch statement by deferring the messages back to the ArgumentMarshaller, but that couples Messages (UI) with business rules and therefore violates SRP.
I think yours is a good argument for eliminating the messages in the ArgsException class altogether. On the other hand, that just pushes the switch statement out into the user code.
Another solution would be to create a hierarchy of ArgsException derivatives. That would get rid of the switch, but does not completely solve your OCP complaint, since we'd have to add new exception derivatives every time we added a new type.
That leaves us with eliminating the messages and pushing that switch out to the application code. I'm not happy that. So I think the switch in ArgsException, though it violates OCP, helps to keep user code cleaner, and maintains a good separation of concerns (SRP)."
|
|
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何在 .NET 中 使用 ANTLR4
· 后端思维之高并发处理方案
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 后端思维之高并发处理方案
· 千万级大表的优化技巧
· 在 VS Code 中,一键安装 MCP Server!
· 想让你多爱自己一些的开源计时器
· 10年+ .NET Coder 心语 ── 继承的思维:从思维模式到架构设计的深度解析