UDAUDA.BLOG

うだうだしながらIT関連の記事を書いていきます!

Effective Rubyのまとめ

業務でRubyを使用しているのですが、なんとなくで理解している部分が多いと気づいたのでRubyへの理解を深くしたいと思い、Effective Rubyを読みました!

 第1章Rubyに身体をならす!

項目1 Rubyは何を真と考えているかを正確に理解しよう

  • Rubyではfalseとnilを除く全ての値が真
  • 多く言語と異なり、Rubyでは数値ゼロは真
  • falseとnilは区別しなければいけない時はnil?メソッドを使うか、falseを左被演算子とする"=="演算子を使う
irb> class Bad
           def  == (other)
               true
           end
irb> false == Bad.new
---> false
irb> Bad.new == false
---> true

オブジェクトを扱う時にはnilかもしれないということを忘れないようにしよう

  • Rubyの型システムの構造上、全てのオブジェクトがnilになる可能性がある
  • nil?メソッドは、レシーバーがnilならtrue、そうでなければfalseを返す        

Rubyの暗号めいたPerl風機能は避けよう

定数がミュータブルなこと事に注意

  • 定数を定義したと.freezeをしてあげないと値を代入した時に定数が変更されてしまう
  • 配列と、配列の中身までfreezeしたい時は↓のようにする
module Animals
   Insects = [
       "kabutomusi",
       "kuwagata",
       "gokiburi"
   ].map!(&:freeze).freeze
end
  • 定数モジュールを作り、その中に定数を定義する。その後、モジュール自体をfreezeすればまとめて定数化できる!

実行時の警告に注意しよう

第2章クラス、オブジェクト、モジュール

Rubyが継承階層をどのように組み立てるかを頭に入れよう

  • オブジェクトは変数の入れ物。各オブジェクトは特定のある1つのクラスとオブジェクトを結びつける特殊な内部変数を持っている
  • クラスは、メソッドと定数の入れ物。
  • スーパークラスはクラス階層内の親クラスのこと
  • モジュールはある1点を除いてはクラスと全く同じ。Ruby内部ではモジュールとクラスは同じデータ構造を使って実装されているが、クラスメソッドによってそのデータ構造をどのように操作できるか制限されている(newメソッドがない)
  • 特異クラスは継承階層に含まれている名前のない不可視のクラスを指すわかりにくい用語。クラスメソッドやモジュールから取り込んだメソッドを格納する場所を提供する。
  • レシーバはメソッドが呼び出されるオブジェクトのこと
customer.name
  ↑レシーバ
  • Rubyはクラス階層をサーチするだけでメソッドを見つけられる。探しているメソッドは見つからない時には、method_missingを探して再びサーチを開始する
  • モジュールをインクルードすると、暗黙のうちに特異クラスが作られ、その特異クラスはクラス階層のインクルードしたクラスの上に挿入される
  • 特異メソッドは同じようにクラス階層に挿入される特異クラスに格納される

superの振る舞いが一通りではないことに注意

  • superは継承先のメソッドを上書きする時に、継承先のメソッドを呼びたい時に使うメソッド
class SuperSilliness < SillyBase
    def m1(x, y)
        super(1, 2)  # 1と2を渡して呼び出し
        super(x, y)  # xとyを渡して呼び出し
        super x, y    # 上と同じ
        super           # 上と同じ
        super           # 引数なしで呼び出し
    end
end

サブクラスを初期化するときにはsuperを呼び出そう

  • Rubyはサブクラスのオブジェクトを作る時にはスーパークラスのinitializeメソッドを自動的に呼び出したりしない。
  • 明示的に継承を使うクラスでinitializeメソッドを書く時にはsuperを使って親クラスを初期化する必要がある

Rubyの最悪の紛らわしい構文に注意しよう

構造化データの表現にはHashではなくStructを使おう

  • 新しいクラスを作るほどでもない構造化データを扱うときにはHashではなくStructを使おう
  • Struct::newの戻り値を定数に代入し、その定数をクラスのように扱おう

モジュールにコードをネストして名前空間を作ろう

  • モジュール内に定義をネストして名前空間を作ろう
  • 名前空間の構造は、ディレクトリ構造と同じにしよう
  • トップレベル定数を修飾なしで使うと曖昧になるときには "::"を使ってフルに修飾しよう

protectedメソッドを使ってプライベートな状態を共有しよう

  • privateメソッドはレシーバーに対して呼び出せない。つまり関数の形だけでしか呼び出せない
  • ちなみに継承先のprivateメソッド、protectedメソッドも呼び出せる
  • protectedメソッドはクラス内のインスタンスに対して呼び出せる
  • レシーバを明示してprotectedメソッドを呼び出せるのは、同じクラスのオブジェクトか共通のスーパークラスからprotectedメソッドを継承しているオブジェクトだけ

クラス変数よりもクラスインスタンス変数を使うようにしよう

  • クラス変数はそのクラス、サブクラス全てに共有される。つまり、サブクラスが2つあるとして同じクラス変数を2つ持っている場合共有している状態になり、どちらかのクラス変数に変更があった場合、もう一方のクラス変数も影響を受ける事になる
  • 基本的にはクラス変数よりもクラスインスタンス変数を使うべき

第3章 コレクション

 コレクションを書き換える前に引数として渡すコレクションのコピーを作っておこう

  • Rubyのコレクションクラス(ArrayやHash)は一覧のオブジェクトを格納しているが、実際の値ではなく参照という形でやり取りされる
  • オブジェクトがメソッドに引数として渡される時にも同じことが当てはまる。メソッドはオブジェクトの新しいコピーではなく、オブジェクトの参照を受け取る
  • オブジェクトに変更を加えるとそのオブジェクトの参照を持っているすべてのコードから変更結果が見える。つまり、配列の要素を書き換えると配列とは別の変数で参照しているかもしれないオリジナルのオブジェクトにも影響が及ぶ
  • 初期化時にコレクションを書き換える必要はないが、コレクションをいじりたい時はコレクションのコピーを使用する
  • rubyのコピーするメソッドはdup, cloneの2つがあるが、dupを使用した方がいじるためには良い
    • cloneはフリーズ状態や特異メソッドを引き継ぐ
    • dupはフリーズ状態や特異メソッドを引き継がない
  • clone, dupは作成するコピーはシャローコピーであり、要素はコピーできない。つまりコピーの要素を変更するとコピー元にも影響が出る
  • オブジェクトをコピーしたいときはMarshalを使えば必要な時にディープコピーを作れる

    nilスカラーオブジェクトを配列に変換するにはArrayメソッドを使おう

  • 下のクラスはtoppingsに配列を期待しているため、toppingに変え全ての引数を1つの配列にまとめてしまって可変長引数リストにしてしまいたくなるかもしれない。これにはinitializeに渡す時に""を使って展開しない限り、直接渡せなくなる
class Pizza
  def initialize(toppings)
     toppings.each do |topping|
        add_and_price_topping(topping)
   end
  end
・・・
end  
  • Arrayメソッドを使って配列化していることを明確にしたほうが良い
  • to_ary, to_aに応答する場合にはそのメソッドが呼び出される。どちらのメソッドも呼び出さない場合は、Arrayは新しい配列で包む
  • ArrayメソッドにHashを渡してはならない。Hashは一連のネストされた配列に変換されてしまう

要素が含まれているかどうかの処理を効率よく行うために集合を使うことを検討しよう

  • 要素が含まれているかの高速チェックはSetを使うことを検討しよう
  • Setを挿入されるオブジェクトはハッシュキーとしても使えなければならない
  • Setを使う前に、require('set')を実行しよう

reduceを使ってコレクションをたたみ込む方法を身につけよう

  • reduceメソッドを使用することでより簡潔に書けるようになる
users.select { |u| u.age >= 21 }.map(&:name)

users.reduce([]) do |names, user|
  names << user.name if user.age >= 21
  names
end
  • アキュムレータの初期値は必ず使おう
  • reduceのブロックは必ずアキュムレータを返すようにする。現在のアキュムレータを書き換えるのは間違いないが、それをブロックから返すのを忘れないようにする ※アキュムレータとは結果が入れられるコレクション 上の例ではnames

ハッシュのデフォルト値を利用することを検討しよう

  • Hashのデフォルト値を使うことを検討しよう
  • ハッシュがキーを含んでいるかどうかをチェックする時には、has_key?またはその別名を使おう。つまり、存在しないキーにアクセスしたらnilを返されることを前提としてコードを書いてはならない
  • 無効なキーを渡すとnilを返すことを前提としているコードにハッシュを渡さなければならない時にはデフォルト値を使ってはならない
  • デフォルト値を使うよりもHash#fetchを使った方が安全な場合がある

コレクションクラスからの継承よりも委譲を使うようにしよう

  • コレクションクラスからの継承よりも委譲を使うようにしよう
  • 委譲のターゲットのコピーを作るinitialize_copyメソッドを忘れずに書こう
  • 委譲ターゲットに対応するメッセージを送ってからsuperを呼び出すという形でfreeze, taint, untaintメソッドを書こう

第4章 例外

raiseにはただの文字列ではなくカスタム例外を渡そう

  • Rubyではエラーを生じさせるときはエラーの詳細がわかるようにクラスを指定してあげるべき
  • 新しい例外クラスは簡単に以下の3つの規則に従うだけで簡単に作成できる
    • 新しい例外クラスは標準例外クラスのどれかを継承しなければならない。ほかのものをraiseに渡そうとするとTypeErrorが起きる
    • 標準例外クラスはExceptionをルートとする回想構造を形成している。しかし、Exceptionとそのサブクラスのいくつかは一般にプログラムをクラッシュさせる低水準エラーと考えられる。標準例外クラスの大多数はStandErrorを継承している
  • 技術的に必要とされているわけではないが、例外クラス名は、Errorで終わるようにするのが一般的なやり方
  • 例外としてraiseに文字列を渡すのは避けよう。この場合汎用のRuntimeErrorオブジェクトが使われる。そうではなく、カスタム例外クラスを作ろう
  • カスタム例外クラスはStandErrorを継承し、クラス名がErrorで終わるようにしよう
  • 1つのプロジェクトのために複数の例外クラスを作る時には、まずStandErrorを継承する基底クラスを作り、他の例外クラスはそのカスタム基底クラスを継承するように構成しよう
  • initializeでエラーメッセージを設定する時には、raiseでエラーメッセージを設定するとinitializeのメッセージが上書きされてしまうことに注意

できる限り最も対象の狭い例外を処理するようにしよう

  • 修復方法がわかっている特定の例外だけをrescueで捕まえよう
  • 例外を捕まえる時には、最も限定されたタイプのものを最初に処理しよう。例外の階層構造の上位にあるものほど、rescue節は下流に作る
  • StandErrorのような汎用例外クラスをrescue節で捕まえるのは避けよう
  • rescue節で例外を生成すると、新しい例外が現在の例外を押し退け、現在のスコープを抜けて例外処理を最初からやり直す

リソースはブロックとensureで管理しよう

  • 確保したリソースを開放するためにはensure節を書こう -リソース管理を抽象化するために、クラスメソッドでblockとensureパターンを使おう
  • ensure節で変数を使う時には、その前に変数が初期化されているかどうかを確かめよう

ensure節は最後まで実行して抜けるように作ろう

  • ensure節のなかでreturn文を明示的に使うのは避けなければならない
  • ensureの中でthrowを使ってはならない
  • 反復処理では、ensure節の中でnextやbreakを使ってはならない。たいていの場合は逆にbeginの中に反復処理を配置する方が正しいものだ
  • より一般的にensure節の中で制御フローを変更してはならない。制御フローの変更はrescue節で行うべき

スコープから飛び出したい時にはraiseではなくthrowを使おう

  • 複雑な制御フローが必要な時には、raiseではなく、throwを使うようにする。throwを使うと、ボーナスとしてスタックの上位にオブジェクトを送ることができる。catchの戻り値はそのオブジェクトになる。
  • できる限り単純な制御構造を使う。 catchとthrowの組み合わせは単純にreturnでスコープから抜け出すメソッドとそれに対する呼び出しで置き換えられることが多い。

メタプログラミング

メタプログラミングとはクラス、モジュール、個々のオブジェクトの振る舞いを変更すること。「メタプログラミング」はRubyの最も強力な機能の1つであり、最も危険な機能

モジュール、クラスフックを使いこなそう

  • 全てのフックメソッドは特異メソッドとして定義しなければならない
  • メソッドが追加、削除、定義解除されるときに呼び出されるフックはメソッド名しか受け取らず、変更が行われるクラスは与えられない。クラス名が知りたい場合にはselfの値を使う
  • singleton_method_addedを定義すると、自分自身の呼び出しが発生する
  • extend_object, append_features, prepend_featuresメソッドをオーバーライドしてはならない。代わりに、extended, inclued, prependedフックを使おう

クラスフックからはsuperを呼び出そう

  • クラスフックの中にはsuper呼び出しを入れよう
  • superを呼ばないとinheritaedを制御できない(他のモジュールでinheritedが使われていたら)

method_missingではなくdefine_methodを使うようにしよう

  • method_missingではなくdefine_methodを使うようにしよう
  • どうしてもmethod_missingを使わなければならない時には、respond_to_missing?を定義することを検討しよう

evalの多様な変種間の違いを把握しよう

  • instance_evalやinstance_execで定義されるメソッドは特異メソッドである
  • class_eval, module_eval, class_exec, module_execメソッドのレシーバはモジュール、クラスに限られる。これらのどれかで定義されたメソッドはインスタンスメソッドになる

モンキーパッチは代わりとなるものを検討しよう

モンキーパッチとは言語の組み込みクラスやライブラリ、その他外部ライブラリの挙動を、動的に拡張する仕組み 参考資料

techlife.cookpad.com

  • Refinementsはもう実験的機能ではなくなったが、機能の成熟とともに変化する可能性はまだ残っている
  • Refinementsはそれを使いたい個々のレキシカルスコープでアクティブ化しなければならない

参考資料

https://www.amazon.co.jp/Effective-Ruby-Peter-J-Jones/dp/4798139823