背景
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)系如下:
實現(xiàn)后臺邏輯以后,可以繼續(xù)考慮UI設(shè)計争便,Pi君給出比較簡單的UI交互圖:
點擊“設(shè)置”级零,如下圖:
點擊“確定”,如下圖:
點擊“確定”,如下圖:
點擊“確定”奏纪,游戲設(shè)置完畢鉴嗤。
點擊“數(shù)據(jù)管理”,如下圖:
點擊“確定”序调,如下圖:
數(shù)據(jù)管理界面也可以在“球隊設(shè)置”界面中被觸發(fā)醉锅。
到此,這個足球小游戲的詳細設(shè)計就差不多了发绢,感興趣的看官們心癢不如手癢硬耍,現(xiàn)實不如Code,實現(xiàn)一下吧~任何問題歡迎留言討論~
單元測試部分的內(nèi)容正在編寫中.....敬請期待吧~