今回は実装方法の説明ではなく、majiang-ui の設計思想について語ってみたいと思います。これは、「なぜjQueryを使うのか」、「なぜ自前でMVCを実装するのか」、「なぜTypescript化しないのか」に対する回答でもあります。
- パッケージに分割する
- Canvas を使わない
- HTMLを「書かない」
- HTML/CSSに依存しない
- jQuery を使い続ける
- MVCモデルを採用する
- 型を定義しない
- Pug と Stylus を採用する
- CSS用のclassを使わない(試み)
パッケージに分割する
電脳麻将 の開発を開始した当初(2015年)から ver. 0.9.9 (2018年)までは prototype構文 を使用しており、プログラムファイルも複数に分けられてはいるものの、UIに関するコードは view.js と paipu.js に分けられているのみでした。牌譜ビューア(paipu.js)とそれ以外(view.js)です。
ver. 1.0.0 (2019年)で Node.js を採用したため、CommonJS での class構文 を使うようになり、ファイルもクラス単位になりました。DOM操作をともなうクラスは Majiang.View 配下に集め、ディレクトリも階層化 しました。
その後、麻雀サーバーを実装するためにパッケージ分割が必要となったので、ver. 2.0.0 (2022年)で以下の3つのパッケージに分割し、DOM操作をともなう機能は majiang-ui に集約し、現在に至ります。
- majiang-core
- 麻雀サーバーに組み込むことを意識した基本パッケージ。CLIで動作する。
- majiang-ai
- AIはサーバーには不要なため、AI部分を分割し開発ツールも含めてパッケージ化したもの。CLIで動作する。
- majiang-ui
- DOM操作を伴う機能はすべてここにまとめた。現在リファクタリングしているのはこれ。
Canvas を使わない
麻雀アプリを書こうと思い立った当初は「Canvas を使うのかなあ」ぐらいの意識でしたが、まずは天鳳の 牌理 相当のページを作ることからはじめました。これならHTMLだけで作成できます。天鳳の牌理は「手の内にある牌」だけが対象ですが、これを副露手にも広げようと思ったときに「横向きの牌」をどう表現するかという問題に直面しました。ですがすぐに CSS の transform: rotate() で牌だけでなく手牌全体も回転させることが できることが分った ので、面倒そうなCanvasを使わず、素のHTMLで盤面を表現することに決めました。
HTMLを「書かない」
開発当初から ver.0.9.9 までは このように HTMLを「書いて」いました。ですが以前から jQuery を使った開発で「雛形になるHTMLをコピーして使う」という手法を使っていた*1のでこれに切り替えることにしました。jQuery はDOMノードのコピーや差し込みが得意なので、極力HTMLを書かないようにすべきだと考えています*2。
HTML/CSSに依存しない
Node.js のパッケージは npm で配布しますが、これは JavaScript の配布に適しており、HTMLやCSSを部品化して配布するには向きません。そこで majiang-ui の担当範囲は「与えられたHTMLを雛形としてHTMLを再構成すること」に限定しました。それをどう見せるかはCSS次第ですが、それは責任範囲外と割り切りました。この決断により、はからずもHTMLとCSSから自由になり、デザインと実装を分離することが可能になったのです。現在の電脳麻将はデザイナー作成のHTML/CSSによる画面配置に「ゲームを張り付ける」ことが可能な実装になっています。
jQuery を使い続ける
よく「jQueryはオワコン」と言われ、「意識高いプログラマ」からは嫌われる傾向にありますが、「DOMノードをコピーし、適切な場所に差し込む」実装においてはこれほど適したライブラリはないでしょう。秀逸なのはすべてのDOM操作を map関数 として実現していることです。つまりセレクタで得た操作対象が単数でも複数でも、さらに素晴らしいことにはゼロであっても問題なく動作するのです。電脳麻将ではAIとの対局の際には対局者名を表示しませんが、majiang-ui 側ではそんなことは気にせずに常に対局者名を差し込むことができます。皆さんの大好きな いわゆる Vanilla JavaScript では適用対象が 0 か 1 かそれ以上かを常に意識しなければならず、NULL操作を避ける if文の嵐になってしまうでしょう*3。
MVCモデルを採用する
私はこれもオワコンと言われている MVCモデル も大好きで、アプリケーションの実装に多用します。電脳麻将の対局において、M (モデル)は 卓情報 です。卓情報は現在の盤面の状況を表しており、UIだけではなくAIも牌譜ビューアも牌譜解析ツールも使用します。そこにはUIのための情報も機能もありません。V (ビュー)は 盤面表示クラス であり、そこではイベントは扱いません。M の更新も行わす、責務は表示することのみに限定します。C (コントローラー)は 対局エンジン です。対局エンジンは適切なタイミングで M である卓情報を更新し、更新したことを V である盤面表示クラスに伝えます。もう1つの C は 対局者UI です*4。対局者UIは対局エンジンから動作を促され、M の状況から適切なユーザーの操作をイベントとして感知し、対局エンジンに伝えます。ここでAI同士の対局の際(あるいは対局中であっても他者の手牌)には対局者UIが不要なことに注意してください。C は V から切り離せるべきです。近頃流行りの V + C 一体型では実現できないこともあるのです。*5
型を定義しない
私はTypescriptが嫌いです。電脳麻将でも面子 ![]()
![]()
を s555= と文字列で表します。文字列で表す利点は、正規表現 が使えることです。面子を表示するプログラムは 正規表現を駆使して書いています。もう一つの利点は見てすぐに分かること、さらには牌譜などシリアライズする際にも そのまま使える ことです。よく「型を使ったほうが安全だ」とアドバイスをくれる方*6がいますが、そもそも「どんな局面で危険」なのでしょうか?電脳麻将には面子をゼロから作るコードは1つもありません。手牌に この牌で鳴けるか と聞けば可能な面子のリストが返ってくるのでその中から選ぶだけです。一応、面子としての形式が正しいかチェックする関数 も用意しましたが、使用しているのはそれを定義したクラスの内部でだけです*7。コンパイル時にチェック可能な型こそ使っていませんが、私はこれも型だと思っており*8、ドキュメント化 もしています。
Pug と Stylus を採用する
HTMLとCSSはパッケージの管轄外にしましたが、これらを書くにも便利なプリプロセッサがあります。私はHTMLとCSSの部品化のために Pug と Stylus を使っています。どちらもメンテナンスされていないように見えるようでオワコン判定する人もいますが、むしろ機能として完成しきっているからこそメンテナンスの必要がないのです。そう思えるくらい私は気に入っています。特にその記法が大好きです。これらは ドキュメントに使う記法としてもすぐれている と思うので、電脳麻将UIに関する記事 でもたびたび使用しています。
CSS用のclassを使わない(試み)
jQueryが嫌われた原因の1つに「プログラマ側がjQueryの目印にするためのclassを乱発して現場が混乱する」ということがあげられると思います。元来はデザイナがHTMLを装飾する目印としてclassを使っていたのに*9、プログラマまでclassを使うようになったため名前の衝突が起こり、適用範囲が不明確になりやすいCSSの特性もあいまって、原因の究明しにくい不可解な表示の乱れを引き起こしたのです。私自身は、プログラマが使う目印のほうにプレフィックスをつけるなりして衝突を避けるのがよいと思いますが、これを検証するために「プログラマだけがclassを使ってもCSSでデザインできるのではないか」という実験をしながらリファクタリングを進めています。今のところギリギリ何とかなっています*10。
以上、電脳麻将のUIに関わる設計思想を思いつくままに書いてみました。私は簡単なことを大げさにやるのが大嫌いです。なので、jQuery、MVCモデル、正規表現、便利なプリプロセッサを愛しています。どうか私にたいそうなフレームワークや型システムを勧めないでください😁
*1:Twitterアプリ を作ったときは、1つ1つのツィートを表示する雛形をコピーしてアイコンやツィート内容を「差し込む」実装にしていました
*2:JSXを使ってHTMLを「書く」React とは別の方向です
*3:そもそもjQueryは既存のHTML文書に「ギミック」を追加する目的で開発されており、目的のDOMノードが見つからないとエラーになっていたのでは使い物にならないのです
*4:すいません。ドキュメント化はまだです
*5:MVCに関して React は「宣言的」にこだわるあまり、描画のタイミングを M の変化のみから「悟ろう」としすぎです。このため M に React のヒントにするための不要な情報が継ぎ足され、M の独立性を損ねる結果になっているのではないでしょうか。ObserverパターンはMVCの本質ではないと思うのです。
*6:主にGeminiとChatGPTです
*7:せっかくクラスメソッドにしたのに
*8:どんな構造をしているかよりどんな操作が可能かで型を考えるのが抽象データ型やオブジェクト指向だというのが私の理解です
*9:まあそれも正しい使い方とは言えませんが
*10:とはいえ最後までこれでいけるかの自信はありません