ファーストクラスコレクションを用いてコードをリファクタリングする過程を、順を追ってみていきましょう。
書店に貯蔵されている書籍の管理をするシステムを考えます。
データストアには複数の書籍データが格納されており、プログラム上ではタイトル・価格をプロパティに持つ書籍(Book
)クラスで書籍1冊を表現しています。
class Book {
private $title;
private $price;
// ...
}
このシステムに、書籍のリストを一覧表示し、さらに合計の価格を表示する機能を実装してみます。
まずは配列で考えてみる
まずは単純に配列で考えてみます。
※ $bookRepository
はデータストアから書籍を取り出すためのモジュールで、getList()
はBook
クラスの配列を返却するメソッド、
$view->print()
は画面に表示するための便利な処理だと思って読んでください。
$books = $bookRepository->getList();
foreach ($books as $book) {
$view->print($book->getTitle());
$view->print($book->getPrice());
}
次に、貯蔵する書籍の合計価格を画面の一番上に表示してみます。
配列の場合は一例ですがこんな感じになります。
$books = $bookRepository->getList();
//価格を表示
$sumPrice = 0;
foreach ($books as $book) {
$sumPrice += $book->getPrice();
}
$view->print($sumPrice);
//リストを表示
foreach ($books as $book) {
$view->print($book->getTitle());
$view->print($book->getPrice());
}
ちょっと見づらくなってきました。
「書籍リスト」をクラス化する
ここでファーストクラスコレクションを導入してみます。
書籍のリストを表す書籍リスト(BookList
)クラスを新たに作ります。
class BookList
{
/** @var Book[] **/
private $books;
/**
* コンストラクタ
*/
public function __construct(array $books)
{
$this->books = $books;
}
/**
* 書籍リストを取得
*/
public function getBooks()
{
return $this->books;
}
}
一覧画面の処理を書き換えます。
$books = $bookRepository->getList();
$bookList = new BookList($books);
//価格を表示
$sumPrice = 0;
foreach ($bookList->getBooks() as $book) {
$sumPrice += $book->getPrice();
}
$view->print($sumPrice);
//リストを表示
foreach ($bookList->getBooks() as $book) {
$view->print($book->getTitle());
$view->print($book->getPrice());
}
まだちょっと見づらいですね。
合計価格を計算する処理を委譲
書籍リストに、合計価格を計算するメソッドを追加します。
class BookList
{
// ...
public function getSumPrice() : int
{
$sumPrice = 0;
foreach ($this->books as $book) {
$sumPrice += $book->getPrice();
}
return $sumPrice;
}
// ...
}
一覧画面では、このメソッドを呼んで合計価格を表示するようにします。
$books = $bookRepository->getList();
$bookList = new BookList($books);
//価格を表示
$view->print($bookList->getSumPrice());
//リストを表示
foreach ($bookList->getBooks() as $book) {
$view->print($book->getTitle());
$view->print($book->getPrice());
}
少し見やすくなりました。
IteratorAggregate
でforeachできるようにする
この部分、ちょっと気持ち悪くないですか?
foreach ($bookList->getBooks() as $book) {
$bookList->getBooks()
の戻り値は配列です。
せっかく書籍リストをモデルとして扱えるようにしたのに、foreachで回すためにわざわざ配列に戻してます。
そこで、BookList
クラスのインスタンスのまま、foreachで回せるようにします。
今回は IteratorAggregate
を使います。
BookList
の定義を変更します。
class BookList implements \IteratorAggregate
{
// ...
public function getIterator()
{
return new \ArrayIterator($this->books);
}
}
\IteratorAggregate
はPHP組み込みのインターフェースです。
これを実装(implements
)して、getIterator
というメソッドで、
foreachしたい配列をコンストラクタに渡した\ArrayIterator
のインスタンスを返すようにします。
すると、BookList
をforeachで直接扱えるようになります。
$books = $bookRepository->getList();
$bookList = new BookList($books);
//価格を表示
$view->print($bookList->getSumPrice());
//リストを表示
foreach ($bookList as $book) {
$view->print($book->getTitle());
$view->print($book->getPrice());
}
リポジトリの実装を変える
いままで$bookRepository->getList()
はBook
の配列を返すメソッドだったので、
一覧画面の処理で以下のようにBookListに変換してました。
$books = $bookRepository->getList();
$bookList = new BookList($books);
でも、そもそもgetList()
の時点でBookList
を返してしまえば、
使う方で変換する必要も無くなります。
実装はデータストアによっていろいろだと思うので省略しますが、
インターフェースで表現するとこうなります。
//変更前
/** @return Book[] */
public getList() : array
//変更後
public getList() : BookList
一覧画面の処理を変更します。
$bookList = $bookRepository->getList();
//価格を表示
$view->print($bookList->getSumPrice());
//リストを表示
foreach ($bookList as $book) {
$view->print($book->getTitle());
$view->print($book->getPrice());
}
一覧画面の処理から、Book
クラスの配列が無くなりました。
PHPには配列の要素の型を制限する機能はありません。
ですので、配列を返却するメソッドの戻り値は以下のように書くしかありません。
/** @return Book[] */
public getList() : array
戻り値の型宣言にはarray
としか書けないので、PHPDocsでBook
クラスの配列であることを明示しています。
これなら確かに、IDEで作業している最中は補完も効くし、間違った型の変数を返そうとしていたら、
何らかの警告を発してくれるかもしれません。
しかし、実際に実行する際には、たとえ間違った型(数値の配列とか)が返却されようとしてもエラーにはなりません。
ここでエラーにならずに全く他の場所で、意味不明なエラーが起こってしまう危険性があります。
ファーストクラスコレクションを使うと、PHPDocsなどを使わずとも、
静的に返却値を指定することができます。
public getList() : BookList
こうしておけば、もし間違った型の配列が返されそうになれば、その時点でエラーになります。
これで返却されるのが書籍リストであるということが保証されるため、
型安全なコードを書くことができるというワケです。