美文网首页程序员首页投稿(暂停使用,暂停投稿)
TDD(测试驱动设计)的项目实践——需求分析

TDD(测试驱动设计)的项目实践——需求分析

作者: 周PI君 | 来源:发表于2016-02-11 21:51 被阅读913次
    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,实现一下吧~任何问题欢迎留言讨论~

    单元测试部分的内容正在编写中.....敬请期待吧~


    相关文章

      网友评论

        本文标题:TDD(测试驱动设计)的项目实践——需求分析

        本文链接:https://www.haomeiwen.com/subject/xzeykttx.html