30
21

More than 5 years have passed since last update.

[ruby]moduleの基礎〜alias_method_chainとprependについて

Last updated at Posted at 2017-02-17

moduleとは何かと使い方

インスタンスを作れないクラス
 =>Module.new が出来ない。

こんな感じにprependやincludeして使う。

spice_wolf.rb
module SpiceWolf
  def rorensu
    return "rorensu"
  end
end

#特定のclassを定義してincludeやprependして利用する
class Test
  include SpiceWolf
end

Test.new.rorensu

定義済のmoduleを上書きする方法

既存のmoduleと同じ名前のmoduleを作ると上書きされる。

Rubyでよく利用されるmodule名を使うとこの破壊的な上書きでバグになる可能性があるので注意。

spice_wolf.rb
module SpiceWolf
  def rorensu
    return "rorensu"
  end
end

module SpiceWolf
  def rorensu
    return "ろれんす"
  end
end

class Test
  include SpiceWolf
end

Test.new.rorensu

実行すると上書きされ、日本語でろれんすと出力される

エイリアスでmoduleの処理を変更

既存メソッドに機能を付け足したいときに使える。

ここでは既存メソッドの処理結果に日本語の文字列を追加。

spice_wolf.rb
module SpiceWolf
  def rorensu
    return "rorensu"
  end
end

#-----エイリアス実装-----
module SpiceWolf
  def rorensu_horo
    return old_rorensu + "とホロ"
  end

  alias old_rorensu rorensu
  alias rorensu     rorensu_horo
end

class Test
  include SpiceWolf
end

Test.new.rorensu

古いメソッドは[old_rorensu]と別名で退避させる

[rorensu_horo]という新しいメソッドを定義し、旧メソッド名[rorensu]で呼び出せる様にする。

この様にすることで古いメソッドはメソッド名が異なるため上書きされない。
=>新しいメソッドの中でも利用出来る。

alias_method_chainを使って短く実装

alias_method_chainで先ほど行った処理を短く実装可能。

spice_wolf.rb
module SpiceWolf
  def rorensu
    return "rorensu"
  end
end

#-----以下alias method chain-----
module SpiceWolf
  def rorensu_with_horo
    return rorensu_without_horo + "とホロ"
  end
  alias_method_chain :rorensu, :horo
end

class Test
  include SpiceWolf
end

Test.new.rorensu

元からあったメソッド 【rorensu】 を拡張したいときに利用できそう、手順は以下の3つ

1:新しいメソッドを【既存メソッド名_with_なんでもいい】の命名規則で定義する
rorensu_with_horo 

2:alias_method_chainでwithの前後のメソッド名を定義  【既存メソッド名 と なんでもいい】
alias_method_chain :rorensu, :horo

3:既存メソッド名「rorensu」で呼び出すと、rorensu_with_horoメソッドが呼ばれるようになる
Test.new.rorensu
=> rorensuとホロ

4:旧メソッドを呼び出す方法は  【既存メソッド名_without_後ろにつけた文字】  で呼び出せる
rorensu_without_horo 
=>rorensu

prependを用いた既存メソッドの書き換え(と追加処理)

Rials5からaliad_method_chain が非推奨となり、prependを用いた実装を推奨されているため、そちらでの記法を@kts_hさんにコメントをいただき追記いたしました。

Ruby 2.0.0から新たに追加されたprependを使うことで、
メソッド探索チェーンの手前に新たに作成したmoduleを追加してalias_method_chainに”似た”の処理を短く簡潔に実装できます。

prepend.rb
# 定義済みのmodule 変更不可。いろんな場所から呼ばれている
module SpiceWolf
  def rorensu #クラスの特異メソッド == クラスメソッド
    return "rorensu"
  end
  module_function :rorensu

end

# 変更用バッチ
module Wolf
  def rorensu
    super + "とホロ"
  end
end

# 定義済みmoduleを再定義、変更用バッチを当てる
module SpiceWolf
  class << self
    prepend Wolf
  end
end

SpiceWolf.rorensu

この様に記述することで、既存のSpiceWolfのメソッドrorensuを拡張可能です。

SpiceWolf.rorensu の rorensu は SpiceWolf の特異メソッド(別名クラスメソッド)なので、
SpiceWolf の特異クラスのスコープで prepend する必要がある。

prepend部分
# 親クラスで特異メソッド(クラスメソッド)なので class << selfを付けて特異メソッドとして定義しないといけない
class << self
  prepend Wolf
end

class << self についての解説はこの記事が参考になります
Ruby 初級者のための class << self の話 (または特異クラスとメタクラス)

alias_method_chainとprependの違い

alias_method_chain
=>リンクを貼り替えるのでclassでinclude済みの場合も、張り替えたリンク先のmodule参照してくれる
なのでalias_method_chainを用いる場合、そのalias_method_chainが実行される以前にinclude、prependしたclassにも変更が適応されます。

prepend
=>module自体を変更し、次回以降の読み込み時に変更がで既往されたmoduleがclassにinclude、prependされる
なのでprependを用いた場合は、そのprependを実行する以前にinclude、prependしたclassの処理は以前の処理のままになります。

以下のコードを実行すると違いがよくわかると思います

prepend.rb
module Dengeki
  module SpiceWolf
    def print
      rorensu
    end

    private
    def rorensu
      return "rorensu"
    end
  end
end

class Test1
  include Dengeki::SpiceWolf
end

module Wolf
  def rorensu
    super + "とホロ"
  end
end

module Dengeki
  module SpiceWolf
    prepend Wolf
  end
end

class Test2
  include Dengeki::SpiceWolf
end

#古いものが実行されてしまう
Test1.new.print
#新しい方が実行される
Test2.new.print

image

alias_method_chainでの実装
module Dengeki
  module SpiceWolf
    def print
      rorensu
    end

    private
    def rorensu
      return "rorensu"
    end
  end
end

class Test1
  include Dengeki::SpiceWolf
end

module Dengeki
  module SpiceWolf
    private
    def rorensu_with_horo
      rorensu_without_horo
      p rorensu_without_horo.to_s + "とhoro"
    end
    alias_method_chain :rorensu, :horo
  end
end

class Test2
  include Dengeki::SpiceWolf
end

#両方とも同じ処理結果が実行される!!!
Test1.new.print
Test2.new.print

image

試しに日本語のファイルを送信するとsafariで文字化けするRails標準moduleを書き換えてみる

日本語のファイルをDLする際にSafariとIEのみ文字化けしてしまう問題があり、それを解決する際に利用したコード

data_streaming.rb
module ActionController
  module DataStreaming
    def send_file_headers_with_utf8!(options)
      send_file_headers_without_utf8!(options)
      match = /(.+); filename="(.+)"/.match(headers['Content-Disposition'])
      encoded = URI.encode_www_form_component(match[2])
      headers['Content-Disposition'] = "#{match[1]}; filename*=UTF-8''#{encoded}" unless encoded == match[2]
    end
    alias_method_chain :send_file_headers!, :utf8
  end
end

ActionControllerのDataStreamingはファイルをDLする際にRailsで呼び出されるメソッド
RailsではSafariとIEのブラウザが来た際に日本語のファイル名をエンコーディングできない。

そこでRailsで定義済みのmoduleのActionController::DataStreaming
をalias_method_chainを用いて書き換えていることがわかった。

ここでprependを用いて書き換えようとすると、前述の理由から変更が反映されない
prependの実行タイミング以前にActionControllerのDataStreamingが各Railsの標準classにincludeされてしまうからだと思う。

ちなみに、定義済みのmoduleをprependやalias_method_chainで書き換える利点は以下です。

  • Railsの実装を読み
  • 呼び出し元メソッドをすべて洗い出し
  • メソッド呼び出し先を自分の作成したメソッドに置き換えるなんという面倒な作業を行わなくてよい上に
  • バージョンアップ時に修正されたらこのprependやalias_method_chain部分の処理を消すだけでよいというメンテナンス性も高い

alias_method_chainはRails5から非推奨なのでprependで書き換えた

application_controller.rb
class ApplicationController < ActionController::Base
  #ActionController::Baseのソースを読むとここで様々なmoduleをincludeしている
  module DataStreamPatch
    def send_file_headers!(options)
      super(options)
      match = /(.+); filename="(.+)"/.match(headers['Content-Disposition'])
      encoded = URI.encode_www_form_component(match[2])
      headers['Content-Disposition'] = "#{match[1]}; filename*=UTF-8''#{encoded}" unless encoded == match[2]
    end
  end

  module ActionController
    module DataStreaming
      prepend DataStreamPatch
    end
  end

  #moduleの動作を書き換えた上で再度includeする必要がある。
  include ActionController::DataStreaming

  ....
end

Classを定義した際にActionController::Baseで様々なmoduleをincludeしている。
ここでActionController::DataStreamingもApplicationContorollerにincludeされている。

前述の通りprependはalias_method_chainとは異なり、一度読み込んで変更を加えたら再度読み直さなければならない。

なのでclass定義の直後にinclude ActionController::DataStreamingとすることで上書きすることができる。
https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/base.rb

試しに実際にダウンロードを行うDownloadControllerから継承関係を確認すると
image
直上の継承位置にActionController::DataStreamingが位置していることがわかり。

send_file_headers!を呼び出すとメソッド探索で一番最初に今回includeした新しいメソッドがヒットする。

また、alias_method_chainを用いた場合の継承関係はこのようになっており、継承関係は変化していない。
image

複数箇所でmoduleを利用しない場合はover writeで実装したほうが単純明快

Classのメソッド探索で「send_file_headers!」を探した際に自分で実装したメソッドが見つかれば良いというのが結論なので

以下のように直接ApplicationControllerにメソッドを定義してover writeしたほうが単純かつ明快で良い

ApplicationController.rb
class ApplicationController < ActionController::Base
  def send_file_headers!(options)
    super(options)
    match = /(.+); filename="(.+)"/.match(headers['Content-Disposition'])
    encoded = URI.encode_www_form_component(match[2])
    headers['Content-Disposition'] = "#{match[1]}; filename*=UTF-8''#{encoded}" unless encoded == match[2]
  end
end

紆余曲折した結果今提供しているサービスではこの実装で動いています。

番外編:module_functionを用いてmoduleの特異メソッドを定義する

あまり利用しないmodule_functionについてです、読み飛ばしてもいいと思います。

moduleは通常classにincludeやprependをして利用します。
しかしclassを定義し、メソッドを呼び出すのではなく、
そのモジュール自体自身のメソッドを定義することも可能となっており、それがmodule_functionです。
=>つまり正直言って使い所不明です。

module_functionを用いることで、モジュール名.メソッド名でメソッド呼び出しを可能にできます。
しかし以下のようなルールがあります。

  • module_functionで定義したメソッドは自動的にprivate宣言となる
  • レシーバをモジュール名にして呼び出す。
  • classでそのモジュールをincludeした際に、そのインスタンスではmodule_functionは利用できない
spice_wolf.rb
module SpiceWolf
  Wolf = 'horo'
  def rorensu
    return "rorensu"
  end

  #モジュール名.rorensuで呼び出せるようにできる
  module_function :rorensu
end

#-----moduleの特異メソッドとして利用-------
SpiceWolf::Wolf    #定数呼び出し
SpiceWolf.rorensu  #module_function呼び出し
  • モジュール名::定数    で定数を呼べる
  • モジュール名.メソッド名  でメソッドを呼べる
30
21
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
21