1. Home
  2. PHP
  3. DesignPatterns
  4. Iteratorパターン | PHPデザインパターン

Iteratorパターン | PHPデザインパターン

  • 公開日
  • 更新日
  • カテゴリ:DesignPatterns
  • タグ:PHP,Behavior,DesignPatterns,Iterator
Iteratorパターン | PHPデザインパターン

Iterator(イテレータ)パターンとは、「反復」を意味し、繰り返し処理を行う一連の流れをパターン化したものになります。

繰り返し処理なので、PHP で言うループ、for, foreach, while などが反復処理としてイメージできると思いますが、それらのような処理を効率的に行う為の1つのモデル(パターン)がこの Iterator パターンになります。

なぜ Iterator パターンなのか

なぜ Iterator パターンを用いるのか。デザインパターンとはその名の通り、そもそも一種のパターンであり、より効率的に処理を実装できるように提案された1つの処理モデルに過ぎません。 Iterator パターンを用いる事によって、反復処理をより短い時間で、変更に強く手間の少ない効率的な実装を行えるようになります。

よくある反復処理のケース

それではここから、具体的な例を用いて解説していきます。まずは、Iterator パターンではない場合の例です。

今回はあるコミュニティの「ユーザー」を扱うクラスと、それをまとめる「名簿」のクラスがあったとします。何も考えずにただ書くと、以下のようになります。

<?php

/**
 * ユーザークラス
 * Class User
 */
class User
{
    protected $name;

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

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

/**
 * 名簿クラス
 * Class Roster
 */
class Roster
{
    protected $userList = [];

    public function setUserList($user)
    {
        $this->userList[] = $user;
    }

    public function getUserList()
    {
        return $this->userList;
    }
}

$roster = new Roster();
$roster->setUserList(new User('name 01'));
$roster->setUserList(new User('name 02'));
$roster->setUserList(new User('name 03'));
$roster->setUserList(new User('name 04'));

foreach($roster->getUserList() as $user) {
    echo $user->getName();
    echo "<br>";
}

ユーザを追加し、リストとしてユーザの名前をループで出力している処理になります。例えば、ここに年齢も加えようとしたら、どういった改修が必要でしょうか?単純に追加していくならば、以下のようになるかと思います。

<?php

/**
 * ユーザークラス
 * Class User
 */
class User
{
    protected $name;
    protected $age;

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

    public function getName()
    {
        return $this->name;
    }
    public function getAge()
    {
        return $this->age;
    }
}

/**
 * 名簿クラス
 * Class Roster
 */
class Roster
{
    protected $userList = [];

    public function setUserList($user)
    {
        $this->userList[] = $user;
    }

    public function getUserList()
    {
        return $this->userList;
    }

}

$roster = new Roster();
$roster->setUserList(new User('name 01', 20));
$roster->setUserList(new User('name 02', 21));
$roster->setUserList(new User('name 03', 22));
$roster->setUserList(new User('name 04', 23));

foreach($roster->getUserList() as $user) {
    echo sprintf("%s (%d)", $user->getName(), $user->getAge());
    echo "<br>";
}

User クラスにはコンストラクタで年齢を追加し、さらに年齢を返す getAge() メソッドを追加で実装しています。

そして forearch 時に、年齢を出力する処理を追加で記述しています。

これらの変更、今回の例は結構単純な構成なのであまり苦にはなりませんが、大規模な構成になった場合に変更点も多く、そのどれもに影響を与えるので、これまで安定して動作していたクラスにも改めて一通りのテストが必要になります。

また、ここでは例として出していないですが、ユーザとして保持している情報が「名前のみ」から「名前と年齢」になった事で、ループ時の変数の取り回しが「配列」から「連想配列」へ変わるパターンの場合、全体の変数の取り回し方を間違えると、ユーザ情報を出力するループ全てにこの変更を適用する必要が出てくるので、大規模なコード構成でこれをやってしまうと結構な手間になります。そしてもちちん、変更すべきどこかをし忘れるとエラーになるので、せっかくの安定稼働のコードから見事に不具合の出来上がりです。

Iterator パターンでの実装

それではこれまでの処理を Iterator パターンで実装します。まずはユーザの名前をリストと出力する例から。

<?php

/**
 * Aggregate Interface
 * Interface UsersAggregate
 */
interface UsersAggregateInterface {
    public function createIterator();
}

/**
 * Iterator Interface
 * Interface UserListIterator
 */
interface UserListIteratorInterface {
    public function hasNext();

    public function next();
}

/**
 * Iterator Class
 * Class UserListIterator
 */
class UserListIterator implements UserListIteratorInterface {

    private $users;
    private $position = 0;

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

    public function hasNext()
    {
        return isset($this->users[$this->position]);
    }

    public function next()
    {
        return $this->users[$this->position++];
    }
}

/**
 * 集約オブジェクト
 * Aggregate Class
 * Class UsersAggregate
 */
class UsersAggregate implements UsersAggregateInterface {

    private $userList;

    function __construct($users)
    {
        $this->userList = $users;
    }

    public function addUsersList($user)
    {
        $this->userList[] = $user;
    }

    public function getUserList()
    {
        return $this->userList;
    }

    public function createIterator()
    {
        return new UserListIterator($this->userList);
    }
}

/**
 * クライアント
 * Client Class
 * Class RosterClient
 */
class RosterClient {

    private $userIterator;

    function __construct(UsersAggregateInterface $user_list)
    {
        $this->userIterator = $user_list->createIterator();
    }

    function getUsers()
    {
        while ($this->userIterator->hasNext()) {
            $user = $this->userIterator->next();
            echo sprintf("%s", $user);
            echo "<br>";
        }
    }
}

$users = [ "name 01", "name 02", "name 03", "name 04", "name 05" ];
$list = new RosterClient(new UsersAggregate($users));

echo $list->getUsers();

集約オブジェクトとして、AggregateInterface を実装した UsersAggregate クラスを、イテレータとして IteratorInterface を実装した UserListIterator クラスを定義し、それらをクライアントである RosterClient クラスでコールする。という流れになっています。

次に、年齢も追加して出力してみます。ソースコードは以下になります。

<?php

/**
 * Aggregate Interface
 * Interface UsersAggregate
 */
interface UsersAggregateInterface {
    public function createIterator();
}

/**
 * Iterator Interface
 * Interface UserListIterator
 */
interface UserListIteratorInterface {
    public function hasNext();

    public function next();
}

/**
 * Iterator Class
 * Class UserListIterator
 */
class UserListIterator implements UserListIteratorInterface {

    private $users;
    private $position = 0;

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

    public function hasNext()
    {
        return isset($this->users[$this->position]);
    }

    public function next()
    {
        //return $this->users[$this->position++];

        $users = $this->users[$this->position++];
        $user['name'] = $users['name'];
        $user['age'] = $users['age'];
        return $user;
    }
}

/**
 * 集約オブジェクト
 * Aggregate Class
 * Class UsersAggregate
 */
class UsersAggregate implements UsersAggregateInterface {

    private $userList;

    function __construct($users)
    {
        $this->userList = $users;
    }

    public function addUsersList($user)
    {
        $this->userList[] = $user;
    }

    public function getUserList()
    {
        return $this->userList;
    }

    public function createIterator()
    {
        return new UserListIterator($this->userList);
    }
}

/**
 * クライアント
 * Client Class
 * Class RosterClient
 */
class RosterClient {

    private $userIterator;

    function __construct(UsersAggregateInterface $user_list)
    {
        $this->userIterator = $user_list->createIterator();
    }

    function getUsers()
    {
        while ($this->userIterator->hasNext()) {
            $user = $this->userIterator->next();
            // echo sprintf("%s", $user);
            echo sprintf("%s (%s)", $user['name'], $user['age']);
            echo "<br>";
        }
    }
}

//$users = ["name 01", "name 02", "name 03", "name 04"];
$users = [
    ["name" => "name 01", "age" => 20],
    ["name" => "name 02", "age" => 21],
    ["name" => "name 03", "age" => 22],
    ["name" => "name 04", "age" => 23]
];

$list = new RosterClient(new UsersAggregate($users));

echo $list->getUsers();

変更箇所をコメントアウトして記述していますが、変更したのは2ヶ所だけです。このように、変更箇所が最小で済むというのも デザインパターンを用いる利点でもあります。

更に特筆すべきは、実際に出力するコードを変更しなくても済む点です。(ここで言うところの最終行の $list->getUsers() です。)

また、今回はイテレータを変更して出力に年齢を追加しましたが、そのせいで名前のみを返す処理は消えてしまいます。 Iterator パターンなら、別のイテレータを作成する事で、名前のみを返すイテレータ、そして名前と年齢を返すイテレータを2つ存在させ、必要な部分に必要なイテレータを当てる事で、細かいハンドリングも可能になります。

<?php

/**
 * Aggregate Interface
 * Interface UsersAggregate
 */
interface UsersAggregateInterface {
    public function createIterator();
}

/**
 * Iterator Interface
 * Interface UserListIterator
 */
interface UserListIteratorInterface {
    public function hasNext();

    public function next();
}

/**
 * イテレータの共通処理
 * Trait SuperUserList
 */
trait SuperUserList
{
    private $users;
    private $position = 0;

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

    public function hasNext()
    {
        return isset($this->users[$this->position]);
    }
}

/**
 * 名前のみを返すイテレータ
 * Iterator Class
 * Class UserListNameIterator
 */
class UserListNameIterator implements UserListIteratorInterface {

    use SuperUserList;

    public function next()
    {
        return $this->users[$this->position++];
    }
}

/**
 * 名前と年齢を返すイテレータ
 * Iterator Class
 * Class UserListIterator
 */
class UserListIterator implements UserListIteratorInterface {

    use SuperUserList;

    public function next()
    {
        $user = $this->users[$this->position++];
        return sprintf("%s (%s)", $user['name'], $user['age']);
    }
}

/**
 * 集約オブジェクトの共通処理
 * Trait SuperUsersAggregate
 */
trait SuperUsersAggregate
{
    private $userList;

    function __construct($users)
    {
        $this->userList = $users;
    }

    public function addUsersList($user)
    {
        $this->userList[] = $user;
    }

    public function getUserList()
    {
        return $this->userList;
    }
}

/**
 * 集約オブジェクト
 * Aggregate Class
 * Class UsersAggregate
 */
class UsersNameAggregate implements UsersAggregateInterface {

    use SuperUsersAggregate;

    public function createIterator()
    {
        return new UserListNameIterator($this->userList);
    }
}

class UsersAggregate implements UsersAggregateInterface {

    use SuperUsersAggregate;

    public function createIterator()
    {
        return new UserListIterator($this->userList);
    }
}

/**
 * クライアント
 * Client Class
 * Class RosterClient
 */
class RosterClient {

    private $userIterator;

    function __construct(UsersAggregateInterface $user_list)
    {
        $this->userIterator = $user_list->createIterator();
    }

    function getUsers()
    {
        while ($this->userIterator->hasNext()) {
            $user = $this->userIterator->next();
            echo $user;
            echo "<br>";
        }
    }
}

$users_01 = ["name 01", "name 02", "name 03", "name 04"];
$users_02 = [
    ["name" => "name 01", "age" => 20],
    ["name" => "name 02", "age" => 21],
    ["name" => "name 03", "age" => 22],
    ["name" => "name 04", "age" => 23]
];


$list = new RosterClient(new UsersNameAggregate($users_01));

echo $list->getUsers();

$list = new RosterClient(new UsersAggregate($users_02));

echo $list->getUsers();

まとめ

Iterator パターンは要素の集合(コレクション)に対して順番にアクセスする為の方法を提供するための手法です。

反復に関する処理をこうしてパターン化した際に、抽象と具象とが関係性を築き、1つの処理モデルを構築する事で、各々が各々の持つ機能の処理だけに集中できます。そうすることで、外部から注入される何らかの新たな処理について気にしなくても良くなります。

そういった疎結合な関係を作る事で変更にも容易に対応できるようになり、またそれが影響範囲を絞る事にもなるので結果として開発も安全に進めていけるようになります。

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級