淺談 Laravel Collections

這兩天看了兩本書《Laravel Collections Unraveled》和 《Refactoring to Collections》。php

學習瞭如何將數組 items 重構成 Collection,以及爲何這麼作。html

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

下面把學到的知識簡單梳理出來,重點學習 Laravel 使用的 Collection。程序員

爲什麼重構

我不想把重構說成是包治百病的萬靈丹,但能夠幫助你始終良好地控制本身的代碼。重構是個工具,它能夠(而且應該)用於如下幾個目的。編程

重構改進軟件設計

一樣一件事,設計不良的程序每每須要更多代碼,這經常是由於代碼在不一樣的地方使用徹底相同的語句作一樣的事。所以改進設計的一個重要方向就是消除重複代碼。這個動做的重要性在於方便將來的修改。代碼量減小並不會使系統運行更快,由於這對程序的運行軌跡幾乎沒有任何明顯影響。然而代碼量減小將使將來可能的程序修改動做容易得多。json

重構使軟件更容易理解

所謂程序設計,很大程度上就是與計算機交談:你編寫代碼告訴計算機作什麼事,它的響應則是精確按照你的指示行動。你得及時填補「想要他作什麼」和「告訴它作什麼」之間的縫隙。這種編程模式的核心就是「準確說出我所要的」。除了計算機外,你的源碼還有其餘讀者:幾個月後可能會有另外一個程序員嘗試讀懂你的代碼並作一些修改。咱們很容易忘記第二位讀者,但他纔是最重要的。計算機是否多花了幾個小時才編譯,又有什麼關係呢?若是一個程序員花費一週時間來修改某段代碼,那纔要命呢——若是他理解了你的代碼,這個修改原來只需一小時。數組

總之一句話,不要讓你的代碼成爲下一個接盤者嘴裏的:垃圾代碼微信

重構幫助找到 bug

對代碼的理解,能夠幫助我找到 bug。我認可我不太擅長調試。有些人只要盯着一大段代碼就能夠找出裏面的 bug,我可不行。但我發現,若是對代碼進行重構,我就能夠深刻理解代碼的做爲,並恰到好處地把新的理解反饋回去。搞清楚程序結構的同時,我也清楚了本身所作的一些假設,因而想不把 bug 揪出來都難。app

這讓我想起了 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 的使用,能夠看以前的文章鏈式編程

對於更多函數的使用,具體能夠參考:

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
相關文章
相關標籤/搜索