原始题目地址:https://www.jinshuju.net/f/EGQL3D
先看一下最终的上层消费程序的调用,操作还是比较简单的,例如:
static void Main(string[] args) { string tempStr = string.Empty; GameManager manager = new GameManager(3, 5, 7); for (int i = 1; i <= 100; i++) { tempStr = string.Format("[{0}]:{1}", i, manager.PlayGame(i)); Console.WriteLine(tempStr); } }
一、设计结构以及工作原理
【方案一】:最简单的算法封装,只是按照要求顺序的处理。
namespace ThroughWorkGame.Core { /// <summary> /// 建议实现的Game操作类 /// </summary> public class SimpleGameManager { private int _fastNumer; private int _secondNumber; private int _thirdNumber; /// <summary> /// 构造Game操作类 /// </summary> /// <param name="fastNumer">第一个数字</param> /// <param name="secondNumber">第二个数字</param> /// <param name="thirdNumber">第三个数字</param> public SimpleGameManager(int fastNumer, int secondNumber, int thirdNumber) { _fastNumer = fastNumer; _secondNumber = secondNumber; _thirdNumber = thirdNumber; } /// <summary> /// 开始游戏:按照构建的游戏规则构建输出结果(根据输入数字输出对应) /// </summary> /// <param name="number">输入的数字</param> /// <returns>对应应该报出的结果</returns> public string PlayGame(int number) { if (number.ToString().Contains(_fastNumer.ToString())) { return "Fizz"; } StringBuilder strResult = new StringBuilder(); strResult.Append(MultipleRule(number, _fastNumer, "Fizz")); strResult.Append(MultipleRule(number, _secondNumber, "Buzz")); strResult.Append(MultipleRule(number, _thirdNumber, "Whizz")); return strResult.Length == 0 ? number.ToString() : strResult.ToString(); } /// <summary> /// 根据是否为倍数转换数据 /// </summary> /// <param name="inputNumber">数据的数据</param> /// <param name="specialNumer">预定的特殊数</param> /// <param name="temp">符合条件需要替换的字符串</param> /// <returns>操作结果</returns> private string MultipleRule(int inputNumber, int specialNumer, string temp) { return inputNumber % specialNumer == 0 ? temp : string.Empty; } } }
【方案二】:采用了类似职责链的模式和理念。把不同的游戏规则抽象出来,按照实际的业务需要把规则按照链表连接起来,让规则依次的处理传入的参数。文本后续主要介绍此种方式。并会和基本的方式进行性能、可扩展性的比较。
基本的业务处理流程如下:
将每一条规则抽象为不同的处理对象。处理链表中依次传递之前处理过的上下文对象。
【GameManager】:游戏管理类,完成构造规则链表,获取游戏输出等工作;
【GameContext】:游戏上下文数据,保存规则链表处理过程的上下文数据;
【RuleHandler】:游戏规则抽象类,每一个实体包含一个指向下一个规则节点的引用,从而构成一个顺序链表。每一个链表节点处理完成之后都可以选择是否终止操作,还是继续执行后续的规则。
【ContainNumerRule】:游戏规则实体,判断是否存在某个数字;
【MultipleRule】:游戏规则实体,如果是再定数字的倍数替换为特殊的输出;
【SimpleRule】:游戏规则实体,如果没有被任何数据处理过则输出输入的数字;
RuleHandler及其子类对上层客户不可见,上层“消费者”使用GameManager和GameContext操作并初始化数据。
UML设计图如下:
GameManager 具体的工作方式 就用Code来表述吧。
namespace ThroughWorkGame.Core { /// <summary> /// 游戏管理类 /// </summary> public class GameManager { #region 私有字段 /// <summary> /// 第一个数字 /// </summary> private int _fastNumer; /// <summary> /// 第二个数字 /// </summary> private int _secondNumber; /// <summary> /// 第三个数字 /// </summary> private int _thirdNumber; /// <summary> /// 规则链表(仅仅保存了第一个节点) /// </summary> private RuleHandler ruleList; #endregion #region 属性 /// <summary> /// 获取第一个数字 /// </summary> public int FastNumer { get { return _fastNumer; } } /// <summary> /// 获取第二个数字 /// </summary> public int SecondNumber { get { return _secondNumber; } } /// <summary> /// 获取第三个数字 /// </summary> public int ThirdNumber { get { return _thirdNumber; } } #endregion /// <summary> /// 构造游戏对象 /// </summary> /// <param name="fastNumer">第一个数字</param> /// <param name="secondNumber">第二个数字</param> /// <param name="thirdNumber">第三个数字</param> public GameManager(int fastNumer, int secondNumber, int thirdNumber) { //初始化私有字段 _fastNumer = fastNumer; _secondNumber = secondNumber; _thirdNumber = thirdNumber; //构建规则链表 BuildRuleList(); } /// <summary> /// 创建规则链表 /// </summary> /// <returns></returns> protected virtual void BuildRuleList() { RuleHandler rule01 = new ContainNumerRule(_fastNumer, "Fizz"); RuleHandler rule02 = new MultipleRule(_fastNumer, "Fizz"); RuleHandler rule03 = new MultipleRule(_secondNumber, "Buzz"); RuleHandler rule04 = new MultipleRule(_thirdNumber, "Whizz"); RuleHandler rule05 = new SimpleRule(); rule04.AddNextRule(rule05); rule03.AddNextRule(rule04); rule02.AddNextRule(rule03); rule01.AddNextRule(rule02); ruleList = rule01; } /// <summary> /// 开始游戏:按照构建的游戏规则构建输出结果(根据输入数字输出对应) /// </summary> /// <param name="inputNumer">输入的数字</param> /// <returns>对应应该报出的结果</returns> public string PlayGame(int inputNumer) { GameContext context = new GameContext { InputNumer = inputNumer };//初始化数据 this.ruleList.ExecuteRule(context); //执行规则 return context.Result; } public string PlayGame(int inputNumer, out RuleFlag ruleFlag) { GameContext context = new GameContext { InputNumer = inputNumer };//初始化数据 this.ruleList.ExecuteRule(context); //执行规则 ruleFlag = context.Flag; return context.Result; } } }
二、单元测试以及分析
代码从四个维度上编写了单元测试:
【Test 01】:常规测试,输入3,5,7.并预设前20个数字进行测试,OriginalUnitTest:
int[] numbers = new int[] { 3, 5, 7 };
string[] originalValueArry = new string[]
{
"1", "2", "Fizz", "4", "Buzz",
"Fizz", "Whizz", "8", "Fizz", "Buzz",
"11", "Fizz", "Fizz", "Whizz", "FizzBuzz",
"16", "17", "Fizz", "19", "Buzz"
};
单元测试结果:
其中:
SimpleManager_Test使用【方法二】的结构。
Manager_Test使用【方法一】的结构。
【Test 02】:特殊值测试,测试输入不同的值时,Code是否能够正常工作,对应的测试Code写在ParticularValueUnitTest:
单元测试结果:
其中:
ParticularValue_GameManagerTest使用【方法二】的结构。
ParticularValue_SimpleGameManagerTest使用【方法一】的结构。
本测试中链式结构【方法二】速度较慢的愿意是开始时构建相关对象消耗的时间后面会对性能进项测试。
【Test 03】:性能测试,编写一份完整的测试用例(覆盖所有可能的路径),多次执行(100)万次,测试Code写在PerformanceUnitTest:
单元测试结果:
运行20万次,每次进行了7次运算结果如下,
其中:
Performance_GameManagerTest使用【方法二】的结构。
Performance_SimpleGameManagerTest使用【方法一】的结构。
【Test 04】:自动化测试,编写Code测试三位数所有排列组合下,所有特殊分支数据是否正确,Code请参见NumberGroupUnitTest:
单元测试结果:
三、扩展点说明
根据测试结果:
【方法二】链式结构在性能上可以和传统的简单编码保持一致(甚至更快),除此之外,还有很多易扩展的店。
【扩展点一】:添加新的规则,根据业务的需要可以新增新的规则只需继承原始规则类实现自己的规则,并根据业务需要添加到构建链表之中即可。
namespace ThroughWorkGame.Core { /// <summary> /// 游戏规则实体 /// </summary> public abstract class RuleHandler { #region 需要子类重写的方法 /// <summary> /// 执行当前规则 /// </summary> public abstract void ExecuteRule(GameContext context); #endregion 需要子类重写的方法 #region 保护字段 /// <summary> /// 指向处理链表的下一个元素 /// </summary> protected RuleHandler next; #endregion #region 添加规则 /// <summary> /// 添加下一个规则节点 /// </summary> /// <param name="nextRule">下一个规则节点</param> public void AddNextRule(RuleHandler nextRule) { this.next = nextRule; } /// <summary> /// 执行下一个规则 /// </summary> /// <param name="context">操作上下文对象</param> protected void ExecuteNextRule(GameContext context) { if (this.next != null) { this.next.ExecuteRule(context); } } #endregion } }
【扩展点二】:GameManager之中的构建链表的方法为虚方法,可以如有其它的游戏方式可以继承此类重写链表的构建规则(真实的应用中也可做成配置):
protected virtual void BuildRuleList();
【扩展点三】:GameContext之中的Flag是一个二进制的标记,可以通过它得知整个处理过程中那些规则被使用,可以以此进行业务分析:
好了,整个实现基本也就这样了,拿出来和大家分享一些,如有什么不合适的地方,还望大神指导。