wataメモ

日々のメモをつらつらと書くだけ

ActiveRecordでSTI(Single Table Inheritance)時に自動でenum定義されるようにした

 Ruby on Railsでサービスを作ってみたのおまけSTI時にenumを書くとDRYの原則に則さないと書いたが、まあ無いなら作ればいいじゃないかということでactiverecord-sti-enumというrubygemを作ってみた。 (プロダクトレベルで使える物ではないのであしからず)

やりたいこと

 ActiveRecordSTIを使っている時にenumで追加されるメソッドを使いたい場合はenum定義を書く必要があった。 それだと継承クラスを追加した時にenumもメンテしなければいけないのでDRYではない。 それを子クラスを追加しただけで、自動でenumメソッドが追加されるようにしたい。

class Question < ActiveRecord::Base
  include ActiveRecord::Sti::Enum # これを書くだけでenumを書きたくない

  # 今までは下記のようにenumを定義する必要があった
  # enum type: {
  #   single:   'SingleChoice',
  #   multiple: 'MultipleChoice',
  #   date:     'DateChoice',
  #   free:     'FreeChoice',
  # }
end

実装方法について

  1. 親クラス(にmoduleをincludeされた時にincludedでフックする
  2. includedフックでinheritedフックメソッドを親クラスに追加する
  3. inheritedフックでは継承されたクラス名をベースにenum定義を追加していく

 継承された時に呼ばれるinheritedフックを利用してenumを追加しているわけだが、これだと初期化の問題が発生する。 例えばDateChoiceクラスが初期化される前に「@question.date_choice?」とか呼ばれるとNoMethodErrorとなってしまう。 最初はmethod_missingを実装してメソッド名からクラス名にして初期化しようかと考えたが、 全部が初期化される前に「types」等で定義されているenum一覧を取得した時におかしくなってしまうのでmodel配下をrequireしてしまう方法で対応。

Dir.glob('./app/models/*.rb').each do |f|
  require File.basename(f)
end

まとめ

 rubyは動的にクラスやメソッドが増減するので、クラスドリブンの場合だとタイミングの問題が起こることになる。 今回作ったのはプロダクトレベルでは使えないものだが、こういうことはやろうとしてみて気がつく事や、基本的な理解が進んだりする。 これからもどんどん試していきたい。