RailsアプリケーションのコードをリファクタリングするためのデザインパターンのひとつにValueオブジェクトというものがあります。このパターンをうまく導入することで、モデルが肥大化するFat Modelという問題を防ぐことができます。
この記事ではValueオブジェクトとはなにか、導入する必要性や導入方法について書いていきます。
Valueオブジェクトとは、その名のとおり値を表すオブジェクトです。ここでいう値とはなんでしょうか。この値とはドメイン駆動設計という設計手法に登場する概念です。
ドメイン駆動設計には、エンティティと値オブジェクトというふたつの概念が登場します。
概念 | 概要 | 例 |
---|---|---|
エンティティ | あるオブジェクトが持つ値が同じなら同一だと言えるもの | ユーザ |
値オブジェクト | 同一性を判断できない情報 | 名前、住所 |
この説明だけだとわかりづらいので、例を見てみます。
たとえば、同じ家に住むAさんとBさんがいます。同じ家に住んでいる、つまりふたりの住所は同じですが、住所が同じだからといってAさんとBさんが同一人物というわけではありません。
住所が同じだけだとその人の同一性が判断できないので、住所は値オブジェクトということになります。同じく血液型も、同じO型でも同一人物だと判断できないので値オブジェクトです。
ではマイナンバーはどうでしょうか。マイナンバーは個人を識別するための一意な番号です。つまり同じマイナンバーを持つ人はいません。マイナンバーが同じなら、それは同一人物をさすことになります。
人がマイナンバーを持つとき、人はエンティティである、ということになります。
RailsにおけるValueオブジェクトというデザインパターンは、ドメイン駆動設計における値オブジェクトをカプセル化する設計手法、ということになります。
Valueオブジェクトは、クラスを責務で分離し、コードをクリーンに保つために必要になります。コードをクリーンに保つことで重複を排除し、テストが書きやすくなり、またFat Modelの問題を解消することができます。
ソフトウェア設計の基本原則のひとつとして、オープン・クローズドの原則というものがあります。簡単にいうと
あるクラスを修正するときに、他のクラスに影響が出ないような設計であること
です。Valueオブジェクトを導入することで、この原則を守ることができます。
なぜValueオブジェクトが必要なのか、例をとおしてみてみます。
たとえばユーザと管理者というふたつのクラスがあるとします。ふたつのクラスはそれぞれ名前として姓と名をもちます。また姓と名からフルネームを取得できるようにします。
ユーザを表すクラスをUserとすると、このコードは次のようになります。
class User
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def full_name
"#{first_name} #{last_name}"
end
private
attr_reader :first_name, :last_name
end
これでよさそうに見えますが、問題がひとつあります。管理者を表すAdminクラスにも、Userクラスと同じ #full_name
メソッドを定義する必要が出てくるのです。さらに、たとえばゲストを表すGuestというクラスを追加することになったとき、同じように定義をふやす必要があります。
また、フルネームにミドルネームを追加したくなったらどうなるでしょうか。ユーザ、管理者、ゲストそれぞれの定義を変更する必要が出てきます。『名前』という値の影響範囲が大きくなってしまうのです。これは上で書いたオープン・クローズドの原則に反してしまいます。
この問題を解決するために、『名前』をひとつのクラスに閉じ込めて、各クラスからはこのクラスを使うようにします。
class Name
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def full_name
"#{first_name} #{last_name}"
end
private
attr_reader :first_name, :last_name
end
class User
def full_name
name = Name.new(first_name, last_name)
name.full_name
end
end
こうすることで、ミドルネームが追加されたときもNameクラスを修正すればいいことになります。ここでいうNameクラスがValueオブジェクトです。Valueオブジェクトを導入することで、コードの再利用性が上がり、テストも書きやすくなります。
それでは実際にValueオブジェクトの使い方について見てみます。ここでは上で示した『名前』に関するValueオブジェクトをRailsアプリケーションに導入してみます。
RailsのActiveRecord::Baseには #composed_of
というメソッドがあります。これは複数のカラムを擬似的にひとつのカラムとして扱うためのメソッドです。Valueオブジェクトを表すのに適しているので、このメソッドを用いて実装します。
この記事の内容は、次の各環境で動作を確認しています。
ライブラリ | バージョン |
---|---|
Ruby | 2.7.2 |
Ruby on Rails | 6.1.0 |
この記事で示すコードは、次のテーブル定義をもとに動作します。コードはユーザや管理者、ゲストなどを想定していますが、ここではユーザのテーブル定義のみを示します。
テーブル | カラム | データ型 | NOT NULL |
---|---|---|---|
users | id | int | ◯ |
first_name | varchar | ||
last_name | varchar | ||
middle_name | varchar | ||
created_at | datetime(6) | ◯ | |
updated_at | datetime(6) | ◯ |
それでは実装例を見てみます。ユーザや管理者がいて、『名前』という値オブジェクトを持っているとします。このとき、ValueオブジェクトであるNameクラスは次のようになります。
class Name
attr_reader :first_name, :last_name
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def full_name
"#{first_name} #{last_name}"
end
end
このNameクラスを、ユーザを表すUserクラスでValueオブジェクトとして持たせるためには、上で説明した #composed_of
メソッドを利用します。
class User < ApplicationRecord
composed_of :name, mapping: [%w(first_name first_name), %w(last_name last_name)]
end
#composed_of
の第一引数には属性名をシンボルで渡します。この属性名からクラス名が推測できる、つまり :name
ならNameクラスである場合は :class_name
オプションを省略できますが、推測できない場合はこのオプションを用いてクラスを指定します。
また、 :mapping
オプションでUserクラスとNameクラスの属性のマッピングを行います。Userクラスにおける first_name
がNameクラスにおける first_name
に対応します、という形です。
以上でValueオブジェクトを利用できるようになりました。これによりNameクラスにミドルネームを追加するといった変更を行っても、Userなど各クラスに影響を及ぼすことがなくなりました。つまり、オープン・クローズドの原則が守られるようになりました。
上記のように #composed_of
でValueオブジェクトを設定すると、 Valueオブジェクトを用いた検索も行えるようになります。
たとえば、上記のNameクラスによる検索は次のようになります。
users = User.where(name: Name.new('Taro', 'Yamada'))
Valueオブジェクトは、ある値が抽象化できる概念であり、かつ複数のクラスから属性として参照されるケースで利用すると良いでしょう。たとえば、次のようなケースです。
Valueオブジェクトは、その値オブジェクトを複数のクラスから利用することになったら導入するとよいでしょう。
たとえば、『名前』を扱うクラスがUserしかないときにNameクラスとして切り出すのは早すぎるかもしれません。名前だけならまだいいですが、それ以外にも値オブジェクトとして切り出せるものはたくさんあります。このすべてをその都度Valueオブジェクトとして扱うと、かえってコードが煩雑になってしまいます。
この目安として、複数のクラスから利用されるようになったらValueオブジェクトとして切り出す、という指針があるとよさそうです。
以上をもとに、Valueオブジェクトを設計するときのルールをまとめてみます。
app/values
下に配置するname.rb
full_name
などValueオブジェクトは、コードをクリーンに保つためのすぐれたデザインパターンのひとつです。複数のクラスから利用される値オブジェクトが見つかったとき、導入することでFat Modelなどの問題を防ぐことができます。
Webエンジニア&プロダクトマネージャ。 スタートアップ創業→上場企業など5社で20以上のプロダクトをリリース。 共著に『現場で使えるRuby on Rails 5』。
プロフィールをみる