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

TDD-PRACTICE

背景


TDD:測試驅(qū)動設(shè)計,各種理論历涝,各種優(yōu)劣诅需,網(wǎng)上有很多的文章來介紹漾唉,但是怎么做TDD,從何處開始堰塌?會遇到什么問題赵刑?怎么解決?OK蔫仙,PI君也是剛接觸TDD沒多久,理論不多說丐箩,直接從一個小項目開始摇邦。

項目需求


① 一個足球比賽類小游戲,用戶可以通過鍵盤操控控球球員前進/后退/左轉(zhuǎn)/右轉(zhuǎn)/加速/傳球/射門屎勘;如果用戶控制的球隊沒有球權(quán)施籍,則用戶可以切換控制球員進行鏟球/防守;用戶可以控制游戲開始概漱,設(shè)置游戲時間丑慎,一般有兩支球隊進行比賽;

② 每個球隊有一個教練瓤摧,有十一個球員竿裂,有自己的球隊隊形,用戶可以自己調(diào)整針對特定隊形的球員站位照弥,有自己的隊服/隊徽腻异;

③ 有一個管理員賬號,管理員可以管理球隊相關(guān)數(shù)據(jù)这揣,包括球員數(shù)據(jù)/教練數(shù)據(jù)/隊形數(shù)據(jù)/隊服數(shù)據(jù)/隊徽數(shù)據(jù)梳猪。

需求分析


根據(jù)需求確定UseCase碴巾,盡可能使用代碼描述UseCase。

UseCase-One:Play football game


① 一個足球比賽類小游戲,用戶可以通過鍵盤操控控球球員前進/后退/左轉(zhuǎn)/右轉(zhuǎn)/加速/傳球/射門鳖孤;如果用戶控制的球隊沒有球權(quán),則用戶可以切換控制球員進行鏟球/防守银舱;用戶可以控制游戲開始屑咳,設(shè)置游戲時間,一般有兩支球隊進行比賽柑蛇;

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

Step-One:

一個足球比賽類小游戲罐旗,有兩個球隊進行比賽,用戶可以控制比賽開始唯蝶,暫停和結(jié)束九秀,Code 描述如下:

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

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

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

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

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

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

注:文中的代碼都是在新建的單元測試?yán)镞M行編寫粘我,其中涉及到的FootballTeam等類型鼓蜒,實際上并不存在痹换,Pi君就是通過代碼把UseCase建立起來,然后確定有哪些類型需要創(chuàng)建都弹,每一個類型又有哪些方法/成員等等娇豫,這些都是TDD的理論基礎(chǔ),不熟悉的看官直接Google吧畅厢。Pi君在此就不啰嗦了冯痢。

注:代碼中有特殊標(biāo)記的部分都是后續(xù)可能會引用分析,并進行修改的部分框杜,暫時可以忽略其效果浦楣。

注:文中Pi君給出的是C#版本的代碼,但是有關(guān)TDD的實踐方式是相通的咪辱,如需java/python/C++版本振劳,Pi君會根據(jù)時間安排進行轉(zhuǎn)換,至于其他語言版本油狂,很遺憾Pi暫時還不擅長历恐。

Step-Two:

每個球隊有十一個球員,比賽過程中专筷,當(dāng)球隊具有球權(quán)時弱贼,則用戶只能通過鍵盤控制控球球員進行前進/后退/左轉(zhuǎn)/右轉(zhuǎn)/加速/傳球/射門的動作;當(dāng)球隊失去球權(quán)時磷蛹,用戶可以切換受控制的球員哮洽,在切換過程中,選取用戶控制球隊距離足球最近的球員弦聂。

detail step by step:

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

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

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

② 比賽過程中鸟辅,當(dāng)球隊具有球權(quán)時:

//“球權(quán)”是比賽過程中的一種狀態(tài)屬性:teamA或teamB

newGame.BallRightTeam= teamA; //球隊A具有球權(quán)

newGame.BallRightTeam= teamB; //球隊A失去球權(quán),球隊B獲得球權(quán)

當(dāng)然莺葫,也有建議可以把“球權(quán)”作為球隊的一個屬性匪凉,類似teamA.HaveBall = true來描述球隊A具有球權(quán),但是這樣做需要一個關(guān)鍵的邏輯處理捺檬,如果teamA.HaveBall = true再层,則teamB.HaveBall = false必須同時成立,既然如此堡纬,Pi君還是建議把“球權(quán)”作為比賽過程中的一個狀態(tài)屬性比較直觀聂受,也無須其他的邏輯處理。

③ 當(dāng)球隊具有球權(quán)時烤镐,用戶只能通過鍵盤控制控球球員進行前進/后退/左轉(zhuǎn)/右轉(zhuǎn)/加速/傳球/射門的動作:

“控球球員”是一個動態(tài)的概念蛋济,隨著足球的運動,控制足球的球員也在隨之變化炮叶,控球球員可以被操控進行各種不同的動作碗旅,所以控球球員需要一個獨立的類來處理渡处,至于為什么不把“控球”作為球員的一個屬性,看官們可以反推祟辟,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; //后退鍵 ...... 類似包含左轉(zhuǎn)/右轉(zhuǎn)/加速/傳球/射門的鍵

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

? ? ? public ControlAthlete()

? ? ? ?{

? ? ? ? ? ? ?/*注冊動作鍵被按下時的響應(yīng)事件*/

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

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

? ? ? ?}

}

看官可能會奇怪旧困,為什么不設(shè)置“球權(quán)”呢醇份,畢竟事件的響應(yīng)是根據(jù)“球權(quán)”狀態(tài)來決定的,想想看吼具,“球權(quán)”是比賽的一個屬性僚纷,并且是一個動態(tài)的屬性,取值范圍固定在球隊A和球隊B馍悟,所以畔濒,需要獲取“球權(quán)”的值剩晴,只需要讓teamA.ControlAthlete知道newGame的信息就OK了锣咒,這樣,每次鍵盤事件響應(yīng)時赞弥,實時判斷當(dāng)前比賽的“球權(quán)”毅整,“控球球員”即可做出正確的動作。

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

④ 當(dāng)球隊失去球權(quán)時拼窥,用戶可以切換受控制的球員戏蔑,在切換過程中,選取用戶控制球隊距離足球最近的球員:

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

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

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

public class ControlAthlete ? //控制球員類

{

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

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

? ? ? public ControlAthlete()

? ? ? {

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

? ? ? }

? ? ?//參數(shù)暫時不用定義

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

? ? ? {

? ? ? ? ? ?//獲取當(dāng)前比賽對象?捍壤?骤视???

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

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

? ? ? ? ? ? ? ?return;

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

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

? ? ? ? ? ? ? ?return;

? ? ? ? ? this._selectAthlete = nextAthlete;

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

? ? ? }

}

出現(xiàn)了一個問題鹃觉,F(xiàn)ootballGame類在“Step-One”中已經(jīng)存在一個構(gòu)造函數(shù)如下:

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

而剛剛,F(xiàn)ootballGame類還存在另外一個構(gòu)造函數(shù)盗扇,如上代碼中黑體+斜體+中劃線的部分笼裳。FootballGame本身是一個單例唯卖,也就是內(nèi)存中始終只有一個該類的實例,并且單例有自己固有的實現(xiàn)方式躬柬,之前FootballGame類的兩種構(gòu)造方式顯然違反了單例的實現(xiàn)方式拜轨,OK,Pi君先給出FootballGame類單例的實現(xiàn)方式:

public class FootballGame

{

? ? ?private FootballGame(){} ? //私有化構(gòu)造函數(shù)

? ? ?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”允青,可能會引起看官們的疑惑橄碾,為啥不用屬性和私有變量,直接讓變量公有颠锉,豈不是破壞了類的封裝法牲?有違習(xí)慣嘛~~其實,這里首先有一個問題需要研究清楚琼掠,為什么會有屬性的概念拒垃,屬性帶來的好處有哪些?為免離題太遠瓷蛙,Pi君只拋出問題悼瓮,歡迎看官們留言討論,說說自己的想法艰猬,也聽一聽別人的想法横堡,一起學(xué)習(xí),一起進步~

OK冠桃,對FootballGame類的實現(xiàn)命贴,意味著需要對之前代碼中獲取或新建FootballGame對象的部分進行調(diào)整和修改。現(xiàn)將修改后的代碼展示如下:

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

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

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

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

OK食听,到此胸蛛,針對UseCase-One的代碼描述基本清晰,但是仍然有一些細節(jié)的問題沒有處理樱报,例如葬项,“控制球員”的每一個動作函數(shù)應(yīng)該怎么編寫,其實肃弟,這是深入層面需要考慮的問題玷室,感興趣的看官們可以思考下,Pi君也會在后續(xù)給出github上的源碼鏈接笤受。

UseCase-Two:FootballTeam Struct


② 每個球隊有一個教練穷缤,有十一個球員,有自己的球隊隊形箩兽,用戶可以自己調(diào)整針對特定隊形的球員站位津肛,有自己的隊服/隊徽;

該條需求直接給出了球隊的基本數(shù)據(jù)結(jié)構(gòu)汗贫,So身坐,代碼描述如下:

public class FootballTeam

{

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

? ? ?private TeamFormation _formation; //隊形

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

? ? ?private string _TShirt; //隊服

? ? ?private string _teamLog; //隊徽

}

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

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

如果“站位”除了開場時球員所處的球場位置以外抹腿,還涵蓋球員的頻繁跑動區(qū)域(防守責(zé)任區(qū)/進攻戰(zhàn)術(shù)責(zé)任區(qū)等)岛请,那么“站位”的概念要豐富的多,“站位”可以理解為一種控制規(guī)則警绩,球員跑動/傳球/防守需要從“站位”中讀取規(guī)則崇败,然后做出相應(yīng)的動作;

既然“站位”的概念被豐富了,那么把“站位”作為球員的屬性就變得很勉強肩祥,OK后室,不如把“站位”獨立出來,更符合單一職責(zé)原則搭幻,二者之間的關(guān)系是“站位”---->“FootballAthlete”咧擂;

→回轉(zhuǎn)查看之前FootballTeam的設(shè)計逞盆,“private FootballAthlete[11] _athletes;? //十一名球員”的存在就顯得的多余了檀蹋,毫不猶豫,先把這一行刪除云芦,后續(xù)也許有新的需求導(dǎo)致該行的重新恢復(fù)俯逾,所以,暫時先注釋掉該行是個不錯的習(xí)慣舅逸。

OK桌肴,現(xiàn)在“隊形”被分解為“站位”,“站位”又包括哪些行為或者屬性呢琉历?繼續(xù)Step by Step:

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

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

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

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

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

現(xiàn)在數(shù)據(jù)有了坠七,怎么觸發(fā)行為,行為又是怎么發(fā)生呢旗笔?繼續(xù)Step by Step:

//帶球跑動的球員是否會觸發(fā)對方球員的防守行為彪置?

這個問題的回答是層級性的,可以設(shè)想為游戲難度蝇恶,因為需求沒有涉及拳魁,理解過程中簡單的假設(shè)有兩種游戲難度:困難/簡單,“困難”級別的游戲撮弧,這個問題的答案自然是:true潘懊,“簡單”級別則是false姚糊。當(dāng)然,如果把游戲難度細分為“新手級”/"普通級"/“困難級”/“專家級”/“變態(tài)級”授舟,那這個問題就不能簡單的使用bool值描述......又是一個新的邏輯處理塊救恨,但是轉(zhuǎn)念思考,暫時沒有這種需求释树,那就采取最簡單的策略:“簡單”級別忿薇,即答案為false,切記不可過度設(shè)計躏哩,這是TDD最給力的地方署浩。

當(dāng)然,如果是用戶控制球員防守扫尺,那就另當(dāng)別論了筋栋。

//球員的無球跑動?

無球跑動正驻,理解為責(zé)任區(qū)內(nèi)的晃動弊攘,及脫離“控制球員”的球員“發(fā)現(xiàn)”自己不在責(zé)任區(qū)內(nèi)時的自動修正跑動」檬铮可以放在“RefreshAthleteStatus(); //刷新球員狀態(tài)(繪制信息)”中添加處理邏輯襟交,不贅述。

OK伤靠,到此有關(guān)UseCase-Two:FootballTeam Struct的基本結(jié)構(gòu)已經(jīng)清晰捣域。

UseCase-Three:DataManager


③ 有一個管理員賬號,管理員可以管理球隊相關(guān)數(shù)據(jù)宴合,包括球員數(shù)據(jù)/教練數(shù)據(jù)/隊形數(shù)據(jù)/隊服數(shù)據(jù)/隊徽數(shù)據(jù)焕梅。

這是一個典型的數(shù)據(jù)管理員模塊,這部分其實談不上TDD卦洽,有很多現(xiàn)有的框架可以使用贞言,核心是數(shù)據(jù)庫的設(shè)計,Pi君不再贅述阀蒂。

總結(jié)

到此该窗,有關(guān)足球小游戲的代碼邏輯基本清晰,總結(jié)來看蚤霞,我們需要實現(xiàn)的核心類有:

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

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

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

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

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

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

他們之間的關(guān)系如下:

類關(guān)系圖

實現(xiàn)后臺邏輯以后,可以繼續(xù)考慮UI設(shè)計争便,Pi君給出比較簡單的UI交互圖:

主界面

點擊“設(shè)置”级零,如下圖:

模式設(shè)置界面

點擊“確定”,如下圖:

球隊設(shè)置

點擊“確定”,如下圖:

操控設(shè)置界面

點擊“確定”奏纪,游戲設(shè)置完畢鉴嗤。

主界面

點擊“數(shù)據(jù)管理”,如下圖:

管理員驗證界面

點擊“確定”序调,如下圖:

數(shù)據(jù)管理界面

數(shù)據(jù)管理界面也可以在“球隊設(shè)置”界面中被觸發(fā)醉锅。

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

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


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末边酒,一起剝皮案震驚了整個濱河市经柴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌墩朦,老刑警劉巖坯认,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異氓涣,居然都是意外死亡牛哺,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門劳吠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來引润,“玉大人,你說我怎么就攤上這事痒玩〈靖剑” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵凰荚,是天一觀的道長燃观。 經(jīng)常有香客問我褒脯,道長便瑟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任番川,我火速辦了婚禮到涂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘颁督。我一直安慰自己践啄,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布沉御。 她就那樣靜靜地躺著屿讽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上伐谈,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天烂完,我揣著相機與錄音,去河邊找鬼诵棵。 笑死抠蚣,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的履澳。 我是一名探鬼主播嘶窄,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼距贷!你這毒婦竟也來了柄冲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤忠蝗,失蹤者是張志新(化名)和其女友劉穎羊初,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體什湘,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡长赞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了闽撤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片得哆。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哟旗,靈堂內(nèi)的尸體忽然破棺而出贩据,到底是詐尸還是另有隱情,我是刑警寧澤闸餐,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布饱亮,位于F島的核電站,受9級特大地震影響舍沙,放射性物質(zhì)發(fā)生泄漏近上。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一拂铡、第九天 我趴在偏房一處隱蔽的房頂上張望壹无。 院中可真熱鬧,春花似錦感帅、人聲如沸斗锭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岖是。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間豺撑,已是汗流浹背作箍。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留前硫,地道東北人胞得。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像屹电,于是被迫代替她去往敵國和親阶剑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容