ドメイン駆動設計にかぶれて値オブジェクトを作ってみたんですが、結構面倒くさかったんで、親クラスを作ってみました。
(あくまでも個人開発で開発スピードを早めるために作っていますので、製品でそのまま使えるかは保証しません)
値オブジェクトとは
https://qiita.com/kichion/items/151c6747f2f1a14305cc
に書いてあるような特徴を持つオブジェクトです。
Rubyで書いてみると↓みたいなオブジェクトになります。この例はユーザの名前を入れる値オブジェクトです。
(正統な定義をするならequal
的なものを定義したほうが良いんでしょうが、あまり使わないので今は定義していません。)
domain/user/name.rb
module Domain::User
class Name
attr_reader :first_name, :last_name
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def update_first_name(first_name)
self.new(first_name, @last_name)
end
def update_last_name(last_name)
self.new(@first_name, last_name)
end
def fullname
"#{@last_name} #{@first_name}"
end
end
end
使うのには便利なんですが、いちいちこのお決まりのフィールドのゲッター、セッターの何行かを書くのが面倒くさいんですよね。
なので、これらを勝手に定義してくれる親クラスを作ってみました。
値オブジェクトの親クラス
domain/base/value_object.rb
module Domain::Base
class ValueObject
attr_reader :changed_fields
FIELDS = []
class << self
# 次のようなメソッドを定義 new_date = date.update_month(11)
def attr_updater(*attrs)
attrs.map(&:to_sym).each do |defining_attr|
define_method("update_#{defining_attr}") do |defining_value|
values = attrs.map { |attribute| attribute == defining_attr ? defining_value : instance_variable_get("@#{attribute}")}
changed_fields = @changed_fields.include?(defining_attr) ? @changed_fields : @changed_fields + [defining_attr]
self.class.new(*values, changed_fields: changed_fields)
end
end
end
end
# NOTE
# - 基本的に値オブジェクト側ではFIELDSとアクセサだけ定義させる
# - initializeは親のこのクラスの物を使わせる。
# - initializeの独自実装をするのは可能。しかしその場合、値オブジェクト側からこの親クラスのinitializeを使うだけで、値オブジェクト使用者にはこのクラスのinitializeを直接は使わせない
# - changed_fieldsなどの変更は内部でのみ行う
# - 値オブジェクト自体の初期化引数はできるだけ単純になるようにケア
def initialize(*field_values, fields: self.class::FIELDS, changed_fields: [])
define_fields(fields, field_values)
@changed_fields = changed_fields
end
private
def define_fields(fields, field_values)
fields.zip(field_values).each do |field, field_value|
instance_variable_set("@#{field}", field_value)
end
end
end
end
これを使うと以下の書き方で、元々のメソッドが提供できます。
domain/user/name.rb
module Domain::User
class Name < ::Domain::Base::ValueObject
FIELDS = %I(first_name last_name)
attr_reader *FIELDS
attr_updater*FIELDS
def fullname
"#{@last_name} #{@first_name}"
end
end
end
備考
- メタプログラミングの黒魔術を使いすぎると、追えなくなる(grepでも目視でも)ので、複雑なメソッドは定義していません
-
attr_reader
とかも自動化できるけど、attr_reader
とかがあると読み手に負荷をかけないので、基本のRubyの書き方ができるところは踏襲しています。 - この書き方だと、
initialize
時にFILEDどおりの順番で引数を入れることを暗黙に示していており、それを明示していないんですがが、自分ではその問題をわかっているので、今の所運用で回避しています。 -
changed_fields
はDB更新時に更新があったフィールドだけ更新したかったので好みで作りました。 - イミュータブルなオブジェクトで作ることは色々メリットがあるような気がしますが、一般ピーポープログラマとしてはまだそんなメリットを享受できず、YAGNIな感じがしています。並行処理とかを行うわけではないので。