興味や関心を持って取り組んでいる領域における重要な概念や要素、関係性をドメインモデルと呼びます。ダイスを振ってその結果を返すダイスローラーのボットを作っている場合には、ダイスを振るという行為に注目しているので、ダイスやダイスを振るという行為がドメインモデルです。くじ引きをするボットならくじの結果とくじ引きをするという行為がドメインモデルになるでしょう。

ダイスローラーを例に挙げたので、これについて考えてみましょう。6面ダイスを1個振ることを模倣するコードはJavaScriptの標準機能を組み合わせれば次のように書けます。

Math.floor(Math.random() * 6) + 1

Math.random()は0以上1未満のランダムな実数を返すので、6倍すると0以上6未満になります。Math.floor() は端数切捨てをするので、0から5の整数値となり、1を加えることで6面ダイスの目と等しくなります。結果として、このコードは6面ダイスを1個振る行為を模倣しているように見えますが、よくよく考えてみればそうではありません。一体どういうことでしょうか?

まずこのコードはダイスの存在を無視しています。6面ダイス1個の実体はなく、あるのは乱数に値を掛けたり足したりしているだけで、それを人間が勝手にダイスロールだと認識しているだけです。さらにツッコミを入れるならば、ダイスロールという行為も無視しています。6面ダイス1個があることと、それを振ること、振った結果はすべて別の事象のはずです。

このように実現したい事象を注意深く観察して、具体的に何をどうしているのか、なにが構成要素となっているのかを抽出することでドメインモデルを作成できますし、よいモデルは柔軟で拡張性のあるものとなるでしょう。

  • 取り組んでいる領域の重要な概念や要素をドメインモデルと呼ぶ
  • 概念をよく観察してコードに落とし込もう

ダイスのモデル

それではダイスのモデルを作ってみましょう。まずはダイスとは何かということを考えてみます。哲学的な話かもしれませんが、実際にこの瞬間、我々は少し哲学者にならなければなりません。

特に断りがなければダイスと言えば6面ダイスを思い浮かべると思います。ボードゲームやテーブル・トークRPGに慣れ親しんだ人ならもっと面数の多い10面ダイスのようなものも思い浮かべてもおかしくはありません。より一般化したダイスを表現するために といった表記も見ることがあります( は個数、 は面数です)。

まずはこの という表記にならってモデルを作ってみましょう。このモデルにDiceと名前をつけて、コード上でひとつの概念として扱えるようにします。

class Dice {
    // rollsは個数、sidesは面数
    constructor(rolls, sides) {
        this.rolls = rolls
        this.sides = sides
    }
 
    // toString()は文字列表現を表す特殊なメソッド
    toString() {
        return `${this.sides}d${this.rolls}`
    }
}
 
// 1d6ダイスを作成する
const dice = new Dice(1, 6)
 
console.log(`個数は${dice.rolls}`)  // 個数は1
console.log(`面数は${dice.sides}`)  // 面数は6
console.log(`表記は${dice}`)        // 表記は1d6

多くのプログラミング言語ではクラスと呼ばれるものを作ることができます。クラスは複数の値を保持することができ、ここでDiceは個数(rolls)と面数(sides)ふたつの情報を保持しています。

new Dice(1, 6) のように new キーワードをつけてクラスを呼び出すことで、クラスのインスタンス(実体)を作ることができます。new による生成ごとに新しいインスタンスが生成されて区別されます(同じ値を持っていたとしてもです)。

さらにクラスはメソッド(動作・振る舞い)を持ちます。コンストラクタの下には toString() が定義されていますが、これはクラスの文字列表現を表す特殊なメソッドです。メソッドは自由に定義できるので、ダイスロールをする機能も追加してみましょう。

ロールの追加

今しがた書いたDiceクラスを拡張してロール機能を追加します。また、ロール結果を保持するDiceResultクラスも新しく定義しました。コードはその分長くなりますが、まだ複雑と言えるほどではありません。

class Dice {
   // ...省略...
 
   // ダイスロールをするメソッド
   roll() {
       const values = []
       // 個数の数だけ繰り返して出た目をvaluesに追加していく
       for (let i = 0; i < this.rolls; i++) {
           const value = Math.floor(Math.random() * this.sides) + 1
           values.push(value)
       }
       return new DiceResult(values)
   }
 
   // ...省略...
}
 
class DiceResult {
    constructor(values) {
        this.values = values
    }
 
    // 結果のうち最大の目を返す
    max() {
        return this.values.reduce((a, x) => Math.max(a, x))
    }
 
    // 結果をすべて足し合わせた値を返す
    sum() {
        return this.values.reduce((a, x) => a + x, 0)
    }
}
 
const dice = new Dice(2, 6)
const result = dice.roll()
console.log(`ロールの合計値は${result.sum()}`)

急にいろいろ増えたことに混乱するかもしれませんが、順に確認していきましょう。まずDiceクラスにroll()メソッドが追加されました。これはその名の通りダイスロールをします。ダイスの個数の回数だけループを回して、ループごとにダイス目をランダムに決定して配列に保存していきます。そして、最後にDiceResultクラスのインスタンスを生成して返します。

なぜ、DiceResultはダイス目の配列を保持するのでしょうか?これも概念をよく観察すれば納得できると思いますが、複数個のダイスを振ったときに求まるのはその合計値ではなく個々の結果だからです。

状況によって重要なものは変わりますし、それはダイス目の合計値だったり、最大のダイス目だったりします。なので、DiceResultは出たダイス目の結果だけ保持していますし、用途に応じて合計値を求める sum() メソッドや最大値を求める max() メソッドを実行して必要になったときにはじめて計算すればいいのです。

reduce() ってなんですか?

Arrayオブジェクトのメソッドで、畳み込み計算 を実行します。 のときは、 となります。もちろん単純にforループで書き直すこともできます。

暗黙知を形式知にしよう

最初のうちはドメインモデルを抽出することが難しく感じるかもしれませんが、訓練によって上達していきます。人間の脳はよくできていて、常識や当たり前と判断した暗黙のうちに行われることは無視してしまいます。しかし、その暗黙に行われることこそが実は非常に重要な概念だったりするのです。

  • 結論を急がず概念や事象を注視しよう
  • 常識や当たり前に潜む概念を明らかにしよう

演習

DiceResult

上記DiceResultクラスに追加のメソッドを定義することを考えます。

  1. 結果が になったダイスの個数を求めるメソッドを追加せよ
  2. 結果を昇順に並び替え、新しいDiceResultのインスタンスとして返すメソッドを追加せよ

CCBロール

クトゥルフ神話TRPGには組合せロールがあり、BCDiceではこれを解決するために 記法が使われています( は判定値)。ここで が 1~5であればクリティカル成功、95~100であれば致命的失敗、 であれば成功、それ以外は失敗を表すものとします。

  1. の式を評価したときに結果として得られる概念はなにか?
  2. を関数 に置き換え可能か?可能であればその定義はなにか?
  3. は評価可能か?また、 は評価可能か?

第4回 エラー処理