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

この記事の概要

  • TDD で開発することで設計上の問題点に気づきやすくなる
  • Singleton はグローバル変数である
  • Singleton の使用はできる限り避けるべきである

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

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

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

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

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

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

Singleton とは

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

  • あるクラスのインスタンスが 1 つであることを保証する
  • 常に同一へのインスタンスを取得するためのアクセスポイントを提供する

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

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

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

要点は以下の 3 点です.

  • コンストラクタ (__construct メソッド) を private にすることで, new を禁止している
  • 代わりに getInstance() という static メソッドでオブジェクトの生成をする
  • getInstance() では初回だけ Config オブジェクトを生成して返すが, 次回以降は最初に作ったオブジェクトをそのまま返す

Singleton をテストする

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

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

Green

テストは問題無く通った

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

Red

テストは壊れてしまった

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

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

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

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

テストは新鮮なうちに

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

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

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

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

さらなる問題

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

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

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

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

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

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

Green

テストは問題無く通った

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

Red

テストはまたしても壊れてしまった

原因は, 先ほど 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 自体を導入する前の段階でも, 設計について新たな目線を得ることができるので, 何かしら良い効果を得ることでしょう.

, , ,

最近 HTML5 化した Slideshare ですが, エラーで表示できないスライドが多すぎてまともに閲覧できないようです.
この記事に掲載している Flash 版は今まで通りの問題なく表示できるのですが…

スライドが完成したのが勉強会への出発 15 分前で, 通しで練習することすらできず, 発表はかなりひどいものとなっていまいました.
最低限, スライドの流れぐらいは頭の中に入れて発表すべきですね…

スライドだけ見てわかるような作りにはなっていないので, 以下で補足したいと思います.
ただし, 序盤は省略し, 本論となる書法編とパターン編についてのみとなります.

それぞれ該当するスライド番号も付記してありますので, よければご活用ください.

書法編 1: ヘルパーメソッドを使う (25 ~ 30)

テスト対象のオブジェクト, それを生成するのに必要な依存オブジェクトの生成には, ヘルパーメソッドの使用を検討しましょう.
new するだけで済むものであればそれでもいいのですが, 生成に複雑なステップを踏む必要がある場合に威力を発揮します.

これは一般的なプログラミングにおけるサブルーチンみたいなものだと思って問題無いと思います.
ただし, 可読性を問題にするのであれば, 多少長くても説明的でわかりやすいメソッドの命名を心がける必要があります.

テスト対象の生成が複雑だと, 複数のテストの比較が大変になります.
ヘルパーメソッドを使うことで, テスト A とテスト B はどこに有意な違いが有り, 結果に違いが生まれるのか, ということがわかりやすくなります.

書籍 [エラー: isbn:0131495054:n というアイテムは見つかりませんでした] では, Fixture Setup Patterns において Creation Method という名前で紹介されています.
また, これの応用として, 引数付きの Parameterized Creation Method というものも紹介されています.

書法編 2: テストメソッドを分割する (31 ~ 36)

ひとつのテストメソッド内で, いろんなことをテストしようとすると, 何をテストしようとしているのかがわかりにくくなります.
例え同じメソッドを対象にしたテストであっても, 前提条件等が違うのであれば, 分割するべきです.

分割することで, それぞれのテストメソッドには, より具体的な名前をつけることができます.
これもまた可読性に寄与すると言えるでしょう.

これは癖のレベルの話なので, 普段からテストを書いている人であれば無意識にそうなると思います.
まずは 1 つのテストメソッドにおいてアサーションは 1 つまで, というルールを課すことで自然とそういう書き方に近づくと思います.

パターン編 1: 依存性は外から差し込む (39 ~ 49)

スライドでは, フレームワークによくある Request クラスを例に, $_SERVER といったスーパーグローバル変数を使ったときに起こる問題について説明しました.

$_SERVER を使った実装でもちゃんと動作するので, 問題と見なさないこともできます.
しかし, 問題はテストを書こうとしたときに顕在化します.

スーパーグローバル変数に限らず, グローバル変数を利用した実装だと, 複数のテストが影響し合う可能性が生まれます.
スライドではコードで例示しているので, 具体的にはスライドの該当箇所を参照してください.
スライドのように unset($_SERVER['HTTPS']) とすることで回避できますが, コードベースが巨大化すると, 暗黙的なコンテキストが肥大化し, テストを書くのがだんだんと困難になります.

その代わりにどうするのかというと, スーパーグローバル変数 $_SERVER を直接参照するのではなく, コンストラクタやセッターメソッド等で, 引数として差し込む, という方法を採ります.
Dependency Injection (DI, 依存性の注入) というパターンです.
DI を採用することで, 再利用性が高くなるので, テストを書くことで設計が向上する例としては比較的よく挙げられます.

パターン編 2: 外部への依存を避ける (50 ~ 62)

ひとつ前の話を逆に言っているだけですが, 別の側面から解説しています.

外部への依存としては, 例えば以下のようなものが挙げられます.

  • グローバル変数/スーパーグローバル変数
  • ファイルシステム
  • Web API やデータベース等のネットワーク

しかし, もちろんこれらを完全に避けてのアプリケーション開発は考えられません.
それではどうするのか, ということで, スライドでは Active Record の問題点と, それを解決したパターンとしての Data Mapper を例に説明しています.

ここでいう Active Record というのはパターンとしての Active Record で, 書籍 [エラー: isbn:0321127420:n というアイテムは見つかりませんでした] で紹介されています.
Ruby on Rails の ORM としての ActiveRecord は, それを実装したものです.

Active Record においては, 1 つのレコードが 1 つのオブジェクトにマッピングされ, それぞれのレコードはデータベースを知っています.
なので, $record->save() といった操作が可能になります.

Data Mapper でも 1 つのレコードが 1 つのオブジェクトに, という部分は変わりませんが, それぞれのレコードはデータベースのことを知りません.
例えば users というテーブルであれば, User クラスがその 1 レコードを表し, UserMapper はテーブルを表します.
(実際にはテーブルをラップしたクラスとして実装されることが多いと思いますが, ここでは単純化しています)
レコードはデータベースのことを知らないので, レコードを保存する際は $mapper->save($record) といったようにする必要があります.

それでは Active Record の何が問題なのか.
Active Record ではレコードがドメインロジック (ビジネスロジック) を持ったドメインオブジェクトとしての側面と, データアクセスを行うオブジェクトとしての側面を持つことになります.
それぞれは全く違った関心事なので, 関心事の分離 (SoC)単一責任原則 (SRP) といったソフトウェア開発の原則に違反することになります.
テストに関していうと, データベースへのアクセス無しにドメインロジックをテストできない, といったことが起こりやすくなります.
(もちろん, 実装によりますが)

逆に Data Mapper ではドメインロジック (上の例でいうと User クラス) をデータベース無しでテストできます.
データベースに対してテストを書きたいときもありますし, そのためのテクニックもいろいろとありますが, まずはそれらを無視してテストできるようなクラス分割を行うことで, 低コストで確実にテストを書くことができます.

パターン編 3: Singleton を避ける (63 ~ 71)

Singleton は GoF のデザインパターンの中では最も理解しやいもののひとつで, よく使われるパターンのひとつです.
しかし, 同時に批判的意見の多いパターンでもあります.

Singleton ではクラスの static プロパティとしてオブジェクトとその状態がグローバル空間に残ってしまいます.
これはつまり, グローバル変数と同様の性質を持っている, ということができます.

また, グローバル変数と違って, 容易に上書くこともできないので, テストをする上ではさらに厄介な存在といえます.

対策としては以下が考えられます.

  • 状態を初期化するためのメソッドを用意する
  • アプリケーション本体では getInstance() といった, 常に同一のインスタンスを取得するメソッドの使用を規約化し, テストでは new でそれぞれ独立したオブジェクトを生成し, テストする

また, PHPUnit にはこれらを回避するための –process-isolation オプションであったり, @runTestsInSeparateProcesses アノテーションといった機能もあるのですが, それらについては説明しませんでした.
Singleton を使わなければ, これらの機能について理解する必要もなくなりますし, これらは Slow Test (テストの実行が遅くなる) 問題を引き起こします.

Singleton で実装したくなったとき, 本当に Singleton である必要があるのかについては, 一度考えてみる必要があるでしょう.

参考文献

スライド中に挙げたものです.
書籍へのリンクにはアフィリエイトタグをつけています.

  • [エラー: isbn:0131495054 というアイテムは見つかりませんでした]
    xUnit とというのは SUnit (Smalltalk), JUnit (Java) といったナントカ Unit 系テスティングフレームの総称です.
    例として示されるコードは Java のものが多めですが, PHPUnit も書き方を含めて xUnit と呼べるものなので, この書籍で挙げられている考え方やテクニックは, ほぼそのまま適用できます.
    鈍器として人を殺せる程の分量がありますが, 気になるトピックをつまみ食いすることもできます.
  • [エラー: isbn:0470872497 というアイテムは見つかりませんでした]
    PHPUnit の作者 Sebastian Bergmann を初めとする豪華な著者陣による, PHP アプリケーションの品質, テスト, 設計についての書籍.
    Symfony の作者 Fabien Potencier による章もあります.
    ソフトウェアの品質についての解説などは他の PHP 本にはまず無いものですし, 非常に刺激的な 1 册です.
  • [エラー: isbn:0321127420 というアイテムは見つかりませんでした]
    アプリケーションのアーキテクチャパターンについての書籍.
    全体的な分量からするとほとんど読めていないのですが, スライド中の Active Record や Data Mapper について参考にしました.
  • [エラー: isbn:4798015164 というアイテムは見つかりませんでした]
    GoF によるデザインパターンを PHP に翻訳して紹介している書籍.
    新卒の頃に読んで衝撃を受けました.
    残念ながら絶版なので買うことはできませんが, 原稿が公開されているので, 読むことはできます.
    是非電子書籍化して欲しい 1 冊です.

こちらはスライドです.

  • Dependency Injection with PHP 5. 3
    
Symfony の作者 Fabien Potencier によるスライド.
    序盤は Dependency Injection の基礎についての説明で, そのあとに PHP 5.3 における DI コンテナの実装について説明しています.
  • BEAR DI
    リソース指向のフレームワーク BEAR の作者の @koriym さんによるスライド.
    序盤は前のスライドを日本語訳しつつ引用し, 後半では BEAR の DI コンテナについて説明しています.
  • Clean PHP
    PHPUnit の作者 Sebastian Bergmann によるスライド.
    これだけ見て何かを理解するのには向きませんが, Active Record と Data Mapper の比較についてはこれを参考にしました.
  • Singletons in PHP – Why they are bad and how you can eliminate them from your applications
    
Singleton の問題点について, 詳細に説明されています.
,

2010-03-07 15:30 追記
Observer Pattern について調べ直してみたところ、一般的な Obsesrver Pattern と、以下のコードは似て非なるものであることに気づきました。
このエントリは GoF パターンの教材にはなり得ないので、その点に注意してお読みください。

入浴中にふと思いつき、書いてみたコードを紹介。

Observer Pattern については詳しく紹介しないので、興味のある方は Wikipedia の Observer パターンの頁をご覧ください。かくいう私自身も、いわゆる GoF パターンとしての Observer Pattern を暗記しているわけではないのですが、Observer Pattern 的にはなっていると思います。

Notifier.php

監視者にメッセージを送る通知者クラス。

class Notifier
{
    private $_observers = array();

    public function addObserver($observer)
    {
        $this->_observers[] = $observer;
        return $this;
    }

    public function __call($methodName, $args)
    {
        foreach ($this->_observers as $observer)
        {
            call_user_func_array(array($observer, $methodName), $args);
        }
        return $this;
    }
}

そして、次に監視者クラスを 2 つほど定義。
まずは文字列をテキストファイルに書き込んでいくロガー。

Logger.php

class Logger
{
    private $_fp;

    public function __construct($fileName)
    {
        $this->_fp = fopen($fileName, 'a');
    }

    public function __destruct()
    {
        fclose($this->_fp);
    }

    public function putString($str)
    {
        $this->_log($str);
    }

    public function putArray($arr)
    {
        foreach ($arr as $str)
        {
            $this->_log($str);
        }
    }

    private function _log($str)
    {
        fputs($this->_fp, date('Y-m-d H:i:s') . "\t" . $str . "\n");
    }
}

次は、顔文字の吹き出しとして、文字列を出力するもの。

Speaker.php

class Speaker
{
    private $_face;

    public function __construct($face)
    {
        $this->_face = $face;
    }

    public function putString($str)
    {
        $this->_speak($str);
    }

    public function putArray($arr)
    {
        foreach ($arr as $str)
        {
            $this->_speak($str);
        }
    }

    public function _speak($str)
    {
        echo $this->_face . ' < ' . $str . "\n";
    }
}

そして、実際に実行するのは以下のコード。

main.php

require_once './Notifier.php';

require_once './Logger.php';
require_once './Speaker.php';

// 通知者オブジェクトの生成
$notifier = new Notifier;
// 通知者に、監視者オブジェクトを登録する
$notifier->addObserver(new Logger('./log.txt'));
$notifier->addObserver(new Speaker('( `・ω・´)'));

$str = "Pentagram / Relentless (1985)";
$arr = array(
    "01. Death Row",
    "02. All Your Sins",
    "03. Sign of the Wolf (Pentagram)",
    "04. The Ghoul",
    "05. Relentless",
    "06. Run My Course",
    "07. Sinister",
    "08. The Deist",
    "09. You're Lost I'm Free",
    "10. Dying World",
    "11. 20 Buck Spin"
);

// いろいろなメソッドを実行
$notifier->putString($str);
$notifier->putArray($arr);

これを実行すると、こうなります。

$ php main.php
( `・ω・´) < Pentagram / Relentless (1985)
( `・ω・´) < 01. Death Row
( `・ω・´) < 02. All Your Sins
( `・ω・´) < 03. Sign of the Wolf (Pentagram)
( `・ω・´) < 04. The Ghoul
( `・ω・´) < 05. Relentless
( `・ω・´) < 06. Run My Course
( `・ω・´) < 07. Sinister
( `・ω・´) < 08. The Deist
( `・ω・´) < 09. You're Lost I'm Free
( `・ω・´) < 10. Dying World
( `・ω・´) < 11. 20 Buck Spin

そして、同時に、以下のようなログファイルまで出力されます。

2010-03-06 22:42:09     Pentagram / Relentless (1985)
2010-03-06 22:42:09     01. Death Row
2010-03-06 22:42:09     02. All Your Sins
2010-03-06 22:42:09     03. Sign of the Wolf (Pentagram)
2010-03-06 22:42:09     04. The Ghoul
2010-03-06 22:42:09     05. Relentless
2010-03-06 22:42:09     06. Run My Course
2010-03-06 22:42:09     07. Sinister
2010-03-06 22:42:09     08. The Deist
2010-03-06 22:42:09     09. You're Lost I'm Free
2010-03-06 22:42:09     10. Dying World
2010-03-06 22:42:09     11. 20 Buck Spin

これがどう嬉しいのか。Observer Pattern の例としてよくあるのは、通知者オブジェクトの notify メソッドが呼び出されると、監視者オブジェクトたちの update メソッドを実行していく、というパターン。対する、この Notifier オブジェクトは、notify 的メソッドをいくつでも追加できるという特性を持ちます。いわば、動的 Observer Pattern

putString メソッドも、putArray メソッドも、Notifier は知りませんが、自分が知らないメソッドを受けとると、監視者オブジェクトたちに「putString してください」「putArray してください」というメッセージを、引数を伴って通知して回るのです。

これを実現するのが、以前のメタプログラミングに関する記事で紹介した、__call というマジックメソッド。以前の繰り返しになりますが説明すると、実行しようとしたメソッドがなかったときの転送先のメソッドです。引数には、実行しようとしたメソッド名と、そのメソッドに渡された引数を受けます。

ところで Ruby には Observable という、Observer Pattern を実現するためのモジュールがあります。非常に動的な特性を持つ Ruby のことですから、上記の Notifier と同じような仕様になっているのではと思い、調べてみたところ、notify されたら update していくだけの普通のものでした。つまり、静的 Observer Pattern

これは我ながらおもしろいアイディアではないかなと思っているのですが、やはり、PHP よりはむしろ Ruby のほうがしっくりパターンかもしれません。method_missing メソッドを使えば、簡単に同じようなことが実現できると思います。

, ,