TDD by example (7) -- 组合
前面已经完成了各个模块(类)的开发,是时候将之组合起来,形成可执行的程序了。首先需要一个GameEngine来驱动整个游戏的流程。
主程序:
在SetupDependencies中,将接口与实现注册到Container当中,这样在Resolve的时候,Container就会自动寻找依赖,创建出正确的对象关系. 如果想要替换某一个组件,例如IGameHistory,只需要添加该接口的一个新的实现,然后修改注册代码,即可实现组建替换.如果将注册代码移至配置文件中,则可以在不重新编译的情况下,替换组建.
至此,我们的功能已经全部完成,但是……这还没完,我们还需要再审视一下设计,看看有没有可以改进的地方。
--------------------===============------------------
我们可以看到,Game类不仅要判断猜测的结果,还要记录,还要判断是否游戏结束,严重违反了SRP。于是,将记录历史记录和判断游戏结束的逻辑提取出来,形成GameController类,同时也需要将TestGame中的相关测试提取到TestGameController中,并作适当的修改。
---------------------------------------------------------------------
其实代码还有很多改进的余地,比如说Game这个类,叫做MagicNumber会不会好一些?GameController改名叫Game是不是更贴切?GameHistory叫做GuessRecord是不是更容易理解?等等等等,就不再继续了。
下载代码
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的逻辑,驱动整个游戏运行.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;
}
}
主程序:
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();
}
}
}
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));
}
}
[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中注册GameControllerpublic 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);
}
}
}
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>();
}
至此,程序完成。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是不是更容易理解?等等等等,就不再继续了。
下载代码