这两天看了两本书《Laravel Collections Unraveled》和 《Refactoring to Collections》。php
学习了如何将数组 items 重构成 Collection,以及为何这么作。html
其中,一个核心思想就是:Never write another loop again。laravel
下面把学到的知识简单梳理出来,重点学习 Laravel 使用的 Collection。程序员
我不想把重构说成是包治百病的万灵丹,但能够帮助你始终良好地控制本身的代码。重构是个工具,它能够(而且应该)用于如下几个目的。编程
一样一件事,设计不良的程序每每须要更多代码,这经常是由于代码在不一样的地方使用彻底相同的语句作一样的事。所以改进设计的一个重要方向就是消除重复代码。这个动做的重要性在于方便将来的修改。代码量减小并不会使系统运行更快,由于这对程序的运行轨迹几乎没有任何明显影响。然而代码量减小将使将来可能的程序修改动做容易得多。json
所谓程序设计,很大程度上就是与计算机交谈:你编写代码告诉计算机作什么事,它的响应则是精确按照你的指示行动。你得及时填补“想要他作什么”和“告诉它作什么”之间的缝隙。这种编程模式的核心就是“准确说出我所要的”。除了计算机外,你的源码还有其余读者:几个月后可能会有另外一个程序员尝试读懂你的代码并作一些修改。咱们很容易忘记第二位读者,但他才是最重要的。计算机是否多花了几个小时才编译,又有什么关系呢?若是一个程序员花费一周时间来修改某段代码,那才要命呢——若是他理解了你的代码,这个修改原来只需一小时。数组
总之一句话,不要让你的代码成为下一个接盘者嘴里的:垃圾代码微信
对代码的理解,能够帮助我找到 bug。我认可我不太擅长调试。有些人只要盯着一大段代码就能够找出里面的 bug,我可不行。但我发现,若是对代码进行重构,我就能够深刻理解代码的做为,并恰到好处地把新的理解反馈回去。搞清楚程序结构的同时,我也清楚了本身所作的一些假设,因而想不把 bug 揪出来都难。app
这让我想起了 Kent Beck 常常形容本身的一句话:“我不是个伟大的程序员,我只是个有着一些优秀习惯的好程序员。”重构可以帮助我更有效地写出强健的代码。框架
我绝对相信:良好的设计是快速开发的根本——事实上,拥有良好设计才可能作到快速开发。若是没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,没法添加新功能。修改时间越来越长,由于你必须花越来越多的时间去理解系统、寻找重复代码。随着你给最初程序打上一个又一个的补丁,新特性须要等多代码才能实现。真是个恶性循环。
良好设计是维持软件开发速度的根本。重构能够帮助你更快速地开发软件,由于它阻止系统腐败变质,它甚至还能够提升设计质量。
我相信这也是为何不少优秀的框架能获得不少人的承认和使用,由于他们的框架能够提升咱们的编程速度,要不咱们为何要去使用他们呢?其中 Laravel 就是其中的表明。
以上主要摘自《重构——改善既有代码的设计》,推荐你们看看此书。
本着「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;
}
复制代码
对数组的操做,这类例子太多了,终究都是经过循环来对数组的每一个元素进行操做。
而咱们重构的思路就是:把循环的地方封装起来,这样最大的避免咱们在写业务逻辑时,本身去写循环语句 (让循环语句见鬼去吧)。
俗称:高阶函数 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,构成的数组。
咱们把这些 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
The Illuminate\Support\Collection class provides a fluent, convenient wrapper for working with arrays of data.
Collection 主要实现了如下几个接口:
- ArrayAccess
- Countable
- IteratorAggregate
- JsonSerializable and Laravel's own Arrayable and Jsonable
下面让我来一个个解说这几个接口的做用。
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 是否存在。
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()
的。
俗称:「聚合式迭代器」接口
/** * 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…
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 — 为数组的每一个元素应用回调函数
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 的使用,能够看以前的文章链式编程
对于更多函数的使用,具体能够参考:
固然,若是这些常规方法还知足不了你,你也能够对 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