🐢
ただのクラスを値オブジェクトのクラスに育てていく例
ただのデータクラス
class C
attr_reader :value
def initialize(value)
@value = value
end
end
C.new(1) # => #<C:0x0000000109785870 @value=1>
いちいち new を書きたくない
def C.[](...)
new(...)
end
C[1] # => #<C:0x00000001097455b8 @value=1>
- なんでも new はダサいので工夫したい
[ ] も書きたくない
def C(...)
C.new(...)
end
C(1) # => #<C:0x000000010971fe80 @value=1>
Object.private_methods.grep(/C/) # => [:C, :Complex]
- この C は C クラスと何も関係ない
- Object のプライベートメソッドになるので潔癖症な方には向かないかもしれないけど、どうせトップレベルでクラスを定義したら Object.constants に入る
inspect の表示を簡潔にしたい
C(1) # => #<C:0x000000010971fe80 @value=1>
class C
def inspect
"<#{value}>"
end
end
C(1) # => <1>
- value.to_s を返すとデバッグ目的の表示と区別つかないので工夫したい
文字列に埋めたときは文字列化してほしい
"#{C(1)}" # => "#<C:0x000000010971e738>"
class C
def to_s
value.to_s
end
end
"#{C(1)}" # => "1"
足し算したい
C(1) + C(2) rescue $! # => #<NoMethodError: undefined method `+' for <1>:C>
class C
def +(other)
self.class.new(value + other.value)
end
end
C(1) + C(2) # => <3>
四則演算用のメソッドをコピペするのが面倒
class C
%i(+ - * /).each do |op|
define_method(op) do |other|
self.class.new(value.send(op, other.value))
end
end
end
C(1) + C(2) # => <3>
- お金値オブジェクトだった場合「お金×お金」は変なのでそこは工夫する
- 加減と剰余でロジックが変わるはず
send を使いたくない
%i(+ - * /).each do |op|
C.class_eval <<-EOT, __FILE__, __LINE__ + 1
def #{op}(other)
self.class.new(value #{op} other.value)
end
EOT
end
C(1) + C(2) # => <3>
Warming up --------------------------------------
define_method 135.218k i/100ms
class_eval 164.677k i/100ms
Calculating -------------------------------------
define_method 1.323M (± 4.2%) i/s - 6.626M in 5.016970s
class_eval 1.610M (± 7.9%) i/s - 8.069M in 5.071298s
Comparison:
class_eval: 1609896.7 i/s
define_method: 1323193.0 i/s - 1.22x (± 0.00) slower
- ちょっとだけ速くなる
- けどエディタでインデントがおかしくなりがち
- ベクトル値オブジェクトで演算がボトルネックになってるなら一考の価値あり
マイナスをつけたらマイナスになってほしい
-C(1) rescue $! # => #<NoMethodError: undefined method `-@' for <1>:C
class C
def -@
self.class.new(-value)
end
end
-C(1) # => <-1>
比較がおかしい
C(1) == C(1) # => false
class C
def ==(other)
self.class == other.class && value == other.value
end
end
C(1) == C(1) # => true
- おかしくはないが中身が同じ値オブジェクトだったら同一視したい
-
==
は緩めな判定をしていいので相手が整数だったらそのまま比較するとかしてもいい - が、値オブジェクトの目的は人為的ミスよる間違った組み合わせを咎めたいからでもあるのできっちり判定した方がよさそう
ハッシュキーとして使ったら別もの扱いされている
{C(1) => "ok"}[C(1)] # => nil
class C
def hash
value.hash
end
def eql?(other)
self.class == other.class && value == other.value
end
end
{C(1) => "ok"}[C(1)] # => "ok"
C(1).eql?(C(1)) && C(1).hash == C(1).hash # => true
- hash と eql? をペアで定義する
- hash だけあればよさそうなのになんで eql? もいるのかはわからない
- hash が衝突したときのため?
ソートできない
[C(2), C(1)].sort rescue $! # => #<ArgumentError: comparison of C with C failed>
class C
def <=>(other)
[self.class, value] <=> [other.class, other.value]
end
end
[C(2), C(1)].sort # => [<1>, <2>]
Immutable にしておく
C(1).instance_eval { @value = 0 } # => 0
class C
def initialize(value)
@value = value
freeze
end
end
C(1).instance_eval { @value = 0 } rescue $! # => #<FrozenError: can't modify frozen #<Class:#<C:0x00007ffe50046630>>>
Discussion