淺談 Laravel Collections

coding01發表於2018-02-26

這兩天看了兩本書《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 的說明看這: php.golaravel.com/class.array…

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 的使用,可以看之前的文章鏈式程式設計

淺談 Laravel Collections

對於更多函式的使用,具體可以參考:

docs.golaravel.com/docs/5.6/co…

當然,如果這些常規方法還滿足不了你,你也可以對 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:docs.golaravel.com/docs/5.6/co…

2. Never write another loop again. adamwathan.me/refactoring…

3. 《laravel collections unraveled》

4. 《重構——改善既有程式碼的設計》

「未完待續」


加個人微信,一起品品 Laravel

qrcode