エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Active Record とクラスメソッド

とある Ruby on Rails アプリケーションを開発するプロジェクトで、次のような雰囲気のコードを目にしました。

class Foo < ApplicationRecord
  # テーブル foos は、カラム a, b, c を持つものとします

  class << self
    def add_a_and_b(foo)
      foo.c = foo.a + foo.b
    end

    # ...
  end

  # ...
end
foo = Foo.find(id)
Foo.add_a_and_b(foo)
foo.save

(このコードを見て色々なことを思い浮かぶことと思いますが、この場では操作がクラスメソッドで実装されていることに焦点を当てたいと思います)

この、特定のインスタンスに閉じた操作をクラスメソッドですることの違和感。

少なくとも、操作がインスタンスに閉じていればインスタンスに任せるのが筋というもの。

foo = Foo.find(id)
foo.add_a_and_b
foo.save

そんなことを気にしながらコードを読み進めてみると、#add_a_and_b は実装されていて、中身はこうなっていました。

class Foo < ApplicationRecord
  # ...
  
  def add_a_and_b
    self.class.add_a_and_b(self)
  end
end

クラスメソッドが中心の世界。

実際、単純なデータを単純にデータベースへ反映するだけであれば、上に書いたように #save を使っていますが、込み入ったデータ構造を持つ場合には次のように永続化のクラスメソッドを用意しておいて、それを利用することがパタンになっていました。

class Foo < ApplicationRecord
  class << self
    # ...

    def save(foo)
      # データベースへ反映する前に foo をいろいろ加工する
      foo.save
    end
  end

  # ...
end
foo = Foo.find(id)
Foo.add_a_and_b(foo)
Foo.save(foo)

これが Elixir であれば、よく見慣れた形ではあるのですが。

person = Friends.Person |> Ecto.Query.first |> Friends.Repo.one

changeset = Friends.Person.changeset(person, %{age: 29})

Friends.Repo.update(changeset)

Getting Started — Ecto v3.13.4 から抜粋)

どうにも、インスタンスはレコードを映し取ったデータの集まりで、レコードを操作する媒介としてのインスタンスをクラスメソッドで操作する、という考えが根底にあるようです。

確かに Active Record Pattern に不自由を感じることもあります。

martinfowler.com

特に大きなデータや関連が込み入ったデータを扱うとき、間接的になされるデータベースへの操作を想像し、アクセスが非効率にならないように意識してコードを組み立てるのは、そこそこ骨が折れます。

それでも大抵は、データベースの設計やロジックの設計を見直すと改善できることが多く、また先達の知恵や蓄積されたライブラリで解消できることも少なくありません。

あえて ActiveRecord クラスを使いながら Active Record Pattern から離れる。

オブジェクト指向言語を使ってオブジェクト指向でないコードを書くような無理を感じて、個人的には批判的な目で眺めてはいます。 が、あえて Active Record として見ないなら、何がより妥当な実装なのか、あるいは完全に ActiveRecord を捨てたら何ができるのか、試みとしては気になるところです。