这是我姗姗来迟的第二篇关于在Delphi里进行测试驱动开发(TDD)的文章。 这是《TDD in Delphi:基础知识》的后续篇, 另外一篇文章是几个月前写的。
我想集中讨论测试驱动开发周期中的一个流行步骤:重构代码。重构意味着优化、整洁、短小、美化、个性(写上你的形容词)。 代码不会破坏(原有的)功能;也就是说,(重构)不会破坏你(已有)的单元测试。
重构之前先写好单元测试,(这样)你才能保证对代码的更改是安全的。重构可能会引入错误。为了避免引入错误你必须精心准备你的单元测试。
重构可以引入别的东西:重构可以在你的代码里面引入设计模式。这意味着你不必在设计的前期就引入设计模式, 因为您的代码可以从“非常质朴的实现”重构成为一个“面向模式的实现”。这就是“重构与模式”。如果你对这个主题感兴趣,建议您阅读一下Joshua Kerievsky写的《重构与模式》
接下来我会以象棋游戏作为我的例子基础。为简单起见,我只会涉及到以下两类规则:马和象. 在这个例子,我仅仅集中重构那些已经准备好单元测试的代码。 一个详细的TDD测试驱动开发周期演示可以在我的前一篇文章找到,那个例子也是基于国际象棋游戏的。
代码非常简单不言自明:基本上,在一个类层次结构中,TPiece是基类,TKnight和TBishop派生自TPiece。快来看一看吧:
unit ChessGame; interface type TPiece = class private FX, FY: Byte; public constructor Create(aX, aY: Integer); function IsWithinBoard(aX, aY: Integer): Boolean; end; TBishop = class (TPiece) public function CanMoveTo(aX, aY: Byte): Boolean; function isValidMove(aX, aY: Byte): Boolean; end; TKnight = class(TPiece) public function CanMoveTo(aX, aY: Byte): Boolean; function isValidMove(aX, aY: Byte): Boolean; end; implementation { TPiece } constructor TPiece.Create(aX, aY: Integer); begin inherited Create; // TODO: check that this assignment is valid. // Not now, ok? :-) FX:= aX; FY:= aY; end; function TPiece.IsWithinBoard(aX, aY: Integer): Boolean; begin Result:= (aX > 0) and (aX < 9) and (aY > 0) and (aY < 9); end; { TKnight } function TKnight.isValidMove(aX, aY: Byte): Boolean; var x_diff, y_diff: Integer; begin x_diff:= abs(aX - FX) ; y_diff:= abs(aY - FY) ; Result:= ((x_diff = 2) and (y_diff = 1)) or ((y_diff = 2) and (x_diff = 1)); end; function TKnight.CanMoveTo(aX, aY: Byte): Boolean; begin Result:= IsWithinBoard(aX, aY) and IsValidMove(aX, aY); end; { TBishop } function TBishop.isValidMove(aX, aY: Byte): Boolean; begin Result:= abs(aX - FX) = abs(aY - FY); end; function TBishop.CanMoveTo(aX, aY: Byte): Boolean; begin Result:= IsWithinBoard(aX, aY) and IsValidMove(aX, aY); end; end. ///////////////////////////////////////////// unit TestChessGame; interface uses TestFramework, ChessGame; type // TPiece 类的测试方法 TestTPiece = class(TTestCase) strict private FPiece: TPiece; public procedure SetUp; override; procedure TearDown; override; published procedure TestIsWithinBoard; end; // TBishop 类的测试方法 TestTBishop = class(TTestCase) strict private FBishop: TBishop; public procedure SetUp; override; procedure TearDown; override; published procedure TestCanMoveTo; procedure TestisValidMove; end; // TKnight类的测试方法 TestTKnight = class(TTestCase) strict private FKnight: TKnight; public procedure SetUp; override; procedure TearDown; override; published procedure TestCanMoveTo; procedure TestisValidMove; end; implementation procedure TestTPiece.SetUp; begin FPiece := TPiece.Create(4, 4); end; procedure TestTPiece.TearDown; begin FPiece.Free; FPiece := nil; end; procedure TestTPiece.TestIsWithinBoard; begin //Test trivial (normal) workflow Check(FPiece.IsWithinBoard(4, 4)); //Tests boundaries Check(FPiece.IsWithinBoard(1, 1)); Check(FPiece.IsWithinBoard(1, 8)); Check(FPiece.IsWithinBoard(8, 1)); Check(FPiece.IsWithinBoard(8, 8)); //Test beyond the boundaries CheckFalse(FPiece.IsWithinBoard(3, 15)); CheckFalse(FPiece.IsWithinBoard(3, -15)); CheckFalse(FPiece.IsWithinBoard(15, 3)); CheckFalse(FPiece.IsWithinBoard(15, 15)); CheckFalse(FPiece.IsWithinBoard(15, -15)); CheckFalse(FPiece.IsWithinBoard(-15, 3)); CheckFalse(FPiece.IsWithinBoard(-15, 15)); CheckFalse(FPiece.IsWithinBoard(-15, -15)); end; procedure TestTBishop.SetUp; begin FBishop := TBishop.Create(4, 4); end; procedure TestTBishop.TearDown; begin FBishop.Free; FBishop := nil; end; procedure TestTBishop.TestCanMoveTo; begin // 嘿, 程序员, 暂时忽略这里: // 相信我, 我会很快完成本段代码的 // 在写任何代码之前, 把测试代码准备好 procedure TestTBishop.TestisValidMove; begin // 嘿, 程序员, 暂时忽略这里: // 相信我, 我会很快完成本段代码的 // 在写任何代码之前, 先把测试代码准备好 end; procedure TestTKnight.SetUp; begin FKnight := TKnight.Create(4, 4); end; procedure TestTKnight.TearDown; begin FKnight.Free; FKnight := nil; end; procedure TestTKnight.TestCanMoveTo; begin // 嘿, 程序员, 暂时忽略这里: // 相信我, 我会很快完成本段代码的 // 在写任何代码之前, 把测试代码准备好 end; procedure TestTKnight.TestisValidMove; begin // 嘿, 程序员, 暂时忽略这里: // 相信我, 我会很快完成本段代码的 // 在写任何代码之前, 把测试代码准备好 end; initialization // Register any test cases with the test runner RegisterTest(TestTPiece.Suite); RegisterTest(TestTBishop.Suite); RegisterTest(TestTKnight.Suite); end.
请注意,CanMoveTo 方法在TKnight类和 TBishop类中有重复;这样不够好, 不是吗?为了解决这个问题,我们可以把CanMoveTo方法提升到基类TPiece中。现在请注意:CanMoveTo 方法现在已经成为一个“模板方法”;因为它是一个通用算法(骨架)适用于所有的棋子类型(TKnight,TBishop等等)。
这些通用的算法(骨架)把一些步骤的实现延迟到它的子类进行;我的意思是,isValidMove方法依然写在子类里面。这样写好不好? 您现在已经重构了您的代码, 并且在重构过程中,你已经引入了模板方法设计模式。
怎样才算最好,(不要忘记这个,因为它是一个关键的部分):就是我们能保证我们精心的重构没有破坏我们预先已经存在的功能。为什么? 因为我们很久以前就已经写好了单元测试的代码。编写单元测试从一开始就给了开发人员一个巨大的定心丸 :-) 请看新的重构代码如下:
unit ChessGameRefactored; interface type TPiece = class private FX, FY: Byte; public constructor Create(aX, aY: Integer); function IsWithinBoard(aX, aY: Integer): Boolean; function CanMoveTo(aX, aY: Byte): Boolean; function isValidMove(aX, aY: Byte): Boolean; virtual; abstract; end; TBishop = class (TPiece) public function isValidMove(aX, aY: Byte): Boolean; override; end; TKnight = class(TPiece) public function isValidMove(aX, aY: Byte): Boolean; override; end; implementation { TPiece } constructor TPiece.Create(aX, aY: Integer); begin inherited Create; // TODO: 检查参数是否合法 // 不过不是现在, ok? :-) FX:= aX; FY:= aY; end; function TPiece.IsWithinBoard(aX, aY: Integer): Boolean; begin Result:= (aX > 0) and (aX < 9) and (aY > 0) and (aY < 9); end; function TPiece.CanMoveTo(aX, aY: Byte): Boolean; begin Result:= IsWithinBoard(aX, aY) and IsValidMove(aX, aY); end; { TKnight } function TKnight.isValidMove(aX, aY: Byte): Boolean; var x_diff, y_diff: Integer; begin x_diff:= abs(aX - FX) ; y_diff:= abs(aY - FY) ; Result:= ((x_diff = 2) and (y_diff = 1)) or ((y_diff = 2) and (x_diff = 1)); end; { TBishop } function TBishop.isValidMove(aX, aY: Byte): Boolean; begin Result:= abs(aX - FX) = abs(aY - FY); end; end.
结论:除此以外,在所有最酷的事情中测试驱动开发(TDD)可以在你重构你代码的过程中精练你的设计, 而不是在(设计的)前期。 设计模式可以在任何时候被引入(设计中), 我们知道相关的介绍, 如果晚了,也不会破坏我们的业务逻辑,因为我们有单元测试, 可以防止这样的事情发生。 下面的一些相关阅读(书籍):
2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务