Ruby on Rails

Railsのデザインパターン: Valueオブジェクト

更新情報
2021年4月15日
目次、参考文献を追加しました

RailsアプリケーションのコードをリファクタリングするためのデザインパターンのひとつにValueオブジェクトというものがあります。このパターンをうまく導入することで、モデルが肥大化するFat Modelという問題を防ぐことができます。

この記事ではValueオブジェクトとはなにか、導入する必要性や導入方法について書いていきます。

著者
ぜに/Hiroki Zenigami

Webエンジニア&プロダクトマネージャ←プログラミング教育で起業←東大院←熊本高専。 共著に「現場で使えるRuby on Rails 5」。

目次

Valueオブジェクトとは

Valueオブジェクトとは、その名のとおり値を表すオブジェクトです。ここでいう値とはなんでしょうか。この値とはドメイン駆動設計という設計手法に登場する概念です。

ドメイン駆動設計には、エンティティ値オブジェクトというふたつの概念が登場します。

概念概要
エンティティあるオブジェクトが持つ値が同じなら同一だと言えるものユーザ
値オブジェクト同一性を判断できない情報名前、住所

値オブジェクトの例

この説明だけだとわかりづらいので、例を見てみます。

たとえば、同じ家に住むAさんとBさんがいます。同じ家に住んでいる、つまりふたりの住所は同じですが、住所が同じだからといってAさんとBさんが同一人物というわけではありません。

住所が同じだけだとその人の同一性が判断できないので、住所は値オブジェクトということになります。同じく血液型も、同じO型でも同一人物だと判断できないので値オブジェクトです。

ではマイナンバーはどうでしょうか。マイナンバーは個人を識別するための一意な番号です。つまり同じマイナンバーを持つ人はいません。マイナンバーが同じなら、それは同一人物をさすことになります。

人がマイナンバーを持つとき、人はエンティティである、ということになります。

値オブジェクトとは、住所や血液型など、値が同じでも同一性を判断できない情報のことをいいます。

RailsにおけるValueオブジェクト

RailsにおけるValueオブジェクトというデザインパターンは、ドメイン駆動設計における値オブジェクトをカプセル化する設計手法、ということになります。

loading...

なぜ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オブジェクトの使い方

それでは実際にValueオブジェクトの使い方について見てみます。ここでは上で示した『名前』に関するValueオブジェクトをRailsアプリケーションに導入してみます。

RailsのActiveRecord::Baseには #composed_of というメソッドがあります。これは複数のカラムを擬似的にひとつのカラムとして扱うためのメソッドです。Valueオブジェクトを表すのに適しているので、このメソッドを用いて実装します。

動作環境

この記事の内容は、次の各環境で動作を確認しています。

ライブラリバージョン
Ruby2.7.2
Ruby on Rails6.1.0

テーブル定義

この記事で示すコードは、次のテーブル定義をもとに動作します。コードはユーザや管理者、ゲストなどを想定していますが、ここではユーザのテーブル定義のみを示します。

テーブルカラムデータ型NOT NULL
usersidint
first_namevarchar
last_namevarchar
middle_namevarchar
created_atdatetime(6)
updated_atdatetime(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など各クラスに影響を及ぼすことがなくなりました。つまり、オープン・クローズドの原則が守られるようになりました。

参考: Valueオブジェクトを用いた検索

上記のように #composed_of でValueオブジェクトを設定すると、 Valueオブジェクトを用いた検索も行えるようになります。

たとえば、上記のNameクラスによる検索は次のようになります。

users = User.where(name: Name.new('Taro', 'Yamada'))

Valueオブジェクトのユースケース

Valueオブジェクトは、ある値が抽象化できる概念であり、かつ複数のクラスから属性として参照されるケースで利用すると良いでしょう。たとえば、次のようなケースです。

  • 名前における姓名やミドルネーム、住所における国や県・市区町村など、複数の値を組み合わせてひとつの値を算出するもの
  • 日付や通過、気温、あるいは商品のレビューにおける星の数など、値の表現や比較を行うもの

いつValueオブジェクトを導入するか

Valueオブジェクトは、その値オブジェクトを複数のクラスから利用することになったら導入するとよいでしょう。

たとえば、『名前』を扱うクラスがUserしかないときにNameクラスとして切り出すのは早すぎるかもしれません。名前だけならまだいいですが、それ以外にも値オブジェクトとして切り出せるものはたくさんあります。このすべてをその都度Valueオブジェクトとして扱うと、かえってコードが煩雑になってしまいます

この目安として、複数のクラスから利用されるようになったらValueオブジェクトとして切り出す、という指針があるとよさそうです。

Valueオブジェクトは、複数のクラスから参照されるようになったときにはじめて導入することで、コードが煩雑になることを防げます。

Valueオブジェクトの設計上のルール

以上をもとに、Valueオブジェクトを設計するときのルールをまとめてみます。

  • ファイルは app/values 下に配置する
  • ファイル名はValueオブジェクトの名前にする。たとえば name.rb
  • 値オブジェクトをカプセル化する目的でつくる
  • クラス名はなんの値オブジェクトかがわかるようにする。たとえばNameやAddressなど。またNameValueのような接尾辞はつけない
  • 必要最低限のインタフェースのみを公開する。たとえば full_name など
  • 公開したインタフェースに対するユニットテストを書く
loading...

まとめ

Valueオブジェクトは、コードをクリーンに保つためのすぐれたデザインパターンのひとつです。複数のクラスから利用される値オブジェクトが見つかったとき、導入することでFat Modelなどの問題を防ぐことができます。

著者
ぜに/Hiroki Zenigami

Webエンジニア&プロダクトマネージャ←プログラミング教育で起業←東大院←熊本高専。 共著に「現場で使えるRuby on Rails 5」。

関連記事関連書籍人気記事
applis
エンジニアとしてのんびり暮らす
お問い合わせ
ご意見・ご質問やお仕事のご依頼などは下記よりお願いいたします
お問い合わせ
© applis