この記事は何
DDDを学んだ事がある方は「バリューオブジェクト」という名前を一度は聞いた事があるのではないでしょうか・
この記事ではRailsでバリューオブジェクトを実装するならどのような実装にすると良さそうかを紹介します。
なお、バリューオブジェクトの具体的な実装方針は千差万別だと思うので、この記事で紹介する実装はあくまで一例だと思っていただけるとありがたいです。
具体的なバリューオブジェクトの解説は↓の記事などが参考になると思います。
バリューオブジェクトの条件
バリューオブジェクトは以下の条件を満たしている必要があります。
条件は以下の通りです。
- オブジェクトはイミュータブルである
- 値の変化はインスタンスの再生成で実現する
- オブジェクトの同一生はバリューオブジェクトが持つattributeの一致で定義される
Railsで実装する際も、これらの条件を満たす必要があります。
そしてバリューオブジェクトとして実装する以上、バリデーションも行えるようにしたいです。
これらの要件を満たすインターフェースを実装してみます。
実装物
実装物はズバリ↓のようなクラスです。
class Value::Base
include ActiveModel::Model
def initialize
fail ActiveRecord::RecordInvalid unless valid?
end
# @param other [::Value::Base]
# @return [true, false]
def ==(other)
evaluate_attributes.all? { |variable| try(variable) == other.try(variable) }
end
private
# @return [Array<String>] attributes used to equal evaluation
def evaluate_attributes
fail NotImplementedError
end
end
このクラスは以下のように継承し、利用します。
class Value::Name < Value::Base
attr_reader :first_name, :last_name
validates :first_name, presence: true
validates :last_name, presence: true
def initialize(first_name:, last_name:)
@first_name = first_name
@last_name = last_name
super()
end
def full_name
"#{first_name} #{last_name}"
end
private
# @note Override {::Value::Base#evaluate_attributes}
def evaluate_attributes
%i(first_name last_name)
end
end
このクラスの機能について説明します。
オブジェクトの不変性
このクラスでは、基本的にsetterを提供せずに、getterのみを公開します。
こうすることで、外部からインスタンスの変更を行うことはできません。
オブジェクトの同一性
このクラスにはevaluate_attributes
というメソッドを実装しています。
また、==
メソッドをオーバーライドし、各attributesの値の同一性を検証することでオブジェクト自体の同一性を判定するようにしています。
このようにすることで、==
メソッドを用いた同一性検証はバリューオブジェクトが満たすべき条件と同じになります。
値のバリデーション
このクラスではActiveModel::Model
モジュールをinclude
しています。
また、initialize
のタイミングでvalid?
がfalse
を返すときにエラーを返すようになっています。
このように実装をし、子クラスで最後にsuper()
を実行することで、インスタンス生成時に検証まで行えるようになっています。