Ruby – 中級者向けの本『Effective Ruby』を読んで

はじめに

Effective Ruby – Peter J.Jones』を読んで学んだ内容を整理する。

Effective Ruby - Peter J.Jones

本書について

中上級者向けのRubyの技術書。文中では以下のように本の内容が説明されている。

本書は、基本構文や高度な実践を示す本とは異なり、クラッシュせず、メンテナンスが楽で、高速なRubyアプリケーションを書くための返事つに行われているベストプラクティスを紹介するという基本線を見事に歩き切っている。(序文)

本書で私が目標としているのは、読者をRubyの深いところまで連れて行くことだ。(イントロダクション)

最近、限られた開発時間で綺麗なコードを書くためには「ベストプラクティスを把握しておくこと」が必要だと感じており、良い本がないか探してる過程でこの本を見つけた。

目次

  • 第1章 Rubyに身体を慣らす
  • 第2章 クラス、オブジェクト、モジュール
  • 第3章 コレクション
  • 第4章 例外
  • 第5章 メタプログラミング
  • 第6章 テスティング
  • 第7章 ツールとライブラリ
  • 第8章 メモリ管理とパフォーマンス

第1章 Rubyに身体を慣らす

詳細に入る前にRubyの流儀を整理。

論理型

  • Rubyでは「false」と「nil」以外は全ての値が「真」
  • よって数値のゼロは「真」
  • 「false」と「nil」を区別するときは、nil?メソッドや “== false” を使う

nilオブジェクト

nilオブジェクトが実行中のプログラムに混じることは多々あるので、それを想定してコードを書く必要がある。例えば、コードがちゃんと通るように、必要に応じてto_s メソッドやto_iメソッドなどで、nilオブジェクトを強制変換するなど。

定数

Rubyでは先頭が大文字になっている識別子のこと。Rubyの定数は、グローバル変数に近く、ミュータブルであるため、書き換えられたくない定数については freezeメソッドを使う。

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

オブジェクトの継承階層

  • モジュールをincludeとした場合特異クラスなり、継承上は無名で現れないが、インスタンスメソッドと定数は共有する。
  • includeはLIFOで、最後にインクルードされたモジュールから探される。
  • includeした場合、クラスのメソッドをオーバーライドすることはできない(クラスの上に特異クラスとして挿入されるため)。

Superの注意点

  • 引数もかっこもなしでsuperを呼び出すと、呼び出し元のメソッドに渡されたすべての引数を渡してオーバーライドされるメソッドを呼び出すのと同じ意味になる。
  • オーバライドされるメソッドに引数を渡さずにsuperを使いたい場合には、super()のように空かっこを使わなければならない。

Rubyの「?」と「!」

  • ? メソッドが論理型の値を返すことを示すためにRubyプログラマが取り入れている命名規則
  • ! レシーバを書き換えるメソッドや有害な副作用を起こす可能性があることを警告する場合に用いられる命名規則

HashでなくStructを使う

  • 新しいクラスを作るほどでもない構造化データを扱うとき(例えば、CSVの処理など)などは、HashでなくStructを使うとよい
  • Struct::newの戻り値を定数に代入すれば、その定数をクラスのように使える(以下、本書より引用)

class AnnualWeather
  Reading = Struct.new(:date, :high, :low)

  def initialize(file_name)
    @readings = []
    
    CSV.foreach(file_name, headers: true) do |row|
      @readings << Reading.new(Date.parse(row[2]),
                               row[10].to_f,
                               row[11].to_f)
    end
  end
end

モジュールで名前空間を作る

名前空間を用いることで、定数の定義のスコープをコントロールする。

privateとprotected

  • privateメソッドは定義したオブジェクト内でのみ呼び出せる(レシーバ指定不可)。
  • protectedメソッドは定義した(もしくはprotectedメソッドを継承した)クラスのオブジェクトであればレシーバを指定して呼び出せる。

第3章 コレクション

コレクションのコピー

Rubyのメソッド引数は、値を渡しているのではなく、参照させている。したがって、引数としてコレクション(配列など)が渡された場合、書き換える前にdupやcloneでコピーを作った方が良い場合がある。

スカラ値を配列にする

Arrayメソッドはスカラーオブジェクトを配列に変換する便利なメソッド。ふるまいは以下の通り。


Array("apple")
# ["apple"]

Array(nil)
# []

Array(['apple', 'orange'])
# ["apple", "orange"]

Set

要素が含まれているか高速でチェックしたい場合は Set を使うことを検討する、

継承より委譲

is-a関係(Apple is a Fruit)の場合は「継承」、has-a関係(House has Bathroom)の場合は「委譲」が向いている。委譲では、サポートしたいメソッドのみをつまみ食いすることができる。

Forwardableモジュールを用いた場合、以下のようにdef_delegators()で、サポートするメソッドを明示する(以下、本書より引用)。


require("forwardable")

calss RaisingHigh
  extend(Forwardable)
  include(Enumerable)
  def_delegators(:@hash, :[], :[]=, :delete, :each,
                         :keys, :values, :length,
                         :empty?, :has_key?)
end

第4章 例外

この目標は、例外に関する一般的な問題を避け、例外を正しく使う方法を読者に示すことだ。

raiseにはカスタム例外渡す

  • raiseに文字列だけ渡すと、その文字列を使ってRuntimeErrorが作られる。
  • RuntimeErrorは汎用的なエラーのため、raiseを使う時は新しい例外クラスを作ることが望ましい。
  • 例外クラスを作成する時は、StandardErrorを継承させる(以下、引用)。

class CoffeeTooWeakError < StandardError; end

特定のエラーをrescueする

  • 処理すべき方法がわかっている例外だけrescueするようにする。
  • rescue節で例外を生成すると、新しい例外が現在の例外より優先されて、現在のスコープを抜けて例外処理が最初からやり直されることに注意する。

ensure

  • 現在のスコープを抜ける前に管理作業を実行したい時に使える
  • スコープの処理が成功しても実行される
  • 制御処理を実行したい場合はrescueを使う

catch & throw

ネストしたループやイテレーションの場合、breakでは一度に全てを抜けることはできない。raise(StopIteration)を使えばループを一気に抜けることができるが、catchとthrowを使った方がよりキレイにかける。(以下、引用)


match = catch(:jump) do
  @characters.each do |character|
    @colors.each do |color|
      if player.valid?(character, color)
        throw(:jump, [character, color])
      end
    end
  end
end
  • catchはブロックを中止し、throwに渡された値を返す。
  • throwに値を渡さない場合は、nilを返す。

第5章 メタプログラミング

フックメソッド

  • 全てのフックメソッドは、特異メソッドとして定義しなければならない。

define_method

  • define_methodは、メソッド名とブロックを指定すると、ブロックが指定する本体と引数を持つインスタンスメソッドを作る。

eval

  • instance_evalやinstance_execは、特異メソッドを定義する。

Refinements

モンキーパッチの代替として利用できる。モジュール内でrefineメソッドを使って定義し、使いたいクラスやモジュールでusingメソッドで有効かする(以下、引用)。


Module OnlySpace
  refine(String) do
    def only_space?
      ...
    end
end

class Person
  using(OnlySpace)
  ...
end

有効化されたレキシカルスコープの外では、自動的に非有効化される。

第6章 テスティング

この章では、これらのツールを使って効果的なテストを書く方法を説明する。

MiniTestユニットテスト

  • 単一クラスのような「ユニット」を対象としたテスト
  • テストクラスはMiniTest::Unit::TestCaseをスーパークラスとする
  • ここのテストはインスタンスメソッドとして書き、「test_」というプレフィックスをつける
  • アサーションが適切であれば、失敗した時のエラーメッセージもより適切になる
  • アサーションの詳細は、MiniTest::Assertionsモジュールのドキュメントを参照
  • オブジェクトごとに重複するテストメソッドは、ヘルパーメソッドに書く

MiniTestスペックテスト

  • ユニットテストはテスト駆動開発との結びつきが強い
  • 一方、スペックテストはビヘイビア駆動開発と結びつきが強い
  • スペックテストは、「プログラマと非プログラマの共有する形式言語で仕様を書くスタイル」と「ホストプログラミング言語の構文で仕様を書くスタイル」に二分される
  • describeメソッドを使って間接的にクラス定義するのが一般的
  • beforeメソッドは、MiniTestユニットテストのsetupメソッドと同じような役割を果たす
  • テストはitメソッドを使って定義する
  • ユニットテストかスペックテストのどちらを選ぶかは好み
  • スペックテストには、RSpecやCucumberなどのRubyGemがある

モックテスト

  • モックテストでは、必要とされる応答をするオブジェクトを組み立てる
  • HTTPクラスは、モックの候補に適任
  • モックテストの欠点は、ホワイトボックステスティング(↔︎ブラックボックステスティング)
  • この欠点に対処するためにモックライブラリはエクスペクテーションをサポートしている

効果的なテスト

  • ハッピーパステスト(↔︎例外パステスト)を防ぐ為に、ファズテストやプロパティテストといったツールが有用
  • ファズテストとはランダムなデータを大量に送り込むことでシステムがクラッシュするかどうかを確かめるテスト
  • Ruby用のファズライブラリにはFuzzBertというものがある
  • プロパティテストも同様に、ランダムな入力を送ることでシステムがクラッシュするかどうかを確かめるテスト
  • プロパティテストはテスト数が有限で、自動化されたユニットテストと並行して実行できるため、ファズテストより利用しやすい
  • ハッピーパス、例外パスの両方が成功したか確かめるために、SimpleCovRubyGemのようなカバレッジツールを利用する
  • テストは自動化する(ソースファイルが変更された時に自動でテストを実行するZenTestのようなツールもある)

第7章 ツールとライブラリ

riコマンド

Ruby Informationの略。クラス、モジュール、メソッド、gem全体のドキュメントをターミナル上で見ることができる。

ri Array
# Arrayの情報を表示

gem

  • バージョンをしていしないとBundkerはそのgemの最新バージョンを選ぶ
  • 新しいバージョンに更新されることでアプリにエラーが発生してしまうことがあるので、特定のバージョンかバージョンの範囲を指定した方がベター
  • gemのバージョンは下限だけでなく上限もしていた方が良い
  • bundle updateはgemを特定して行う

第8章 メモリ管理とパフォーマンス

この章は、まずガベージコレクタの仕組み、パフォーマンスの調整方法など、ガベージコレクタを深く理解するところから始める。それ以降の部分は、パフォーマンス問題の突き止め方とその修正のためのヒントに充てる。

ガベージゴレクタ

  • ガベージコレクタは、もう使わないオブジェクトを開放する機能
  • Rubyのカベージコレクタは、マークアンドスウィープというプロセスを使っている
  • このプロセスでは、まずコードからアクセス可能なオブジェクトにマークをつけ、マークのないオブジェクトはゴミとして捨てる
  • マークをつけるフェーズには、メジャーとマイナーの2つのモードがある
  • メジャーマークでは全てのオブジェクトを対象とし、マイナーマークでは若いオブジェクトのみが対象となる
  • ガベージコレクタは、指定しない限りマイナーマークを使う
  • スウィープフェーズも即時と遅延の2つのモードがある
  • 即時ではマークのない全てのオブジェクトを開放、遅延ではすぐに必要なメモリ分だけを開放
  • ガベージコレクタは、スロットに分割されるページを集めたヒープを維持してメモリ管理を行う

プロファイル

  • ruby-prof gemは広く使われているプロファイルライブラリ
  • Ruby 2.1以降えは、stackprofやmemory_profiler gemの利用も検討する

ループ内でのオブジェクトリテラルは避ける

  • ループ内でのオブジェクトリテラルを書き換えれない場合は、定数に昇格させる
  • Ruby 2.1以降では、フリーズした文字列リテラルは、定数と同じであり、実行中のプログラム全体で共有される

メモ化

  • コストの高い計算は"||="演算子を使ってメモ化する
  • コード全体にメモ化が散らばる場合は、プロファイリングを行い、その要否を検討する

所感

内容は中級者向け。本書のイントロに記載があった通り、Rubyへの網羅的な理解をもう一段階掘り下げるのに役立った。実務的に気をつけておくべきポイントが整理されているのでイメージがつきやすく内容も有用なものが多かった。また、課題ごとに項目が分けられているため、必要な箇所を必要な時に調べることもできると思った。

Source

Effective Ruby - Peter J.Jones

Effective Ruby - Peter J.Jones

兵庫県西宮市生まれのフリーランスRailsエンジニア。海外を拠点にデジタルノマド生活中。/ 前職・資格:公認会計士 / プログラミング言語:Ruby, JavaScript, HTML, CSS / 日本語・英語
コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です