Ruby Patterns

この記事には、Rubyを書いているときに「これは言語化されたり公式化されたりしていないけれど基本的には必ずこのパターンに則ってプログラムを書いているな」ということをふと思い出したときにやってきてそのパターンを書く。多分3パターンぐらいで終わると思う。ウケが良ければ思い出す確率が高くなると思う。題材さえあれば何も考えずに書けるので、これを書くコストは全然高くない。

名前が"?"で終わるメソッドは必ずtrueまたはfalseのどちらかを返す

trueとfalse以外(例えばnil)が返る可能性がある場合は、必ず式の先頭に!!を付けてtrueかfalseになるようにしてる。 このパターンを守ることがとても大事だという風には全く考えていないけど、もしhas_user?がuserを返すとして、has_user?という名前のメソッドがUserオブジェクトを返すというのは一体どういう意味を持っているのだろうということに対して良い回答が出来る自信がない。

class Entry
  def has_user?
    !!user
  end

  def user
    User.find_by(id: user_id)
  end
end

名前が"?"で終わるメソッド名は"形容詞(句)?"か"has_名詞(句)?"

  • has_user?
  • rejected?
  • written_in_english?

例えばRSpecの記法に合わせたときに、 a.should be_xxx や a.should have_xxx, a.should has_xxx(yyy) と自然に読めるかどうかで考えている。「自然に読める」というのを見ると「はい出ました〜はい出ました"自然に"読める〜」という風に過剰に反応してしまうこともあると思うけど、1つの処理を複数の書き方で実現できるという事実がある以上、どのような文字でそれを実現するかというのは結局読み手1人1人とのコミュニケーションの問題なので、どの文字で実現すれば読み手が自然に読めるのかという答えのない、常に答えの変わる問題に対して自分の中での最善手を考えないといけない。

引数を取ることは滅多と無い (200回に1回程度) けど、たまに上手くはまる場合がある。例えば、引数となる名詞をメソッド名が修飾する形の文法になっているパターンがたまに上手くいく。has_granted?(user) みたいな感じだけど、これはそこまで上手くハマっていないので使うかどうか怪しいところだと思う。形容詞と名詞の共起頻度が著しく高いような場合だけ使う。has_犬が歩けば?(棒に当たる) みたいな感じです。has_ では無くて 形容詞(句)?(名詞) というパターンもたまにある。これも形容詞部分と名詞部分の仲が良いときだけ使う。世界一?(可愛いよ) 的な感じです。このメソッドが読まれるときのコンテキストが明らかに世界一可愛いよに相応しい感じであれば使ってもいい。メソッド名が妥当かどうかの判断材料に、読まれるコンテキストや読み手の理解度というものが影響する。

上手くはまったときのメリットが大きいので、引数は絶対に取ってはいけないという風には考えてない。しかし detect?(user) のように 動詞?(名詞) みたいなメソッド名は完全にアンチパターンだと思っている。もう1つアンチパターンを書いておくと、is_xxx? のように先頭にis_が付いているのも避けたほうが良いと思う。何故なら、いつisを付けていつisを付けないのかということに関して良い指針が持てないので、常に付けないという方に傾けておいた方が安全だから。

特に順序に意味が無い場合はABC順に書く

特に何もルールが無いような場合、アルファベット順というのは非常に合意が取りやすく、また機械的な判定も行いやすい。 要素が1行ずつ書かれている場合などには、編集しやすいという利便性もある。 コードを書くとき「このコードは機械にとって書きやすいだろうか」ということを判断材料にすることが多い。

  • 例えば、メソッドの定義順序。
  • 例えば、Hashリテラルのキー。
  • 例えば、Gemfile。

Overrideしてほしいメソッドの実装にNotImplementedErrorを利用する

このメソッドは必ず子クラスにおいて実装して欲しいという意図を示すために、NotImplementedErrorをraiseするメソッドを書く。 コードは少し冗長性になるが、例外時のメッセージは毎度書いている。 (NotImplementedErrorのデフォルトメッセージこうなってほしい) 表題とは関係無いけど、親クラスと子クラスの2つを実装する場合、クラス名の付け方は下記のコードのようにすることが多い。 複数形のmodule名を名前空間として、抽象クラスの名前をBaseとする。 具象クラスの名前はFooCachersではなくFooとする。 名前空間名でクラスの責任を表現させ、具象クラスの名前でその責任の中での種類を表現させることが多い。

module Cachers
  class Base
    def initialize(env)
      @env = env
    end

    def call
      if cached?
        store.fetch(key, options) { yield }
      else
        yield
      end
    end

    private

    def cached?
      raise NotImplementedError, "You must implement #{self.class}##{__method__}"
    end

    def key
      raise NotImplementedError, "You must implement #{self.class}##{__method__}"
    end

    def store
      raise NotImplementedError, "You must implement #{self.class}##{__method__}"
    end
  end
end