Tech Racho エンジニアの「?」を「!」に。
  • 開発

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をうまく使い分ける(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。