Rubyのprivateを考える
RuboCopのIssueを眺めていると、いろいろな人のRubyの考え方に触れることができて面白い。例えば、多くのRubyistにとってprivateなメソッドを宣言したいときには、以下のような書き方をすると思う。
class Cat def meow puts "Meow!" end private def bowwow puts "Bowwow!" end def cock_a_doodle_doo puts "cock-a-doodle-doo" end end
private
の後にインデントするとかしないとか、微妙な差異こそあれど、大体こんな感じ。でも、これをよくないと考える人もいる。ではどうするのかというと、以下のようにインラインでアクセス修飾子を書くべきだという主張。
class Cat def meow puts "Meow!" end private def bowwow puts "Bowwow!" end private def cock_a_doodle_doo puts "cock-a-doodle-doo" end end
以下はこのスタイルを強制することができるCopを追加するプルリクエスト。RuboCop v0.57.0 以上を使っているならば、EnforcedStyle
をinline
にすれば、すぐに試すことができる(ただ、v0.57.2 時点でいくつかの問題が報告されているので、有効にして運用するのはちょっとオススメしない...)
インラインでアクセス修飾子を書くと何が嬉しいのか
前者で書いてきた人間からすれば、突然、メソッド宣言ごとにアクセス修飾子を書いてくれ、と言われると納得できないと思う。でも、もちろんこれには理由があって、前者の書き方(以下、グループスタイルと呼ぶ)はメソッドの可視性が暗黙的に決められており、その罠に気づかない人が多いという。
例えば、以下のCat.bowbow
はプライベートにならない(これは有名かもしれない)
class Cat def meow puts "Meow!" end private def self.bowwow puts "Bowwow!" end end
[2] pry(main)> Cat.bowwow Bowwow! => nil
クラスメソッドをprivateにするためにはprivate_class_method
でメソッド名を指定する、またはメソッド宣言の頭に明示的にprivate_class_method
を書かなくてはいけない。
class Cat def meow puts "Meow!" end def self.bowwow puts "Bowwow!" end private_class_method :bowwow end
[4] pry(main)> Cat.bowwow NoMethodError: private method `bowwow' called for Cat:Class from (pry):23:in `__pry__'
これはRuboCopでチェックする方法もあって、Lint/IneffectiveAccessModifier
Copというやつでチェックできて便利。
では、以下はどうだろうか。
require 'forwardable' class Cat def meow puts "Meow!" end end class CatCage extend Forwardable def initialize(cat) @cat = cat end private def_delegator :@cat, :meow end
def_delegator
はオブジェクトへ処理を委譲するためのメソッド。これによって、CatCage#meow
が定義される。では、このメソッドはprivate
以下にあるので、これはprivateなメソッドになるだろうか?答えはならない。
[8] pry(main)> CatCage.new(Cat.new).meow Meow! => nil
Rubyにちょっと詳しい人ならば、def_delegator
の実装を考えれば、これがprivateにならないことはすぐに気づくと思うが、そうでない人がこの問題に気づくのはとても難しいと思う(最初見たときバグかと思った)
これは以下のような短い実装でも再現できる。つまり、自分で作ったメソッドや、ライブラリが提供している便利メソッドが、実はグループスタイルのprivate
を受け付けていないかもしれないということである。こうやってベタ書きされているのを見ると、いやmodule_eval
なんてしてるんだから当然でしょ...という気分になるが...
class Cat private self.module_eval(&proc { def meow; puts "Meow!" end }) end
[6] pry(main)> Cat.new.meow Meow! => nil
ちなみに、これはどうすればprivateにできるのかといえば、明示的にメソッド名をシンボルとしてprivate
に渡せば良い。
require 'forwardable' class Cat def meow puts "Meow!" end end class CatCage extend Forwardable def initialize(cat) @cat = cat end def_delegator :@cat, :meow private :meow end
[14] pry(main)> CatCage.new(Cat.new).meow NoMethodError: private method `meow' called for #<CatCage:0x007fa7751a4c48 @cat=#<Cat:0x007fa7751a4c70>> from (pry):27:in `__pry__'
つまり、このようなスタイルを常に強制すれば、グループスタイルのprivate
の罠にハマることは無いよね、という話。
どうするべきなのか
これはとても理にかなった提案だと思うし、確かに価値はあると思う。でも、今まで書いてきたグループスタイルのprivate
を全部虱潰しに直していくのは結構しんどいし、残念ながら、このスタイルは今現在マイノリティなので、これを強制するのも大変だと思う。みんなで話し合って、同意が取れるなら、それで統一するのは全然良いと思う。
でも、これとはまた違った話で、そもそもprivate
はアクセスコントロールとして設計されたものではないので、そのように使うべきではない。という意見(そもそもprivate
不要論)もあって、これはこれで面白い。
「Rubyのpublic/private/protectedはaccess controlとしては壊れている」と言われたが、その答えは「確かに。access controlとして作ってないからな」というものであった。
— Yukihiro Matsumoto (@yukihiro_matz) November 15, 2017
より良い書き方を追求していくことは意味があると思うが、そのために対立して空気が悪くなるのは本末転倒なので、まぁほどほどにみんなが納得できる書き方を見つけていけばいいと思う... 僕はたぶん気分でグループスタイルのprivate
を書いたり、書かなかったりすると思います。