“回忆总是残酷的”——在“设计业务对象与对象职责划分(2)”中,对旧版本的代码进行了剖析,也发现了不少臭味道,本篇将记录我是如何建设新版的业务对象职责划分。
一、复习设计模式
当初自学设计模式的路径是:从《大话设计模式》开始(做了笔记),到Gof的《设计模式》,再到辛勤网友们的各篇总结日志(只看C#的可能会有些局限~)。此后,每当我有需要更新代码的时候,或者觉得不太记得清23种经典设计模式的时候,我就会回翻我的笔记,主要看:模式目的、应用场景,以最快速度在脑子里回放。在复习的同时,会不自觉就想到这个设计模式可能可以解决要更新项目中的某些问题,然后立即翻看第(2)篇中的代码剖析,并在草稿板(买了块白板当草稿纸)上做uml类图简单设计,并在脑海里过示例代码。我觉得这个做法就好像、也应该像日后需要熟能生巧的动作一样,要不断重复,直到娴熟。
写到这里的时候,觉得有必要分享一份笔记中一句话总结23中设计模式的部分,所以写了:一句话的设计模式,相信必是槽点满满:)
经过复习,初步觉得也许能用上的模式有:
创建型:单例(Setting),候选:工厂;
结构型:装饰(处理各角色间的结构关系),候选:桥接、代理;
行为型:观察者(定时更新)、中介者(由Game作为各参与者的中介)、状态(投死状态改变,可能会设计过度),候选:访问者;
二、优化游戏流程
(1)旧版:进入后有旁观/报名按钮供登陆者选择,Table类负责维护所有人的名单列表,退出旁观/报名都会不断地变更类;
优化:进入后旁观者什么也不需要做,而是报名者需要点击“入座”按钮——较符合现实场景;Table类不再维护旁观者列表——因为Table游戏桌只需要管谁参与游戏,旁观者叫什么名字在整个游戏流程中并没有贡献;游戏开始前的站起/入座(也就是报名与旁观)都不会有新的类生成——因为具体是平民、白痴还是鬼的身份应该在游戏开始后才生成,而不是这一开始就确定下来,如此做法,也避免了(2)篇中类之间复杂转换的问题。
(2)旧版:投票没做完
优化:由AddBallot()增加投票—IsVoteEnd()是否投票结束——Roll()唱票,三个部分。即:投票环节时,每当有一位参与者点选了要投的人或弃权票,投票管理者都会检查是否已经结束,若没结束,则继续接收投票,若已结束,就开始唱票。很好理解吧~
三、划分对象职责与对象协作顺序
相比这是本篇,也是本系列最重要的一节:在后续代码实现过程中将不断回顾此节内容,甚至发现此节可进一步优化之处,并更新之。
此节以UML2.0作为表示法,使用Rational Rose工具,从类图、顺序图角度描述了关键业务类之间的关系,以及这些类在游戏主流程中是如何分发消息、互相协作完成任务的。
(1)首先看类图
Class Diagram
不算复杂吧,其中还省略了部分关联关系(如Ghost可直接与SpeakManager、VoteManager关联,解决一开始的鬼内部讨论首轮发言顺序的问题),只显示主要关系,我们先从旧版中沿用的类开始说:
旧版的7个类:Table、Game、Subject、Setting、Audience、Civilian、Ghost,除了Audience外,其他都沿用写来了。
按照游戏顺序来吧:
Table:是游戏程序一启动就会存在的由单例模式创建的游戏桌类,内部维护了一个Game类与PlayerManager类。
为什么没有方法呢?当然是有的,只是现在属于初步设计阶段,如前述提到的,这些类图、顺序图,甚至游戏流程都可能在后续代码实现中发现不合理之处,到时候再回头修改(我也会回到相应文章进行修改,专门写一篇此系列的日志——用于记录关键的修改行为)。
PlayerManager:旧版Table类的臭味道之一就是职责过多,不但负责了通知Game类开始游戏,还负责维护桌面人数、核查是否开始,此处就将这些不必要的职责进行了分离,新建了专门维护人数的PlayerManager类,可见其方法是对Player进行Set、Delete与Get。关键属性是维护了一个9个成员的String数组,NameArray,负责游戏开始前入座者名单的确定。以便列出顺序。游戏开始并分配角色后,转而维护9个成员的Player数组,PlayerArray。为什么用数组Array而不是旧版中的列表List——因为本来就定好了标配人数(可在xml中维护,此处以标配来说),只等对应入座,且所需内存更少,类之间传递的运算速度更快,便于存储。
Setting:与旧版唯一不同的是,由Setting来负责检查入座报名参加的人是否已经满了IsFull(),以此返回布尔值类型,再一路向上汇报回到Table,由Table向Game发起开始游戏Start()的通知。
Game:原来的一大堆职责都分配出去了,无事一身轻了吧~嘿嘿!的确,分配出职责之后就只剩下开始Start(),与重新开始Restart()——结束一场又开启一场时用,当然其实也可以由Table负责重新创建一个Game对象,那么Game就更轻松了~现在活脱脱一嘴巴叼着烟斗的看门儿大爷,乐呵呵看着下边儿小弟干活,自己只管发号开工、收工的指令,喔对了,连收工都不用说,底下小弟自己会判断游戏是否结束~
Subject:与旧版唯一不同的是,由Subject来负责向词库获取题目GetSubject(),并填充本身维护的三个存放词的属性,以后外界要词,就都找他(旧版中还复制了一份给到各参与者手中,这看似与现实不符,但实际上是优化现实中可能存在手动改题作弊的信息化流程的优势),所以需要建立全局访问点,故还是考虑单例模式创建。
Player、Civilian、Idiot、Ghost:后三者继承Player,眼疾手快的应该看出来了——PlayerManager维护的Player数组是对抽象类Player的维护,此后若需要从中提出制定的一类对象,就可考虑用lambda表达式完成了,如:List<Ghost> ghostList = PlayerManager.Player.ToList().Where(g=>g.Type().Equals(Ghost)); ——直接打的没在vs运行过,有bug请多包涵哈[憨笑]!在旧版当中,Ghost继承自Civilian,Civilian继承自Audience,此处不考虑旁观者类(在代码实现部分在考虑补充,也许还会增加回来这个Audience类,就看增在哪了),且Ghost实际上不是一个Civilian,所以不应使用继承关系表示,所有他们相似的方法,应该提升到Player抽象类来完成,如设计模式中提及的:非is-a关系的,不应该用继承关系表示。那么如何区分Civilian平民、Idiot白痴、Ghost鬼呢——别忘了装饰模式喔~(回顾本篇第一节复习设计模式后,列出的可能用得上的设计模式部分)
好了,剩下没讲到的类,都是从旧版Game中分离出来的职责:
RoleManager负责分配角色、SpeakManager负责管理对话列表、LoopManager负责检查此轮发言(包括PK时的发言)是否结束、VoteManager负责管投票环节、DeathManager负责充当刽子手,WinManager负责检查鬼是否胜利(鬼没胜利就继续,直到鬼全死或者鬼胜利,所以无需以好人们胜利作为结束游戏的标准)。
好了,如此一来各个类都比较专一的负责自己要做的事情了——单一职责原则——如果现实生活中真有这样单一职责的工作该多好,而偏偏社会需要的是全才,就好比学计算机的也会被领导叫去搬主机箱一样(比喻,比喻~),关于工作的话题小生我也就4年工作经验,不敢在各位老江湖面前班门弄斧阅历少见识短的大道理,还是趁年轻、趁着梦没被现实叫醒,赶紧做能承受的起的事情吧~
(2)进入与游戏开始顺序图
Enter Diagram
顺序图是个啥东西,此处相信认识的朋友们也能听懂一二。
1-4. 每当有人入座,Table就会将此人昵称丢给PlayerManager,由PlayerManager问Setting“人齐了没?”(IsFull)。有人起立的时也一样告诉PlayerManager要删掉人名,此时PlayerManager维护的是String数组。若Setting告诉PlayerManager“人齐啦!”,消息会传回Table的耳朵里,Table拔出一根猴毛那么一吹啊(作为绘画爱好者,一定要向燃起的国产动画《大圣归来》致敬!),就蹦出个Game类,游戏就此开始。
5-6. Game先向Subject要题,再叫来RoleManager随机给PlayerManager维护的String数组分配角色,并将角色依次披上Player抽象类外衣,再列队回到PlayerManager中。是不是感觉RoleManager有点像建造者模式中的指挥者——大老远敢来只带一身才华(类的方法),匆忙按要求完成任务后也不带走一片云彩。哈哈,是有这么个意思,具体能否结合建造者模式、是否有必要结合建造者模式,我们在代码实现部分与大家共同探讨。
7-8. 题和身份角色都准备好了,就有系统说话,向Player参赛者们说明他们各自的身份与题目,并说明现在进入鬼指定首轮发言人的时候。SpeakManager当之无愧作为此任务的完成这,显示记录下系统说了什么SystemSpeak(),再将记录发布到前台ShowRecord()。
(3)鬼讨论顺序图
Ghost Speak Diagram
非常简单,找SpeakManager就够了~
(4)鬼投票(决定首轮发言人)顺序图
Ghost Vote Diagram
1-3. 在鬼投票决定首轮发言人的时候,前台界面会出现所有人名字的按钮(PlayerManager维护的String人名数组又派上用场了吧),每当有一个鬼点选投票的时候,Ghost对象将票递给检票官VoteManager,由检票官查看大家都投完了吗IsVoteEnd(),投完了咱就唱票Roll()。
4-5. 属于备选事件流(来自OOAD的提法,事件流分为主事件流与备选事件流),在鬼投票结果不一致(有人按错,或意见不统一)时发生,此时检票官VoteManager会让SpeakManager帮忙发布圣旨SystemSpeak(),告诉前台ShowRecord(),让鬼们赶紧统一意见。
6-7. 当鬼的投票一致时,检票官可算松了口气,赶紧把接力棒交给LoopManager,让其记录本轮开始,并让SpeakManager来设定允许发言的鬼投出来的首轮发言人。什么叫“允许发言的”——如果非SpeakManager官方SetSpeaker()的玩家,无论他们说什么都不予记录在案,当然也就不会ShowRecord()给别人看,也就是“不需场外”,此允许谁说话的职责,肯定要落到SpeakManager的身上。
(5)所有角色玩家发言顺序图
Player Speak Diagram
与鬼发言不同的是,玩家发言要按顺序,且每人发言后LoopManager都会不厌其烦的看一眼是否本轮结束。具体过程我就不赘述了。
(6)所有角色玩家投票顺序图
Player Vote Diagram
1-5. 正常投票,不赘述。
6-7. 属于备选事件流,用于投票结果出现相同票数时的PK发言环节。显示投票官将同票者交给LoopManager,让其决定谁先说SetLoopStarter(),别忘了要告诉SpeakManager允许记录他的话SetSpeaker(),说完后就会回到1-5步骤,继续投票。当然,如系列中的游戏流程介绍篇所言,PK台上的人是不能进行他们自己的投票的,这一点在顺序图中不体现,在实际代码中可通过Player的是否有投票权属性来标识(暂时性剥夺投票权利法治制度的赶脚哈哈)。
8-9. 无论如何,每轮结束肯定得有人被检票官VoteManager交给死神DeathManager,并由死神执行死刑SetPlayerDie()——旧版中是玩家自杀(SetDie的方法在玩家对象中),这里是刽子手处决,感觉更合理了吧~每次有人升天,WinManager大仙都会探出头看看凡间这桌的这个游戏(Table.Game)是否结束,结束的标准是鬼是否胜利IsGhostWin()。
10-11. 如果游戏还没结束,那么游戏的指挥棒再次回到VoteManager手中(毕竟整个都是投票环节产生的一系列互动,让其他任何Manager接棒都不合适),检票官会通知LoopManager开始新的一轮玩家发言,当然别忘了经过SpeakManager允许的SetSpeaker(),才能言论自由喔~
四、总结此篇步骤
本篇末尾特此增加一节,来讲述上述那一堆(我自认为还算是比较)清晰的类职责划分与流程,是如何在旧版思维潜移默化根植的情况下建立出来的。先上图:
为了让大家看清笔记,我就不缩略了。
这是我设计时考虑的第一张图,可以看到左上角,当时还是沿用的三层继承的方式处理各个角色,因为一时没想通,就不希望在此环节卡壳,就继续了对Table和Game的职责分离。看左下角,最初的PlayerManager还只是一个孩子(PlayerList,一个属性而已),针对的是继承与Audience的Player抽象类(但仍然没解决三层继承的问题,且Audience思维根深蒂固)。在看到图中间Game分出来的职责,重新招聘新员工后,将这些职责分配了他们,他们最初的名字还是Roler、Speaker、LoopChecker、Voter、WinChecker、Death、StartChecker(最后通过职责分析,更应该被分到Setting中,才有了如今花名IsFull()的方法,从此远离尘世,隐退江湖……)。各个类之间的大体顺序可在图中右侧1-13的序号查看,眼尖的朋友可能看出了判断Y/N逻辑,哈哈~
这第二幅,是为了专门解决各角色建的关系问题所画的。可以看到,这时候已经诞生了PlayerManager类,并和Game类平起平坐被Table大哥掌管。图中凌乱不堪的笔记是雨落邕城的日子里思绪纠缠的痕迹,细心的读者也许看到了枚举Role:Enum,是的,当时我是想Player类继承自Audience类,这样就化三层继承为两层继承,所有角色以枚举类型的Role属性来区分,但此时因考虑到开放封闭原则(为扩展开放,为修改封闭),不适用于枚举的增加——如,如果有一天游戏升级,规则中加入了华佗角色,可以起死复生(有点像杀人游戏的桌游升级版),难道还要进入枚举中进行修改吗?为何不通过增加一个新类的方式来解决呢,因此就有了图中Person类的框框,其他角色继承自Person——没错,那这么一来不就又回到旧版本的三层继承?不,虽然多层继承问题还没解决,但至少解决了一个重要问题:非is-a关系不能用继承——Civilian与Ghost不再是继承关系了!这点太重要了。请大家记住,如果只是为了方法调用方便,完全可以通过模板模式解决,再不行代理、外观模式也成啊,反正继承关系真是不到“是一个”(is-a)关系的时候,就不要考虑,否则可能出现庞大的继承树问题,或者像旧版一样陷入父子类频繁转换漩涡当中。
观察力强的朋友们也许注意到了右上角围着桌子做的玩家座位图。没错,当时是觉得前台界面不但要略微优化,还要解决Audience类的问题,就萌生了类似德州扑克那样“圆桌会议+坐下按钮”的方式,以此代替原来报名/旁观的按钮。拜德州扑克所赐,忽然间觉得Audience这类人真的对游戏贡献很不大,好像你去澳门赌场,你也许只关心同桌竞技的对手是何种人,而一点不关心围桌站起的旁观者(不要脑补赌神的作弊旁观者啊~),甚至连旁观者的名字都不想知道,顶多知道围着大概多少人就行了(通过cookie统计数字即可,且不需要即时更新,定较长的时间更新都不影响,可节省流量)。
因此我认为界面应该重新稍微布局一下,顺便理顺第一张草稿图中的主要顺序,因此有了第三张图:
哈哈,第一眼都看图去了吧,好像除了列出圆桌也没啥区别,好吧……我承认的确是的。
图中右侧形如大括号、箭头的,就是顺序图中的信息流箭头。是不是发现很粗糙,甚至整个流程(顺序图)那么多,怎么几行就搞定了:画到第三张图时,我觉得混沌的思路已经打开,职责划分、流程优化、对象关系等主要问题已经基本迎刃而解,只剩下规规整整的列出一份能见世面的图纸罢了——即,草图整理思路的环节已告破,可进入汇总思路、整理文档的环节,进而我就转入了上述第三节类图、顺序图的绘制过程。
如果你一定要问上述三张图都是什么表示法,那我只能拍脑袋随便起个毫无意义的名儿了——不要局限于手中的绘图工具(rational rose或visio或vs的modeling项目),一开始是无法对着如此工整的电脑软件将大脑中思维跳跃、混乱待整的脑电波表现出来的,个人建议还是在草稿纸上进行,框框线线、粗糙标记,以最快速度记下所想所悟,别忘了,软件再高级也是为人类服务的,相信自己的大脑与握笔的手吧!
如果一定要说顺序,那请参照RUP(统一软件开发过程),多了解OOAD(面向对象分析与设计),结合SOLID原则(单一职责、开放封闭、里氏替换、接口隔离、依赖倒转)在整个设计、代码编写过程不断迭代审视,最终做到perfect——不禁想起我的一位高龄素描老师,赵晋,赵老师热爱画油画,有一副大榕树下油画他花了很多年,今天钓鱼回来添几笔,明年大年初一高兴又添几笔,如此反复……
Coder们加油,我们要做的事情还有很多,即便不在技术的道路上走,也能交交朋友,从代码中看到态度、领悟世事,不要枉费曾在IT之路走一遭。
PS:也许会有读者疑问,怎么设计的结果没体现具体设计模式的精髓?因笔者考虑到,设计模式不能离开代码,且设计模式是思路、是建议,而非终极目标,能在设计环节考虑到、思考到,待到代码实现环节体现出其内涵与精髓,甚至模式变形,也比陷入设计过度要好。
(写了三四个小时,先发布吃个晚饭,再来校对错别字哈~ #校对后删此行#)