タイトル
Rubyベストプラクティス -プロフェッショナルによるコードとテクニック
著者
Gregory Brown (著), 高橋 征義 (監訳), 笹井 崇司 (翻訳)
出版社
オライリージャパン
Amazonで購入する

本書は、Ruby プログラミングの中級者向け指南書のようなものです。様々なコーディングテクニック(例えば、順序付き引数の使いどころ、ブロックの使い方、メソッド名のつけ方、メタプログラミング、関数型プログラミングテクニック、プロジェクトでRubyを使う際の慣習などなど)が満載です。

さすがオライリーといえる深い内容になっていて、Ruby を始めたばかりの人よりは、一通り Ruby でプログラミングをしたことがある人、Ruby を使いこなせているのか不安な人が読むと良いと思います。

今すぐ使えるテクニック!とはちょっと違うかもしれませんが、Ruby の動的な振る舞いや柔軟な拡張性を理解してさらに Ruby を使いこなすための一冊になると思います。

なお、Ruby1.9に対応していますので、サンプルコードは Ruby1.9で動きますし、最新 Ruby ではどうするの?を一発で解決できるようになっています。

目次

  • 1章 テストでコードを駆動する
  • 2章 美しい API を設計する
  • 3章 動的な機能を使いこなす
  • 4章 テキスト処理とファイル管理
  • 5章 関数型プログラミングのテクニック
  • 6章 うまういかないとき
  • 7章 文化の壁を取り払う
  • 8章 上手なプロジェクトメンテナンス
  • 付録A 後方互換性のあるコードを書く
  • 付録B Ruby の標準ライブラリを活用する
  • 付録C Ruby ワーストプラクティス

2章 美しい API を設計する - 覚書

メソッドの引数にデフォルト値を持つパラメータが複数ある場合は擬似キーワード引数を使う

デフォルト値をもつパラメータが複数ある場合は、Ruby の「メソッドの引数の末尾に要素がひとつ以上のハッシュを渡す際は中括弧({,})を省略できる」という仕様を利用して、擬似キーワード引数が使えます。

1
2
3
4
5
6
7
8
9
def hello(name, options = {})
  options = { nickname: "hamasyou", age: 28 }.merge(options)
  p "Hello #{name}! " + options.to_s
end

hello("Syougo")
# => "Hello Syougo! {:nickname=>"hamasyou", :age=>28}"
hello("Syougo", age: 27)
# => "Hello Syougo! {:nickname=>"hamasyou", :age=>27}"

インターフェースをシンプルにするためのブロック

Rails の Configuration に使われているオブジェクトショートカットのことです。次のようなコードを

1
2
3
4
5
6
7
server = Server.new

server.handle(/hello/i) { "Hello from server at #{Time.now}" }
server.handle(/goodby/i) { "Goodby from server at #{Time.now}" }
server.handle(/name is (\w+)/) {|m| "Nice to meet you #{m[1]}!" }

server.run

次のように書けるようにします。

1
2
3
4
5
Server.run do
  handle(/hello/i) { "Hello from server at #{Time.now}" }
  handle(/goodby/i) { "Goodby from server at #{Time.now}" }
  handle(/name is (\w+)/) {|m| "Nice to meet you #{m[1]}!" }
end

これを実現するには、次のようなコードになります。

1
2
3
4
5
6
7
class Server
  def self.run(port, &block)
    server = Server.new(port)
    server.instance_eval(&block)
    server.run
  end
end

インスタンス化したオブジェクトの instance_eval() メソッドにブロックを渡すことで、ブロックをそのインスタンスのコンテキストで実行しています。

この方法を使うと、ブロックはインスタンス化したオブジェクトのコンテキストで実行されるため、ブロックのスコープ内で定義されたローカル変数にしかアクセスできません。つまり、次のコードは動きません

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass
  def nickname
    "hamasyou"
  end

  def my_method
    Server.run do
      p "Hello #{nickname}"
    end
  end
end

MyClass.new.my_method
# => NameError: undefined local variable or method `nickname' for #<Server:0x000001020478a8>

この問題を解決するには、ブロックをインスタンスのコンテキストで評価するのではなく、クロージャとして実行すればよいです。

1
2
3
4
5
6
7
class Server
  def self.run(&block)
    server = Server.new
    block.arity < 1 ? server.instance_eval(&block) : block.call(server)
    server.run
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass
  def nickname
    "hamasyou"
  end

  def my_method
    Server.run do |server|
      p "Hello #{nickname}"
    end
  end
end

MyClass.new.my_method
# => "Hello hamasyou"

block.arity を使ってコードブロックに引数がいくつあるかを調べて、引数がひとつ以上あればブロックをクロージャとして呼び出すようにしています。 

method? と method! の意味

method? 疑問符

method? のようにメソッド名の末尾に疑問符(?)をつけるのは目的は、オブジェクトに何かを問い合わせることになります。条件分岐などにメソッドを利用する際に使えます。

疑問符をつけたメソッドの戻り値は、truefalse or nil を返すようにします。

method! 感嘆符

method! のようにメソッド名の末尾に感嘆符(!)をつける目的は、このメソッドは特別だ、「注意しろ!」になります。

よくある誤解は、受け取ったオブジェクトを変更することを知らせたいときに感嘆符を使う、というものだ。たいていの場合、感嘆符は私たちに何か警告をするものだからだろう。

method?とmethod!が何を意味しているか理解しよう - 本書 P.57

Ruby の組み込みクラスのメソッドには破壊的メソッドでも感嘆符がついているのとついていないものがあります。

これはすなわち、メソッドに感嘆符をつける目的はこのメソッドが特別であることを知らせるのであって、破壊的であるとか危険であることを知らせるのではないということです。

したがって、同じようなことをする foo() メソッドがないのに foo!() メソッドだけがあるのは、あまり意味のないことだ。(中略)感嘆符は必ずしもそのメソッドが破壊的な操作をすることを意味するわけではないと考えると、…

本書 P.59

2章のポイント

引数

  • options ハッシュによる擬似キーワード引数が使えないか検討する
  • 順序付き引数と options ハッシュを組み合わせて使うときは、配列 splat 演算子(*)は使わない
  • 必須パラメータは、options ハッシュには入れないこと。必須パラメータは順序付き引数として扱う

ブロック

  • 前処理後処理の間に、ブロックを yield するようなヘルパメソッドを検討する
  • &block と instance_eval() を組み合わせると任意のオブジェクトのコンテキストでブロックを実行できる
  • yield と block.call の戻り値は、与えられた戻り値と同じにする

3章 動的な機能を使いこなす - 覚書

define_method() を使って動的にインスタンスメソッドを定義する

メソッドを定義するというのは、クラスのインスタンスメソッドを定義するということなので、動的にインスタンスメソッドを定義するにはクラスのスコープで define_method() を呼び出します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass
  def self.define(method_name, &block)
    define_method(method_name, &block)
  end
end

obj = MyClass.new
obj.hello
# => NoMethodError: undefined method `hello'

MyClass.define(:hello) do
  "Hello World"
end

obj.hello
# => "Hello World"

define_method() を使って動的にクラスメソッドを定義する

クラスメソッドを定義するには、クラスの特異クラスにメソッドを定義する必要があります。特異クラスをオープンするには、class << obj 構文を使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass
  def self.define(method_name, &block)
    obj = class << self; self; end
    obj.send(:define_method, method_name, &block)
  end
end

MyClass.define(:hello) do
  "Hello World"
end

MyClass.hello
# => "Hello World"

define_method() は private メソッド

define_method() は特異クラス上でプライベートになっているため、レシーバを指定して呼び出すには send() メソッドを使う必要があります。

モジュールのメソッドをモジュールのクラスメソッドにする

extend self

extend self を使うと、自信のインスタンスメソッドを特異クラスに定義することになりクラスメソッド化することができます。

1
2
3
4
5
6
7
8
9
10
module MyModule
  extend self

  def hello
    "Hello World"
  end
end

MyModule.hello
# => "Hello World"

3章のポイント

  • Ruby ではすべてのクラスがオープン。振る舞いを実行時に変更することができる
  • オブジェクト毎の振る舞いは、class << obj 構文を使ってオブジェクトの特異クラスにアクセスすることで実装できる
  • 拡張するときはできるだけオブジェクトごとの振る舞いを拡張するほうがよい。obj.extend() を使うようにする
  • クラスもモジュールも動的につくることができる。メソッドを定義するためのブロックを受け付けるようにする
  • モジュールをクラスに混ぜるとき、include を使うとインスタンスレベルで利用可能になり、extend を使うとクラスレベルで利用可能になる
  • フックは特定のクラスやモジュールに実装することができ、それより下位のすべてを捕捉する

3章のまとめ

3章で学習したことが詰まったコードの読み解きです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
module NativeCampingRoutes

  extend self

  def R(url)
    route_lookup = routes

    klass = Class.new
    meta = class << klass; self; end
    meta.send(:define_method, :inherited) do |base|
      raise "Already defined" if route_lookup[url]
      route_lookup[url] = base
    end
    klass
  end

  def routes
    @routes ||= {}
  end

  def process(url, params = {})
    routes[url].new.get(params)
  end
end

module NativeCampingRoutes
  class Hello < R '/hello'
    def get(params)
      puts "hello #{params[:name]}"
    end
  end

  class Goodbye < R '/goodbye'
    def get(params)
      puts "goodbye #{params[:name]}"
    end
  end
end

NativeCampingRoutes.process('/hello', name: "greg")
# => hello greg
NativeCampingRoutes.process('/goodbye', name: "joe")
# => goodbye joe

3行目 extend self

Object#extend は引数で渡されたモジュールのインスタンスメソッドを特異クラスのメソッド(つまり、クラスメソッド)として追加します。

すなわち、この後に続く def で定義されたモジュールのインスタンスメソッドを自身のクラスメソッドに再定義しています。

6行目 route_lookup = routes

10行目で呼び出している define_method() メソッドに渡すブロックはクロージャなので、ローカル変数にアクセスできます。define_method() メソッド内で @routes にアクセスしたいので、ローカル変数に格納しています。

9行目 meta = class << klass; self; end

10行目で定義する Class#inherited メソッドは、継承されるクラス(klass)のクラスメソッドとして定義します。クラスメソッドは特異クラスのメソッドとして定義する必要があるので、特異クラスを取り出しています。

10行目 meta.send(:define_method, :inherited) do |base|

define_method() メソッドは private メソッドなので、meta.define_method() という呼出はできません。

そこで、send() メソッドを使って private メソッドを呼び出しています。特異クラスである meta に対して inherited メソッド(クラスが継承された際に呼び出されるフックメソッド)を定義しています。

inherited メソッドは、呼び出される際に引数として継承先の子クラス(NativeCampingRoutes::Hello、NativeCampingRoutes::Goodbye)が渡されるので、ブロック引数の base として受け取っています。

14行目 klass

R() メソッドは継承元として使うことを想定しているので、クラスを返しています。

22行目 routes[url].new.get(params)

routes メソッドで返される @routes ハッシュに対して url をキーにアクセスします。

R() メソッドで @routes[url] に継承先クラスが格納されているので、Class#new を使ってインスタンス化し、get() メソッドを呼び出しています。

27, 28行目 class Hello < R ‘/hello’

定義した NativeCamppingRoutes モジュールのサブクラスとして Hello を定義し、R() メソッドで返される無名クラスを継承しています。

クラスに get() メソッドを定義して、21行目の process メソッドでインスタンス化したオブジェクトから呼び出せるようにしています。

6章 うまくいかないとき - 覚書

データ構造を確認するのに YAML がつかえる

YAML というデータシリアライゼーションのための標準ライブラリを使うと、データ構造をプリントしてくれる y() メソッドが使えるようになる。

1
2
3
4
5
6
7
8
9
10
11
require "yaml"

data = { name: "hamasyou", age: 28, address: { zip: "272-0000", pref: "Chiba", city: "Ichikawa" } }
y data
#---
#:name: hamasyou
#:age: 28
#:address:
#  :zip: 272-0000
#  :pref: Chiba
#  :city: Ichikawa

テストデータ生成用ライブラリ faker

テスト用のデータ生成に、Faker というライブラリが使えます。gem install faker でインストールできます。

Terminal

$
sudo gem install faker

次のように使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
require "faker"
require "pp"

data = 5.times.map do
  { name: Faker::Name.name, phone: Faker::PhoneNumber.phone_number }
end

pp data
#[{:name=>"Johnathan Lowe III", :phone=>"(859)707-2471 x1926"},
# {:name=>"Lucius Murray", :phone=>"(760)338-6980"},
# {:name=>"Queen Beahan II", :phone=>"1-085-613-9274 x52563"},
# {:name=>"Daniela Boyle", :phone=>"956.964.3848"},
# {:name=>"Mrs. Jarret Wisozk", :phone=>"(760)687-0168 x68429"}]

Faker で作れるダミーデータには次のようなものがあります。

クラス 作れるデータ サンプル
Faker::Address 住所データ
  • zip_code
    “15832-6995”
  • city
    “South Verlie”
Faker::Company 会社データ
  • name
    “Carroll, Kuhlman and Glover”
  • bs
    “orchestrate vertical action-items”
Faker::Internet ネットワークデータ
  • email
    [email protected]
  • free_email
    [email protected]
  • domain_name
    “littel.com”
Faker::Lorem 文章データ
  • words
    [“quaerat”, “blanditiis”, “qui”]
  • sentences
    [“Maiores dicta sed voluptas corrupti repudiandae eos aliquam eligendi.”, “Dolorem eius ut nam esse nihil illum.”, “Non sapiente accusamus maiores neque eum est ea.”]
Faker::Name 名前データ
  • name
    “Nicklaus Swift”
  • first_name
    “Angus”
  • last_name
    “Morissette”
Faker::PhoneNumber 電話番号
  • phone_number
    “024-597-6027 x86091”

Faker::PhoneNumber::Formats に phone_number() メソッドで返される電話番号のフォーマットの一覧が入っています。テスト時にフォーマットをいじることで、phone_number() メソッドの戻り値の形式を変更できます。

1
2
3
Faker::PhoneNumber::Formats = ["(###)##-####", "###-####-####"]
Faker::PhoneNumber::phone_number
# => "(109)29-6592"

7章 文化の壁を取り払う - 覚書

ソースコードのエンコーディングを明示する

Ruby1.9 から多言語対応に注意を払わなければいけなくなりました。M17N(MultilingualizatioN) です。

M17N 可能なプロジェクトで作成するソースコードには、ソースコード中にマジックコメントを埋め込む必要があります。

Ruby ソースコード中に #! がない限り、マジックコメントはファイルの一行目に書きます。#! がある場合は2行目に書きます。

マジックコメントのフォーマットは次のとおりです。

# coding: UTF-8
# -*- coding: utf-8 -*-

ファイルを扱う場合

例えば、EUC-JP で書かれたファイルを UTF-8 で書かれた Ruby のソースコード上で処理したい場合、次のようにします。

1
2
3
File.open("euc.txt", encoding: "EUC-JP:UTF-8").each do |line|
  p line
end

encoding: パラメータを指定してファイルを開きます。encoding パラメータは “<ファイルのエンコード>:<処理するソースコードのエンコード>” のように書きます。

例の場合、EUC-JP で書かれた euc.txt ファイルを UTF-8 のソースコードで処理するので、encoding:”EUC-JP:UTF-8” としています。

なお、ファイルのエンコーディングがソースコードのエンコーディングと同じ場合は、encoding: “UTF-8” と書くことができます。

encodingオプションを指定しない場合、Ruby は Encoding#default_external で指定されているエンコーディングでファイルを解釈しようとします。

バイナリファイルを扱う場合

1
img = File.read("hoge.png");

上記のようにバイナリデータを読み込んでいる場合は注意が必要です。Ruby1.9 からは encoding が指定されない場合、Encoding#default_external の値がエンコーディングとして使われます。

そのため、read() メソッドで encoding を指定しないと、中身がバイナリデータであっても default_external のエンコーディングだと解釈されてしまいます。

バイナリデータを読み込む際は、File#binread() メソッドを使うようにします。

閑話休題

7章の P.223 に L10N の話題が載っています。そこで見つけたソースコード。

1
2
3
4
data = { given_name: "姉ヶ崎", surname: "寧々" }
Gibberish::Simple.use_language(:ja) do
  p T("{given_name}{surname}", [:name, data]) #=> "姉ヶ崎寧々"
end

ね、寧々さん!!?

8章 プロジェクトメンテナンス - 覚書

README ファイルに書くとよいこと

Description(説明)
なんのためのプロジェクトなのか、何を解決するものなのか、1〜2段落程度で説明する。
Documentation(ドキュメント)
プロジェクトの公開 API となっている主要なクラスを2〜3個紹介するとよい。
Examples(サンプル)
基本的な使い方、何が出来るのか?どうやってクラスを使うのか?の概要を説明するとよい
Install(インストール方法)
インストール手順が簡単であれば、README にインストール手順を書いておくとよい。
Q&A(質問の宛先)
自分たちへの質問の方法を記述する。Eメール、電話、会社の住所などなど。

ライブラリのレイアウト

ライブラリディレクトリ

lib フォルダを作り、ひとつのファイルとひとつのサブディレクトリを用意します。

ひとつのファイルとは、プロジェクト名と同じファイルになっており、依存関係のあるライブラリなどをロードするための出発点としての役割を果たすものになります。

ひとつのサブディレクトリには、プロジェクト名と同じディレクトリ名にしておき、必要なライブラリやソースコードをすべてこの中に閉じ込める。

- lib
  - csvparser/
    - ...ライブラリ群...
  - csvparser.rb

クラス名とファイル名の対応等は、Ruby コーディング規約 - Shugo.net 等を参考にする。

実行ファイル

実行ファイルは bin ディレクトリに置く。

テストコード

テストコードは、test ディレクトリに置く。

サンプルコード

サンプルコードがあれば、examples ディレクトリに置く。

ここまでをまとめると

次のようなディレクトリ構成になる。

-Projectルート/
  - README
  - bin/
  - examples/
  - lib/
    - csvparser/
      - ...ソースコード...
    - csvparser.rb
  - test