TDD(測試驅動設計)的項目實踐——需求分析

TDD-PRACTICE

背景


TDD:測試驅動設計,各種理論,各種優劣,網上有很多的文章來介紹,但是怎么做TDD,從何處開始?會遇到什么問題?怎么解決?OK,PI君也是剛接觸TDD沒多久,理論不多說,直接從一個小項目開始。

項目需求


① 一個足球比賽類小游戲,用戶可以通過鍵盤操控控球球員前進/后退/左轉/右轉/加速/傳球/射門;如果用戶控制的球隊沒有球權,則用戶可以切換控制球員進行鏟球/防守;用戶可以控制游戲開始,設置游戲時間,一般有兩支球隊進行比賽;

② 每個球隊有一個教練,有十一個球員,有自己的球隊隊形,用戶可以自己調整針對特定隊形的球員站位,有自己的隊服/隊徽;

③ 有一個管理員賬號,管理員可以管理球隊相關數據,包括球員數據/教練數據/隊形數據/隊服數據/隊徽數據。

需求分析


根據需求確定UseCase,盡可能使用代碼描述UseCase。

UseCase-One:Play football game


① 一個足球比賽類小游戲,用戶可以通過鍵盤操控控球球員前進/后退/左轉/右轉/加速/傳球/射門;如果用戶控制的球隊沒有球權,則用戶可以切換控制球員進行鏟球/防守;用戶可以控制游戲開始,設置游戲時間,一般有兩支球隊進行比賽;

Pi君把需求①進行逐句分析如下:

Step-One:

一個足球比賽類小游戲,有兩個球隊進行比賽,用戶可以控制比賽開始,暫停和結束,Code 描述如下:

FootballTeam teamA=newFootballTeam(); ?//新建一個球隊A

FootballTeam teamB=newFootballTeam(); ?//新建一個球隊B

FootballGame newGame=newFootballGame(team_A, team_B); ?//新建一個游戲,并用A和B球隊初始化該游戲

newGame.Start(); //開始游戲

newGame.Pause(); //暫停游戲

newGame.GameOver(); //結束游戲

注:文中的代碼都是在新建的單元測試里進行編寫,其中涉及到的FootballTeam等類型,實際上并不存在,Pi君就是通過代碼把UseCase建立起來,然后確定有哪些類型需要創建,每一個類型又有哪些方法/成員等等,這些都是TDD的理論基礎,不熟悉的看官直接Google吧。Pi君在此就不啰嗦了。

注:代碼中有特殊標記的部分都是后續可能會引用分析,并進行修改的部分,暫時可以忽略其效果。

注:文中Pi君給出的是C#版本的代碼,但是有關TDD的實踐方式是相通的,如需java/python/C++版本,Pi君會根據時間安排進行轉換,至于其他語言版本,很遺憾Pi暫時還不擅長。

Step-Two:

每個球隊有十一個球員,比賽過程中,當球隊具有球權時,則用戶只能通過鍵盤控制控球球員進行前進/后退/左轉/右轉/加速/傳球/射門的動作;當球隊失去球權時,用戶可以切換受控制的球員,在切換過程中,選取用戶控制球隊距離足球最近的球員。

detail step by step:

① 每個球隊有十一個球員:

FootballAthlete piAthlete = new FootballAthlete(“Pi君”); ?//新建一個名字叫做Pi君的球員(類似新建11個球員)

team_A.AddAthlete(piAthlete); //把PI君等11個球員依次添加至球隊A中

② 比賽過程中,當球隊具有球權時:

//“球權”是比賽過程中的一種狀態屬性:teamA或teamB

newGame.BallRightTeam= teamA; //球隊A具有球權

newGame.BallRightTeam= teamB; //球隊A失去球權,球隊B獲得球權

當然,也有建議可以把“球權”作為球隊的一個屬性,類似teamA.HaveBall = true來描述球隊A具有球權,但是這樣做需要一個關鍵的邏輯處理,如果teamA.HaveBall = true,則teamB.HaveBall = false必須同時成立,既然如此,Pi君還是建議把“球權”作為比賽過程中的一個狀態屬性比較直觀,也無須其他的邏輯處理。

③ 當球隊具有球權時,用戶只能通過鍵盤控制控球球員進行前進/后退/左轉/右轉/加速/傳球/射門的動作:

“控球球員”是一個動態的概念,隨著足球的運動,控制足球的球員也在隨之變化,控球球員可以被操控進行各種不同的動作,所以控球球員需要一個獨立的類來處理,至于為什么不把“控球”作為球員的一個屬性,看官們可以反推,Pi君不贅述。

如果不考慮下一條,代碼描述可以這么寫:

teamA.ControlAthlete = new ControlAthlete(); //新建球隊A的控制球員(球隊B格式類似)

teamA.ControlAthlete.SetControlAthlete(piAthlete); //A球隊的Pi君為控球球員

public class ControlAthlete ? ?//控制球員類

{

? ? ? private FootballAthlete _selectAthlete;? //控制球員

? ? ? private string _teamType; //所屬球隊類型

? ? ? private Key _goKey; //前進鍵

? ? ? private Key _backKey; //后退鍵 ...... 類似包含左轉/右轉/加速/傳球/射門的鍵

? ? ? public void SetControlAthlete(FootballAthlete){......} //設置控球球員

? ? ? public ControlAthlete()

? ? ? ?{

? ? ? ? ? ? ?/*注冊動作鍵被按下時的響應事件*/

? ? ? ? ? ? ?_goKey.DownEvent += goKey_DownEvent;

? ? ? ? ? ? ......

? ? ? ?}

}

看官可能會奇怪,為什么不設置“球權”呢,畢竟事件的響應是根據“球權”狀態來決定的,想想看,“球權”是比賽的一個屬性,并且是一個動態的屬性,取值范圍固定在球隊A和球隊B,所以,需要獲取“球權”的值,只需要讓teamA.ControlAthlete知道newGame的信息就OK了,這樣,每次鍵盤事件響應時,實時判斷當前比賽的“球權”,“控球球員”即可做出正確的動作。

怎么讓teamA.ControlAthlete知道newGame的信息呢?且看后續分解吧,畢竟這不是一個難點。

④ 當球隊失去球權時,用戶可以切換受控制的球員,在切換過程中,選取用戶控制球隊距離足球最近的球員:

基于第③步的分析和代碼描述,這一步的需求可以這么描述:“切換受控制的球員”,即重新設置了“控球球員”(注意,這里的球員不一定真實控球的那個球員)?

//比賽進行時,必然有且只有一場比賽在進行,所以比賽本身是個單例(單例模式)

//在控制球員類中添加“切換鍵”及其響應事件

public class ControlAthlete ? //控制球員類

{

? ? ? private FootballAthlete _selectAthlete; ?//控制球員

? ? ? private SwitchKey _switchKey; //切換球員按鍵

? ? ? public ControlAthlete()

? ? ? {

? ? ? ? ? ?this._switchKey.KeyDown += switchKey_KeyDown;

? ? ? }

? ? ?//參數暫時不用定義

? ? ? private void switchKey_KeyDown(object e, KeyArgs args)

? ? ? {

? ? ? ? ? ?//獲取當前比賽對象?????

? ? ? ? ? ?FootballGame currentGame = new FootballGame();

? ? ? ? ? ?if(currentGame == null)

? ? ? ? ? ? ? ?return;

? ? ? ? ? FootballAthlete nextAthlete = ? ? ? ? ? ? currentGame.GetNeareastAthletefromBall(this._teamType); //獲取指定球隊舉例足球最近的球員

? ? ? ? ? if(nextAthlete == null)

? ? ? ? ? ? ? ?return;

? ? ? ? ? this._selectAthlete = nextAthlete;

? ? ? ? ? RefreshAthleteStatus(); ? ? //刷新球員狀態(繪制信息)

? ? ? }

}

出現了一個問題,FootballGame類在“Step-One”中已經存在一個構造函數如下:

FootballGame newGame=newFootballGame(team_A, team_B);? //新建一個游戲,并用A和B球隊初始化該游戲

而剛剛,FootballGame類還存在另外一個構造函數,如上代碼中黑體+斜體+中劃線的部分。FootballGame本身是一個單例,也就是內存中始終只有一個該類的實例,并且單例有自己固有的實現方式,之前FootballGame類的兩種構造方式顯然違反了單例的實現方式,OK,Pi君先給出FootballGame類單例的實現方式:

public class FootballGame

{

? ? ?private FootballGame(){} ? //私有化構造函數

? ? ?private static FootballGame _instance; ?//唯一實例

? ? ?public FootballTeam _teamA; ?//參賽球隊A

? ? ?public FootballTeam _teamB;? //參賽球隊B

? ? ?public static FootballGame GetInstance() ?//獲取球隊比賽實例

? ? ?{

? ? ? ? ? ?if(_instance == null)

? ? ? ? ? ? ? ? ?_instance = new FootballGame();?

? ? ? ? ? ?return _instance;

? ? ?}

? ? ?......

}

擴展:以上代碼中加粗的“public”,可能會引起看官們的疑惑,為啥不用屬性和私有變量,直接讓變量公有,豈不是破壞了類的封裝?有違習慣嘛~~其實,這里首先有一個問題需要研究清楚,為什么會有屬性的概念,屬性帶來的好處有哪些?為免離題太遠,Pi君只拋出問題,歡迎看官們留言討論,說說自己的想法,也聽一聽別人的想法,一起學習,一起進步~

OK,對FootballGame類的實現,意味著需要對之前代碼中獲取或新建FootballGame對象的部分進行調整和修改。現將修改后的代碼展示如下:

FootballGame newGame = FootballGame.GetInstance();//新建一個游戲

newGame._teamA = teamA; //添加球隊A參加比賽

newGame._teamB = teamB; //添加球隊B參加比賽

FootballGame currentGame = FootballGame.GetInstance(); //獲取當前比賽對象

OK,到此,針對UseCase-One的代碼描述基本清晰,但是仍然有一些細節的問題沒有處理,例如,“控制球員”的每一個動作函數應該怎么編寫,其實,這是深入層面需要考慮的問題,感興趣的看官們可以思考下,Pi君也會在后續給出github上的源碼鏈接。

UseCase-Two:FootballTeam Struct


② 每個球隊有一個教練,有十一個球員,有自己的球隊隊形,用戶可以自己調整針對特定隊形的球員站位,有自己的隊服/隊徽;

該條需求直接給出了球隊的基本數據結構,So,代碼描述如下:

public class FootballTeam

{

? ? ?private FootballAthlete[11] _athletes; ? //十一名球員

? ? ?private TeamFormation _formation; //隊形

? ? ?private FootballTrainer _trainer; //一個足球教練

? ? ?private string _TShirt; //隊服

? ? ?private string _teamLog; //隊徽

}

針對“用戶可以自己調整針對特定的球員站位”,又該怎么描述?這是一個需要深挖的需求點,請隨Pi君Step by Step:

如果“站位”只是球員開場時所處的球場位置,那么可以直接將“站位”作為球員自身的屬性,這樣不但可以知道球員開始的位置,隨著比賽的進行,這個位置也會隨之變動;

如果“站位”除了開場時球員所處的球場位置以外,還涵蓋球員的頻繁跑動區域(防守責任區/進攻戰術責任區等),那么“站位”的概念要豐富的多,“站位”可以理解為一種控制規則,球員跑動/傳球/防守需要從“站位”中讀取規則,然后做出相應的動作;

既然“站位”的概念被豐富了,那么把“站位”作為球員的屬性就變得很勉強,OK,不如把“站位”獨立出來,更符合單一職責原則,二者之間的關系是“站位”---->“FootballAthlete”;

→回轉查看之前FootballTeam的設計,“private FootballAthlete[11] _athletes;? //十一名球員”的存在就顯得的多余了,毫不猶豫,先把這一行刪除,后續也許有新的需求導致該行的重新恢復,所以,暫時先注釋掉該行是個不錯的習慣。

OK,現在“隊形”被分解為“站位”,“站位”又包括哪些行為或者屬性呢?繼續Step by Step:

Station oneStation = new Station(); ?//新建一個“站位”

oneStation.Athlete = piAthlete //把Pi君設置為該“站位”的球員

oneStation.DefendArea.Add(new Point(xxx, yyy)); //添加該“站位”的防守區域

Point startPosition = oneStation.GetStartPos(); ?//獲取當前“站位”的起始位置

teamA._formation.AddStation(oneStation); //將當前“站位”添加至球隊“隊形”中

現在數據有了,怎么觸發行為,行為又是怎么發生呢?繼續Step by Step:

//帶球跑動的球員是否會觸發對方球員的防守行為?

這個問題的回答是層級性的,可以設想為游戲難度,因為需求沒有涉及,理解過程中簡單的假設有兩種游戲難度:困難/簡單,“困難”級別的游戲,這個問題的答案自然是:true,“簡單”級別則是false。當然,如果把游戲難度細分為“新手級”/"普通級"/“困難級”/“專家級”/“變態級”,那這個問題就不能簡單的使用bool值描述......又是一個新的邏輯處理塊,但是轉念思考,暫時沒有這種需求,那就采取最簡單的策略:“簡單”級別,即答案為false,切記不可過度設計,這是TDD最給力的地方。

當然,如果是用戶控制球員防守,那就另當別論了。

//球員的無球跑動?

無球跑動,理解為責任區內的晃動,及脫離“控制球員”的球員“發現”自己不在責任區內時的自動修正跑動。可以放在“RefreshAthleteStatus(); //刷新球員狀態(繪制信息)”中添加處理邏輯,不贅述。

OK,到此有關UseCase-Two:FootballTeam Struct的基本結構已經清晰。

UseCase-Three:DataManager


③ 有一個管理員賬號,管理員可以管理球隊相關數據,包括球員數據/教練數據/隊形數據/隊服數據/隊徽數據。

這是一個典型的數據管理員模塊,這部分其實談不上TDD,有很多現有的框架可以使用,核心是數據庫的設計,Pi君不再贅述。

總結

到此,有關足球小游戲的代碼邏輯基本清晰,總結來看,我們需要實現的核心類有:

public class FootballGame{......} //足球比賽,這是一個單例

public class FootballTeam{......} //球隊

public class FootballAthlete{......} //球員

public class ControlAthlete{......} //控制球員

public class TeamFormation{......} //隊形

public class Station{......} //站位

他們之間的關系如下:

類關系圖

實現后臺邏輯以后,可以繼續考慮UI設計,Pi君給出比較簡單的UI交互圖:

主界面

點擊“設置”,如下圖:

模式設置界面

點擊“確定”,如下圖:

球隊設置

點擊“確定”,如下圖:

操控設置界面

點擊“確定”,游戲設置完畢。

主界面

點擊“數據管理”,如下圖:

管理員驗證界面

點擊“確定”,如下圖:

數據管理界面

數據管理界面也可以在“球隊設置”界面中被觸發。

到此,這個足球小游戲的詳細設計就差不多了,感興趣的看官們心癢不如手癢,現實不如Code,實現一下吧~任何問題歡迎留言討論~

單元測試部分的內容正在編寫中.....敬請期待吧~


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容