Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rubyスタイルガイドを読む: クラスとモジュール(3)クラスメソッド、スコープ、エイリアスなど

こんにちは、hachi8833です。Rubyスタイルガイドを読むシリーズ、今回は「クラスとモジュール編」その3をお送りします。

クラスとモジュール(3)メソッドのスコープやエイリアスなど

5-16【統一】クラス変数(@@で始まる変数)はできるだけ避ける

Avoid the usage of class (@@) variables due to their "nasty" behavior in inheritance.

クラス変数は、以下のように継承で予想外の動作を引き起こすことがあります。

class Parent
  @@class_var = 'parent'

  def self.print_class_var
    puts @@class_var
  end
end

class Child < Parent
  @@class_var = 'child'
end

Parent.print_class_var # => 'parent'ではなく'child'が出力されてしまう

As you can see all the classes in a class hierarchy actually share one class variable. Class instance variables should usually be preferred over class variables.

上のParentクラスとChildクラスが同じクラス変数@@class_varを共有していますが、場合によっては好ましくない動作を引き起こします。インスタンス変数(@で始まる変数)を初期化すればできることはクラス変数でやらないようにしましょう。

5-17【統一】メソッドの公開範囲を用途に応じて適切に設定する

Assign proper visibility levels to methods (private, protected) in accordance with their intended usage. Don't go off leaving everything public (which is the default). After all we're coding in Ruby now, not in Python.

全メソッドをデフォルトのpublicのままにせず、必要なものはprivateprotectedを使って公開範囲を適切に絞り込みます。

このあたりはPythonのコーディングスタイルとは異なるようです。

Python においては、すべては、真の意味でプライベートではない; Pythonの内部的には、プライベート・メソッドやアトリビュートの名前はこっそりとバラバラにされてしまうため、もともと与えられたそれらの名前のみではアクセスができないかのように見えるのである。 MP3FileInfoクラスの__parse メソッドには、__MP3FileInfo__parse という名前によって、はじめてアクセスが可能である。 これが大変興味深いことは認めよう。しかし、このような方法でのアクセスは、実際のコードの中では決して行わない事を約束してほしい。 プライベート・メソッドは、その命名規則にもとづいて、プライベートになっている。 しかし、Python における他の多くのことと同じように、それがプライベートであることは単なる慣習上の決め事であり、強制ではないのである。 
5.9. プライベート関数 (Private Functions)より

Rubyのprivateメソッドは、そのクラス自身からの呼び出しのみが想定されます。privateメソッドはレシーバを指定できないので、関数形式でしか呼び出せません。このメカニズムを使って、privateメソッドはそれが属するクラス自身からしか呼べないようになっています。

Object#sendを使うと無理やり外部から呼び出すことも一応できますが、乱用は避けたいところです。「望ましくない使い方は、わざと使いにくくする」というRuby言語設計者のポリシーを感じてしまいました。

追伸: privateメソッドはサブクラスのインスタンスメソッドから呼べる

Webチームのkazzさん(Java出身)が試してみたところ、スーパークラスのprivateなメソッドはサブクラスのインスタンスメソッドから呼び出せました

以下は私もRuby 2.4でダブルチェックしましたが、1.9でも動作は同じでした。

class Parent1
  private

  def priv1
    "I'm private"
  end
end

class Child1 < Parent1
  def call_priv1
    priv1
  end
end

Child1.new.call_priv1 #=> "I'm private"

もちろん、クラスのインスタンスをレシーバとしてprivateメソッドを呼ぶと普通にエラーになります。

class Parent1
  private

  def priv1
    "I'm private"
  end
end

Parent1.new.call_priv1 #=> NoMethodError

当初この「サブクラスのインスタンスメソッドからスーパークラスのprivateメソッドを呼び出せる」挙動について悩んでしまいましたが、morimorihogeさんが「これはまったく仕様どおりです」と「[Ruby] privateメソッドの本質とそれを理解するメリット」という記事を教えてくれました。

同記事を元に次のように理解しました。

  • privateのついたメソッドを呼び出す時は、レシーバは指定できないようになっている(Rubyではそれ以外の制限をかけていない
  • スーパークラスのprivateメソッドはサブクラスにもprivateメソッドとして継承される
  • したがって、継承したprivateメソッドはサブクラスのインスタンスメソッド内で呼び出せる

以下はネットで見つけたコード例を若干変更したものですが、こんなふうにスーパークラスのprivateメソッドをサブクラスでpublicに変更するといった利用法も考えられます。もし仮にスーパークラスのprivateメソッドはサブクラスに継承されないとしたら、このようなサブクラスでのメソッドスコープは変更できないことになりますね。

class Parent
  private

  def some_method
    "private in Parent"
  end
end

class Child < Parent
  public :some_method
end

Child.new.some_method #=> "private in Parent"

5-18【統一】privateprotectedは対象となるメソッド定義とインデントを揃える

Indent the public, protected, and private methods as much as the method definitions they apply to. Leave one blank line above the visibility modifier and one blank line below in order to emphasize that it applies to all methods below it.

インデントを揃えて、privateprotectedの対象が明確になるようにします。
さらにprivateprotectedの前後には空行を置いて読みやすくします。

class SomeClass
  def public_method
    # (本文は略)
  end

  private

  def private_method
    # (本文は略)
  end

  def another_private_method
    # (本文は略)
  end
end

追伸: Rubyのprotectedメソッド

Rubyのprotectedはわかりにくいので有名です。Ruby 2.4リファレンスマニュアルより引用します。

public に設定されたメソッドは制限なしに呼び出せます。
private に設定されたメソッドは関数形式でしか呼び出せません。
protected に設定されたメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せます。
Ruby 2.4リファレンスマニュアルより

私の目から見て、上述のprivateprotectedの説明は「メカニズムの一部」を説明しているだけで、「目的」「使い方」について説明がない点に戸惑いを感じてしまいました。

この点については上述の「Ruby の private と protected 。歴史と使い分け」で非常に詳しく説明されていますが、特にJavaやC++からRubyにやってきた開発者がprotectedの定義の違いに戸惑うことが多いようです。

5-19【統一】クラスメソッドはdef self.methodで定義する

Use def self.method to define class methods. This makes the code easier to refactor since the class name is not repeated.

クラス名を繰り返し書かなくてよくなるので、リファクタリングしやすくなります。

class TestClass
  # 不可
  def TestClass.some_method
    # (本文は略)
  end

  # 良好
  def self.some_other_method
    # (本文は略)
  end

  # 以下の書き方もOK: クラスメソッドを多数定義する場合向け
  class << self
    def first_method
      # (本文は略)
    end

    def second_method_etc
      # (本文は略)
    end
  end
end

5-20【統一】メソッドをレキシカルクラススコープでエイリアスする場合はaliasを使うのが望ましい

Prefer alias when aliasing methods in lexical class scope as the resolution of self in this context is also lexical, and it communicates clearly to the user that the indirection of your alias will not be altered at runtime or by any subclass unless made explicit.

#aliasによるメソッドのエイリアスはレキシカル(≒静的)なクラススコープのコンテキストで作成されるので、selfもレキシカルに解決されます。また、そのエイリアスが実行時に変更されず、サブクラスによっても変更されない(サブクラスで明示的に変更する場合を除く)ことがAPI使用者に明確に伝わります。

class Westerner
  def first_name
    @names.first
  end

  alias given_name first_name
end

Since alias, like def, is a keyword, prefer bareword arguments over symbols or strings. In other words, do alias foo bar, not alias :foo :bar.
Also be aware of how Ruby handles aliases and inheritance: an alias references the method that was resolved at the time the alias was defined; it is not dispatched dynamically.

Rubyのaliasdefなどと同様キーワードなので、メソッド名やエイリアス名は文字列やシンボルではなく、むき出しで記述するようにします。

また、Rubyでエイリアスや継承がどのように扱われているかについても知っておきましょう。Rubyの#aliasによるエイリアス参照は、エイリアスの定義時に解決されます(動的には解決されません)。

class Fugitive < Westerner
  def first_name
    'Nobody'
  end
end

In this example, Fugitive#given_name would still call the original Westerner#first_name method, not Fugitive#first_name. To override the behavior of Fugitive#given_name as well, you'd have to redefine it in the derived class.

上述の例のFugitive#given_name呼び出しは、実際にはFugitive#first_nameではなく元のWesterner#first_nameを呼び出します。この動作をオーバーライドするには、以下のように派生クラスでaliasを使って再定義する必要があります。

class Fugitive < Westerner
  def first_name
    'Nobody'
  end

  alias given_name first_name # aliasしないと`Westerner#first_name`が呼ばれる
end

5-21【統一】モジュールやクラスやシングルトンクラスのメソッドを実行時にエイリアスする場合は常にalias_methodを使う

Always use alias_method when aliasing methods of modules, classes, or singleton classes at runtime, as the lexical scope of alias leads to unpredictability in these cases.

前述のとおり、aliasはレキシカルなので定義時に参照が解決されます。以下のような動的なエイリアス作成にaliasを使っても期待どおりに動作しません。

module Mononymous
  def self.included(other)
    other.class_eval { alias_method :full_name, :given_name }
  end
end

class Sting < Westerner
  include Mononymous
end

5-22【統一】クラスやモジュールの中で同じクラスやモジュール内の他のメソッドを呼ぶ場合は、呼び出しのself.を省略する

When class (or module) methods call other such methods, omit the use of a leading self or own name followed by a . when calling other such methods. This is often seen in "service classes" or other similar concepts where a class is treated as though it were a function. This convention tends to reduce repetitive boilerplate in such classes.

self.*メソッド名.*を省略する記法は、いわゆる「サービスクラス」のようにクラスを関数のように扱う場合によく見られます。この省略記法は、そうしたクラス内での冗長な呼び出しを削減するのに役に立ちます。

class TestClass
  # 不可 -- クラス名を変えるかメソッドを移動する方がよい
  def self.call(param1, param2)
    TestClass.new(param1).call(param2)
  end

  # 不可 -- 冗長
  def self.call(param1, param2)
    self.new(param1).call(param2)
  end

  # 良好
  def self.call(param1, param2)
    new(param1).call(param2)
  end

  # ...(他のメソッド定義)...
end

今回は以上です。スタイルガイドを読むシリーズ、次回は「例外処理」編をお送りします。ご期待ください。

関連記事



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。