値オブジェクト
値オブジェクトを説明する前に、、、
不変
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については
実際に、エンティティが値オブジェクトを持つ例
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)がエンティティの場合に値オブジェクトを使用する場合はどうしよ。
こいつを使えば解決らしい。
ざっくり説明すると、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クラスのが良さそう。
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
参考文献
Discussion