关于某道C#上机题的OO 我也来个OO设计吧,大家鉴赏鉴赏
题目
17个人围成一圈,从第一个人开始报数,报到3的倍数的退出,一直到剩下最后一个人,用面向对象的思想去做这道题。
需求分析
题目可抽象为N个游戏参与者(拟人,可非人。指任何参与游戏的物体,例如机器人等)根据某个规则玩游戏。解法要求为使用面向对象思想。
题目并未对输出进行要求,属于未定因素。但一般来讲,是游戏就要有结果,而我们需要将结果输出出来,才能知道游戏已结束。所以结果输出为必要条件,输出格式与详细程度为可变条件。
既然题目要求使用面向对象思想,在我的理解,就是要在应对变化时提高程序代码的复用率(继承),减少程序代码的修改率(尽量为添加,而非修改),并在这一条件要求下,尽可能提供便利的程序的使用方式(运行时代码组织方式)。
题目很短,简单分析即可得知:
题目可能产生的变量为:
- 1. 参与游戏物体的个数(本题为17)
- 2. 参与游戏物体的形式(本题为人)
- 3. 参与游戏物体的组织形式(本题为“围成一圈”)
- 4. 游戏规则
- 4.1 游戏起始位置(本题为“从第一个人开始”)
- 4.2 游戏形式(本题为“报数”)
- 4.3 游戏规则(本题为“报到3的倍数退出”)
- 4.4 游戏结束标志(本题为“一直到剩下最后一个人”)
- 5. 游戏结果输出形式
经过以上分析,我们基本在脑中已经大概有了对象组织结构的概念了,我们应该有Source类,来代表参与游戏的物体,应该有Game类来代表游戏本身等等。我们现在就开始组织这些零碎的观点,形成一个高可伸缩性的代码结构。
架构设计
根据需求分析,我们很容易得出一些想当然的结论,这个题目比较简单,我们就定义一个类代表人(Person),一个类代表游戏(Game),游戏类有两个方法,开始(GameStart)和结束(GameOver),不就可以了吗?
问题1:如何改变游戏物体的个数
简单,创建Person对象,然后加入到Game里去嘛,循环17次。所以,游戏需要多两个方法,添加,删除(Add,Remove)
问题2:如何改变游戏物体的类型(人或其他)
简单,定义一个基类(SourceBase),表示一个“游戏物体”好了。
问题3:如何表示“围成一圈”
简单,人嘛,围成一圈自然是手拉手了,所以人要有两个属性“左手(LeftHand)”和“右手(RightHand)”,分别指向上一个人和下一个人。
问题4:更换“游戏规则”
恩。。。Game类自身的逻辑就是游戏规则吧。如果要换一个规则,就重新写一个Game类。
问题5:如何输出结果?
简单,在GameOver的时候输出结果就行了嘛。。。恩。。所以GameStart和GameOver应该为两个事件。分别在游戏开始和结束时被触发,并输出相应的结果。
问题6:如果要在执行的每一步都输出结果呢?
恩。。。改变Game类中的逻辑吧(Game类:汗,又要改我。。。我为什么要说又呢?)
问题7:如果我要处理的不是人(没有左右手),而是一个怪物(只有一只手)?
晕。。。那就在创建一个怪物对象,只有一只手的!。。。等等。。好像添加删除的逻辑也要改。。(GAME类:大哥别改了。。服了。。。等会儿人家又变成人了可咋办?)
问题8,问题9,问题10.。。
。。。。。。。。。以上废话请自动略过。。。。。。。。。。。
在应对诸多变化的情况下,我们似乎没有办法都实现,即便都实现了,感觉也和传统的面向过程没什么区别,一个无敌巨大的GAME类。面向对象就是如此了吗(废话,当然不是。)?原因很简单,这是因为我们停留在仅使用“封装”(类)的层面上,还没有使用所谓的“面向对象”。
表面上看,这个程序是由“游戏”和“人”两个简单的对象组成,但,什么是人?
简单的来说,为了区分所有的source,我们必须有个标识(HashId)。人作为一个独特的对象,拥有左右手及记忆对话(Conversation)的能力。
我们再来分析一下,什么是游戏?
广泛意义上来说,游戏即为对游戏对象根据游戏规则进行重规划的活动。游戏更像一个Manager,来依据一定的规则管理Source。这样,游戏就变成了一个SourceManager。那么这个Manager有些什么功能呢?依据这个题目,Manager必须要有“添加”,“删除”Source的能力才能进行所谓的“Management”(管理)。
我们便有了如下这个类:
为了提高代码复用与规范代码结构,我们把BaseSourceManager,创建为抽象类,T用来表示参与游戏的物体的类型,Methods均为抽象方法。针对此题目,我们是对人进行操作,我们便继承这个Base,实现一个对人进行操作的SourceManager:
RefreshPeopleConnection这个方法是用来更新人的左右手链接的(当然,我们可以在添加删除的方法中更新,偷懒就这么做了,而且感觉不会出问题,逻辑也统一一点 哈哈)。
至此,我们已经有一个简单的SourceManager了,但是游戏的逻辑啊规则啊,开始结束啊完全都没加进去,该怎么处理呢?
我们在此引入Decorator(装饰模式)的设计模式,为什么要使用它而不是别的呢?简单的说,我认为游戏规则只是对游戏的一种装饰,我们可以一次使用一个规则,也可以使用好几个规则,这都不应该让我们对现有规则产生任何影响,使用装饰模式会极大的精简各种功能组合子类的数量,也就提高了代码复用度及可维护性。
这个类按照经典的装饰模式编写:
1: namespace OO_Program
2: {
3: public abstract class DecoratorSourceManager : BaseSourceManager<IPerson>
4: {
5: private BaseSourceManager<IPerson> sourceManager;
6:
7: public DecoratorSourceManager(BaseSourceManager<IPerson> sourceManager)
8: {
9: this.sourceManager = sourceManager;
10: }
11:
12: public override void Add(IPerson source)
13: {
14: this.sourceManager.Add(source);
15: }
16:
17: public override void Remove(IPerson source)
18: {
19: this.sourceManager.Remove(source);
20: }
21:
22: public override IList<IPerson> GetAllSource()
23: {
24: return this.sourceManager.GetAllSource();
25: }
26: }
27: }
感觉是一点改动也没有吧,完全和教科书上的一致。
好了,终于到游戏规则出场的时候了:
把这个游戏作为SourceManager的一种装饰类的形式出现,并添加了自己的方法(Play)和事件(GameOver, GameStarted).
至此,整个程序主体架构已经搭建完毕了,我们该如何使用呢?
1: var game = new PersonNumberOffGame(new PNOGameSourceManager());
2: //初始化游戏参与者
3: for (int i = 0; i < 17; i++)
4: {
5: Person p = new Person();
6: game.Add(p);
7: }
8:
9: game.GameStarted += new EventHandler<GameEventArgs>(game_GameStarted);
10: game.GameOver += new EventHandler<OO_Program.EventHandlers.GameEventArgs>(game_GameOver);
11: game.Play(0);
自己感觉还是比较有语义的,逻辑也很流畅,没有什么废代码。一行一行解释一下:
1 创建一个用“PNOGameSourceManager”管理Source的,依据“PersonNumberOffGame”为规则的game对象。
3-7 创建17个person,加入游戏中来(注意,此Add方法实际为“PNOGameSourceManager”的方法。这就是装饰模式的好处啊~~~)
9 如果游戏开始,执行“game_GameStarted”
10 如果游戏结束,执行“game_GameOver”
11从第一个元素开始玩游戏。
看一下结果处理的代码:
1: static void game_GameOver(object sender, OO_Program.EventHandlers.GameEventArgs e)
2: {
3: if (e.State == GameStates.SuccessfulEnd)
4: {
5: var game = sender as PersonNumberOffGame;
6: //Console.WriteLine("最后剩下的人为:[{0}]", game.GetResult().ResultSource.HashId);
7: game.GetResult().Print();
8: Console.WriteLine("游戏成功结束");
9: }
10: }
这个更简单了:
3 如果游戏成功结束
5 不得不写的废话
7 获取游戏结果并打印。
8 打印一句废话 :)。
至此,程序架构完毕。(耐心看至此的童鞋请在下面报到~~~~ 每人赠送回复一条~~)
我们费了这么多神,把程序搞得这么复杂(其实我认为是更清晰了),获得了什么好处了吗?
场景一:这个程序只能获得最后剩下的那个人,我们需要知道每一步退出的人员的列表。
合情合理的需求,直接上图:
创建一个名为“LogableSourceManager”的装饰类,在Remove方法中加入Log输出:
1: public class LogableSourceManager : DecoratorSourceManager, ILogable
2: {
3: public List<PNOGameLog> Logs
4: {
5: get;
6: set;
7: }
8:
9: public LogableSourceManager(BaseSourceManager<IPerson> sourceManager)
10: : base(sourceManager)
11: {
12: this.Logs = new List<PNOGameLog>();
13: }
14:
15: public override void Remove(IPerson source)
16: {
17: base.Remove(source);
18: var log = new PNOGameLog(source, "移除出队列");
19: this.Logs.Add(log);
20: Console.WriteLine("[{0}]说了{1},被{2}",
21: log.EventSource.HashId,
22: log.EventSource.Conversation,
23: log.Message);
24: }
重点为15-23行。
我们并不需要重新实现一边remove只需加入我们需要增添的功能。
使用方法为:
1: static void Main(string[] args)
2: {
3: var game = new PersonNumberOffGame(new LogableSourceManager(new PNOGameSourceManager()));
4: //var game = new PersonNumberOffGame(new PNOGameSourceManager());
5:
6: //初始化游戏参与者
7: for (int i = 0; i < 17; i++)
8: {
9: Person p = new Person();
10: game.Add(p);
11: }
12:
13: game.GameStarted += new EventHandler<GameEventArgs>(game_GameStarted);
14: game.GameOver += new EventHandler<OO_Program.EventHandlers.GameEventArgs>(game_GameOver);
15: game.Play(0);
16: }
只许修改一行调用代码,便加入了Log输出功能。原有程序结构一个字都不用改。是不是很爽。哈哈哈哈。。(咳,咳。。岔气。。)
来个整体架构图:
P.S.谁知道怎么用live writer直接上附件?
代码全文如下:
/Files/pandora/OO_Program.zip