いえらぶ > ブログ > 記事詳細

  • faebook
  • ツイッター
  • グーグル+
  • bookmark
  • LINEで送る
  • pocket

PHPのジェネレータを使ってコード簡素化&メモリ節約する方法

こんにちは、鰆目です。

サービスを長く保守・運用していると、機能の追加などでシステムが肥大化し、プログラムのソースコードが冗長化したりメモリを無駄に消費してしまっていたりします。

ソースコードが冗長化してしまうと保守性が低下しバグの原因になったしますし、メモリ消費も放置しておくとシステム障害に直結してしまう深刻な問題になったりするんですよね。

今回はPHPのジェネレータという機能を使ってソースコードを簡素化し、且つメモリ消費も抑えるテクニックを紹介したいと思います。

あ、これは肥大化したウチの猫助です。餌の与え過ぎには注意しましょう。

クラスの肥大化

例えばメンバ変数の配列の値を表示するというだけの単純なクラスがあったとします。

<?php
class hoge
{
    private $piyo = array(
        1 => 'a',
        2 => 'b',
        3 => 'c',
    );
    public function huga1()
    {
        foreach($this->piyo as $key => $value)
        {
            echo $value. "\n";
        }
    }
}

$hoge = new hoge();

$hoge->huga1();

このクラスに機能を追加して配列の添字を表示したいってなった時には、どのように改修すればいいでしょうか。

単純に機能を追加する場合はメソッドを追加して対応します。

<?php
class hoge
{
    private $piyo = array(
        1 => 'a',
        2 => 'b',
        3 => 'c',
    );
    public function huga1()
    {
        foreach($this->piyo as $key => $value)
        {
            echo $value. "\n";
        }
    }
    public function huga2()
    {
        foreach($this->piyo as $key => $value)
        {
            echo $key. "\n";
        }
    }
}

$hoge = new hoge();

$hoge->huga1();
$hoge->huga2();

huga2()を追加しました。

では、更に配列の添字と値を両方表示する機能や、添字の合計を表示する機能を追加する必要が出てきた場合。

上記のようにhuga3()・huga4()・・・とメソッドを追加していてはクラスがどんどん長くなってしまいますし、そのクラスの一つのメソッドの機能のみを利用したい場合に長くなったクラスを読み込まなければならずメモリも無駄に消費してしまいます。これがクラスの肥大化です。

ループ処理は同じで中身の処理が違うというケースはサブルーチン化してまとめることが難しいように感じますが、クロージャという機能を使ってこのような肥大化を防ぐことが出来ます。

クロージャを使った方法

クロージャとは無名関数といい、関数を関数の引数として利用することが出来ます。

<?php
class hoge
{
    private $piyo = array(
        1 => 'a',
        2 => 'b',
        3 => 'c',
    );
    public function hugaCallback(callable $function)
    {
        foreach($this->piyo as $key => $value)
        {
            if(false === $function($key, $value))
            {
                break;
            }
        }
    }
}

$hoge = new hoge();

$hoge->hugaCallback(function($key, $value)
{
    echo $value. "\n";
});

$hoge->hugaCallback(function($key, $value)
{
    if($key > 2)
    {
        return false;
    }
    echo $key. "\n";
});

このようにhugaCallbackに中身の処理を行う関数を渡して実行することが出来ます。falseを返すとbreakされるようになっています。continueする場合はfalse以外を返せばOKです。

クロージャを使用する事によってループ処理は共通で中身の処理を変更することが出来ました。これで肥大化しがちなクラスの実装を抑えることが出来ます。ですが、ジェネレータを使うと更にシンプルに機能を実装することが出来ます。

ジェネレータを使った方法

ジェネレータとは関数の動きを一時停止して再度再開することが出来る機能です。ジェネレータを使用するにはyieldキーワードを使用します。

<?php
class hoge
{
    private $piyo = array(
        1 => 'a',
        2 => 'b',
        3 => 'c',
    );
    public function hugaGenerator()
    {
        foreach($this->piyo as $key => $value)
        {
            yield $key => $value;
        }
    }
}

$hoge = new hoge();

foreach($hoge->hugaGenerator() as $key => $value)
{
    echo $value. "\n";
}

foreach($hoge->hugaGenerator() as $key => $value)
{
    if($key > 2)
    {
        break;
    }
    echo $key. "\n";
}

yieldはreturnの様に値を返します。またループで処理を再開することができるので、そのままforeachで回せる様になっています。

さらにIteratorAggregateインターフェースを使用すると、オブジェクトをまるっとforeachで回せるようになるので、更にコードが簡素化出来ます。

通常オブジェクトをforeachで回すとpublicなメンバ変数でループされますが、IteratorAggregateではgetIterator()でループされるようになります。

<?php
class hoge implements IteratorAggregate
{
    private $piyo = array(
        1 => 'a',
        2 => 'b',
        3 => 'c',
    );
    public function getIterator()
    {
        foreach($this->piyo as $key => $value)
        {
            yield $key => $value;
        }
    }
}

$hoge = new hoge();

foreach($hoge as $key => $value)
{
    echo $value. "\n";
}

foreach($hoge as $key => $value)
{
    if($key > 2)
    {
        break;
    }
    echo $key. "\n";
}

クロージャを使った場合よりも読みやすくシンプルになりましたね。まあ、個人的好みかもしれませんが。。。breakがそのままイケるっていうのはわかりやすくていいと思います。

で、ジェネレータで実際何が出来るのか

では実際にジェネレータを使ったもう少し具体的なクラスを紹介します。

<?php
class Gen implements IteratorAggregate
{
    /**
     * データベースサーバーへの接続リソース
     * @var mysqli
     */
    protected $_link = null;

    /**
     * SQLの実行結果セット
     * @var mysqli_result
     */
    protected $_result = null;

    /**
     * MySQLサーバーへ接続する
     */
    public function __construct()
    {
        $this->_link = mysqli_init();
        return mysqli_real_connect(
            $this->_link
            ,'localhost'
            ,'root'
            ,'password'
            ,'test'
        );
    }

    /**
     * SQLを実行する
     * @param  string $sql SQL文
     * @return Gen         このオブジェクト
     */
    public function query($sql)
    {
        $this->free();
        $this->_result = mysqli_query($this->_link, $sql);
        return $this;
    }

    /**
     * SQL実行結果の行を取得する
     * @return array SQLで取得した行
     */
    public function getIterator()
    {
        $i = 0;
        while($row = mysqli_fetch_assoc($this->_result))
        {
            yield ($i++) => $row;
        }
        $this->free();
    }

    /**
     * 結果セット開放
     * @return null
     */
    public function free()
    {
        if($this->_result instanceof mysqli_result)
        {
            mysqli_free_result($this->_result);
        }
        $this->_result = null;
    }

    /**
     * MySQLサーバーへの接続を切断
     */
    public function close()
    {
        $this->free();
        if($this->_link instanceof mysqli)
        {
            mysqli_close($this->_link);
        }
        $this->_link = null;
    }
}

データベース接続・切断・SQLを実行する機能を実装し、SQLの実行結果をジェネレータを使ってループ処理で返すようにしています。

このクラスの使用例は以下の様になります。

<?php
$gen = new Gen();

foreach($gen->query('SELECT * FROM table1') as $key => $row)
{
    echo $row['id'];
}

foreach($gen->query('SELECT * FROM table2') as $key => $row)
{
    echo $row['id'];
}

SQLを実行したオブジェクトをそのままforeachで回せますので、ソースコードが非常にシンプルになります。

また、ジェネレータを使わずにSQLの実行結果をforeachで回す場合、一旦配列などに落としこんだりする必要が有り、データ量に応じてメモリも消費してしまうという問題が発生してしまいますが、ジェネレータを使うとストリーミング的にforeachでデータを取得出来ますので、データ量が増えてもメモリが消費されてしまうという事はありません。ジェネレータを使用することによって大規模データも扱えるようになります。

まとめ

ジェネレータを使うと

1. ソースコードの簡素化

2. クラスメソッドをまとめることによりインスタンス作成時のメモリ削減

3. ストリーミング処理により大規模データを扱えるようになる

ということが実現できます。まさにイイことづくめ。

こんな便利なジェネレータ機能はPHP5.5から利用できます。それ未満のバージョンを使用している場合は、先に紹介したクロージャでもある程度何でも出来ますのでクロージャを使うといいと思います。

クロージャも利用できるのはPHP5.3からなので、それ未満のバージョンの場合は。。。

PHPのバージョンアップを検討しましょう。

ではまた。


ページトップ

戻る