クラス・モジュールをみつける
Rubyを書いてると、特定のグループに属するクラス/モジュールの一覧が欲しくなることがある。
例えば、一ヶ月に一度実行するお掃除バッチ処理の中で、Redisを使用するクラスの一覧を取ってきて、全てのクラスに対して cleanup!メソッドを実行したい、というようなユースケースがある。
code:ruby
RedisClient.all_modules_using_redis_client.each do |mod|
mod.cleanup!
end
上記コード中の all_modules_using_redis はどのようにして実装できるだろうか。
以下で、その実現方法について述べる。
名前をベタ書きする
一番簡単なのは、モジュール名をソースコード中に列挙する方法だ。
以下のように、Redisを使用するクラスが増えるたびに、そのクラスの名前を追加していく。
code:ruby
class RedisClient
def self.all_modules_using_redis_client
[
RankingRedisClient,
MessageRedisClient,
CacheRedisClient,
]
end
...
end
この手法の良いところは以下の2つだ
非常にわかりやすい
柔軟性がある
例えば、一部のモジュールはここに登録したくない、というような場合、単に追記しないだけで実現できる
一方で、以下のような欠点がある
書き漏れが容易に発生する
名前ベースのメタプログラミング
少し工夫して、書き漏れが起こらないようにすることができる。
ActiveSupport を使用すると、以下のようにクラスの一覧を取得できる
code:ruby
class RedisClient
def self.all_modules_using_redis_client
Dir.glob('lib/redis_client/*.rb').map do |fname|
File.basename(fname, '.rb').classify.constantize
end
end
...
end
このコードは、Redisを使用するクラスが、 lib/redis_client以下に保存されていることと、ファイル名とクラス・モジュール名の命名規約がRailsのそれに一致していることを前提にしている。
この手法の良いところは以下の2つだ
わかりやすい
書き漏れが発生しにくい
一方で、以下のような欠点がある
命名規約が強く、柔軟性に欠ける
例えば、Redisを使用するモジュールだが、他のディレクトリに保存したい、ということが許されない
命名規約の強さ自体は、歓迎する考えの人もいるだろう
inheritedによる列挙
些か技巧的な実現方法として、inheritedコールバックを使用するものがある。
これは、Redisを使用する全てのクラスが、RedisClientクラスを継承することを前提としている。
code:ruby
class RedisClient
def self.all_modules_using_redis_client
@subclasses || []
end
def self.inherited(subclass)
@subclasses ||= []
@subclasses << subclass
end
...
end
class HogeRedisClient < RedisClient
end
この方法の注意点だが、RedisClientのサブクラスのサブクラスは、 all_modules_using_redis_client には含まれない。この問題は、ActiveSupportの Concern を使用すればおそらく回避できる。詳しい方法は読者の練習問題とする。
この手法の良いところは以下の2つだ
書き漏れが発生しにくい
命名規約が必要ない
一方で、以下のような欠点もある
柔軟性に欠ける
例えば、RedisClientを使用しているが cleanup! は呼ばなくていい、というような要求に答えるのが面倒だ(inheritedメソッドの中にそれ用の分岐が必要になる気がする)。 サブクラス側の cleanup! を空実装にしておけば実質的に問題ないといえば無いが...
継承を使っている
included を使えば、継承を使わずに module の includeで実現することも可能なはず。読者の練習問題とする。