メソッドを上書きするときにModule#prependが便利だったという話

こんにちは〜! GA technologiesの澤田です。つい先日Ruby Goldを取得しまして、その勉強の際に知ったModule#prependがメソッドの動作を変更するのにすごく便利だなと思ったので記事にしてみました。

Module#prependとは

最初にメソッドの説明をしてしまいます。
その後は実際にこのメソッドを使った実例を紹介します。

リファレンスより抜粋

docs.ruby-lang.org

指定したモジュールを self の継承チェインの先頭に「追加する」ことで self の定数、メソッド、モジュール変数を「上書き」します。
継承チェイン上で、self のモジュール/クラスよりも「手前」に 追加されるため、結果として self で定義されたメソッドは override されます。
modules で指定したモジュールは後ろから順に処理されるため、 modules の先頭が最も優先されます。
また、継承によってこの「上書き」を処理するため、prependの引数として 渡したモジュールのインスタンスメソッドでsuperを呼ぶことで self のモジュール/クラスのメソッドを呼び出すことができます。

つまり、既存のメソッドを上書きしつつ「その内部で既存のメソッドを呼び出す」ことができます。

前処理、後処理、引数を変更して渡す...... などといった色々なことが実現できます。

そもそもメソッドの動作を変更したい時って?

ある時業務でGoogle Spreadsheetを扱うコードをrubyで書く必要が出てきました。とすればgoogle-api-ruby-client gemの出番ですが、
この本家gemのAPIは大層ややこしいものであり(注:個人差があります)、また実装に時間をかけれいられない事情もあったため
今回は私用で使用したことのある google-drive-ruby と言うgemを利用することにしました。

github.com

代表的なGoogleのAPIを利用しやすい形でまとめたgemになります。
特にSpreadsheetを扱うコードは直感的で書きやすく、個人的に気に入っていました。

そんなこんなでコーディングを進めていたのですが、途中で以下のロジックが必要になりこのgemの手に余る事態になってしまいました......

  1. ワークシートをコピーする
  2. ワークシートのindexを操作する

はっきり言えば事前の調査不足です。反省点ですね。
では諦めて google-api-ruby-client を使って自前でメソッド書こう!

......と思えれば良いのでしょうが、私はひねくれ者なので
「よっしゃ、モンキーパッチ作ったろ!w」
と思ったのでした。

※gemの挙動を変えることは当然リスクのある行為です。今回は影響範囲が狭いためこの方法を取ることに)

ちなみに、後日google-drive-rubyに対して上記ロジックを可能にするきちんとした修正の
Pull Requestを提出しています。

ワークシートをコピーする処理を追加する

これに関しては簡単です。メソッドを追加しました。

copy_toメソッドの追加
class GoogleDrive::Worksheet
  def copy_to(spreadsheet_or_id)
    destination_spreadsheet_id =
      spreadsheet_or_id.respond_to?(:id) ? spreadsheet_or_id.id : spreadsheet_or_id
    request = Google::Apis::SheetsV4::CopySheetToAnotherSpreadsheetRequest.new(
      destination_spreadsheet_id: destination_spreadsheet_id,
    )
    @session.sheets_service.copy_spreadsheet(spreadsheet.id, sheet_id, request)
    nil
  end
end

こんな感じでrubyのクラスは「再オープン」することができ、そのコンテキストでメソッドを定義すれば新しいメソッドをクラスに追加することができます。

ワークシートのindexを操作する処理を追加する

問題はこれです。

indexに関して新規にメソッドを作るだけでなく、既存のメソッドの動作を変更させる必要が出てきました。

set_propertiesの上書き(before)
class GoogleDrive::Worksheet
  def set_properties(properties)
    @index = properties.index # 先頭にこの行だけ追加したい!!!

    # ↓ここから元々の内容
    @properties = properties
    @title = @remote_title = properties.title
    
    #
    # 〜中略〜
    #
  end
end

クラスを再オープンし、上のようなコードを書くことで既に定義済みのset_propertiesメソッドを丸ごと上書きしてしまうことができます。

実際私は当時そう書いたのですが、こういう「前処理だけ追加したい!」場合に Module#prepend を知っていると少し綺麗に書くことができます。

set_propertiesの上書き(after)
class GoogleDrive::Worksheet
  # 変更するメソッドをまとめたモジュール
  module PrependMethods
    def set_properties(properties)
      @index = properties.index
      super
    end
  end

  prepend PrependMethods # コレ
end

コードがスッキリして、変更の意図も明確になりました。そして既存のコードを誤って変更してしまうこともありません。

最後に

この記事を読んで「なるほど!」と思ったなら、あなたも是非色々と調べてみて下さい。まずは ruby 2.6.0 リファレンスマニュアルを読むことをオススメします。