浅谈 Laravel Collections

这两天看了两本书《Laravel Collections Unraveled》和 《Refactoring to Collections》。

学习了如何将数组 items 重构成 Collection,以及为什么这么做。

其中,一个核心思想就是:Never write another loop again。

下面把学到的知识简单梳理出来,重点学习 Laravel 使用的 Collection。

为何重构

我不想把重构说成是包治百病的万灵丹,但可以帮助你始终良好地控制自己的代码。重构是个工具,它可以(并且应该)用于以下几个目的。

重构改进软件设计

同样一件事,设计不良的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同的语句做同样的事。因此改进设计的一个重要方向就是消除重复代码。这个动作的重要性在于方便未来的修改。代码量减少并不会使系统运行更快,因为这对程序的运行轨迹几乎没有任何明显影响。然而代码量减少将使未来可能的程序修改动作容易得多。

重构使软件更容易理解

所谓程序设计,很大程度上就是与计算机交谈:你编写代码告诉计算机做什么事,它的响应则是精确按照你的指示行动。你得及时填补“想要他做什么”和“告诉它做什么”之间的缝隙。这种编程模式的核心就是“准确说出我所要的”。除了计算机外,你的源码还有其他读者:几个月后可能会有另一个程序员尝试读懂你的代码并做一些修改。我们很容易忘记第二位读者,但他才是最重要的。计算机是否多花了几个小时才编译,又有什么关系呢?如果一个程序员花费一周时间来修改某段代码,那才要命呢——如果他理解了你的代码,这个修改原来只需一小时。

总之一句话,不要让你的代码成为下一个接盘者嘴里的:垃圾代码

重构帮助找到 bug

对代码的理解,可以帮助我找到 bug。我承认我不太擅长调试。有些人只要盯着一大段代码就可以找出里面的 bug,我可不行。但我发现,如果对代码进行重构,我就可以深入理解代码的作为,并恰到好处地把新的理解反馈回去。搞清楚程序结构的同时,我也清楚了自己所做的一些假设,于是想不把 bug 揪出来都难。

这让我想起了 Kent Beck 经常形容自己的一句话:“我不是个伟大的程序员,我只是个有着一些优秀习惯的好程序员。”重构能够帮助我更有效地写出强健的代码。

重构提高编程速度

我绝对相信:良好的设计是快速开发的根本——事实上,拥有良好设计才可能做到快速开发。如果没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,无法添加新功能。修改时间愈来愈长,因为你必须花愈来愈多的时间去理解系统、寻找重复代码。随着你给最初程序打上一个又一个的补丁,新特性需要等多代码才能实现。真是个恶性循环。

良好设计是维持软件开发速度的根本。重构可以帮助你更快速地开发软件,因为它阻止系统腐败变质,它甚至还可以提高设计质量。

我相信这也是为什么很多优秀的框架能得到很多人的认可和使用,因为他们的框架可以提高我们的编程速度,要不我们为什么要去使用他们呢?其中 Laravel 就是其中的代表。

以上主要摘自《重构——改善既有代码的设计》,推荐大家看看此书。

Refactoring to Collection 三要素

本着「Never write another loop again」此重构原则,我们需要找出 array 使用频率最多的「循环语句」,封装它,然后做成各种通用的高阶函数,最后形成 Collection 类。最后我们在使用 array 时,只要转变成 Collection 对象,就可以尽可能的 Never write another loop again。

循环语句

在对数组 items 进行操作时,我们避免不了使用循环语句去处理我们的逻辑。

如,我们想拿到所有用户的邮箱地址,也许我们这么写:

function getUserEmails($users) {

    // 1. 创建空数组用于保存结果
    $emails = [];

    // 2. 初始化变量 $i,用于遍历所有用户
    for ($i = 0; $i < count($users); $i++) {
        $emails[] = $$users[$i]->email;
    }

    return $emails;
}

又如,我们要对数组每个元素 *3 计算:

function cheng3($data) {
    for ($i = 0; $i < count($data); $i++) {
        $data[$i] *= 3;
    }
}

又如,我们要把贵的商品挑出来:

function expensive($products) {
    $expensiveProducts = [];

    foreach ($products as $product) { 
        if ($product->price > 100) { 
            $expensiveProducts[] = $product; 
        } 
    }

    return $expensiveProducts;
}

对数组的操作,这类例子太多了,终究都是通过循环来对数组的每个元素进行操作。

而我们重构的思路就是:把循环的地方封装起来,这样最大的避免我们在写业务逻辑时,自己去写循环语句 (让循环语句见鬼去吧)。

Higher Order Functions

俗称:高阶函数 A higher order function is a function that takes another function as a parameter, returns a function, or does both.

使用高阶函数对上面四个例子进行改造。

第一个例子,主要的业务逻辑在于这条语句,获取每个用户的邮箱:

$emails[] = $$users[$i]->email;

将其他代码封装成如下 map 函数:

function map($items, $func) { 
    $results = [];

    foreach ($items as $item) { 
        $results[] = $func($item); 
    }

    return $results;
}

这样使用该 map 函数进行重构就简单:

function getUserEmails($users) {
    return $this->map($users, function ($user) {
        return $user->email;
    });
}

相比较刚开始的写法,明显简单多了,而且也避免了不必要的变量。

一样的,对第二个例子进行重构,将循环语句封装成 each 函数:

function each($items, $func) {
    foreach ($items as $item) {
        $func($item);
    } 
}

这个 each 和 map 函数最大的区别在于,each 函数是对每个元素的处理逻辑,且没有返回新的数组。

使用 each 函数就比较简单:

function cube($data) {
    $this->each($data, function ($item) {
       return $item * 3;
    });
}

同样的对第三个例子进行重构,重构的对象在于价格的筛选判断上

if ($product->price > 100) { 
    $expensiveProducts[] = $product; 
}

我们参考 map 函数进行重构:

function filter($items, $func) { 
    $result = [];

    foreach ($items as $item) { 
        if ($func($item)) { 
            $result[] = $item; 
        } 
    }

    return $result;
}

当满足于 $func($item)条件的 item 都放入 $result 数组中。

使用就很简单:

return $this->filter($products, function ($product) {
    return $product->price > 100;
});

这里的 filter 函数和 map 函数的区别在于,map 函数是获取原有数组对应的属性集或者计算产生的新数组;而 filter 更多的是通过筛选符合条件的 item,构成的数组。

构造 Collection 类

我们把这些 map、each、filter 方法整合在一起构成一个 Collection 类

A collection is an object that bundles up an array and lets us perform array operations by calling methods on the collection instead of passing the array into functions.

其中 items 是唯一属性。核心的都是对 items 遍历,做各种各样的操作,具体看代码:

class Collection {
    protected $items;

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

    function map($items, $func) {
        $results = [];

        foreach ($items as $item) {
            $results[] = $func($item);
        }

        return $results;
    }

    function each($items, $func) {
        foreach ($items as $item) {
            $func($item);
        }
    }

    function filter($items, $func) {
        $result = [];

        foreach ($items as $item) {
            if ($func($item)) {
                $result[] = $item;
            }
        }

        return $result;
    }

    public function toArray() {
        return $this->items;
    }

}

当然到目前为止,自己封装的 Collection 雏形就已经有了,但还是达不到可以通用的水平。所以我们需要看看别人是怎么写的,当然这时候要祭出大招 —— Laravel 使用的

Illuminate\Support\Collection

解说 Illuminate\Support\Collection.

The Illuminate\Support\Collection class provides a fluent, convenient wrapper for working with arrays of data.

Collection 主要实现了以下几个接口:

  1. ArrayAccess
  2. Countable
  3. IteratorAggregate
  4. JsonSerializable and Laravel's own Arrayable and Jsonable

下面让我来一个个解说这几个接口的作用。

ArrayAccess

interface ArrayAccess {
    public function offsetExists($offset);
    public function offsetGet($offset);
    public function offsetSet($offset, $value);
    public function offsetUnset($offset);
}

实现这四个函数:

/**
     * Determine if an item exists at an offset.
     *
     * @param  mixed  $key
     * @return bool
     */
    public function offsetExists($key)
    {
        return array_key_exists($key, $this->items);
    }

    /**
     * Get an item at a given offset.
     *
     * @param  mixed  $key
     * @return mixed
     */
    public function offsetGet($key)
    {
        return $this->items[$key];
    }

    /**
     * Set the item at a given offset.
     *
     * @param  mixed  $key
     * @param  mixed  $value
     * @return void
     */
    public function offsetSet($key, $value)
    {
        if (is_null($key)) {
            $this->items[] = $value;
        } else {
            $this->items[$key] = $value;
        }
    }

    /**
     * Unset the item at a given offset.
     *
     * @param  string  $key
     * @return void
     */
    public function offsetUnset($key)
    {
        unset($this->items[$key]);
    }

这个接口更多的职责是让 Collection 类看起来像是个 array,主要是对 items 进行增删查和判断 item 是否存在。

Countable

interface Countable {

    /**
     * Count elements of an object
     * @link http://php.net/manual/en/countable.count.php
     * @return int The custom count as an integer.
     * </p>
     * <p>
     * The return value is cast to an integer.
     * @since 5.1.0
     */
    public function count();
}

具体实现:

    /**
     * Count the number of items in the collection.
     *
     * @return int
     */
    public function count()
    {
        return count($this->items);
    }

count() 这个方法使用率很高,而且在 PHP 中,arrays 没有具体实现该接口,我们基本没看到类似这样的 array->count()的。

IteratorAggregate

俗称:「聚合式迭代器」接口

/**
 * Interface to create an external Iterator.
 * @link http://php.net/manual/en/class.iteratoraggregate.php
 */
interface IteratorAggregate extends Traversable {

    /**
     * Retrieve an external iterator
     * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
     * @return Traversable An instance of an object implementing <b>Iterator</b> or
     * <b>Traversable</b>
     * @since 5.0.0
     */
    public function getIterator();
}

实现也简单,只是实例化 ArrayIterator:

/**
     * Get an iterator for the items.
     *
     * @return \ArrayIterator
     */
    public function getIterator()
    {
        return new ArrayIterator($this->items);
    }

ArrayIterator 的说明看这: https://php.golaravel.com/class.arrayiterator.html

Arrayable

interface Arrayable
{
    /**
     * Get the instance as an array.
     *
     * @return array
     */
    public function toArray();
}

具体实现,数组输出:

/**
     * Get the collection of items as a plain array.
     *
     * @return array
     */
    public function toArray()
    {
        return array_map(function ($value) {
            return $value instanceof Arrayable ? $value->toArray() : $value;
        }, $this->items);
    }

array_map — 为数组的每个元素应用回调函数

Jsonable + JsonSerializable

interface Jsonable
{
    /**
     * Convert the object to its JSON representation.
     *
     * @param  int  $options
     * @return string
     */
    public function toJson($options = 0);
}

具体实现,转成 JSON 格式,这方法比较常规使用:

/**
     * Convert the object into something JSON serializable.
     *
     * @return array
     */
    public function jsonSerialize()
    {
        return array_map(function ($value) {
            if ($value instanceof JsonSerializable) {
                return $value->jsonSerialize();
            } elseif ($value instanceof Jsonable) {
                return json_decode($value->toJson(), true);
            } elseif ($value instanceof Arrayable) {
                return $value->toArray();
            }

            return $value;
        }, $this->items);
    }

    /**
     * Get the collection of items as JSON.
     *
     * @param  int  $options
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this->jsonSerialize(), $options);
    }

其他函数

tap() 发现在 Collection 类中,有个 tap 函数:

/**
     * Pass the collection to the given callback and then return it.
     *
     * @param  callable  $callback
     * @return $this
     */
    public function tap(callable $callback)
    {
        $callback(new static($this->items));

        return $this;
    }

关于 tap 的使用,可以看之前的文章链式编程

对于更多函数的使用,具体可以参考:

https://docs.golaravel.com/docs/5.6/collections/

当然,如果这些常规方法还满足不了你,你也可以对 Collection 类使用 Collection::macro 方法进行扩展:

use Illuminate\Support\Str;

Collection::macro('toUpper', function () {
    return $this->map(function ($value) {
        return Str::upper($value);
    });
});

$collection = collect(['first', 'second']);

$upper = $collection->toUpper();

// ['FIRST', 'SECOND']

具体实现看 Macroable:

trait Macroable
{
    /**
     * The registered string macros.
     *
     * @var array
     */
    protected static $macros = [];

    /**
     * Register a custom macro.
     *
     * @param  string $name
     * @param  object|callable  $macro
     *
     * @return void
     */
    public static function macro($name, $macro)
    {
        static::$macros[$name] = $macro;
    }

    /**
     * Mix another object into the class.
     *
     * @param  object  $mixin
     * @return void
     */
    public static function mixin($mixin)
    {
        $methods = (new ReflectionClass($mixin))->getMethods(
            ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
        );

        foreach ($methods as $method) {
            $method->setAccessible(true);

            static::macro($method->name, $method->invoke($mixin));
        }
    }

    /**
     * Checks if macro is registered.
     *
     * @param  string  $name
     * @return bool
     */
    public static function hasMacro($name)
    {
        return isset(static::$macros[$name]);
    }

    /**
     * Dynamically handle calls to the class.
     *
     * @param  string  $method
     * @param  array   $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    public static function __callStatic($method, $parameters)
    {
        if (! static::hasMacro($method)) {
            throw new BadMethodCallException("Method {$method} does not exist.");
        }

        if (static::$macros[$method] instanceof Closure) {
            return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters);
        }

        return call_user_func_array(static::$macros[$method], $parameters);
    }

    /**
     * Dynamically handle calls to the class.
     *
     * @param  string  $method
     * @param  array   $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    public function __call($method, $parameters)
    {
        if (! static::hasMacro($method)) {
            throw new BadMethodCallException("Method {$method} does not exist.");
        }

        $macro = static::$macros[$method];

        if ($macro instanceof Closure) {
            return call_user_func_array($macro->bindTo($this, static::class), $parameters);
        }

        return call_user_func_array($macro, $parameters);
    }
}

总结

从这个 Collection 类我们可以看出 Laravel 的用心,和为什么我们能优雅的使用 Laravel 框架了。

只要涉及到 array 的操作和使用,我们都建议先转成 collect($items) —— Collection 对象,这样可以很方便的对数组进行操作。

接下来我们再好好学习学习用 Collection 作为基类的 Eloquent: Collections 的使用。

参考

1. Collections:https://docs.golaravel.com/docs/5.6/collections/

2. Never write another loop again. https://adamwathan.me/refactoring-to-collections/

3. 《laravel collections unraveled》

4. 《重构——改善既有代码的设计》

「未完待续」


coding01 期待您继续关注

qrcode

Copyright © coding01 2017 all right reserved,powered by Gitbook该文件修订时间: 2018-06-17 22:20:20

results matching ""

    No results matching ""