与 Narnians 世界进行交易





5.00/5 (1投票)
利用 LINQ / PRISM / UnityContainer 实现基本的语言处理/解析的面向对象实现。
引言
纳尼亚人居住在一个叫做 纳尼亚 的虚构世界里。纳尼亚人不会说英语。此外,他们还有一套不同的数字系统。在地球上,我们懂 阿拉伯数字,在一定程度上也懂 罗马数字。纳尼亚人的数字系统接近于 罗马数字。然而,需要某种语言处理来将纳尼亚数字映射到 罗马数字。
与纳尼亚世界交易的解决方案演示了
- 面向对象的语言解析和缓存管理设计。以下设计模式主要应用于面向对象解决方案中:
- 责任链模式
- 策略
- 面向对象实现还利用了 LINQ 进行函数式编程。
- 通过 PRISM 4.1 / UnityContainer 应用了控制反转 (IoC) 和依赖注入。
- 在枚举值上标记约束作为属性
背景
在《狮子、女巫和魔衣橱》这一集中,有 4 个人类——彼得、苏珊、埃德蒙和露西——从地球来到纳尼亚并居住了一段时间。他们在纳尼亚的经历让他们意识到一些与纳尼亚人进行商业/贸易的机会。
然而,与纳尼亚人进行贸易需要他们进行数字和单位的转换。他们决定将一个软件项目外包给我。
这部分软件应该能帮助人类与纳尼亚人进行贸易。该软件应该能够处理输入的陈述。当一个问题被输入软件时,软件应该以一个回答陈述进行响应。
纳尼亚数字与罗马数字的映射
一组 7 个纳尼亚单词映射到 7 个罗马符号。
罗马符号 | 纳尼亚单词 |
I | cat |
V | fish |
X | pig |
L | ant |
C | dog |
D | lion |
M | elephant |
罗马数字符号和约束
用于纳尼亚交易的数字遵循与罗马数字相似的约定。罗马数字基于七个符号:
来源:http://en.wikipedia.org/wiki/Roman_numerals
符号 | 值 |
我 | 1 |
V | 5 |
X | 10 |
L | 50 |
C | 100 |
D | 500 |
M | 1000 |
数字通过组合符号并相加值来形成。因此 II 是两个一,即 2,XIII 是一个十和三个一,即 13。这个系统没有零,所以例如 207 是 CCVII,使用了代表两个百、一个五和两个一的符号。1066 是 MLXVI,一千、五十、十、一个五和一个一。
符号按照从大到小的顺序从左到右放置。然而,在一些特殊情况下,为了避免连续重复四个字符(如 IIII 或 XXXX),可以使用减法表示法来缩减,如下所示:
- 数字 I 可以放在 V 和 X 前面,分别表示 4 (IV) 和 9 (IX)。
- X 可以放在 L 和 C 前面,分别表示 40 (XL) 和 90 (XC)。
- C 可以放在 D 和 M 前面,按照相同的模式表示 400 和 900。
一个使用上述规则的例子是 1904:它由 1 (一千)、9 (九百)、0 (零十) 和 4 (四个一) 组成。要写罗马数字,每个非零数字都应该单独处理。因此 1,000 = M,900 = CM,4 = IV。所以,1904 是 MCMIV。这反映了典型的现代用法,而不是普遍接受的约定:历史上罗马数字的书写常常不太一致。
在将小值放在大值前面以减少字符数量的惯例中,一个常见的例外是使用 IIII 而不是 IV 来表示 4。
您的程序的输入包括陈述和问题。程序应处理异常。
如何处理纳尼亚输入?
示例陈述 - cat cat Brass is 10 Credits
- cat cat 转换为罗马数字表示 II
- II 转换为阿拉伯数字表示 2
- 现在处理后的陈述将是 - 2 Brass is 10 Credits
- 进一步解释 - 1 单位的 Brass 价值 10 / 2 = 5 Credits。即 1 单位的 Brass 价格为 5 Credits
示例问题 - cat fish Brass 是多少 Credits?
- cat fish 转换为罗马数字表示 IV
- IV 转换为阿拉伯数字表示 4
- 现在处理后的问题将是 - 4 单位的 Brass 是多少 Credits?
- 从上面的陈述中,我们知道1 单位的 Brass 价格为 5 Credits。因此,4 单位的 Brass 将花费 5 x 4 = 20 Credits
程序的示例输入/输出
测试输入
- cat is I
- fish is V
- pig is X
- ant is L
- cat cat Brass is 10 Credits
- cat fish Copper is 4000 Credits
- pig pig Aluminum is 2000 Credits
- pig ant cat 是多少?
- cat fish Brass 是多少 Credits?
- cat fish Copper 是多少 Credits?
- cat fish Aluminum 是多少 Credits?
- 把 Aslan 带到地球需要多少钱?
测试输出
- pig ant cat is 41
- cat fish Brass is 20 Credits
- cat fish Copper is 4000 Credits
- cat fish Aluminum is 400 Credits
- 异常 - 无法解析
使用代码
- 解决上述问题的解决方案是用 C# .NET 实现的。
- 该解决方案使用 Visual Studio 2012、.NET Framework 3.5 构建。解决方案文件 - .\TradeWithNarnia\TradeWithNarnia.sln
- 解决方案包含两个项目
- TradeWithNarnia - 此项目实现核心逻辑
- TradeWithNarniaTest - 此项目实现 TradeWithNarnia 的 NUnit 测试
/// <summary>
/// Test Fixture to get the given input in the problem statement tested
/// </summary>
[TestFixture]
public class TestGivenInput
{
private KeyValuePair<string, string>[] inputOutputPairs = new []
{
new KeyValuePair<string, string>("cat is I", string.Empty),
new KeyValuePair<string, string>("fish is V", string.Empty),
new KeyValuePair<string, string>("pig is X", string.Empty),
new KeyValuePair<string, string>("ant is L", string.Empty),
new KeyValuePair<string, string>("cat cat Brass is 10 Credits", string.Empty),
new KeyValuePair<string, string>("cat fish Copper is 4000 Credits", string.Empty),
new KeyValuePair<string, string>("pig pig Aluminium is 2000 Credits", string.Empty),
new KeyValuePair<string, string>("how much is pig ant cat ?", "pig ant cat is 41"),
new KeyValuePair<string, string>("how many Credits is cat fish Brass ?",
"cat fish Brass is 20 Credits"),
new KeyValuePair<string, string>("how many Credits is cat fish Copper ?",
"cat fish Copper is 4000 Credits"),
new KeyValuePair<string, string>("how many Credits is cat fish Aluminium ?",
"cat fish Aluminium is 400 Credits"),
new KeyValuePair<string, string>("how much would be the cost to " +
"get Aslan on to the earth ?", "Exception - unable to parse"),
};
[Test]
public void ExecuteTest1()
{
TradeWithNarnia.Helper.UnityContainerHelper.Initialize();
var singleLineProcessor = new SingleLineProcessor();
foreach (var inputOutpuPair in inputOutputPairs)
{
string output = singleLineProcessor.Process(new SingleLineText(inputOutpuPair.Key));
Assert.True(inputOutpuPair.Value.Equals(output),
"Expected output: " + inputOutpuPair.Value + " Actual output: " + output);
}
}
- 外部依赖
- Prism 4.1
- 该项目利用 Prism 4.1 框架,因为它使用 UnityContainer 进行依赖注入。
- 这些依赖项可从以下网址下载:http://www.microsoft.com/en-in/download/details.aspx?id=28950
- NUnit
- 测试项目依赖于 NUnit framework 2.6.2.12296。
- NUnit 依赖项可从以下网址下载:http://launchpad.net/nunitv2/2.6.2/2.6.2/+download/NUnit-2.6.2.zip
- 单元测试
- TestGivenInput - 使用 NUnit 运行 TestGivenInput 测试用例。这些测试用例将问题陈述中给出的示例输出与程序的实际输出进行断言。
- TestRomanNumberValidator - 测试罗马数字的给定约束,即重复次数、用于减法的较小数字前缀较大数字。
- TestPriceConverter - 测试各种输入的罗马数字到阿拉伯数字的价格转换。
关注点
使用 LINQ 进行即时解析
/// <summary>
/// Default implementation of IParsedWords that implements parsing for TradeWithNarnia problem
/// </summary>
public class ParsedWordsImpl : IParsedWords
{
private IEnumerable<string> _words = new List<string>();
[Dependency]
public AliasManager AliasMgr { get; set; }
private IEnumerable<string> LeftHalfWords
{
get
{
return _words.TakeWhile(word => !WordConstants.IN_THE_MIDDLE_WORDS.Contains(word)).ToList();
}
}
用属性标记枚举值,用反射应用约束
对罗马符号 I 应用以下约束:
- I 只能从 V 和 X 中减去。
- I 最多可以重复 3 次。
/// <summary>
/// Enum that represents each RomanNumber with their Constraints specified as attributes
/// </summary>
public enum RomanSymbol
{
[RomanSymbolConstraint(CanBeSubtractedFrom = new[] { V, X }, MaxRepetition = 3)]
I = 1,
V = 5,
[RomanSymbolConstraint(CanBeSubtractedFrom = new [] { L, C }, MaxRepetition = 3)]
X = 10,
L = 50,
[RomanSymbolConstraint(CanBeSubtractedFrom = new [] { D, M }, MaxRepetition = 3)]
C = 100,
D = 500,
[RomanSymbolConstraint(MaxRepetition = 3)]
M = 1000,
}
利用 EnumHelper
类通过反射获取这些属性。
public static class EnumHelper
{
/// <summary>
/// Gets an attribute on an enum field value
/// </summary>
/// <typeparam name="T">The type of the attribute you want to retrieve</typeparam>
/// <param name="enumVal">The enum value</param>
/// <returns>The attribute of type T that exists on the enum value</returns>
public static T GetAttributeOfType<T>(this Enum enumVal) where T : System.Attribute
{
var type = enumVal.GetType();
var memInfo = type.GetMember(enumVal.ToString());
var attributes = memInfo[0].GetCustomAttributes(typeof(T), false);
if (attributes.Any())
{
return (T) attributes[0];
}
return null;
}
}
确保对罗马数字验证应用的枚举约束。
/// <summary>
/// Checks the validity of a given roman number
/// </summary>
public class RomanNumberValidator
{
public static bool IsRomanNumberValid(IEnumerable<RomanSymbol> romanSymbols_)
{
bool isRomanNumberValid = true;
RomanSymbol[] symbols = romanSymbols_.ToArray();
for (int i = 0; i < symbols.Length; i++)
{
var attribute = symbols[i].GetAttributeOfType<RomanSymbolConstraintAttribute>();
if (attribute != null)
{
// Check if the roman number can be subtracted from the next number
if (attribute.CanBeSubtracted)
{
if (i < (symbols.Length - 1) && ((int)symbols[i] < (int)symbols[i + 1]))
{
if (attribute.CanBeSubtractedFrom.Contains(symbols[i + 1]) == false)
{
isRomanNumberValid = false;
}
}
}
// check if the roman number has the right number of repetitions
if (attribute.CanRepeat)
{
string symbolName = Enum.GetName(symbols[i].GetType(), symbols[i]);
string potentiallyInvalidSubstring = GetSubstring(symbolName, attribute.MaxRepetition + 1);
if (GetRomanNumberAsString(romanSymbols_).Contains(potentiallyInvalidSubstring))
{
isRomanNumberValid = false;
}
}
}
}
return isRomanNumberValid;
}
责任链模式
StatementParser
无法解析除陈述之外的任何内容。如果遇到其他内容,它会将解析责任委托给 ErrorParser
的实例。
/// <summary>
/// Processes statements, populates Alias cache, Commodity cache
/// </summary>
public class ParsedStatement : ParsedBase, IParsedLine
{
public string Process()
{
var result = string.Empty;
if(IsAliasStatement)
{
ProcessAliasStatement();
}
else if(IsCommodityStatement)
{
ProcessCommodityStatement();
}
else
{
// Unable to process as a statement, Redirect to Error highlighting
result = new ParsedError().Process();
}
return result.Trim();
}
商品缓存
在处理完输入陈述后,将商品价格缓存到 CommodityManager
索引器中。
/// <summary>
/// Maintains a cache of commodities
/// </summary>
public class CommodityManager
{
/// <summary>
/// key : value => "brass" : Commodity("Brass", 5)
/// </summary>
private Dictionary<string, Commodity> _commodityCache = new Dictionary<string, Commodity>();
public Commodity this[string commodityName_]
{
get
{
return _commodityCache.ContainsKey(commodityName_.ToLower()) ? _commodityCache[commodityName_.ToLower()] : null;
}
set
{
_commodityCache.Add(value.Name.ToLower(), value);
}
}
}
使用 UnityContainer 进行依赖注入
/// <summary>
/// Base class for processing various types of parsed lines viz. Question, Statement, Error
/// Contains dependency injected properties and utility methods
/// </summary>
public class ParsedBase
{
[Dependency]
public IParsedWords ParsedWords { get; set; }
[Dependency]
public AliasManager AliasMgr { get; set; }
[Dependency]
public CommodityManager CommodityMgr { get; set; }
[Dependency]
public PriceConverter PriceCnvtr { get; set; }
期望的改进
- 为每个可能的执行路径编写单元测试,以实现 100% 的代码覆盖率。
- 将 Array / Lists 类型的集合实例更新为
IEnumerable
。 - 可以将缓存放在
ParsedWordsImpl
内部,以节省 CPU 周期。 SingleLineProcessor
可以公开一个静态 API 进行处理。- 单词常量可以从配置文件读取,而不是数组,可以使用 Set。
- Alias Manager / Commodity Manager 可以实现一个接口以方便单元测试。
- 可以利用日志记录。
- 语言处理的边界情况,例如最后一个单词和 '?' 之间没有空格。
- 利用构成性解析树或依赖性解析树进行语言处理。
- 通过构造函数进行依赖注入(而不是公共属性)。