軽量DDDではじめるゲーム開発 ドメイン駆動設計の基本と実践を解説

形から入ったドメイン駆動設計によるゲーム開発の光と闇 #1/2 >> 2はこちら

ドメイン駆動設計によるゲーム開発の光と闇

中榮健二氏(以下、中榮):最初にいきなりなんですが、お詫びと訂正から。

サブタイトルに「DDDは果たしてゲーム開発に向いているのか!?」と書いていただいたんですけど、本日はDDDの核心部分の話はしません。この煽り文の答えが出ないことをお詫びして訂正いたします。すみませんでした。

(会場笑)

自己紹介です。株式会社Nextatの取締役の中榮と申します。取締役と書いてありますが、コードをバリバリ書いています。国産CMSの「baserCMS」のコミッターをしていますが、最近コミットはしていません。ふだんはLaravelメインでサーバサイドがメインなんですけど、たまにはUnityやJavaScriptを使ってフロントエンドもやっています。

本日の発表概要です。おそらくconnpassに書いたものは網羅できているのではないかと思います。

最初に参加者のみなさまがどんな方なのかわからないので聞かせていただきたいんですけど、ゲーム業界の方はどれぐらいいらっしゃいますか。趣味でゲームを開発している方?

(会場挙手)

なるほど。サーバサイドの開発の方?

(会場挙手)

あっ、よかった。ちなみにスマホアプリとか、フロントの開発の方はどれぐらいいらっしゃいます?

(会場挙手)

両方やっている人がいる。ちなみにソーシャルゲームをやったことがある方?

(会場挙手)

大丈夫そうですね。ありがとうございます。

次、DDDについて。DDDを聞いたことがない方? さすがにいないですね(笑)。開発に取り入れている方はどれぐらいいらっしゃいますか?

(会場挙手)

3分の1ぐらいかな。『エリック・エヴァンスのドメイン駆動設計』、読んだことある方?

エリック・エヴァンスのドメイン駆動設計

(会場挙手)

すごい。意外といますね。ありがとうございます。調査はここまでです。

DDDの概要

それでは、最初にDDDの軽い概要説明から入りたいと思います。DDDは、開発者・顧客や業務の専門家を含む関係者・チームの間で、共通の言葉とメンタルモデルを育ててシステムを継続的に改善・開発していく手法といわれています。これだけ見てもほとんどわからないですよね。

大きく分けて、ドメインモデリングの戦略と、戦略を実現するための細かい実装パターンに分かれています。後者はパターンランゲージと本の中では言われています。

最初によく使う用語だけ軽く解説します。ドメインとは、英語で「領域」という意味なんですけど、ソフトウェアを作る対象となる問題領域のことを指します。

次にモデル。事象の特定の側面だけを抜き出して簡略化して表したものです。モデルというとIT限定かと思われるんですけど、基本的に自然科学でよく使われる言葉で、シミュレーションのモデル……例えばエアコンで温度の時間的変化を微分方程式で表して目的の温度に早く収束させるようなところにも数理モデルが使われています。

次にドメインエキスパート。これも繰り返し出てくるのですが、その問題領域に詳しい専門家のことを指します。

最後にユビキタス言語。ドメインのモデルを作るにあたってチームメンバーで齟齬がないように、意味を同じに解釈できるように共通の語彙を作る。これをユビキタス言語と呼びます。

このDDDなんですけど、ドメインモデリングをしていくにあたって、ドメインエキスパートと協力してドメインを理解して、モデルを継続的・イテレーティブに改善していく。これが本筋になっています。

ところが、この仮定が成立しない開発現場ももちろんあると思います。例えば、開発がイテレーティブではないウォーターフォールだったり、上流から某ソフトで書かれた設計書だけが流れてきたり、ドメインエキスパート・専門家の人と開発者が対話ができる環境にないことだったり、そもそもドメインエキスパートは空想上の生き物じゃないのかということだったり、よくありますよね。

現実は厳しいです。いちエンジニアが組織やプロセスの根本の改善から始めるか? それとも俺自身がドメインエキスパートになるか? このままではDDDを実践することはできないのか? このようなことも思ったりするかもしれません。

軽量DDDへの批判、失敗談

ただ、そこにも若干の光はありまして、DDDの一部を抜き出したパターンの「軽量DDD」があります。「戦術的DDD」とも言いますけど、最近はおそらく軽量DDDという言い方のほうが一般的なので、こちらを採用しています。

例えば、DDD本で紹介されている、レイヤ化アーキテクチャ、Repository、Factory、Entity、Value Object。こういう戦術的パターンをパターンだけ取り入れて、形から入っちゃおうというのが軽量DDDです。今日の発表のタイトルの「形から」はこのあたりを指しています。

軽量DDD、戦術的DDDというと単語的にはかっこいいんですけど、悪く言えば「形から入ったDDDもどきにしかならない」という批判がけっこう聞かれます。ちゃんと専門家と話し合ってドメインモデリングをしたときのようなメリットは薄いんじゃないかと。Twitterを調べるとよく出るんですけど、「〇〇フレームワークでDDDするには?」というと、だいたいこの軽量DDDに陥っていることが多いです。

ただ、今日はこの軽量DDDを中心にした話をします。メリットが薄いとはいえ、効果がないわけではないです。

それにエヴァンスの本を読んでみると、この戦術的なパターンの解説だけでもけっこうな分量が割かれています。なので、ドメインエキスパートがいない場合でも、開発者が触る部分をその戦術パターンのベストプラクティスを参考にできる。

ドメインエキスパートと話して、DDDをちゃんと実践していこくときでも、その基礎として戦術の部分を理解していれば、モデリングするときに細部にとらわれることがなく、モデリングに集中できるんじゃないかと思っております。

レイヤ化アーキテクチャ

本日、実例から学ぶ形式になっているんですけれども、ソーシャルゲーム、一般的なブラウザとスマホでプレイできる簡易RPGみたいな形式のゲームを想定しております。

ゲームシステムの主な構成ですが、ゲームサーバがあってブラウザ版、Android版、iOS版があるような感じになっています。基本的にゲームサーバがJSONをAPIで返して、それぞれのクライアントが表示を処理をする構成です。

それに加えて、ゲームサーバ側にゲームのバランス調整をするためのマスタデータ管理画面。そしてユーザーのお問い合わせに対応するために履歴などを調査できるようなCS管理画面。こちらはAPIではなくて、単にHTMLで画面を表示するものになっております。

今回のプロジェクトは、先ほどドメインエキスパートがいない場合などと煽ってたのですが、いいプロジェクトでした。2つ目、実際のコードと用語をそのまま出すと特定などの恐れがあるので、若干簡略化したり変更していることをご了承ください。

1つ目です。これがけっこう長くて、これで半分ぐらいまでいこうかなと思っています。1つ目、レイヤ・クラス分割の話。光と闇が3対7。さっそく光が少なくてあれなんですけど。レイヤ分割をする前は、僕たちは、基本的にMVCフレームワークとかを使って、それに沿った開発をしていました。

Model。基本的にLaravelを使っているんですけど、LaravelのORMがEloquentといってActiveRecord系の一般的なORMです。モデルクラスを継承すると、クエリ、DB保存、ロジックをそこにつけ加えたりできるようになる。

ControllerでそのModelを呼び出して、なにか処理をしてレスポンスを返します。これはHTMLじゃないのでViewといってもわかりづらいかもしれないんですけど、一応JSONを返すところがMVCのViewです。

ここにみなさんよくやっていると思われる定番のDDDの戦術を取り入れます。1つ目がレイヤ化アーキテクチャ。レイヤードととも言います。

アプリケーションを4つの層に分けます。UI、Application、Domain、Infrastructure。UIはユーザーの入出力を受け取る層。Applicationがソフトウェアのユースケースみたいなものを定義する層です。Domainが、エリック・エヴァンスの本で強調されているソフトウェアの核心、ビジネスの概念・ルールを表現する。その下に、Domainのレイヤの下で使う永続化やメッセージ送信などの一般的な技術的機能を提供するInfrastructure層があります。

複雑なプログラム。複雑なものはだいたい分解して簡単にしようというのが一般的な話ですよね。レイヤに分割しようと。先ほどのレイヤ4つあるのですが、それぞれ下方向にしか依存しないようにします。DomainがUIに依存したりするとダメです。

エヴァンス本では、とくにこのレイヤードアーキテクチャを何のために導入するかというと、ドメイン層を隔離してドメインオブジェクトがドメインの仕事だけに専念できるようにすることが主目的だと語られています。

ドメイン、インフラ層のクラスたち

2つ目。これもよく見ると思うんですけど、ドメイン、インフラ層でこういうクラス分けをするとやりやすいと紹介されています。Entity、VO、Service、Factory、Repository。1つずつ軽く紹介しますね。

まず1つ目、Entity。これがドメインの主役的なオブジェクトでして、時間経過で状態が変化したとしても同一とみなすようなオブジェクト、簡単に言えばIDを持っているようなオブジェクトのことです。ソーシャルゲームの例で言うと、例えばユーザーがガチャで手に入れたキャラクターはレベルアップしたりしても同じものと見なしますよね。

これは簡易的なコードなので、あとからスライドを上げるので細かいところは適当に見ておいてもらえればと思います。IDがあって、キャラクターのマスタデータを持っていて、ロックできたりします。

2つ目、Value Object、VOと略されます。先ほどのIDで識別するEntityと違って、値だけが重要なオブジェクトをVOと呼んでいます。Entityみたいに変化しないで、1回作れば同じものだとして扱うものです。

比較のときも、IDの比較じゃなくて、オブジェクトが違ったとしてもその中の値が同じであればイコールであるとみなすオブジェクトです。例えば、ユーザー所持キャラクターのレベルやレアリティ、名前など、スカラー値をラップしたようなものが多いですかね。

例えば、キャラクターのレアリティ。単にintを中に閉じ込めたオブジェクトですが、レアリティがこのゲームに1〜5しかないとなったら、不正なレアリティができないように防ぐことができるようになります。

Factoryは、基本的にEntityの内部構造を外にさらけ出さないためのもので、Entityを変えるときに、new……インスタンスの生成がそこら中で行われていると影響範囲が広い。だから、その生成過程をカプセル化することがFactoryの主目的です。Entityを新規生成したり、DBからもう1回作り直すときによく使います。

CharacterFactoryはDBから再生成する例です。CharacterRecordというDBのレコードに相当するオブジェクトからそのプロパティを取り出して、EntityとVOで先ほどのキャラクターみたいなものを作る。

4つ目のRepositoryも重要なオブジェクトで、最初のEntityをデータストアに永続化したり、データストアから取得してきたり、あるいはもう少し複雑に検索して持ってきたりというオブジェクトです。

コレクションの抽象化、本当はDBに永続化されているんですけど、あたかもメモリ上にあるかのように扱えるようにというパターンの1つです。

通常はこのRepositoryをそのまま使うのではなくインターフェースを用意して、ドメイン層からどこに永続化しているか、どんな永続化をしているか、どんなクエリを発行しているかなどは気にさせないようにするのが一般的です。

CharacterRepository。まぁ、これはいいかな。

Serviceもドメイン・アプリケーションに属するものなんですけど、EntityとかVOの機能としてモデル化しにくい……例えば、複数のEntityが絡むことや、ドメインの言葉だけど名詞ではなく動詞のようなものになっているもの。例えばユーザー所持キャラクターを売却できないようにロックすることや、強化することなどの動詞的なものをサービスとして表すことが多いです。

これも簡単な例なんですが、リポジトリでキャラクターを持ってきて、ロックして、もう1回永続化する。これらの概念を用いて、最初のMVCみたいなものの対応を表したのがこの図です。

見ておいてほしいのは、Infrastructure層とUI層にフレームワークの機能が押し込まれていて、Application・Domainはほとんど自分たちのソースコードで書いていけるのが1つのポイントかと思います。

レイヤ、クラス分割時の「光」

これを実践した結果どうなったか。1つ目です。最初MVCフレームワークLaravelを使っていたときはORMのモデルがだいぶ太ってたんですね。

その前にFat Controllerという段階があって、それに比べれば、だいぶマシなんですけれども、それでもまだクエリの役目と永続化の役目とドメインロジックの役目と3つ持っていたものがちゃんと分離されます。

永続化に関してはリポジトリがやってくれるので、POPO(Plain Old PHP Object)で実装したEntityやVOにドメインの機能だけが実装されます。見てわかりやすくなるのが、1つのいいところでした。

もともとクエリがどこでも発行できたんですけど、だいたいRepository、もう少し言ったらFactoryぐらいで、発行箇所がある程度抑えられます。もう1つ、最初分けたときはクラスが多すぎて「なんなんだ」と思ってたんですけど、Value Objectを使ったことによって、ただのintやstringなどのスカラーの型になっているよりはコードがドメインの言葉に直接表現されていて、わかりやすくなったことが挙げられますす。

また不正なオブジェクトが、先ほどのコンストラクタでレアリティ1〜5までしか作らせないみたいなように、防止できるところが1つのいいところです。

あと、これは完全に技術的なところなんですけど、例えばEntityになにかint的な属性が2つあったときに引数の順番を間違えるとか、単純なミスが減るのもいいところだと思います。

光は残念ながらここまでです。

レイヤ、クラス分割時の「闇」

闇です。1つ目はクラス数の増大。僕たちが使っていたLaravelは記述量をめちゃめちゃ短く書けるのが1つのウリでした。短い記述になれていたので、VOやEntity、FactoryやRepositoryなどのクラス数の増加に恐れおののいて、慣れるまでに時間がかかる問題がありました。

だから、最初のうちは何がどこに書いてあるのかわかりません。プロジェクトに入ってきた人たちから「把握していないうちはぜんぜんわからない」みたいな感想をよく聞きます。どのクラスで何をするのか・何をしてはいけないのかをちゃんと共有しておかないと、いっそ分けないほうがマシだという状態にもなりかねないです。

例えば、Repositoryに永続化の詳細を閉じ込めたと言っているんですけど、実はFactoryやServiceで発行されている脱法クエリがたまにありました。

もう1つ、貧血ドメインモデル。ただ単に「分ければいいんでしょ。分ければ」みたいな感じに最初なりがちなので、DomainやEntityを守るために分けたはずなのに、守るべきEntityにそこまでの価値がない状態になることがあります。

レイヤ分割して、Repositoryを作ってEntityの属性をVOでラップして「これでDDDできたぞ」ってなりがちなんですけど、実は軽量DDD的にもまだどうかなというレベルなんですよね。

例えばDBレコードのオブジェクトがそのままGetter・Setterつきでドメインに出てきたと。これでは好き放題されてしまうと。Entityの項目の組み合わせがこういう条件を守らなきゃいけないことがドメインのルールとしてあるはずなんですけど、Setterをすべて開放していると、こういう不変条件が守れなくなります。

書いた本人に感想を聞くと、「無駄にクラスが増えただけじゃん。Eloquent最高だな」という感想が返ってくる。そんな感じになったりしました。

フロントエンド分割が過剰だった話

さらに、これはサーバサイドの話だったんですけど、フロントエンドも「サーバがある程度うまくいったからフロントもやろう」みたいな感じでやってたんですけど、先ほどの図よりさらにクラスが増えてまして、インターフェースがめちゃめちゃ多かったんですね。ViewModelにあったり、APIにあったり。中には仕事をしてないクラスすらありました。

分けすぎたことによって新規実装・修正のコストがだいぶかさみました。フロントの責務はほとんどバックから来たJSONをそのまま表示するぐらいの機能が多かったんですけど、そこでやるにはレイヤが過剰だったかなという感じですね。

ただ、フロント側の処理の比重が高いドメインというか機能がいくつかあって、そこでは有用でした。例えばユーザーは装備品を強化していくんですけど、強化後のパラメータをいちいち全部サーバに問い合わせていたら重いので、フロントでもプレビューできるようにしたケースなどではフロントが複雑になるので、そういうところではこれぐらい分けてもいいかなというところです。

教訓としては、フロントとサーバでやることが違うのに、アーキテクチャをそのまま持ってきても成功するとは限らないなという感じです。

これはアプリケーション全体として見た図なんですけど、過剰に分割されたせいでほかの弊害も起きていました。

やりがちなんですけど、Viewを見てDB項目を考え始めることやバックエンドのAPIを考え始めることがよくあるんですよね。ひどいところで言えば、仕様書に「Viewのこの部分はDBのこれが対応するよ」みたいなものが書いてあるとか、そういうViewとDBが直結しているようなケース。

UIに新しいものが必要になったら、全層修正、前の項目でめちゃめちゃ増えていたAPIからViewまでをすべて修正みたいなことが起きたりする。これもレイヤを分けすぎた弊害だったかなと思っています。

どっちかというと、やっぱりバックエンドのEntityを先にもう少し練っておいて、「UIでこんなんが来るかもなぁ」みたいなものを先に検討しておければよかったかなと。検討の向きを逆にできればよかったかなと思っています。

レイヤ分割が長かったんですけど、まとめです。疎結合になっていれば、各レイヤ内、ドメインの実装に専念できる。ただ、各レイヤ・クラスの役割と意味を開発メンバーに共有しておかないと形骸化して、逆にひどいことになる。

そして最後です。過剰に分割するとつらくなることもあるので、やはりもともとのレイヤの責務と本当に自分たちのアプリケーションにいるのかなというのを考えるのが必要だなと思いました。

ユビキタス言語が統一されなかった話

次です。ユビキタス言語。先ほど説明してましたチームで共有すべきドメインに関する語彙が統一されない話。残念ながらこれ以降は光がほとんどないです。

参考イメージのバベルの塔です。The city and its tower.

そのプロジェクトでは、仕様作成者、バックエンドの開発者、フロントの開発者、さらにフロントのUIの開発者と分業して仕事が行われていました。共有しようとがんばっていたはずのゲーム用語が、先ほどのレイヤ化や分業のせいもあり、ぜんぜん共有できなかったケースがわりと見られました。

1つ目が仕様書からしてそもそも揺れているケース。これはよく見ると思うんですけど、キャラクターがあったとして、これはいらすとやの竜騎士さんの絵ですけど、その名前がところによっては「キャラ」と呼ばれていたり「キャラクター」と呼ばれていたり。

この2つは似ているのでセーフかなというところなんですけど、ところによって「ユニット」と呼ばれてたりするんですよね。これがなんでユニットかというと、たぶんバトルに出たときの戦術の駒としての扱いからの命名だとは思います。仕様書も揺れているので、当然バックエンドもフロントエンドも揺れました。これが1つ目です。

これは仕様書を書く側にも言葉を統一することの重要性を説いて、やってもらうしかないかなと思います。それでも完璧に書けるわけではないので、揺れに気づいたら指摘して修正してもらうことがが必要かなと思います。

2つ目、用語の指す概念を正しく共有できないケース。先ほどキャラクターと言ったんですけど、厳密に分けるとキャラクターという文脈で、キャラクターのマスタデータを指しているのか、ユーザーが持っているキャラクターの個別を指しているのか、2通りのケースがあります。

仕様書だとキャラクターやキャラクターIDだけ言及される場合も多いんですけど、例えば「ガチャの排出キャラクター」といったら、基本的にはマスタのことを指します。まだユーザーのものになっていないものを指します。「パーティに編成されたキャラクター」というと、ユーザーが持っている個別のユーザー所持キャラクターを指していることがあります。

この辺を認識しきれていないと、こういう悲劇が起きます。バックエンドは優秀なので、UserCharacterとCharacterをちゃんと分けていました。実は一部分かれてなかったんですけど。フロントエンドは、なぜか所持系がCharacterになってました。

これで一番怖かったのが、所持系のIDとマスタ系のIDが混乱するじゃないかと。APIに送るためのパラメータがズレるみたいなバグがたまにあって、怖かったです。

これは開発メンバーでちゃんと共有しておかないとなんともならないので、用語集ぐらいは作って共有の場を設けて、あとは最終防壁としてレビューでは指摘しないといけないですね。レビューで指摘したときに、書き上げてから指摘するとあれなので、プロジェクトに入ってきたばかりの人は早め早めにレビューをしていくべきだなと思います。

3つ目。これはたぶん英語圏ではぜんぜん気にしないことだと思うんですけど、例えばアイテムの数量があって、AmountだったりQuantityだったりCountだったり、数量は人によって訳が分かれるんですよね。それぞれ分業した人によって割れています。これぐらいならまだギリギリセーフかなと思うんですけど、Entityの名前が揺れてたりするとつらい。

英語圏はたぶんそのままユビキタス言語を定義すればプログラムに使えるんでしょうけど、我々はそうではないです。対訳表ぐらいは作ったほうがいいんじゃないかなと思いました。

プログラムを日本語で書くのはあり?

これは完全に蛇足なんですけど、プログラムを日本語で書く手が逆転の発想としてありますね。翻訳がつらかったら日本語を使えばいいじゃないかと。やってみたことあるんですけど、つらいです。

複数形をなんて書くか? キャラクター“たち”? キャラクター“リスト”? ちょっと違う。英語の「s」って便利だなと思いますよね。

真偽値を返すメソッド。英語だったら「is〜」。日本語だったら「〇〇かどうか」。うーん、これもなんか違和感がありますね。

そもそも日本語ファイル名を使うと問題が起こるツールもけっこうあるので、そこらへんは考えていかないといけないかなと思います。

書いていて一番違和感があったのが、英語と日本語の語順が違うことです。オブジェクト指向で書いていると、英語はSVOの形式が多いんですよね。「The user character attacks to the enemy.」($userCharacter->attack($enemy))みたいな。日本語でこれを書こうとすると、「ユーザー所持キャラクター 攻撃する 敵」($ユーザー所持キャラクター->攻撃する($敵))みたいな、助詞もないし片言っぽい日本語になるんですよね。つらいなと。

「これを解決するんだったら、そもそもプログラミング言語から変えたらいいんじゃない?」みたいな気がしたんですけど、「なでしこ」は実用レベルかどうかはまだわからないです。ただ、「クジラが『こんにちは』と言う。」って、こんな自然な日本語がプログラミングのコードになるってすごくないですか?

ほかはドリトルとかプロデルとかあって、プロデルのほうはオブジェクト指向にも対応しているらしいです。今回初めて知ったんですけど。

完全に蛇足でした。逸れましたけれども、開発者の間だけでも用語を正しく共有しておかないとソースコードの理解に時間がかかるようになるし、ちゃんと共有していきましょう。当たり前ですけど、重要な教訓を得ました。

<続きは近日公開>