17
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

配列の処理をファーストクラスコレクションに組み替えてみる

Last updated at Posted at 2019-06-06

ファーストクラスコレクションを用いてコードをリファクタリングする過程を、順を追ってみていきましょう。

書店に貯蔵されている書籍の管理をするシステムを考えます。
データストアには複数の書籍データが格納されており、プログラム上ではタイトル・価格をプロパティに持つ書籍(Book)クラスで書籍1冊を表現しています。

Book.php
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)クラスを新たに作ります。

BookList.php
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());
}

まだちょっと見づらいですね。

合計価格を計算する処理を委譲

書籍リストに、合計価格を計算するメソッドを追加します。

BookList.php
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の定義を変更します。

BookList.php
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

こうしておけば、もし間違った型の配列が返されそうになれば、その時点でエラーになります。
これで返却されるのが書籍リストであるということが保証されるため、
型安全なコードを書くことができるというワケです。

17
17
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?