moduleとは何かと使い方
インスタンスを作れないクラス
=>Module.new が出来ない。
こんな感じにprependやincludeして使う。
module SpiceWolf
def rorensu
return "rorensu"
end
end
#特定のclassを定義してincludeやprependして利用する
class Test
include SpiceWolf
end
Test.new.rorensu
定義済のmoduleを上書きする方法
既存のmoduleと同じ名前のmoduleを作ると上書きされる。
Rubyでよく利用されるmodule名を使うとこの破壊的な上書きでバグになる可能性があるので注意。
module SpiceWolf
def rorensu
return "rorensu"
end
end
module SpiceWolf
def rorensu
return "ろれんす"
end
end
class Test
include SpiceWolf
end
Test.new.rorensu
実行すると上書きされ、日本語でろれんすと出力される
エイリアスでmoduleの処理を変更
既存メソッドに機能を付け足したいときに使える。
ここでは既存メソッドの処理結果に日本語の文字列を追加。
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で先ほど行った処理を短く実装可能。
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に”似た”の処理を短く簡潔に実装できます。
# 定義済みの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 する必要がある。
# 親クラスで特異メソッド(クラスメソッド)なので 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の処理は以前の処理のままになります。
以下のコードを実行すると違いがよくわかると思います
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
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
試しに日本語のファイルを送信するとsafariで文字化けするRails標準moduleを書き換えてみる
日本語のファイルをDLする際にSafariとIEのみ文字化けしてしまう問題があり、それを解決する際に利用したコード
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で書き換えた
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から継承関係を確認すると
直上の継承位置にActionController::DataStreamingが位置していることがわかり。
send_file_headers!を呼び出すとメソッド探索で一番最初に今回includeした新しいメソッドがヒットする。
また、alias_method_chainを用いた場合の継承関係はこのようになっており、継承関係は変化していない。
複数箇所でmoduleを利用しない場合はover writeで実装したほうが単純明快
Classのメソッド探索で「send_file_headers!」を探した際に自分で実装したメソッドが見つかれば良いというのが結論なので
以下のように直接ApplicationControllerにメソッドを定義してover writeしたほうが単純かつ明快で良い
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は利用できない
module SpiceWolf
Wolf = 'horo'
def rorensu
return "rorensu"
end
#モジュール名.rorensuで呼び出せるようにできる
module_function :rorensu
end
#-----moduleの特異メソッドとして利用-------
SpiceWolf::Wolf #定数呼び出し
SpiceWolf.rorensu #module_function呼び出し
- モジュール名::定数 で定数を呼べる
- モジュール名.メソッド名 でメソッドを呼べる