PHP 设计模式-迭代模式

PHP 最有用的功能之一是 foreach 结构,使用foreach,我们可以轻松的迭代(循环)数组值和对象属性。迭代(iterator)模式允许我们将foreach的性能添加到任何对象的内部存储数据,而不仅仅添加到其公共属性。它覆盖了默认的foreach行为,并允许我们为循环注入业务逻辑。

  • Iterator ------ 最基本的迭代器
  • IteratorAggregate ------ 可以提供一个迭代器对象,但是它本身并不是一个迭代器。
  • RecursiveIteratorIterator ------ 用来遍历RecursiveIterators
  • FilterIterator ------ 可以对数据进行过滤的迭代器,只返回与过滤器相匹配的数据。
  • RegexIterator ------ FilterIterator中一个内置的具体实现,它使用正则表达式作为过滤器。
  • MultipleIterator ------ 可以依次遍历多个迭代的迭代器。
  • LimitIterator ------ 对其数据子集的迭代进行限制的过滤器(类似于 SQL 中的 LIMIT 、OFFSET 和 COUNT)。
  • ......

深入理解迭代器

让我们从迭代器开始。如果能深入的了解如何在 PHP 中遍历数组,那么你将很容易理解迭代器。
因为PHP从内部执行的所有动作都是可用的功能,所以我们大可以用do/while 循环编写一个自己的foreach

$array = array("Hello", "World");

reset($array);
do {
    echo '<pre>' . key($array) . ': ' . current($array) . '</pre>' . PHP_EOL;
} while (next($array));

正如你所见,首先我们调用 reset() 方法重置这个迭代。接着,我们在 while 条件内部调用 next(),如果到达了这个数组的末尾,next()将返回false,否则返回true,而且内部指针继续递增。最后,我们调用key()current(),他们将分别返回数组元素的键和值,用于指示内部指针的当前位置。这个脚本和foreach结构一样输出相同的结果。
这个脚本简单的输出是:

0: Hello
1: World

迭代器接口

现在让我们来看看迭代器的接口(注意这个接口使用rewind()而不是reset())。

interface Iterator extends Traversable {
    public current (  ); // 返回当前元素
    public key (  );  // 返回当前元素的键
    public next (  ); // 向前移动到下一个元素
    public rewind (  ); // 返回到迭代器的第一个元素
    public valid (  ); // 检查当前位置是否有效
}

这个迭代器介绍了valid()方法,它和next()结合调用。我们调用next()方法仅仅是为了递增指针,而valid()方法则负责将内部next()函数的返回结果返回true/false结果。

实例 1

让我们来看看下面的例子,其中使用了迭代器。

<?php

class BasicIterator implements Iterator
{
    private $key = 0;
    private $data = array(
        'hello',
        'world',
    );

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

    // 返回到迭代器的第一个元素
    public function rewind()
    {
        $this->key = 0;
    }

    // 返回当前元素
    public function current()
    {
        return $this->data[$this->key];
    }

    // 返回当前元素的键
    public function key()
    {
        return $this->key;
    }

    // 向前移动到下一个元素
    public function next()
    {
        $this->key++;
        return true;
    }

    // 检查当前位置是否有效
    public function valid()
    {
        return isset($this->data[$this->key]);
    }
}

echo 'do while -------------------------------------------';
$iterator = new BasicIterator();
$iterator->rewind();
do {
    $key = $iterator->key();
    $value = $iterator->current();
    echo '<pre>'. $key . ': ' . $value .'</pre>' . PHP_EOL;
} while ($iterator->next() && $iterator->valid());

echo 'foreach -------------------------------------------';
$iterator = new BasicIterator();
foreach ($iterator as $key => $value){
    echo '<pre>'. $key . ': ' . $value .'</pre>' . PHP_EOL;
}

在这个迭代器中,我们将数组指定给BasicIterator->data 属性,这个属性是受保护的,因此不能直接访问,我们必须使用类的方法来遍历和存取这些数据。
最终输出结果:

do while -------------------------------------------
0: hello
1: world
foreach -------------------------------------------
0: hello
1: world

正如你所见,最终结果与上面完全一致。尽管这个例子相当简单,但是我们的数据并不一定是一个简单的数组,它可能是数据库中被迭代提取得到的结果(即PDOStatement->fetch() 所为),或者一个 Web 服务或其他什么的结果。

在迭代器设计模式中,OuterIterator 是最好的理念之一,这是一个实际迭代器的代理。对于外部世界而言,OuterIterator 本身就是迭代器,但实际上,它仅用于代理调用一个内部迭代器。这允许OuterIterator 在内部迭代器毫不知情的情况下用某些特殊功能包裹该迭代器。

OuterIterator 是另外一种模式即代理模式的一个完美示例。如果将它和 ArrayIterator 联合使用,你可以使用任何数组作为内部迭代器,并且声称一个具有相同迭代行为的对象作为数组。

递归迭代

迭代器另一个重要特点就是递归(recursion) 递归迭代似乎常常让人犯错,而且许多开发者也不清楚 RecursiveIteratorRecursiveIteratorIterator 的区别。

这两个类之间的关系非常简单, RecursiveIterator 是数据结构(它是一个迭代器),其数据中包含有其他迭代器。 RecursiveIterator 的目的就是提供一个标准的方式,用于检查每一个迭代中是否包含子迭代。我们使用 hasChildren()getChildren() 方法可以做到这些。

另外,RecursiveIteratorIterator 用于实际遍历数据结构;它调用 hasChildren() ,如果有必要,也调用 getChildren() 方法,并且遍历子迭代。这意味着你可以使用一个简单的 foreach 结构遍历嵌套结构。(在多少次情况下你不得不嵌套多个 foreach 结构?)

实例2-多维 => 一维数组

让我们来看一个使用 内置 RecursiveArrayIterator 的简单示例,它将检查每一个数组的子元素是否也是一个数组,如果是,就会递归遍历该数组。

<?php
/**
 * 将多维数组 遍历为 一维数组
 */
$array = array(
    "Hello",
    array(
        "World",
    ),
    array(
        "How",
        array(
            "are",
            "you",
        )
    ),
    "doing?",
);

$recursiveIterator = new RecursiveArrayIterator($array);

$recursiveIteratorIterator = new RecursiveIteratorIterator($recursiveIterator);

foreach ($recursiveIteratorIterator as $key => $value) {
    echo '<pre>Depth: '. $recursiveIteratorIterator->getDepth() .'</pre>' . PHP_EOL;
    echo '<pre>Key: '. $key .'</pre>' . PHP_EOL;
    echo '<pre>Value: '. $value .'</pre>' . PHP_EOL;
    echo '----------------------------------------' . PHP_EOL;
}

因此,只使用一层 foreach,我们就可将这个三层多维数组递归遍历完毕。
file

这让递归树结构变的超级简单。

实例3-FilterIterator

让我们继续了解一些更为复杂的迭代器,名单上第一个就是 FilterIteratorFilterIterator 是一个必须被扩展的抽象类,它的用途正如你所期望的一样:对迭代进行过滤,跳过不符合筛选条件的值。FilterIterator 通过添加一个简单的 accept() 方法工作,这个方法必须返回一个布尔值,以表示当前的迭代是否可以接受。除了 next()valid() 之外,我们在每一次迭代中都调用 FilterIterator 方法。如果返回 false,迭代器将跳过这个值。

现在我们将创建一个只接受相同键值的过滤器。

<?php

/**
 * 让 foreach 只可以遍历出键奇数的元素
 */
class EvenFilterIterator extends FilterIterator
{
    // 检查迭代器的当前元素是否可接受
    public function accept()
    {
        // 获取内部迭代器
        $interator = $this->getInnerIterator();

        // 获取当前的 key
        $key = $interator->key();
        // 只接受 偶数 键值
        if ($key % 2 == 0) {
            return true;
        }
        return false;
    }
}

$array = array(
    0 => "Hello",
    1 => "Everybody Is",
    2 => "I'm",
    3 => "Amazing",
    4 => "The",
    5 => "Who",
    6 => "Doctor",
    7 => "Lives",
);

// 这个迭代器允许在遍历数组和对象时删除和更新值与键
$iterator = new ArrayIterator($array);

$filterIterator = new EvenFilterIterator($iterator);
foreach ($filterIterator as $key => $value) {
    echo '<pre>' . $key . ': ' . $value . '</pre>' . PHP_EOL;
}

请记住,我们未改变 ArrayIterator 的功能,这是使用 FilterIterator 概念的关键。这也意味着我们可以创建一个只接受奇数键值的 OddFilterIterator 或者 StepFilterIterator,它接受一个参数中的每一个 "n" 值。
前面代码的输出是:

0: Hello
2: I'm
4: The
6: Doctor

你可以对键或值进行筛选,并可以根据应用程序的要求设置你的 accept() 逻辑。

实例4-RegexIterator

另一个类似的迭代器是 RegexIterator (它实际上扩展了 FilterIterator ),它的 accept() 方法对当前值使用正则表达式。如果该值匹配正则表达式即表示接受accept() 方法。我们可以使用 RegexIterator 做一些很酷的东西,比如使用它和 RecursiveDirectoryIterator 找到所有的 PHP 文件。

<?php
/**
 * 列出当前工作目录中所有以 .php、phtml、php3、php4、5 为扩展名的文件
 */

// 创建一个 RecursiveDirectoryIterator
$directoryIterator = new RecursiveDirectoryIterator("./");

// 创建一个 RecursiveIteratorIterator 来递归迭代
$recursiveIterator = new RecursiveIteratorIterator($directoryIterator);

// 为 PHP 文件创建一个过滤器
$regexFilter = new RegexIterator($recursiveIterator, '/(.*?)\.(php|phtml|php3|php4|php5)$/');

// 遍历
foreach ($regexFilter as $key => $file) {
    echo $file->getFilename() . PHP_EOL;
}

输出:

json.php
xml.php
ajax.php
pdo.php
index.php
log.php
one.php

实例5.1-LimitIterator

另外一个类似的迭代器是 LimitIterator,正如我们前面所提到过的,它在使用时很像 SQL 中的 LIMIT 子句。

<?php
/**
 * 限制(LimitIterator)迭代器的使用
 */

$array = array(
    'Hello',
    'World',
    'How',
    'are',
    'you',
    'doing?',
);

// 创建一个迭代器
$iterator = new ArrayIterator($array);

// 创建限制迭代器,获取前2个元素
$limitIterator = new LimitIterator($iterator, 0, 2);

foreach ($limitIterator as $key => $value) {
    echo '<pre>' . $key . ': ' . $value . '</pre>' . PHP_EOL;
}

输出:

0: Hello
1: World

实例5.2-LimitIterator

由于 OuterIterator 概念的代理性质,我们实际上可以将他们叠加在一起使用,这才真正体现了迭代器的能力。在下面这个例子中,我们将 RecursiveIteratorIteratorLimitIterator 结合起来使用。

<?php
/**
 * 限制(LimitIterator)迭代器的使用
 */

$array = array(
    "Hello",
    array(
        "World",
    ),
    array(
        "How",
        array(
            "are",
            "you",
        ),
    ),
    "doing?",
);

$recursiveIterator = new RecursiveArrayIterator($array);

$recursiveIteratorIterator = new RecursiveIteratorIterator($recursiveIterator);

foreach ($recursiveIteratorIterator as $key => $value) {
    echo '<pre>Depth: ' . $recursiveIteratorIterator->getDepth() . '</pre>' . PHP_EOL;
    echo '<pre>Key: ' . $key . '</pre>' . PHP_EOL;
    echo '<pre>Value: ' . $value . '</pre>' . PHP_EOL;
    echo '-----------------------------------------' . PHP_EOL;
}
echo PHP_EOL . "==================================================================" . PHP_EOL;

$limitIterator = new LimitIterator($recursiveIteratorIterator, 2, 5);
foreach ($limitIterator as $key => $value) {
    $innerIterator = $limitIterator->getInnerIterator();
    echo '<pre>Depth: ' . $innerIterator->getDepth() . '</pre>' . PHP_EOL;
    echo '<pre>Key: ' . $key . '</pre>' . PHP_EOL;
    echo '<pre>Value: ' . $value . '</pre>' . PHP_EOL;
    echo '-----------------------------------------' . PHP_EOL;
}

在这种情况下,因为 RecursiveIteratorIterator 可以有效平面化多维结构,所有这个限制适用于扁平的数据,如果将一个家族数表示为一个数组,比如,我们可以使用 LimitIterator 来显示这个家族母系一方的曾祖父母。在任何情况下,我们的输出都是:
file

总结

迭代器模式是 PHP 中最通用和有益的模式之一。这种多功能性部分归功于数组在 PHP 主要数据结构中扮演的角色。随着 PHP 内部对迭代的支持,他们将更快速、更灵活、更易于理解并且易于使用。
通过使用 OuterIterator, 我们可以在一个面向对象的方式中轻而易举地重用以及扩充代码行为。坦率地说,这非常酷!

讨论数量: 0

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!