Thin框架的應用(一) 單機雙人對戰象棋程式

民工精髓發表於2013-08-22

使用JavaScript建立模組化的雙人對戰象棋程式

1. 關於這篇文章

2004年,我花兩天時間,用JavaScript和VML建立了一個單機雙人象棋,並且作了簡短的分析。在那個時代,沒有AngularJS,沒有BackBone,沒有所有這些前端MV*框架。甚至沒有jQuery,沒有prototype,沒有mootools,因此沒有什麼可借鑑的模組劃分方式。我只好用很原始的辦法,做了一種偽繼承,實際是組合,來實現棋子和棋局之間的關係。

現在是2013年,9年過去了,Web的世界早已不是過去的樣子,開發方式發生了翻天覆地的變化,我們有了Gmail,有了Google docs等等把Web技術應用到極致的優秀產品,有了asm.js、pdf.js等等讓我們目瞪口呆的技術,更催生了各種MV*框架的興起,我們有更多,更強大的方式去寫Web程式。

前一段時間,我建立了一個簡單的JavaScript框架叫做thin,實現了模組的定義、非同步載入和使用,並且為它寫了一個比較簡短的Demo,但這個Demo實在太簡單了,當一個應用更大、更復雜的時候,我們應該如何組織自己的程式呢?

為了說明我們這個簡單框架的能力,我把之前寫過的這個象棋在這個thin框架基礎上重寫一遍,並且作更深入的分析,以便使一些入門不久的讀者得到幫助,同時也順便檢驗我的新框架模組化是否是可用的。

另外一個方面,我們看到VML已經徹底衰落了,各種基於SVG和Canvas的繪圖技術取代了它,因此在本例中,我們也與時俱進,改用SVG來繪製棋盤和棋子。RaphaelJS是一個很好的跨平臺繪相簿,它封裝了SVG和VML,在能夠使用SVG的瀏覽器中,它用SVG繪圖,否則嘗試使用VML,對於上層應用,操作繪圖的API是毫無區別的,開發者不會感知到它的具體實現差別。

之前我寫過一些文字,用於探討軟體開發模組的劃分原則,感覺它們放在這篇文章裡非常合適,所以略微修改之後,加了進來。

2. 模組劃分的一些原則

2.1. 物件導向

物件導向可以算是老生常談了,在現代軟體開發中,它是個主流的選擇,相對於程式導向,有一些改進。

假設我們是上帝,要創造世界,因為這個過程太過複雜,無從入手,所以先從一件簡單的事情看起。現在我們要設計一個方法,用於描述狼吃羊這個事情,某隻狼吃了某隻羊,你可以程式導向地吃,eat(狼A, 羊A),也可以物件導向地吃,狼A.eat(羊A)。差別在哪裡?只是寫法有點變化。

好,那麼我們幫上帝模擬整個生物界,這裡面很多東西可以吃,大魚吃小魚,小魚吃蝦米,吃不吃皮,吐不吐骨頭,這個時候再來修改這個eat函式,複雜嗎?eat裡面要判斷很多東西,假如上帝很勤勞,所有程式碼都自己設計,那沒關係,沒太大區別,判斷就判斷唄。

假設上帝沒足夠精力來管理整個東西了,僱了一群天使來協助設計,每個人都來修改這個eat函式,當然可以拆分,wolfEatSheep(), tigerEatWolf(),然後在eat裡面判斷引數來分別呼叫,把函式分下去讓每個人做,可以。

動物不光要做吃這個事情,要能跑能跳,會說會叫,又多了一堆函式,每個裡面都這麼判斷,相當相當的煩。怎麼辦?我們來物件導向一下。

現在開始按照動物拆分,100個天使,每個天使創造一種動物。創造哪種動物,就站在哪種動物的角度考慮問題,我吃的時候怎麼吃,跑的時候怎麼跑,都跟別人無關,這麼一來,每個人就專注多了。每個動物只關注我要怎麼才能活著,不必站在上帝的角度考慮問題。這個過程,是類的劃分過程,也就是封裝的過程。

這時候,上帝覺得自然界光有動物是不行的,還要有植物,剛才說的這些都是動物,植物的特點跟動物有很大區別。假設你是上帝,為每種生物安排衣食住行,那是相當複雜的。偷懶吧,上帝說,植物們,你們自己生長吧,動物們,你們吃喝玩樂吧,假如能達到這個效果,那很省事。

上帝用一個迴圈來遍歷所有動物,讓他們吃喝玩樂,用另外一個迴圈讓植物欣欣向榮。動物跟植物為什麼要區別對待?因為它們不是同樣的東西,能做的事情不同。所有動物派生於動物這個基礎型別,從動物這個種類下,又分出各種綱,各種目,各種屬。獅子是哺乳動物,猴子也是,但是獅子是貓科動物,猴子是靈長動物,這就構成了一個倒著的樹狀體系,一層一層形成繼承關係。哺乳動物會餵奶,那麼所有繼承自哺乳動物的,都自動擁有這個特徵。整個這一切,構成了繼承鏈。

假設有一天由於變異出現了新物種,不必勞煩上帝關照,只要鑑別一下它屬於什麼型別,就知道能做什麼事了,它的一舉一動,都必然擁有它所繼承的種類的特徵。

這樣就能描述生物界了嗎?不,還有那麼一些怪胎的存在。你認為哺乳動物都不會飛,那就錯了,因為蝙蝠會飛。蝙蝠會飛是它自身的特性,並非繼承自哺乳動物,但是“飛”這個動作,卻非蝙蝠獨有。如果把“飛”定義成介面,那就很美好了,蝙蝠實現了它的飛行介面,雖然內部實現跟鳥類有所不同,而且這並不影響它的哺乳動物特性。

總之,是否物件導向只是思維方式的不同。做一個軟體,物件導向也能做,不物件導向也能做。我的觀點,如果關注可維護性和協作性,從目前的角度,物件導向是很好的選擇,它很自然,很優雅,優雅得只要打一個“.”,你就能想起來什麼事能做,什麼事不能做。

2.2. 模組的職責劃分

物件導向的一個基本原則是分而治之(Divide and Conquer),這種方法論提倡將程式模組化,各模組實現單獨的功能,在統一的管理下協同工作,構成整個系統。

在具體實施的時候,又有兩種傾向:將功能高度集中於主控制模組;將功能下放到各部件。這兩種做法都有很高的可行性,也分別有大量支持者。我覺得在一些程度上,後者更貼近人類的思維方式,更適合用人性化的觀念來解釋。

將兩種型別的程式對應到生物叢集,第一種相當於一個蟻群,第二個相當於人群。蟻群的特點是,個體能夠完成的事務非常有限,但是因為在一個非常強有力的統治者蟻后的控制下,它們能夠協同工作,統一排程,完成不可想象的事件。人群的特點是,每個人都可以獨立思考,能夠理解別人的指令,並且根據這些指令做到力所能及的事情。作為人群的統治者,他的智慧不需要比其他人的高太多,只需要從巨集觀上來把握一些事情即可。

從系統的實現來說,第一種方式難度很高。完成單個螞蟻(小模組)的功能並不複雜,建立大量的螞蟻也只不過是需要的時間多一點,但是,當開始設計蟻后(總控模組)的時候,噩夢開始了,整個排程演算法實在是一件令人頭疼的事情。對於比較複雜一點的系統,讓一個人去設計這個模組簡直是不可思議,但是如果由多個人共同完成這個模組,又面臨著互相理解的問題,每個人的思路都不相同,在努力協作的過程中,大量的時間被浪費在交流和意見的統一上。與此同時,製作螞蟻的程式設計師日益煩躁,覺得自己的工作沒有難度,無聊,士氣低下……

換一種思路,從人類管理的角度來看問題。假設有一支龐大的軍隊(假設是一個集團軍),司令官需要他的士兵列隊,我們來為這個系統設計排程演算法。先假設所有士兵跟螞蟻一樣笨,他們只能明白“站到司令部大門往東50米,往北100米的地方”這樣的簡單指令,請同情一下這位司令官,他不得不為每個士兵來指定一個位置,並且不得不研究列隊的規則,他需要整天忙碌來完成這樣一個龐大的任務(而且還不一定能完成)。他嘆息道:哦,上帝……

讓我們設法來減輕他的煩惱吧,目標是讓每個人都主動參與這個事件,不再那麼被動,大家都努力完成自己力所能及的工作。於是我們授權各級指揮官讓他自己的士兵列隊,這樣一來,司令官的工作簡單多了,他釋出命令:各位軍長請注意,我命令你們列隊,按照番號順序,分佈到司令部門口的空地上(假設這個空地足夠大,姑且認為能夠容納整個集團軍),各軍之間保持50米間隔。

接到命令以後,軍長們開始忙碌,而司令官先生已經可以搬一把椅子坐到電話機旁,等待列隊完畢的報告了。同樣,軍長要做的事情,也就是告訴屬下的各位師長,讓他們按照番號順序列隊,就這樣,命令被傳遞到最下面一級。班長大喊:夥計們,按照個頭排成一列,矮的在前面,高的在後面,前後間隔一米!於是,所有人站到了他應該站的位置,望著在短時間內迅速列隊的整個集團軍,司令官太滿意了。

我們發現了什麼?很顯然,下放權力的方式要省事得多,更關鍵的是,它使得每個人都做一定的事情,但是又不成為負擔。在設計者思路清晰化的同時,負責為系統每個部分編寫程式碼的人員也更容易享受到程式設計的樂趣,就算是最低層的程式設計師也有了發揮自己才能、用自己的思路去影響系統的機會,而且,系統整合的過程將變得更加簡單。

對於一名軟體設計師來說,他的思想決定了他所設計出來的軟體結構,將自己的靈魂注入到冰冷的程式碼中,這是一種藝術。然而,不同的人有不同的風格,設計者對於世界的認知方式不同,他們對於同樣的需求,可能採用的設計方式也多種多樣。

3. 怎麼設計我們的象棋程式

3.1. 為象棋程式劃分模組

做一個象棋程式,有哪些事情要做呢?

首先,我們要能夠初始化一個棋局,把棋盤和棋子繪製出來,點選棋子的時候,能給出它可以走的地方,可以移動這個棋子,也可以吃掉對方的棋子。走完一步,要能夠判斷是否將軍,如果吃掉了對方的將帥,能夠判斷棋局的終止。這些東西,除了繪圖之外,我們都放在棋局模組裡,我們有個第一個模組Game。繪圖模組的職責比較單一,我們把它放在一個棋盤模組中,這是第二個模組ChessBoard。

天下之事,事事都是棋局,人在局中為名來,為利往,都是棋子。可見,與棋局相對的就是棋子了。按照我們在第二部分提到的思路,棋子應當是要承擔一些職責的,那麼,哪些事情適合交給棋子來做呢?

我們定義這麼一個規則:做一件事,如果有多個參與者,其中某個參與者要付出的代價最大,這一步就由這個參與者來負責做。

我們把走棋的這個過程分解,這裡有四個部分:

  • 判斷我有沒有可能出現在那個位置,比如說,象不能過河,老將和衛士不能出九宮格。
  • 判斷目標位置有沒有己方棋子,如果有,也過不去。
  • 判斷能否直接到達目標位置,比如說,馬腿是否被擋著了?象眼是否被塞著了?
  • 移動過去,如果有對方棋子,吃掉它。

從第一步來看,這個過程不依賴於其他任何東西,每個棋子都應當能夠牢記自己能去什麼地方,不能去什麼地方,只要你給它一個棋盤座標,它自己是可以知道能不能去的。比如象知道自己不能過河,如果你給的座標就超過了,它可以知道自己不能去。所以,這個職責我們放給棋子。

再看第二步,這個我們怎麼判斷呢?假設我們是一個士兵,在平原上打仗,我想知道前面山頂有沒有人,怎麼辦?看了很多電影的我們表示,很好辦。“總部總部,請偵察對面山頂。”所以,這個過程我們可以看到,檢索目標位置不是棋子自身的職責,他只是呼叫了某個別的東西(己方司令部),得到了結果。

下面是第三步,這裡面有可能不需要依賴於其他模組,也可能要依賴,怎麼解釋呢?比如說衛士,他走路只看距離,如果是他的合法可達位置,並且和當前位置距離的平方為1+1=2,那就可以直接過去,不需要依賴任何外部模組。但是如果是馬,要先看距離的平方是不是1+4=5,然後再找馬腿的位置,再去看那個位置有沒有棋子。所以這種情況下,就要依賴外部模組。

第四步看似很簡單,過去的時候發個通知給司令部,我換地方了!但司令部那邊要把當前所有人分別在哪都記錄著,所以他要做的事情其實比棋子更復雜,所以這一步可以讓他做。

於是,我們得出結論,棋子的職責應該是這些:

  • 判斷自己是否可能出現在某位置
  • 判斷自己能否到達某位置

現在我們就有了Chess模組,並且從它派生出各種棋子,同時,為這些棋子實現一個工廠模組ChessFactory,用於根據引數建立這些棋子。

現在我們來考慮,誰來提供這個查詢的服務,承擔司令部的這些職責,棋子的位置都儲存在棋局中了,所以,很自然地,棋局承擔了這個職責。使用。

簡單地考慮一下,我們的棋局應當能夠:

  • 初始化。初始化方法做的是把棋局恢復成初始狀態,每次重新開局之前,我們可以這麼做一下。

  • 走棋。走棋是把給出的棋子移動到指定位置,如果目標位置沒有別的棋子,只做移動,否則還要把對方殺死。走棋之前有一些判斷條件,我們也把它們列出來。

  • 列出某棋子的可達範圍。這其實是一個輔助功能,當使用者點選某棋子的時候,介面上能夠標示出所有該棋子的可到達位置,便於使用者選擇,當使用者選擇其中某一個的時候,把棋子移動過去。

  • 判斷是否終局。每一步棋走完,我們都需要看一下是否有一方獲勝,如果有,本局應當終止。

3.2. 程式碼結構

根據上述的結論,我們建立了這麼4個程式碼檔案,用於存放不同的模組:

  • game,存放棋局相關的功能
  • chessboard,存放繪製棋盤和棋子相關的功能,點選操作也由它負責傳遞
  • config,存放各種配置資訊,比如棋盤大小等等
  • chessman,存放各種棋子的功能和棋子生成器

注意到我在chessman.js裡面,定義了多個模組,這其實就是我這個thin框架的核心理念,模組跟檔案不一一對應,模組對應於Java中的class檔案,而js檔案對應於Java中的jar檔案,是模組的集合。這麼做當然也有弊端,因為無法得知模組是否有衝突,或者存在被覆蓋的情況,引用也不是很方面,所以我為此還建立了一套管理和釋出機制,專門用來解決這個問題。在小型專案純手寫程式碼的情況下,直接這樣用就可以了。程式碼細節不一一列出,請讀者自行檢視。

4. 可能的改進

上面我們實現了一個可以在單機下雙人對戰的象棋程式,執行得還不錯,但是我們想要給它一些增強,應當如何去做呢?

如果我們想要本機開多個棋局,怎麼辦?

在我們現有結構下,其實很簡單,因為我們模組化做得還是挺好的,Game可以作為頂層模型,然後建立出對應的DOM容器,用我們上次寫的Bind來掃描一遍,自動建立例項,就可以了。

另外一個很典型的增強是,既然我們都做了單機的對戰了,是不是可以搞一個服務端,變成聯機的對戰呢?當然可以,要做這個,我們需要改動的程式碼是Game模組,這一步不再適合直接建立了,而是要放在新建棋局的服務端回撥裡面,棋局的狀態也需要在服務端儲存一份,然後每次下棋,把走的棋子和座標放過去,對其他任何模組都沒有改動。從這裡我們也可以看到如果程式碼進行了合理的分層,當需要改進的時候,對原始碼改動有多麼容易。

再有這麼一天,我們還要做棋局的撤銷怎麼辦?雖然這個很不好,有損大丈夫的威名,但我們只從實現角度來分析一下。在設計模式中,有一種叫做命令模式,這種模式其實就很適合做undo跟redo,只要把每個事情都封裝為步驟,那麼,這兩種操作就變成了正向和反向的兩種步驟了,做起來也就非常容易。

還不滿意,要新增人工智慧怎麼辦?拋開人工智慧常用的剪枝演算法不談,我們假設已有這麼個演算法,只需要在一方移動棋子之後,把當前局勢傳遞給這個演算法得到下一步即可。

綜上所述,做大一點的Web應用,必須先做模組化,把模組按照功能劃分,理清它們之間的關係,然後再用合適的框架去管理維護。作者正在編寫的thin框架就是試圖從模組入手,一步一步新增其他功能,把它做成一個有一定可用性的框架。

本文的Demo地址是:http://xufei.github.io/thin/demo/chess.html

相關文章