11章
ドメイン
アプリケーションが対象とする問題領域のこと
モデリング
ドメインを分析して構成概念を抽出すること
ドメインモデル
モデリングによって得られた概念
ドメインモデルは、その概念に関連する属性と振る舞いをもったオブジェクトとして定義される。
振る舞いのことをドメインロジック・ビジネスロジックと呼ぶ(?)
Railsのモデルはドメインモデルとドメインロジックを実装するレイヤー。
ドメインモデルは単にドメインの概念を抽出しただけのものなので、リレーショナルデータベースなどのデータストアにドメインモデルの状態をデータとして保存する。
Railsではアクティブレコード(ActiveRecordに非ず)と呼ばれるアーキテクチャパターンを用いてデータベースのレコードとオブジェクトを対応付けている。
アクティブレコード
データの取得・保存処理とドメインロジックをあわせてカプセル化するアーキテクチャパターン
Railsでは、データベースのテーブルをクラス、レコードをクラスのインスタンス、カラムをインスタンスの属性に対応させる。
単純で理解しやすい一方、単体では複雑なドメインロジックを表現しきれないという欠点がある。
- ドメイン固有の「値」に関するロジック
- 複数のオブジェクトを組み合わせて表現するロジック
値オブジェクト
エンティティ
属性の値に関わらず一意に識別されるオブジェクト
「1月1日生まれの山田太郎」2人が同一人物ということにはならんやろ??
「山田花子」が結婚して「佐藤花子」になっても別人ということではないやろ??
識別子
エンティティが持つ同一性を識別するための情報
Railsのモデルのインスタンスは「id」を識別子とするエンティティです。
属性の型はデータベースのデータ型をRubyのデータ型へ変換したものということ(Date, Integer, Stringなど)
この中にはドメインの重要な概念が含まれていることがある。このとき、関連する振る舞いと属性を値オブジェクトとして定義することで、ドメインロジックをうまく表現できる。
例えば……
phone_number
という文字列属性を持つUserモデルのとき、phone_number
が携帯かどうか判別したいとする。
そのmobile_phone_number?
メソッドをuserモデルで定義するのはなーんか違和感。そのロジックはphone_number
という値として切り出したいよね。
(´-`).。oO(phone_number
はUserに紐づくわけではなく、例えばCompanyモデルでも扱うかもしれないよなあ)
実装例
|
|
|
|
phone_number
メソッドを上書きしたので、userインスタンスのphone_numberはself[:phone_number]
で取得する。
([:phone_number]
で取得できるのはActiveRecordによるところっぽい。いまいちわからなかった)
rails/read.rb at master · rails/rails
|
|
user.phone_number=
の引数にはPhoneNumber
のインスタンスを渡す。文字列が変わるたびに新しいPhoneNumberを作成する。なので、インスタンス作成時にvalue.dup.freeze
をしている。
composed_of
による値オブジェクトの利用
上記のコードは以下のように書き換えられる。
|
|
第一引数は値オブジェクトを用いる属性名。
mapping
オプションで、["モデルの属性名", "値オブジェクトの属性名"]
の対応を配列で指定する。複数の場合は二要素配列[["", ""], ["", ""]]
こんな感じ。
|
|
オプション名 | 内容 |
---|---|
:class_name | 値オブジェクトのクラス名を文字列で指定する。指定しない場合、属性名をclassyしたクラス名が用いられる。 |
:mapping | モデルと値オブジェクトの属性の対応関係を指定する。 |
:allow_nil | nilを許可するかどうかtrueかfalseで指定する。trueの場合、対応するモデルの属性の値がすべてnilの場合、値オブジェクトの参照もnilとなる。デフォルトはfalseで、この場合、nilが値オブジェクトのコンストラクタに渡される。 |
:constructor | 値オブジェクトのコンストラクタを指定する。値オブジェクトのクラスメソッド名のシンボルか、モデルの属性の値を受け取って値オブジェクトを初期化するProcオブジェクトを指定する。デフォルトは:new |
:converter | 値オブジェクトのクラスとは異なるクラスのインスタンスが代入された場合の変換方法を指定する。値オブジェクトのクラスメソッド名のシンボルか、代入された値を受け取って値オブジェクトに変換するProcオブジェクトを指定する。デフォルトはnil で、この場合変換は行われない。 |
サービスオブジェクト
複数のオブジェクトを組み合わせて表現するロジックなどを独立したオブジェクトとして定義する。
「ユーザー認証サービス」「合計金額計算サービス」のように、ドメインロジックそのものを指す名前がつけられる。
サービスオブジェクトは自身の動作を変更するような状態を持たない。=入力が同じなら常に同じ結果を返す。インスタンス変数とか変更しない。
|
|
name で指定したクラスメソッド (クラスの特異メソッド) の可視性を private に変更します。
Module#private_class_method (Ruby 2.7.0 リファレンスマニュアル)
注意点
モデルに実装すべきロジックまで実装しない
引数のオブジェクトの属性を直接参照・更新しているような箇所は、その操作を対応するモデルのインスタンスメソッドとして定義する。
withdraw
とか。
|
|
イベントの見落としがないか確認する
送金・受け取りの記録を行う場合、データベースの中間テーブルが存在することになる。
送金イベントに注目するとテーブルが必要になるため、ActiveRecordに送金ロジックを書けばいいので、サービスクラスは不要になる。
欠点:テストがしづらい
サービスオブジェクトでは、ロジックの組み立てに必要なオブジェクトを外から渡すため、テストダブルに置き換えれば簡単にテストできる。
イベントに着目すると、モデルのコールバックにロジックが実装されるので、本物のオブジェクトを使うことになる。
実装ルールについて
1つのドメインロジックを指すものにする。
クラスメソッドは1つにする。名前はcall, run, execute, performなど……。
ここではProc.call
に合わせてcallにしている。
(自分が見たのもcallが多い印象)