標籤:
“回憶總是殘酷的”——在“設計業務對象與對象職責劃分(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:也許會有讀者疑問,怎麼設計的結果沒體現具體設計模式的精髓?因筆者考慮到,設計模式不能離開代碼,且設計模式是思路、是建議,而非終極目標,能在設計環節考慮到、思考到,待到代碼實現環節體現出其內涵與精髓,甚至模式變形,也比陷入設計過度要好。
(寫了三四個小時,先發布吃個晚飯,再來校對錯別字哈~ #校對後刪此行#)
線上捉鬼遊戲開發之二 - 設計業務對象與對象職責劃分(3)