【読書】ドメイン駆動設計入門を読み始めた# 2章
ドメイン駆動設計入門の第2章
一番最初に値オブジェクトについて、読み始めました。
書籍上のコードはJavaだったりJavaScriptだったりなのですが、
せっかくなので業務で使うことが多いRubyに置き換えています。(合っているかは分からない)
値オブジェクト
値オブジェクト
はシステム固有の値を表したオブジェクトです。
値でもあり、オブジェクトでもある。分かりにくい。
例えば、性と名を表現するオブジェクトが該当します。
class FullName attr_accessor :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end full_name = FullName.new('masaru', 'hogehoge') puts full_name.first_name
FullNameオブジェクトは、氏名を表現するオブジェクトで、
first_name
とlast_name
をシステムに要求する形で表現します。
システムに必要とされる処理にしたがって、値を表現するオブジェクトが値オブジェクトになります。
値の性質
値オブジェクトは値であり、オブジェクトでもあります。
値
は次の性質を持ちます。
不変であること
- 代入は値そのものを変更している訳ではない
- 値を変更するオブジェクトは値オブジェクトの責務ではない
交換が可能であること
- 値オブジェクトの交換は代入操作によって交換されることを表現する
class FullName attr_accessor :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end full_name = FullName.new('masaru', 'hogehoge') # 代入によって値を交換する full_name = FullName.new('masaru', 'fugafuga')
- 等価性によって比較されること
- オブジェクトが保持する値で直接的な比較をするべきではない。(値オブジェクト自体は値であるため)
- 比較用のメソッド
equals
とかを実装して、比較には値オブジェクトを受け取れるようにする
# bad case class FullName attr_accessor :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end full_name = FullName.new('masaru', 'hogehoge') compare_full_name = FullName.new('masaru', 'fugafuga') puts (full_name.first_name == compare_full_name.first_name) && (full_name.last_name == compare_full_name.last_name) # better case(型定義がないのであっているかは分からん) class FullName attr_accessor :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end def equals(full_name) return false full_name.null? || !full_name.is_a(FullName) return first_name.eql?(full_name.first_name) && last_name.eql?(full_name.last_name) end end full_name = FullName.new('masaru', 'hogehoge') compare_full_name = FullName.new('masaru', 'fugafuga') puts full_name.equals(compare_full_name)
値オブジェクトにする基準
コンテキストによるため、厳密な判断基準はありません。
参考材料として、
ルールが存在しているか
- 上の
氏名
では「性と名」で構成されるルールがある - バリデーションも同様
- 上の
単体で扱いたい値か
- 性、名だけで取り扱う場合には値オブジェクトにしない方が都合が良い
振る舞いもつ値オブジェクト
値オブジェクトは、オブジェクトでもあるため、振る舞いを持つことができます。
通過の概念Moneyクラスに通貨加算の振る舞いを足してみます。
class Money attr_accessor :amount, :currency # # コンストラクタ とか # def add(money_obj) # 型チェック raise Exception.new("通過以外を受け取りました") unless money_obj.is_a(Money) # nullチェック raise Exception.new("引数がnullです") unless money_obj # currencyチェック raise Exception.new("通貨単位が異なります") unless currency.eql?(money_obj.currency) return Money.new(amount + money_obj.amount, currency) end end
計算処理内でルール(チェック)を記述すうることで、誤操作が発生しないようにします。
(Rubyは動的型つけなので、型に関するチェックをあえて入れてます)
振る舞いを追加することで、値オブジェクトは自分自身のルールを語ることでき、
ドメインオブジェクトらしさを表現することができます。
値オブジェクトを導入するメリット
表現性が高くなる
例えば製品番号の場合、
プリミティブな文字列を導入するとこんな感じになりそう。
model_number = "a20401-01-1" model_number_hoge = 1100201 # あるメソッドでは文字数のように当てられている def hogehoge(model_number) model_number.split("-") end # 別のメソッドでは数字のように扱われる def fugafuga(model_number) model_numer * 100 end
ここまで極端にひどいコードはあまり無いとは思いますが、
model_numberにどのような値が入ってくるかは、呼び出し下まで探らないといけないのは確かです。
製品番号というドメインオブジェクトを定義した場合はどうでしょうか。
(製品番号を固有番号-拠点番号-ロット
で定義すると仮定します。)
class ModelNumber attr_accessor :product_code, :branch, :lot def initialize(product_code, branch, lot) raise Exception.new("引数が指定されていません") if product_code.null? || branch.null? || lot.null? @product_code = product_code @branch = branch @lot = lot end def to_s return "#{product_code}-#{branch}-#{lot}" end end
Javaみたいな感じの実装でいいのかあれですが、
to_s
をオーバライドして、製品番号を構成する文字列を連結して返すようにします。
製品番号というドメイン(概念)がどのようなもので構成されるのか、
オブジェクト自体が文書化できるようになります。
不正な値を存在させない
バリデーションです。
値をオブジェクトとして定義することで、不正な値が入る前にエラーを履かせて弾くことができます。
例えば、先ほどの製品番号にbranchは3桁以上という制限がある場合、
コンストラクタで不正な値をチェックする処理を追加してみます。
class ModelNumber attr_accessor :product_code, :branch, :lot def initialize(product_code, branch, lot) raise Exception.new("引数が指定されていません") if product_code.null? || branch.null? || lot.null? validate_branch_character_length(branch) @product_code = product_code @branch = branch @lot = lot end def to_s return "#{product_code}-#{branch}-#{lot}" end private def validate_branch_character_length(branch) raise Exception.new("ブランチは3文字以上の入力が必要です") if branch.length < 3 end end
システムが扱う値をオブジェクトとして、検査を振る舞いとして実装することで、
不正な値を持つデータを生成することを防ぐことができます。
ロジックの散財を防ぐ
DRYの原則があるように、なるべく重複した処理は1箇所にまとめることが必要になってきます。
例えば、引数にユーザ名を受け取り、新しくユーザを生成する処理を考えます。
def create_user(user_name) # 検証 raise Exception.new("ユーザ名が指定されていません") unless user_name raise Exception.new("ユーザ名は3文字以上入力してください") unless user_name.length < 3 user = User.new(user_name) end
ユーザの生成時にバリデーションをかけるという意味合いでは不自然なものではありません。
ただ、更新処理の実装が必要になった場合、同じような処理をまた書く必要が出てきます。(メソッドに切り出すとかもありますが)
ユーザ名を値オブジェクトとして切り出しておくことで、
ユーザ名の生成に関わる処理を1箇所に集中することができます。
1箇所に集中させることによって、仮にルールが増えた場合や変更させる場合にも、
影響箇所を最小限にすることができます。
class UserName attr_accessor :value def initialize(name) # 検証処理 raise Exception.new("ユーザ名が指定されていません") unless name raise Exception.new("ユーザ名は3文字以上入力してください") unless name.length < 3 # ルールが増えたら、追加とかメソッドに切り出せばいい @value = name end end
まとめ
プリミティブな値を使って、システムが扱う値を表現することもできますし、
開発速度を考えるとそちらの方が都合がいいと思います。
ただ、表現力には乏しく、どんな値が入るのかについては、変数名から察していくことになります。
一方で、クラスの数が増えてしまう物の、値オブジェクトとして定義した場合は下記の利点を享受できます。
- 値に関するルールを定義することができる
- コード自体がドキュメントとして機能するようになる
早速第2章を読んでみてですが、
システムが扱う値自体をクラスとして切り出して、バリデーションを書いたりと、
なんだか学生時代のフレームワークを使わないアプリ設計とかを思い出しました。