cheka.jp 超不定期で更新する写真達。お口直しにどうぞ。

[写経] PHPで学ぶデザインパターン08

— 注意 —
PHPによるデザインパターンの写経ですが、まんま書くと問題があるのでアウトプットとして備忘録メモです

初心者には分かりやすくて良い本なので、是非本屋でチェックして下さい。

URL: http://www.amazon.co.jp/PHPによるデザインパターン入門-下岡-秀幸/dp/4798015164
PHPによるデザインパターン入門

Bridgeで機能と目的を分離する

構造 + オブジェクト

主観ですが、このパターンを使うためには事前の設計でいかにシンプルにするかが重要だと感じてます。
後々の機能追加、目的に合わせた拡張を行うために、最初の設計を出来るだけシンプルにしましょう。
今回の写経では目的と機能追加をシナリオにして進めていきます。

シナリオ説明

1.初期状態
レンタル中の本を管理するために本と貸出状況の一覧をCSV形式にファイルへ出力する用に実装します。

2.目的の追加
運用していく中で何日のデータなのか分からないと不便になったのでCSVの最初の行に日付を出力するように拡張を行います。

3.機能の追加
サービスをより便利にするために、json形式にて出力出来るように機能を追加していきます。

まずはいつものBookクラスとBookDaoクラスの準備

/*
貸し出される本クラス
*/
class Book {
	//本のID
	private $id;
	//本の名前
	private $title;
	//貸し出しフラグ
	private $rental;
	//コンストラクタ
	public function __construct($id,$title) {
		$this->id = $id;
		$this->title = $title;
		$this->rental = false;
	}
	//ID取得
	public function getId() {
		return $this->id;
	}
	//名称取得
	public function getTitle() {
		return $this->title;
	}
	//貸し出しフラグ取得
	public function getRental() {
		return $this->rental;
	}
	//貸し出しフラグの設定
	public function setRental($rental) {
		$this->rental = $rental;
	}
}

Daoでは出来るだけコード数を少なくするために不要なメソッドは削除しています。

/*
本の一覧を管理するDaoクラス
*/
class BookDao {
	//シングルトン用のインスタンス
	private static $instance;
	//本の一覧
	private $books;
	//コンストラクタ
	private function __construct() {
		$this->books = array();
		for ($i=1;$i<=10;$i++) { 			$book = new Book($i,"本タイトル_{$i}"); 			//何冊かは貸出中にする 			if( !($i%3) ) $book->setRental(true);
			$this->books[$book->getId()] = $book;
		}
	}
	//複製を禁止する(継承されてもエラーにするようにfinalで宣言する)
	public final function __clone() {
		throw new RuntimeException ("複製したらダメだよ!!");
	}
	//シングルトンの取得
	public static function getInstance() {
		if(!isset(self::$instance)){
			self::$instance = new self();
		}
		return self::$instance;
	}
	//全て検索
	public function findAll() {
		return $this->books;
	}
}

初期状態の機能追加

初期の目的に合わせてシンプルなインターフェイスを準備します。

/*
機能を提供を定義するインターフェイス
*/
interface DataSource{
	public function getData(BookDao $book);
}

コード数を節約するためにファイルポインタ操作などは省いています。

/*
機能を実装するクラス
*/
class CsvDataSource implements DataSource{
	//CSV形式で表示する
	public function getData(BookDao $book){
		print "ID,TITLE,RENTAL\n";
		foreach ($book->findAll() as $book) {
			print sprintf("%d,%s,%s\n",$book->getId(),$book->getTitle(),(($book->getRental()) ? "レンタル中" : "在庫あり"));
		}
	}
}

初期状態の目的追加

クライアントに利用されるクラスです。

/*
目的を提供するクラス : 本の一覧をCSVとしてファイルへ出力する
*/
class ExportFile {
	//機能を提供するIFを実装したクラスを保持
	protected $dataSource;
	//コンストラクタ
	public function __construct($dataSource) {
		$this->dataSource = $dataSource;
	}
	//ファイルへ出力する
	public function getData(){
		//今回は表示するだけだが、本来の目的はファイル出力
		$this->dataSource->getData(BookDao::getInstance());
	}
}

一応動作確認しておきましょう。

$export = new ExportFile(new CsvDataSource());
$export->getData();
$php bridge.php
ID,TITLE,RENTAL
1,本タイトル_1,在庫あり
2,本タイトル_2,在庫あり
3,本タイトル_3,レンタル中
4,本タイトル_4,在庫あり
5,本タイトル_5,在庫あり
6,本タイトル_6,レンタル中
7,本タイトル_7,在庫あり
8,本タイトル_8,在庫あり
9,本タイトル_9,レンタル中
10,本タイトル_10,在庫あり

シナリオ2の目的追加

クライアントに日付けが分からないと不便だと言われました。
システムを拡張してCSVのヘッダに日付けを追加しましょう。

— 念のため —
自分もそうでしたが、実践で慣れるまでは腑に落ちない事が多いのがデザインパターンだと思います。
今回の記事で記述しているクラスは目的と機能の分離を説明するためのクラス設計です。
設計にアンチパターンはあっても基本は自由なので「日付け追加」は機能クラスへ拡張したほうが良いという意見も正しいと思います。
デザインパターンのHow Toに興味がある方は「エリック・エヴァンスのドメイン駆動設計」を読むと悩みが解決するでしょう!

では進めます。

オーバーライドしてみました。
内容によってはメソッド追加でも良いと思います。

/*
目的を提供するクラス : CSVのヘッダに日付を追加
*/
class ExportFileAddDate extends ExportFile{
	//ファイルへ出力する
	public function getData(){
		//日付けを出力
		print date("Y年m月d日に出力しました!")."\n";
		//今回は表示するだけだが、本来の目的はファイル出力
		$this->dataSource->getData(BookDao::getInstance());
	}
}

動作確認してみましょう。

$export = new ExportFileAddDate(new CsvDataSource());
$export->getData();
$php bridge.php
2014年03月07日に出力しました!
ID,TITLE,RENTAL
1,本タイトル_1,在庫あり
2,本タイトル_2,在庫あり
3,本タイトル_3,レンタル中
4,本タイトル_4,在庫あり
5,本タイトル_5,在庫あり
6,本タイトル_6,レンタル中
7,本タイトル_7,在庫あり
8,本タイトル_8,在庫あり
9,本タイトル_9,レンタル中
10,本タイトル_10,在庫あり

シナリオ3の機能追加

クライアントにweb apiを公開出来るようにjson形式の出力に対応したいと言われました。

/*
機能を実装するクラス : JSON形式に対応する
*/
class JsonDataSource implements DataSource{
	//CSV形式で表示する
	public function getData(BookDao $book){
		print "{date:'".date("Y/m/d")."',[\n";
		foreach ($book->findAll() as $book) {
			print sprintf("{id:%d,title:'%s',status:'%s'}\n",$book->getId(),$book->getTitle(),(($book->getRental()) ? "レンタル中" : "在庫あり"));
		}
		print "]}";
	}
}

動作確認してみましょう。

$export = new ExportFileAddDate(new JsonDataSource());
$export->getData();
$php bridge.php
2014年03月07日に出力しました!
{date:'2014/03/07',[
{id:1,title:'本タイトル_1',status:'在庫あり'}
{id:2,title:'本タイトル_2',status:'在庫あり'}
{id:3,title:'本タイトル_3',status:'レンタル中'}
{id:4,title:'本タイトル_4',status:'在庫あり'}
{id:5,title:'本タイトル_5',status:'在庫あり'}
{id:6,title:'本タイトル_6',status:'レンタル中'}
{id:7,title:'本タイトル_7',status:'在庫あり'}
{id:8,title:'本タイトル_8',status:'在庫あり'}
{id:9,title:'本タイトル_9',status:'レンタル中'}
{id:10,title:'本タイトル_10',status:'在庫あり'}
]}

どうでしょうか?
目的の追加と機能追加を分離することでクラス設計がシンプルになることが分かると思います。
データ生成のパターンと組み合わせるとより使いやすくなるので、どんどん使っていきたいですね!