PHPでデザインパターン【Iterator】


デザインパターンの学習の一環で、各デザインパターンの実装をPHPで行ってみます。
また、振る舞いの確認としてPHPUnitを使用しています。

デザインパターンとは

ソフトウェア開発におけるデザインパターン(型紙(かたがみ)または設計パターン、英: design pattern)とは、過去のソフトウェア設計者が発見し編み出した設計ノウハウを蓄積し、名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものである。

引用元:デザインパターン (ソフトウェア) – Wikipedia( https://ja.wikipedia.org/wiki/デザインパターン_(ソフトウェア)

つまり過去のエンジニアが解決してきた方法が、パターンとしてまとめられているのがデザインパターンです。

デザインパターンと言ってもいろいろとありますが、最も有名なのはGoF(Gnag of Four)のまとめた23パターンかと思います。

今回はその23パターンのうちの一つである、Iteratorパターンについて実装していきます。

Iteratorパターンとは

イテレータ(英語: iterator)とは、プログラミング言語において配列やそれに類似する集合的データ構造(コレクションあるいはコンテナ)の各要素に対する繰り返し処理の抽象化である。実際のプログラミング言語では、オブジェクトまたは文法などとして現れる。JISでは反復子(はんぷくし)と翻訳されている

引用元: イテレータ – Wikipedia( http://ja.wikipedia.org/wiki/イテレータ

iterateという単語を辞書で引くと、「繰り返す、反復する」という意味が出てきます。
つまりiteratorパターンとは、集合体データの要素1つ1つに対して処理を行う方法を提供するパターンと言えます。

コード

PHPでIteratorパターンを実装する方法としては主に2種類あり、どちらもPHPの組み込みインターフェースを使用します。

1つ目は、Iteratorインターフェースを使う方法です。
2つ目は、IteratorAggregateインターフェースを使う方法です。

このどちらかのインターフェースを実装することで、foreach文でループ処理を行うことが可能となります。
(※ PHPのオブジェクトは特に何もしなくてもforeachで中身を取得することができます。ただし、取得できるものはpublicなメンバ変数に限ります。)

今回はサンプルとして、Department(部署)クラスとEmployee(従業員)クラスを用意し、
当該部署に所属する従業員に順次アクセスする、という動作イメージで実装していました。

Iteratorインターフェースを使う方法

実装(Iterator)

まずはEmployee(従業員)クラスを用意します。
サンプルのため、番号と名前を持つだけのシンプルなクラスとしています。

Employee.php
<?php
...
class Employee
{
    private $number;
    private $name;

    public function __construct(int $number, string $name)
    {
        $this->number = $number;
        $this->name   = $name;
    }

    public function getNumber(): int
    {
        return $this->number;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

そして、Employeeクラスで作成した従業員を所属させるDepartment(部署)のクラスを作成します。
PHPのIteratorインターフェース使用する場合、以下の5つのメソッドを実装する必要があります。

  • current()
  • key()
  • next()
  • rewind()
  • valid()
DepartmentIterator.php
<?php
...
class DepartmentIterator implements \Iterator
{
    private $name;
    private $employees = [];

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

    public function current()
    {
        return current($this->employees);
    }

    public function key()
    {
        return key($this->employees);
    }

    public function next()
    {
        return next($this->employees);
    }

    public function rewind()
    {
        reset($this->employees);
    }

    public function valid()
    {
        return $this->current() !== false;
    }

    public function addEmployee(Employee $employee): DepartmentIterator
    {
        $this->employees[] = $employee;
        return $this;
    }
}

動作確認(Iterator)

テストコードは以下のように実装しました。
DepartmentIteratorクラスのインスタンスがiterableであるかどうかの確認を行っています。

DepartmentIteratorTest.php
<?php
...
class DepartmentIteratorTest extends TestCase
{
    private $testEmployee;

    public function setUp()
    {
        parent::setUp();
        $this->testEmployee = new Employee(1, 'Test Employee1');
    }

    ... <省略> ...

    public function testIsIteratable()
    {
        $department = new DepartmentIterator('Test Department');
        $this->assertTrue(is_iterable($department));
    }
}

PHPUnitを実行してみます。

# PHPUnitを実行
./vendor/bin/phpunit tests/iterator/DepartmentIteratorTest.php --colors=always

# 結果
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

..........                                                        10 / 10 (100%)

Time: 32 ms, Memory: 4.00MB

OK (10 tests, 12 assertions)

無事テストが通ったので、DepartmentIteratorクラスはiterableなクラスであることが証明できました!

IteratorAggregateインターフェースを使う方法

実装(IteratorAggregate)

Employee(従業員)クラスは上記と同じものを使用するため省略します。

今回もEmployee(従業員)をまとめるクラスとして、部署のクラスを用意しますが、今回のクラスではIteratorAggregateインターフェースを実装します。

Iteratorインターフェースを実装する際は、5つものメソッドを実装する必要がありましたが、IteratorAggregateインターフェースを実装する場合は、実装するメソッドはgetIteratorのみとなります。

DepartmentIteratorAggregate.php
<?php
...
class DepartmentIteratorAggregate implements \IteratorAggregate
{
    private $name;
    private $employees;

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

    public function getIterator()
    {
        foreach ($this->employees as $key => $employee) {
            yield $key => $employee;
        }
    }

    public function addEmployee(Employee $employee): DepartmentIteratorAggregate
    {
        $this->employees[] = $employee;
        return $this;
    }
}

getIteratorメソッドは返り値として、IteratorあるいはTraversableインターフェースを実装したオブジェクトのインスタンスを返す必要があります。
(SPLにそれなりの数のIteratorクラスは用意されているため、それらを使用するといいかもしれません:http://php.net/manual/ja/spl.iterators.php)

またはサンプルのように、yieldを使ってgetIteratorメソッド自体をジェネレータ関数とすることも可能です。

これだけで、iterableなクラスを実装できました!
Iteratorインターフェースと比べて、はるかに簡単に実装できることがわかるかと思います。

動作確認(IteratorAggregate)

DepartmentIteratorのテストと同様、DepartmentIteratorAggregateクラスのインスタンスがiterableであるかどうかの確認を行っています。

DepartmentIteratorAggregateTest.php
<?php
...
class DepartmentIteratorAggregateTest extends TestCase
{
    private $testEmployee;

    public function setUp()
    {
        $this->testEmployee = new Employee(1, 'Test Employee1');
    }

    public function testIsIteratable()
    {
        $department = new DepartmentIteratorAggregate('Test Department');
        $this->assertTrue(is_iterable($department));
    }
    ... <省略> ...
}

PHPUnitを実行してみます。

# PHPUnitを実行
./vendor/bin/phpunit tests/iterator/DepartmentIteratorAggregateTest.php  --colors=always

# 結果
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 1.35 seconds, Memory: 4.00MB

無事テストが通ったので、DepartmentIteratorAggregateクラスはiterableなクラスであることが証明できました!

まとめ

オブジェクト内のpublicでないデータをループ処理で逐次実行する際には、このIteratorパターンは便利なのではないかと思います。

また、Iteratorを使用することで、メモリの消費を抑えられる場合もあるそうです。
(参考: https://qiita.com/yuya_takeyama/items/51fb058ed20d3df8209e

実装としては、Iteratorインターフェースを使用する方法と、
IteratorAggregateを使用する方法の2種類ありましたが、
IteratorAggregateの方が実装するメソッドも1つですみますし、ループのネストにも対応できます(参考: https://hnw.hatenablog.com/entry/20131025 )ので、特に理由がなければIteratorAggregateを使うことをお勧めします。

Iteratorパターンに関しては、以上となります。

■ コード
https://github.com/masato-d/design-pattern

■ リンク
- PHPでデザインパターン【Singleton】

※ 情報の誤りなどがありましたら、ぜひご指摘ください。