GameEngine public class GameEngine{ private readonly Game game; private readonly InputValidator validator; private Action onExit; private bool stop; public GameEngine(Game game, InputValidator validator) { this.game = game; this.validator = validator; game.GameClear += game_GameClear; game.GameOver += game_GameOver; } private void game_GameOver() { Exit(() => Console.WriteLine("Game Over! The answer is {0}!", game.Answer)); } private void game_GameClear() { Exit(() => Console.WriteLine("Bingo! You win the game!")); } private void Exit(Action onExitAction) { stop = true; onExit = onExitAction; } public void Run() { while (!stop) { string input = Console.ReadLine(); if (!validator.Validate(input)) { Console.WriteLine("Unknown input format. Please input numbers like this: 1 2 3 4"); continue; } int[] guessNum = GetGuessNums(input); Console.WriteLine(game.Guess(guessNum)); PrintGuessHistory(); } onExit(); } private void PrintGuessHistory() { Console.WriteLine("---------------Guess Attempts-------------------"); Console.Write(game.GetGuessHistory()); Console.WriteLine("------------------------------------------------"); Console.WriteLine(); } private static int[] GetGuessNums(string input) { string[] numbers = input.Split(new[] { ' '}, StringSplitOptions.RemoveEmptyEntries); var result = new int[numbers.Length]; for (int i = 0; i < numbers.Length; i++) { result[i] = Convert.ToInt32(numbers[i]); } return result; }}
GameEngine的主要作用是协调输入输出,调用Game的逻辑,驱动整个游戏运行. 主程序: Program internal class Program{ private static readonly ContainerBuilder builder = new ContainerBuilder(); private static void Main(string[] args) { SetupDependencies(); PrintUsage(); StartGame(); } private static void SetupDependencies() { builder.Register<AnswerGenerator>().As<IAnswerGenerator>(); builder.Register<GameHistory>().As<IGameHistory>(); builder.Register<RandomIntGenerator>().As<IRandomIntGenerator>(); builder.Register<InputValidator>(); builder.Register<Game>(); builder.Register<GameEngine>(); } private static void PrintUsage() { Console.WriteLine("Guess number v1.0"); } private static void StartGame() { using(var container = builder.Build()) { var engine = container.Resolve<GameEngine>(); engine.Run(); } }}
在SetupDependencies中,将接口与实现注册到Container当中,这样在Resolve的时候,Container就会自动寻找依赖,创建出正确的对象关系. 如果想要替换某一个组件,例如IGameHistory,只需要添加该接口的一个新的实现,然后修改注册代码,即可实现组建替换.如果将注册代码移至配置文件中,则可以在不重新编译的情况下,替换组建. 至此,我们的功能已经全部完成,但是……这还没完,我们还需要再审视一下设计,看看有没有可以改进的地方。 --------------------===============------------------ 我们可以看到,Game类不仅要判断猜测的结果,还要记录,还要判断是否游戏结束,严重违反了SRP。于是,将记录历史记录和判断游戏结束的逻辑提取出来,形成GameController类,同时也需要将TestGame中的相关测试提取到TestGameController中,并作适当的修改。 Test [TestFixture]public class TestGame{ [SetUp] public void Setup() { var mockAnswerGenerator = new Mock<IAnswerGenerator>(); mockAnswerGenerator.Setup(generator => generator.Generate()).Returns(new[] { 1, 2, 3, 4}); game = new Game(mockAnswerGenerator.Object); } private Game game; [Test] public void should_get_the_answer_string() { Assert.That(game.Answer, Is.EqualTo("1 2 3 4")); } [Test] public void should_return_0A0B_when_no_number_is_correct() { Assert.That(game.Guess(new[] { 5, 6, 7, 8}), Is.EqualTo("0A0B")); } [Test] public void should_return_0A2B_when_two_numbers_are_correct_but_positions_are_not_correct() { Assert.That(game.Guess(new[] { 3, 4, 5, 6}), Is.EqualTo("0A2B")); } [Test] public void should_return_1A0B_when_one_number_is_correct_and_position_is_correct_too() { Assert.That(game.Guess(new[] { 1, 5, 6, 7}), Is.EqualTo("1A0B")); } [Test] public void should_return_2A2B_when_two_numbers_are_pisition_correct_and_two_are_nunmber_correct() { Assert.That(game.Guess(new[] { 1, 2, 4, 3}), Is.EqualTo("2A2B")); } [Test] public void should_return_4A0B_when_all_numbers_are_pisition_correct() { Assert.That(game.Guess(new[] { 1, 2, 3, 4}), Is.EqualTo("4A0B")); }}[TestFixture]public class TestGameController{ public interface IGameObserver { void GameOver(); void GameClear(); } private Mock<IGameHistory> mockHistory; private Mock<IGame> mockGame; private GameController controller; [SetUp] public void Setup() { mockHistory = new Mock<IGameHistory>(); mockGame = new Mock<IGame>(); controller = new GameController(mockGame.Object, mockHistory.Object); } [Test] public void should_record_guess_history() { mockGame.Setup(game => game.Guess(It.IsAny<int[]>())).Returns("0A0B"); controller.Guess(new[] { 5, 6, 7, 8 }); mockHistory.Verify(h => h.Add(new[] { 5, 6, 7, 8 }, "0A0B")); } [Test] public void should_fail_game_when_guess_six_times_and_still_wrong() { var wrongGuess = new[] { 5, 6, 7, 8 }; var mockObserver = new Mock<IGameObserver>(); mockGame.Setup(game => game.Guess(wrongGuess)).Returns("0A0B"); controller.GameOver += mockObserver.Object.GameOver; for (int i = 0; i < 5; i++) { controller.Guess(wrongGuess); } mockObserver.Verify(m => m.GameOver(), Times.Never()); controller.Guess(wrongGuess); mockObserver.Verify(m => m.GameOver(), Times.Exactly(1)); mockGame.Verify(m=>m.Guess(wrongGuess), Times.Exactly(6)); } [Test] public void should_win_when_guess_correct() { var correctAnswer = new[] { 1, 2, 3, 4 }; var mockObserver = new Mock<IGameObserver>(); mockGame.Setup(game => game.Guess(correctAnswer)).Returns("4A0B"); controller.GameClear += mockObserver.Object.GameClear; controller.Guess(correctAnswer); mockObserver.Verify(m => m.GameClear(), Times.Exactly(1)); mockGame.Verify(m => m.Guess(correctAnswer), Times.Exactly(1)); }}
Code public interface IGame { string Guess(int[] guess); string Answer { get; } }public class Game : IGame{ private readonly int[] answer; public Game(IAnswerGenerator answerGenerator) { answer = answerGenerator.Generate(); } public string Answer { get { return string.Join(" ", answer.Select(a => a.ToString()).ToArray()); } } #region IGame Members public string Guess(int[] guess) { int aCount = 0; int bCount = 0; for (int i = 0; i < guess.Length; i++) { if (answer[i] == guess[i]) { aCount++; } else if (answer.Contains(guess[i])) { bCount++; } } return string.Format("{0}A{1}B", aCount, bCount); } #endregion}public class GameController{ public delegate void GameEventHandler(); private const string CorrectGuess = "4A0B"; private const int MaxGuessTimes = 6; private readonly IGame game; private readonly IGameHistory history; private int guessTimes; public GameController(IGame game, IGameHistory history) { this.game = game; this.history = history; } public string Answer { get { return game.Answer; } } public string Guess(int[] guess) { guessTimes++; string result = game.Guess(guess); RecordGuess(guess, result); if (IsGameClear(result)) { OnGameClear(); } else if (CanContinueGuess()) { OnGameOver(); } return result; } public event GameEventHandler GameOver; public event GameEventHandler GameClear; private static bool IsGameClear(string result) { return result == CorrectGuess; } private bool CanContinueGuess() { return guessTimes >= MaxGuessTimes; } protected void OnGameClear() { if (GameClear != null) { GameClear(); } } protected void OnGameOver() { if (GameOver != null) { GameOver(); } } public string GetGuessHistory() { return history.GetAll(); } private void RecordGuess(int[] guess, string result) { if (history != null) { history.Add(guess, result); } }}
还有别忘了在SetupDependencies中注册GameController Program private static void SetupDependencies(){ builder.Register<AnswerGenerator>().As<IAnswerGenerator>(); builder.Register<GameHistory>().As<IGameHistory>(); builder.Register<RandomIntGenerator>().As<IRandomIntGenerator>(); builder.Register<InputValidator>(); builder.Register<Game>().As<IGame>(); builder.Register<GameController>(); builder.Register<GameEngine>();}
至此,程序完成。 --------------------------------------------------------------------- 其实代码还有很多改进的余地,比如说Game这个类,叫做MagicNumber会不会好一些?GameController改名叫Game是不是更贴切?GameHistory叫做GuessRecord是不是更容易理解?等等等等,就不再继续了。