外部ライブラリを直接使わずAdapterを挟む理由|TypeScriptで依存をまとめる

ゲーム制作では、自分ですべての処理を作らず外部ライブラリを利用することも多い。
今回の麻雀ゲームでも、牌山の生成や手牌解析、AI、対局処理などに外部ライブラリを利用している。
ただし、アプリのさまざまな場所からライブラリを直接呼ぶのではなく、間にAdapterとなる処理を挟むようにした。

外部ライブラリを直接呼ぶ場合

例えば手牌のシャンテン数を求めるだけなら、使う場所で直接ライブラリを呼ぶこともできる。

const hand = new Majiang.Shoupai(tiles);
const shanten =
  Majiang.Util.xiangting(hand);

1か所だけならこれでも問題ない。
しかし同じようなコードが、

画面A → 外部ライブラリ
画面B → 外部ライブラリ
ゲーム処理 → 外部ライブラリ
テスト → 外部ライブラリ

と増えていくと、アプリ全体がそのライブラリの仕様を知ることになる。

Adapterを1枚挟む

そこで、外部ライブラリを呼び出す処理を別ファイルへまとめる。
例えば今回の手牌解析なら、アプリ側では、

getShantenNumber(tiles);

だけを呼べるようにした。
中では、

export function getShantenNumber(
  tileCodes: string[],
): number {
  const hand =
    new Majiang.Shoupai(tileCodes);

  return Majiang.Util.xiangting(hand);
}

のように外部ライブラリを使っている。
構造としては、

アプリ
↓
Adapter
↓
外部ライブラリ

になる。

アプリ側がライブラリの使い方を知らなくて済む

この形の大きなメリットは、アプリ側が外部ライブラリ特有のAPIを知らなくてもいいこと。
例えば、

Majiang.Util.xiangting(...)

という書き方を知らなくても、

getShantenNumber(...)

なら何をする処理なのか分かりやすい。
麻雀ゲーム側の言葉に置き換えて、

isTenpaiHand(...)
getTenpaiSeats(...)

のような関数も作れる。
外部ライブラリのAPIより、アプリの目的に合わせた名前で処理を呼び出せるようになる。

ライブラリ固有のデータも変換できる

Adapterが便利だったもう一つの場面が、対局結果の取得。
外部ライブラリから返ってくる和了データは、そのライブラリ独自の形式になっている。
アプリ側では本当に必要なのは、

type WinResult = {
  winner: Seat;
  loser: Seat | null;
  method: "tsumo" | "ron";
  points: number;
  fu: number | null;
  han: number | null;
};

といった情報。
そこで、ライブラリ側のデータを一度受け取り、アプリ用の形式へ変換してから渡す。

外部ライブラリの和了データ
↓
Adapterで変換
↓
アプリ用のWinResult
↓
画面表示

これならUI側は外部ライブラリの複雑なデータ構造を意識しなくて済む。

ライブラリを変更するときの影響を減らせる

仮に将来、別の麻雀ライブラリへ変更するとする。
アプリ中で直接、

Majiang.○○

を書いていたら、そのコードを大量に修正する必要がある。
一方、

getShantenNumber()
createAiPlayer()
getWinResult()

のような自分のアプリ側の関数を経由していれば、主にAdapter側を修正すれば済む。
もちろん完全に影響をゼロにはできないが、依存する場所をまとめておくだけでもかなり違う。

テストもしやすくなる

Adapterに処理を分けておくと、その機能単体でテストしやすい。
例えば、

この手牌 → シャンテン数0

や、

この牌譜 → ロン和了

といった確認を、Reactの画面とは切り離して行える。
「画面が正しく動くか」と「外部ライブラリから正しい結果を取れるか」を別々に確認できるのは大きい。

Adapterを作りすぎる必要はない

とはいえ、外部ライブラリを使ったら必ずAdapterを作らなければならないわけではない。
1回しか使わない単純な処理なら、直接呼んだほうが分かりやすいこともある。

自分の場合は、

  • 複数の場所で使いそう
  • ライブラリ独自の型がある
  • データを変換したい
  • 今後機能が増えそう

といった処理をAdapterへ寄せるようにした。

今回のゲームでは役割ごとに分けた

今回のプロジェクトでは、用途ごとに処理を分けている。

majiangAdapter
→ 牌山・手牌解析など

majiangAiAdapter
→ AIプレイヤーの生成

majiangGameAdapter
→ 対局の開始・結果の変換など

全部を1つの巨大なファイルにまとめず、外部ライブラリのどの機能を扱うかで分けた。
機能が増えてきても、どこを見ればいいのか分かりやすい。

まとめ

外部ライブラリは便利だが、そのAPIをアプリ全体へ直接広げると、あとから扱いにくくなることがある。
そこで、

アプリ
↓
自分のアプリ向けの処理
↓
外部ライブラリ

という境界を作っておく。
今回のゲーム制作では、この形にしたことで、

  • ライブラリ固有の処理をまとめられる
  • アプリ向けの名前や型へ変換できる
  • UIが外部ライブラリの仕様を知らなくて済む
  • テストする範囲を分けやすい
  • 将来の変更箇所を絞りやすい

というメリットがあった。
小規模な個人開発では少し遠回りにも見えるが、外部ライブラリに任せる部分が増えてくるほど、間に境界を作っておく意味も大きくなってくる。

コメント

タイトルとURLをコピーしました