Born Too Late

Yuya's old tech blog.

例えば, Singleton を避ける

2011-12-14 00:25:53

この記事は TDD Advent Calendar jp: 2011 の 14 日目です.

この記事の概要

テスタビリティを意識しよう

TDD では, 原則としてユニットテストを書いてから実際のコードを実装します.
なので, 自然と「テストのしやすさ (テスタビリティ)」を意識して実装することになります.

そして, TDD においては一般的に, テスタビリティを意識することで, 設計が改善されるとされています.

オブジェクト指向には難しい概念がたくさん登場します.
単一責任の原則 (Single Responsibility Principle) とか, 関心事の分離 (Separation of Concerns)とか, 依存性の注入 (Dependency Injection) とか…
これらを, 書籍などで読むだけで理解できた, という人は少ないのではないでしょうか.
少なくとも私は, TDD を実践することで初めて, これらの原則の言いたいことを実感し, 理解につなげることができたと考えています.

オブジェクト指向設計の大切なことは TDD に学んだ, と言っても過言ではありません.
あまりにもたくさんのことを学んだので, それらをここで全て紹介するのは無理なことです.
なので, 今回はその中から一つだけ, 簡単なものを紹介しようと思います.

この記事では, 明日から意識できるポイントとして, 例えば, Singleton を避ける, ということについて紹介します.

Singleton とは

いわゆる Gang of Four の書籍で紹介されているデザインパターンのひとつで, 以下のようなことを実現するために利用されます.

数あるデザインパターンの中でも比較的理解しやすく, 実装も簡単であるため, これを最初に覚えたという方も多いでしょう.
私自身もそうであったように思います.

Singleton パターンを適用してみる

例えば, PHP においては以下のように実装されます.
ここでは, ありがちな例として, アプリケーション中からグローバルに参照される Config (設定) クラスを作ってみましょう.

要点は以下の 3 点です.

Singleton をテストする

次に Config オブジェクトをテストするコードを書いてみましょう.
テスティングフレームワークには PHPUnit を使用します.

このふたつのテストはいずれも成功します.

[caption id="attachment_1525" align="aligncenter" width="166" caption="テストは問題無く通った"]Green[/caption]

ですが, 記述する順序を逆にしてみると, いとも簡単に失敗してしまいます.

[caption id="attachment_1526" align="aligncenter" width="459" caption="テストは壊れてしまった"]Red[/caption]

一体, どうしてでしょうか.

Singleton はグローバル変数である

クラスを定義して, クラスメソッドを通してのアクセスを行っていますが, Config クラスのオブジェクトはグローバルに参照可能です.
本来ローカルスコープであるはずのメソッド間を超えて, 状態が共有されてしまいます.
そのため, 一度設定値 foo がセットされると, その後のテストで「foo がセットされていない状態」をテストすることができなくなってしまいました.

このように, Singleton パターンを適用したクラスのオブジェクトは, 本質的にはほとんどグローバル変数なのです.

テストは新鮮なうちに

ユニットテストを書く上で大切なことは, 出来る限りクリーンな状態で始めることです.

グローバル変数, データベース, ファイルシステムなどは, 状態として考えることができます.
コードやテストがこれらの状態に依存してしまうと, 状態の変化に弱くなり, 壊れやすくなります.

Singleton でない通常のクラスであれば, 複数のテストメソッド間で状態を共有されません.
スコープがテストメソッドごとに区切られるので, テスト対象のオブジェクトはガベージコレクションにより, 二度と再利用されないためです.
(もちろん, テストケースクラスのインスタンス変数などに入れてしまえば別ですが)

Singleton では, このガベージコレクションによる状態のクリアが起こらないため, 前のテストコンテキストを引きずったまま, 次のテストを実行することになってしまいます.
複数のテストが影響し合うことによりメンテナンス性が下がるだけでなく, 暗黙的なコンテキストの増大により, 理解もしづらくなります.

さらなる問題

Config 自体はとてもシンプルなので, さほど問題にはならないかもしれません.
ですが, Config::getInstance() がアプリケーションのあちこちで呼び出されていたらどうでしょうか.

例えば, ウェブアプリケーションにおいて, アプリケーションそれ自体を表す Application クラスについて考えてみます.

Application は設定によりデバッグモードの On/Off が切り替えられるとしましょう.
その設定は, Config オブジェクトが保持することになります.

デバッグモードであるかどうかは, Application オブジェクトの isDebug() メソッドで確認できます.
今度はそれをテストするコードを書いてみましょう.

そしてこのテストが通るように Application クラスを実装します.

これで, テストは問題なく通ります.
以下は Config のテストもあわせて実行した結果です.

[caption id="attachment_1527" align="aligncenter" width="161" caption="テストは問題無く通った"]Green[/caption]

ですが, さっきと同じで, このテストも順序を逆にすると失敗してしまいます.

[caption id="attachment_1528" align="aligncenter" width="497" caption="テストはまたしても壊れてしまった"]Red[/caption]

原因は, 先ほど Config のテストが失敗したのと全く同じです.
既に debug という設定値が入ってしまっているため, false を明示的に値をセットしない限りは, デバッグする設定のままになります.

このように, Singleton で実装したクラスそれ自体だけでなく, それを呼び出すクラスにまで, テストのしにくさが伝染してしまう可能性があるのです.

処方箋 1: 状態を初期化できるようにする

Config クラスのテストしにくさは, 状態がクリーンでないことにありました.
そこで, init() メソッドを実装することで, 状態を初期化できるようにしてみます.

テストもこのように書き換えます.

変更点は, Config::getInstance() した後に, init() を呼び出すようにしている点のみです.

これにより, いずれのテストもクリーンな状態でテストが実行されることになったので, 例え順番を変えても失敗しなくなりました.
Application クラスにおいても, 同じアプローチを採ることで, テストの実行順序に依存することは無くなります.

処方箋 2: 依存性の注入 (Dependency Injection) の利用

とはいえ, これで問題が無くなったわけではありません.
この Application クラスには, Config というクラス名がハードコードされており, 2 つのクラスは密結合になってしまっています.

これでは, Config に似た動きをした別のクラス (例えば CachedConfig とか) を作っても, 差し替えるには全ての Config を書き換える必要が出てしまいます.

ではどうするか.
オブジェクト指向が本来持つモジュール性を活かすのであれば, 引数で Config オブジェクトを渡すことを検討しましょう.
ここでいう引数とは, コンストラクタ引数でも, メソッド引数のどちらでも構いません.

先ほどよりもややコードが増えてしまいましたが, Config クラスのハードコードが無くなりました.
Application と Config の間の結合は緩くなり, CachedConfig や YamlConfig など, 同じように振る舞う別クラスへの差し替えが容易になりました.
また, モックスタブを使ってのユニットテストも書きやすくなっています.

このように, 依存するオブジェクトのクラス名をハードコードするのではなく, 引数で渡すことを依存性の注入 (Dependency Injection) といいます.

ところで, クラス名のハードコードが問題になるのは, 何も Singleton だけの問題ではありません.
例え Singleton ではなくとも, new するクラス名をハードコードすれば, 同じようなことが問題になります.

とはいえ, Singleton を利用すると, getInstance() メソッドを利用した依存にしてしまいがちです.
出来る限り依存性の注入を利用し, クラス名をハードコードする箇所を最小限に留めることで, 後の改修時のコストを大幅に下げることができます.

処方箋 3: そのクラスは本当に Singleton なのか

あなたが今実装しようとしているクラスは, 本当に唯一なのでしょうか.
また, 通常唯一であるとしても, コンストラクタを private にしないと実現できないことなのでしょうか.

確かに, Singleton の「常に同一のインスタンスを返す」という特性は便利です.
ですが, 少なくとも私の経験においては, 「インスタンスが 1 つであることの保証」は, そんなに気張るほどのことでは無かったのではないか, と考えてしまいます.

また, 見方を変えれば, Singleton は, そのクラス本来の役割 (Config であれば設定の保持) とは別にもうひとつ, 「オブジェクト生成の管理」という, 全く違った責任を持ってしまっています.
これは, 単一責任の原則に違反していると言えるでしょう.

これをどう解決するか.
少々場当たり的ですが, 例えば Config と, その生成を責任とする ConfigManager に分割する, といったことが考えられます.

この実装自体はあまりオススメしませんが, とりあえず設定の保持 (Config) という責任と, 設定オブジェクト生成の管理 (ConfigManager) という責任を, 個別のクラスに分割することができました.
これにより, Config クラスのコンストラクタを private にするという縛りは無くとも, ConfigManager を利用する限りにおいては, 常に同一のインスタンスを得ることができるようになりました.

コンストラクタが使用できる以上は, オブジェクトの生成は自由になったので, テスト時は毎回オブジェクトを生成・破棄し, クリーンな状態でのテストが可能になりました.
もう init() は必要ありません.

テストメソッドごとにコンテキストが分割されたので, ガベージコレクション任せで安心してテストに集中することができるようになりました.

まとめ

Singleton の話ばかりしましたが, この記事で私が言いたいのは, TDD で開発することで設計上の問題点に気づきやすくなる, ということです.

TDD の実践から学べることは多いにあるので, まだやったことないという方には, まず趣味の開発でいいので, 軽い気持ちでやってみることをオススメします.
業務に TDD 自体を導入する前の段階でも, 設計について新たな目線を得ることができるので, 何かしら良い効果を得ることでしょう.

第57回PHP勉強会@東京で開発環境構築について話して来た

2011-11-13 00:17:27

最近イチオシの php-buildphpenv について紹介してきました.

Building Development Environment with php-build and phpenv
View more presentations from Yuya Takeyama

内容はほぼ最近のブログ記事をまとめた感じです.

一度ブログに書いたものを敢えて発表ネタにしたのにはいくつか理由があります.
(もちろん, 一度記事にまとめたネタなのでスライドに起こしやすい, というのもありますが...)

というわけで皆さんどんどん使いましょう.

類似のツールについて

php-build および phpenv と類似の機能をもったツールは他にもあります.
勉強会でも, 例えば phpall とはどう違うのか, という質問がありました.

結論から言うと, 私は他のツールは特に使ったことが無いのでよくわかりません.
ですが, もともと rbenv は Ruby コミュニティではかなり人気のツールですし, 便利な機能はいろいろと揃っており, 比較的失敗の無い選択肢だと考えています.

また, phpall の違いとして, phpall は各バージョンのバイナリをそれぞれ php-5.2.8 といった, バージョン番号付きのファイル名にしているようですが, phpenv は php コマンド自体を自由に切り替えます.
特定のコードを複数のバージョンで一気に実行する, というときには phpall が便利そうですが, ライブラリの開発等には phpenv の方が適していると思われます.
また, phpenv においては pyrus, phar, phpunit といったコマンドも各バージョンごとに試すことができる, というのも大きな利点です.

phpenv のプラグイン機能について

元々, これまでのブログ記事をまとめただけの発表にするつもりでしたが, ひとつだけおもしろい機能を見つけたので, そこだけ新ネタです.
phpenv の元になっている rbenv にはプラグインという機能があり, 専用のディレクトリにシェルスクリプトを配置するだけで簡単にサブコマンドを登録できる, というものです.
当然 phpenv でも使える機能です.

phpenv each によるコマンドの一斉実行

rbenv のプラグインとして公開されているものの中で, とりあえず rbenv-each というものをつかってみました.
これは rbnev (もしくは phpenv) の管理下にある全てのバージョンのバイナリで, 指定したコマンドを実行する, というものです.

とりあえず, php -v によるバージョンの確認を試してみました.

[caption id="attachment_1472" align="aligncenter" width="300" caption="phpenv each によるバージョンの一斉確認"]phpenv each によるバージョンの一斉確認[/caption]

これ自体はあまり意味の無い例ですが, 例えば PHPUnit によるユニットテストを, 複数のバージョンの PHP で一斉に実行できるすれば, ライブラリ作者に取ってはかなり便利なのではないでしょうか.
それについてはいずれ調べて, また記事にしたいと思います.

このように, php-buildphpenv にはまだまだ便利な使い方がたくさんあるはずです.
利用者が増えることで, そういったノウハウが発掘され, 共有されることを心から願います.

最後になりますが, 今回の勉強会の主催の @gusagi さん, そして会場提供の株式会社 VOYAGE GROUP さん, 楽しい勉強会を本当にありがとうございました.

phpenv で複数の PHP 環境を管理する

2011-10-23 17:01:21

phpenv とは

phpenv

CHH/phpenv - GitHub

複数のバージョンの PHP を管理し, コマンドで簡単に切り替えるためのものです.
Ruby でいう rbenvRVM, Perl でいうところの perlbrew にあたるものです.

なお, phpenv の中身は rbenv を流用することで動いています.

インストール

まず, 以下のコマンドでインストールスクリプトを実行します.

$ curl https://raw.github.com/CHH/phpenv/master/install.sh | sh

中では rbenv を git clone していたりするので, Git をインストールしている必要があります.

上記コマンドが成功したら, ~/.bashrc もしくは zsh 使いなら ~/.zshrc に以下のコマンドを追記します.

あとはいま編集したファイルを再度読み込み直すことでインストールは完了です.

# bash の場合
$ source ~/.bashrc

# zsh の場合
$ source ~/.zshrc
# phpenv
export PATH=$HOME/.phpenv/bin:$PATH
eval "$(phpenv init -)"

rbenv をインストールしている場合

先にも書きましたが, phpenv は rbenv を利用した作りになっています.
その関係もあって, PATH の設定は rbenv が phpenv よりも先になっていないと, 正常に動作しません.
~/.bashrc や ~/.zshrc の設定は, 例えば以下のようにする必要があるので注意が必要です.

# rbenv & phpenv
export PATH=$HOME/.rbenv/bin:$HOME/.phpenv/bin:$PATH
eval "$(rbenv init -)"
eval "$(phpenv init -)"

PHP のインストール

phpenv は PHP の環境を管理するためのものであって, RVM や perlbrew のようにインストールするための機能はありません.
その点は rbenv も同様です.

rbenv では ruby-build を使って Ruby をインストールしますが, phpenv では php-build というツールを使うことができます.

php-build のインストールについては以下の記事を参考にしてください.

php-build で PHP 5.4.0 beta1 をビルドする

以下は php-build のインストールを済ませた, という前提で進めます.

phpenv で管理できるようにインストールするには, ~/.phpenv/versions ディレクトリ内にサブディレクトリを作って, それぞれのバージョンの PHP をインストールする必要があるので, 以下のようにします.

$ php-build 5.4.0beta2 ~/.phpenv/versions/5.4.0beta2

インストールが完了すれば phpenv の管理下に置かれることになります.

$ phpenv versions
  5.4.0beta1
* 5.4.0beta2 (set by /home/yuya/.phpenv/version)

使用する PHP のバージョンを切り替える

以下のようなのコマンドで, インストール済みの PHP のバージョンを切り替えることができます.

$ phpenv global 5.4.0beta2

その他, 使い方は基本的に rbenv と同様なので, そちらを参考にしてください.

sstephenson/rbenv - GitHub

phpenv と php-build の連携

php-build でのビルドでも充分に簡単ですが, せっかくなので phpenv と連携させてもっと簡単にしてみましょう.
以下のようなコマンドだけで好きなバージョンの PHP をビルドし, phpenv の管理下にインストールできるようにします.

$ phpenv install 5.4.0beta2

rbenv でも ruby-build と連携することで rbenv install というサブコマンドを利用することができるようになります.
そこで, rbenv install するためのスクリプト rbenv-install を流用して, こんなものを用意してみました.

このスクリプトを ~/.phpenv/libexec/rbenv-install として保存するだけでインストールは完了です.
これで先のような phpenv install サブコマンドが使えるようになっています.