Ruby: マジックを取り除くマジック(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Ruby: マジックを取り除くマジック(翻訳)

gemの中には、inherited_resourcesのように自動的に定義を提供することでコード量の削減に役に立つものがあります。しかし、中にはマジックを繰り出すgemもあるので、そうしたgemを削除したいと考えました。

この「マジック」はメソッドの定義、ここではコントローラのアクションを、gemが提供する関数を一切呼び出さずに定義することを指します。そうした暗黙の振る舞いがあると、レガシーコードと戦うのがつらくなります。こうした機能やモデルを削除してよいかどうかを調べるのは大変な作業です。同様に、機能を正しく追加するのも難しい作業です。私たちのプログラム内でのユースケースや依存関係を見落としがちだからです。見落としがあると悲惨なバグが発生してコードが止まってしまうかもしれません。

そういうわけで、私たちのアプリからこのinherited_resources gemを何としても削除したかったのです。明示的な書き方といえばこれしかありませんでした。

class Admin::BaseController < InheritedResources::Base
  ...
end

しかもBaseControllerは管理画面のすべてのコントローラの親クラスなので、暗黙の動作がどのコントローラに影響しているかわかったものではありません。これをきれいに取り除くのは骨です。最終的に利用されている場所を特定できない状態で、どうすればこういうgemを安全に取り除けるのでしょうか。gemをえいやっと取り除いてproductionがメラメラと焼け落ちるのを眺めるという方法もないわけではありませんが、勇気はそういうことに使うものではありません。目には目を、マジックと戦うにはマジックです。

まさにこの瞬間、Rubyがオープンクラスであることのありがたみを実感できます。私は、InheritedResourcesgemを動的にextendして、gemが使われた場所を通知する振る舞いを追加する、以下のコードを書きました。

module InheritedResourcesRemoval
  InheritedResourcesUsed = Class.new(StandardError)

  def index(options = {}, &block)
    Honeybadger.notify(InheritedResourcesUsed.new("Inherited resources used at controller `#{params[:controller]}` and action `#{params[:action]}`"))
    super(options, &block)
  end

  def show(options = {}, &block)
    Honeybadger.notify(InheritedResourcesUsed.new("Inherited resources used at controller `#{params[:controller]}` and action `#{params[:action]}`"))
    super(options, &block)
  end

  def new(options = {}, &block)
    Honeybadger.notify(InheritedResourcesUsed.new("Inherited resources used at controller `#{params[:controller]}` and action `#{params[:action]}`"))
    super(options, &block)
  end

  def edit(options = {}, &block)
    Honeybadger.notify(InheritedResourcesUsed.new("Inherited resources used at controller `#{params[:controller]}` and action `#{params[:action]}`"))
    super(options, &block)
  end

  def create(options = {}, &block)
    Honeybadger.notify(InheritedResourcesUsed.new("Inherited resources used at controller `#{params[:controller]}` and action `#{params[:action]}`"))
    super(options, &block)
  end

  def update(options = {}, &block)
    Honeybadger.notify(InheritedResourcesUsed.new("Inherited resources used at controller `#{params[:controller]}` and action `#{params[:action]}`"))
    super(options, &block)
  end

  def destroy(options = {}, &block)
    Honeybadger.notify(InheritedResourcesUsed.new("Inherited resources used at controller `#{params[:controller]}` and action `#{params[:action]}`"))
    super(options, &block)
  end
end

後は、特定のモジュールをひとつオープンしてこのハックをprependするだけです。

module InheritedResources
  module Actions
    prepend(::InheritedResourcesRemoval)
  end
end

ご覧のとおり、メソッドのどれかが利用されるたびにHoneybadgerに通知が飛び、gemが使われたコントローラとアクションを正確に知らせてくれます。parser gemで自動書き換えすることで必要なコードを追加する手もありますが、今回の場合gemを使っていたアクションは数個しかなかったので、そこまでする意味はありませんでした。私はコントローラのコードを数行手書きし、対応するビューを変更して目障りな@resource変数名を使わないようにしただけで済みました。

このコードをproductionで数週間動かしてからコードを取り除き、めでたくgemを削除できたのでした😊。

詳しく知りたい方へ

本記事をお楽しみいただけましたら、ぜひ私たちのニュースレターの購読をお願いします。私たちが日々追求している、開発者を驚かさないメンテ可能なRailsアプリの構築方法を皆さんにお届けいたします。

以下の記事も参考にどうぞ。

関連記事

Ruby: 年に1度だけ発生する夏時間バグ(翻訳)

Ruby: injectとeach_with_objectをうまく使い分ける(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ