😇

値オブジェクト

2024/07/12に公開

値オブジェクトを説明する前に、、、

不変

class Money {
  constructor(public amount: number, public currency: string) {
    if (amount < 0) {
      throw new Error("金額は0以上で指定してください");
    }
    if (!currency) {
      throw new Error("通貨単位を指定してください");
    }
  }

  add(other: number): void {
    this.amount += other;
  }
}

addメソッドではインスタンス変数(amount)の値が変更されている。
インスタンス変数の値が変更されると、

  • いつ変更したのかわからん
  • 仕様変更したとき意図しない値に置き換わる

などの副作用が起きてしまう。

=> インスタンス変数はイミュータブル(不変)であるべき

でもでも、インスタンス変数を不変にすると、値を変更できなくない?(あたりまえだが)
=> 変更値を持ったインスタンスを返せば良い

class Money {
  constructor(private readonly amount: number, private readonly currency: string) {
    if (amount < 0) {
      throw new Error("金額は0以上で指定してください");
    }
    if (!currency) {
      throw new Error("通貨単位を指定してください");
    }
  }

  add(other: number): Money {
    const added = this.amount + other;
    return new Money(added, this.currency);
  }
}

(Rubyであれば、attr_accessorからattr_readerにしたり)

「これで完璧」、、、かと思えるが

なぜ人は値オブジェクトを使用するのか

実際に使ってみたところ、

const money: Money = new Money(3000, 'yen');
const ticketCount: number = 3;
money.add(ticketCount);
//=> Money(amount: 3003, currency: 'yen')

金額ではなく、チケットの枚数を加算してしまった。。。。
確実に金額が渡るようにしたい
=> このMoneyクラスをメソッドの引数にくるような設計にする

class Money {
  // 略

  // Moneyオブジェクトがくるように型指定
  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new Error("通貨単位が違います");
    }
    const added = this.amount + other.amount;
    return new Money(added, this.currency);
  }
}

エンティティがないMoneyクラスこそ値オブジェクトである
(エンティティとは概念的な同一性)

他のクラスでもお金を扱うメソッドがあれば、「引数は金額が確実に来るようにしたい!」なんてこともあるはず。
=> 別のクラス(モデル)のメソッドの引数にこのMoneyクラスを使用すれば解決

では我らがRuby on Railsで値オブジェクトを作成し、カッコよく使用したい!!!!!

できる人みたいに値オブジェクトをRailsで使いたい

Railsで値オブジェクトを表現する方法の一つにStructクラスの継承を行う方法があるみたい。
(他には、普通にattr_reader :amount, :currencyの素のクラスを作って、それを値オブジェクトとしたり、ApplicationRecordを継承してゴニョゴニョ)

class Money < Struct.new(:amount, :currency)
end

Structについては
https://qiita.com/k-penguin-sato/items/54189d5ed4e5f7463266

実際に、エンティティが値オブジェクトを持つ例

class User
  attr_accessor :name, :age, :money

  def initialize(name, age, money)
    @name = name
    @age = age
    @money = money
  end
end


money = Money.new(1000, 'yen')
user = User.new('shunya', 18, money)
puts user.money
#=> <struct Money amount=1000, currency="yen">

じゃあ、ActiveRecord(ApplicationRecord)がエンティティの場合に値オブジェクトを使用する場合はどうしよ。

こいつを使えば解決らしい。
https://railsdoc.com/page/composed_of

ざっくり説明すると、Moneyクラスをモデルに埋め込んでしまうことができるらしい。

使ってみる。

とりあえず、値オブジェクトを作成

class Money < Struct.new(:amount, :currency)
end

(↑置き場所ってどこがよいのかな)

モデルの作成

rails g model User name:string age:integer money_amount:integer money_currency:string
class User < ApplicationRecord
  composed_of :money, mapping: [['money_amount', 'amount'], ['money_currency', 'currency']]
end

money = Money.new(1000, 'yen')
user = User.new(name: 'shunya', age: 18, money: money)
#=> <User:0x0000000109b68a50 id: nil, name: "shunya", age: 18, money_amount: 1000, money_currency: "yen", created_at: nil, updated_at: nil>
user.save
puts user.money
#=> <struct Money amount=1000, currencfy="yen">

「いろんなパターン」
shojikinという名前で扱いたい場合

class User < ApplicationRecord
  composed_of :shojikin, class_name: 'Money', mapping: [['money_amount', 'amount'], ['money_currency', 'currency']]
end

money = Money.new(1000, 'yen')
user = User.new(name: 'shunya', age: 18, shojikin: money)
#=> <User:0x0000000109b68a50 id: nil, name: "shunya", age: 18, money_amount: 1000, money_currency: "yen", created_at: nil, updated_at: nil>

Userモデルのカラムがmoney_amountしかない場合(money_currencyが存在しない)

class User < ApplicationRecord
  composed_of :money, mapping: [['money_amount', 'amount']], constructor: Proc.new { |amount| Money.new(amount, 'yen') }
end

money = Money.new(1000, 'yen')
user = User.new(name: 'shunya', age: 18, money: money)
#=> <User:0x000000010fc931e0 id: nil, name: "shunya", age: 18, money_amount: 1000, created_at: nil, updated_at: nil>
user.money
=> #<struct Money amount=1000, currency="yen">

注意点

値オブジェクトを使用せず、直接Userのmoney_amountを変更することができてしまう。

money = Money.new(1000, 'yen')
user = User.new(name: 'shunya', age: 18, money: money)
# 直接money_amountの値を変更する
user.money_amount = 3000

user.money
#=> <struct Money amount=1000, currency="yen">
user.money.amount
#=> 1000
user.money_amount
#=> 3000

追加情報

今回、値オブジェクトをStructクラスの継承で行ったが、Dataクラスのが良さそう。
https://docs.ruby-lang.org/ja/latest/method/Data/s/define.html

Structクラスだと値の変更ができてしまう。

money = Money.new(1000, 'doller')
=> #<struct Money amount=1000, currency="doller">
money.amount = 3000
=> 3000
money
=> #<struct Money amount=3000, currency="doller">

MoneyをDataクラスにすると、、

Money = Data.define(:amount, :currency)
data_money = Money.new(amount: 1000, currency: 'yen')
#=> <data Money amount=1000, currency="yen">
data_money.amount = 3000
#=> <anonymous>': eval:4:in `<main>': undefined method `amount=' for #<data Money amount=1000, currency="yen"> (NoMethodError)

ちなみにこんな感じに、型のチェックもできるよ(完全コンストラクタ)

Money = Data.define(:amount, :currency) do
  def initialize(amount:, currency:)
    if !amount.is_a?(Integer) || !currency.is_a?(String)
      raise StandardError, "型がまちがってるよ。頑張ろうね。"
    end

    super
  end
end

参考文献

https://qiita.com/takuya0301/items/07f1393a8d0f62664ba2
https://amzn.asia/d/06fywJv8

Discussion