本書は、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 |
|
インターフェースをシンプルにするためのブロック
Rails の Configuration に使われているオブジェクトショートカットのことです。次のようなコードを
1 2 3 4 5 6 7 |
|
次のように書けるようにします。
1 2 3 4 5 |
|
これを実現するには、次のようなコードになります。
1 2 3 4 5 6 7 |
|
インスタンス化したオブジェクトの instance_eval() メソッドにブロックを渡すことで、ブロックをそのインスタンスのコンテキストで実行しています。
この方法を使うと、ブロックはインスタンス化したオブジェクトのコンテキストで実行されるため、ブロックのスコープ内で定義されたローカル変数にしかアクセスできません。つまり、次のコードは動きません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
この問題を解決するには、ブロックをインスタンスのコンテキストで評価するのではなく、クロージャとして実行すればよいです。
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
block.arity を使ってコードブロックに引数がいくつあるかを調べて、引数がひとつ以上あればブロックをクロージャとして呼び出すようにしています。
method? と method! の意味
method? 疑問符
method? のようにメソッド名の末尾に疑問符(?)をつけるのは目的は、オブジェクトに何かを問い合わせることになります。条件分岐などにメソッドを利用する際に使えます。
疑問符をつけたメソッドの戻り値は、true か false or nil を返すようにします。
method! 感嘆符
method! のようにメソッド名の末尾に感嘆符(!)をつける目的は、このメソッドは特別だ、「注意しろ!」になります。
よくある誤解は、受け取ったオブジェクトを変更することを知らせたいときに感嘆符を使う、というものだ。たいていの場合、感嘆符は私たちに何か警告をするものだからだろう。
Ruby の組み込みクラスのメソッドには破壊的メソッドでも感嘆符がついているのとついていないものがあります。
これはすなわち、メソッドに感嘆符をつける目的はこのメソッドが特別であることを知らせるのであって、破壊的であるとか危険であることを知らせるのではないということです。
したがって、同じようなことをする foo() メソッドがないのに foo!() メソッドだけがあるのは、あまり意味のないことだ。(中略)感嘆符は必ずしもそのメソッドが破壊的な操作をすることを意味するわけではないと考えると、…
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 |
|
define_method() を使って動的にクラスメソッドを定義する
クラスメソッドを定義するには、クラスの特異クラスにメソッドを定義する必要があります。特異クラスをオープンするには、class << obj 構文を使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
define_method() は private メソッド
define_method() は特異クラス上でプライベートになっているため、レシーバを指定して呼び出すには send() メソッドを使う必要があります。
モジュールのメソッドをモジュールのクラスメソッドにする
extend self
extend self を使うと、自信のインスタンスメソッドを特異クラスに定義することになりクラスメソッド化することができます。
1 2 3 4 5 6 7 8 9 10 |
|
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 |
|
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 |
|
テストデータ生成用ライブラリ faker
テスト用のデータ生成に、Faker というライブラリが使えます。gem install faker でインストールできます。
Terminal
$ |
|
次のように使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Faker で作れるダミーデータには次のようなものがあります。
クラス | 作れるデータ | サンプル |
---|---|---|
Faker::Address | 住所データ |
|
Faker::Company | 会社データ |
|
Faker::Internet | ネットワークデータ |
|
Faker::Lorem | 文章データ |
|
Faker::Name | 名前データ |
|
Faker::PhoneNumber | 電話番号 |
|
Faker::PhoneNumber::Formats に phone_number() メソッドで返される電話番号のフォーマットの一覧が入っています。テスト時にフォーマットをいじることで、phone_number() メソッドの戻り値の形式を変更できます。
1 2 3 |
|
7章 文化の壁を取り払う - 覚書
ソースコードのエンコーディングを明示する
Ruby1.9 から多言語対応に注意を払わなければいけなくなりました。M17N(MultilingualizatioN) です。
M17N 可能なプロジェクトで作成するソースコードには、ソースコード中にマジックコメントを埋め込む必要があります。
Ruby ソースコード中に #! がない限り、マジックコメントはファイルの一行目に書きます。#! がある場合は2行目に書きます。
マジックコメントのフォーマットは次のとおりです。
# coding: UTF-8
# -*- coding: utf-8 -*-
ファイルを扱う場合
例えば、EUC-JP で書かれたファイルを UTF-8 で書かれた Ruby のソースコード上で処理したい場合、次のようにします。
1 2 3 |
|
encoding: パラメータを指定してファイルを開きます。encoding パラメータは "<ファイルのエンコード>:<処理するソースコードのエンコード>" のように書きます。
例の場合、EUC-JP で書かれた euc.txt ファイルを UTF-8 のソースコードで処理するので、encoding:"EUC-JP:UTF-8" としています。
なお、ファイルのエンコーディングがソースコードのエンコーディングと同じ場合は、encoding: "UTF-8" と書くことができます。
encodingオプションを指定しない場合、Ruby は Encoding#default_external で指定されているエンコーディングでファイルを解釈しようとします。
バイナリファイルを扱う場合
1
|
|
上記のようにバイナリデータを読み込んでいる場合は注意が必要です。Ruby1.9 からは encoding が指定されない場合、Encoding#default_external の値がエンコーディングとして使われます。
そのため、read() メソッドで encoding を指定しないと、中身がバイナリデータであっても default_external のエンコーディングだと解釈されてしまいます。
バイナリデータを読み込む際は、File#binread() メソッドを使うようにします。
閑話休題
7章の P.223 に L10N の話題が載っています。そこで見つけたソースコード。
1 2 3 4 |
|
ね、寧々さん!!?
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