sometimes I laugh

高専卒WEBエンジニアがいろいろ残しておくブログ

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 以上を使っているならば、EnforcedStyleinlineにすれば、すぐに試すことができる(ただ、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不要論)もあって、これはこれで面白い。

より良い書き方を追求していくことは意味があると思うが、そのために対立して空気が悪くなるのは本末転倒なので、まぁほどほどにみんなが納得できる書き方を見つけていけばいいと思う... 僕はたぶん気分でグループスタイルのprivateを書いたり、書かなかったりすると思います。