Immutable な Value Object とメモ化が相性悪すぎる件

2022/11/03に公開約1,300字2件のコメント

例がいまいちだけど次のようにURLを受け取ってURLの引数を簡単に取得できるクラスがあったとする

require "rack"

class FooUrl
  def initialize(url)
    @url = url
  end

  def xxx
    params["xxx"]
  end

  private

  def params
    @params ||= Rack::Utils.parse_query(URI(@url).query)
  end
end

foo_url = FooUrl.new("https://example.com/?xxx=1")
foo_url.xxx # => "1"

これはインスタンスに対して get しかしてないので Value Object と考えられる
Value Object だったら Immutable にした方がいいだろう
というわけでコンストラクタの最後に freeze を入れてみる

require "rack"

class FooUrl
  def initialize(url)
    @url = url
    freeze
  end

  def xxx
    params["xxx"]
  end

  private

  def params
    @params ||= Rack::Utils.parse_query(URI(@url).query)
  end
end

foo_url = FooUrl.new("https://example.com/?xxx=1")
foo_url.xxx # => 

するとこれ

# ~> -:16:in `params': can't modify frozen FooUrl (FrozenError)
# ~> 	from -:10:in `xxx'
# ~> 	from -:21:in `<main>'

freeze する前に params を実行しておくとかダサすぎるしいったいどうしたらいいんだともう何年も悩んでいる

というのを投稿したらコメントで良いアイデアを教えてもらったのでこうしてみた

require "rack"

class FooUrl
  def initialize(url)
    @url = url
    @memo = {}
    freeze
  end

  def xxx
    params["xxx"]
  end

  private

  def params
    @memo[:params] ||= Rack::Utils.parse_query(URI(@url).query)
  end
end

foo_url = FooUrl.new("https://example.com/?xxx=1")
foo_url.xxx # => "1"

これでええやん
解決!

Discussion

initialize で @params = [] しておいて、params の中では @params[0] ||= ... のようにすると、とりえあず動くようになりますね。

コメントありがとうございます
self を freeze しても既存のインスタンス変数は変更できるんですね

ログインするとコメントできます