TDD by example (7) -- 组合

本文介绍了一个通过测试驱动开发(TDD)方式实现的猜数游戏。游戏中玩家需猜出由系统随机生成的四位数字。文章详细展示了从设计类结构到实现完整功能的过程,包括GameEngine、GameController等关键组件的设计。
前面已经完成了各个模块(类)的开发,是时候将之组合起来,形成可执行的程序了。首先需要一个GameEngine来驱动整个游戏的流程。
ContractedBlock.gif ExpandedBlockStart.gif 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的逻辑,驱动整个游戏运行.

主程序:
ContractedBlock.gif ExpandedBlockStart.gif 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中,并作适当的修改。
ContractedBlock.gif ExpandedBlockStart.gif Test
[TestFixture]
public class TestGame
{
    [SetUp]
    
public void Setup()
    {
        var mockAnswerGenerator 
= new Mock<IAnswerGenerator>();
        mockAnswerGenerator.Setup(generator 
=> generator.Generate()).Returns(new[] {1234});

        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[] {5678}), Is.EqualTo("0A0B"));
    }

    [Test]
    
public void should_return_0A2B_when_two_numbers_are_correct_but_positions_are_not_correct()
    {
        Assert.That(game.Guess(
new[] {3456}), Is.EqualTo("0A2B"));
    }

    [Test]
    
public void should_return_1A0B_when_one_number_is_correct_and_position_is_correct_too()
    {
        Assert.That(game.Guess(
new[] {1567}), 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[] {1243}), Is.EqualTo("2A2B"));
    }

    [Test]
    
public void should_return_4A0B_when_all_numbers_are_pisition_correct()
    {
        Assert.That(game.Guess(
new[] {1234}), 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[] { 5678 });
        
        mockHistory.Verify(h 
=> h.Add(new[] { 5678 }, "0A0B"));
    }

    [Test]
    
public void should_fail_game_when_guess_six_times_and_still_wrong()
    {
        var wrongGuess 
= new[] { 5678 };
        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[] { 1234 };
        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));

    }
}

ContractedBlock.gif ExpandedBlockStart.gif 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

ContractedBlock.gif ExpandedBlockStart.gif 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是不是更容易理解?等等等等,就不再继续了。
下载代码

转载于:https://www.cnblogs.com/wangyh/archive/2009/09/23/TDD-by-example-7.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值