Refactoring to collection(译)

《Refactoring To Collection》git

本文是翻译Adam Wathan 的《Collection To Refactoring》的试读篇章,这篇文章内容很少,可是能够为咱们Laraver使用者能更好使用collection提供了可能性,很是值得看看。虽然是试读部分,可是Wathan仍是颇有诚意的,试读文章仍是能学到东西的,可是遗憾的是,我大概搜了下,目前好像尚未中文版,为此,我决定翻译这篇文章,让英文不太好的朋友,能够学习这篇文章。
获取试读文章:https://adamwathan.me/refactoring-to-collections/#sample

高阶函数

高阶函数就是参数为能够为function,而且返回值也可为function的函数。咱们举一个用高阶函数实现数据库事务的例子.代码以下:github

public function transaction($func)
   { 
    $this->beginTransaction();
    
    try { 
        $result = $func(); 
        $this->commitTransaction();
     } catch (Exception $e) {
        $this->rollbackTransaction(); throw $e; 
     }
        return $result;
    }

看下它的使用:web

try { 
        $databaseConnection->transaction(function () use ($comment) { 
            $comment->save(); 
        }); 
    } catch (Exception $e) { 
        echo "Something went wrong!"; 
    }

Noticing Patterns(注意模式)

高阶函数是很是强大的,由于咱们能够经过它把其余编程模式下所不能重用的部分逻辑给抽象出来。
比方说,咱们如今有顾客名单,但咱们须要获得他们的邮箱地址.咱们如今不用高阶函数,用一个foreach来实现它,代码以下。面试

$customerEmails = [];
    
    foreach ($customers as $customer) {   
        $customerEmails[] = $customer->email; 
    }
    
    return $customerEmails;

如今咱们有一批商品库存,咱们想知道每种商品的总价,咱们可能会这样处理:数据库

$stockTotals = [];
    
    foreach ($inventoryItems as $item) { 
        $stockTotals[] = [ 'product' => $item->productName, 'total_value' =>$item->quantity * $item->price, ]; 
     }
    
    return $stockTotals;

乍看之下,两个例子可能不太同样,可是把它们再抽象一下,若是你仔细观察,你会意识到其实两个例子之间只有一点是不同的.编程

在这两个例子中,咱们作的只是对数组中的每一个元素进行相应的操做再将其赋给一个新数组.两个例子真正的不一样点在于咱们对数组元素的处理不同。
在第一个例子中,咱们须要'email'属性。json

# $customerEmails = [];

    #foreach ($customers as $customer) { 
       $email = $customer->email;
       #$customerEmails[] = $email; 
    #}
    
    #return $customerEmails;

在第二个例子中,咱们用$item中的几个字段建立了一个新的关联数组.api

# $stockTotals = [];
    
    #foreach ($inventoryItems as $item) { 
        $stockTotal = [ 
        'product' => $item->productName, 
        'total_value' => $item->quantity * $item->price, 
        ];
       # $stockTotals[] = $stockTotal; 
    # }

   # return $stockTotals;

咱们把两个例子的逻辑处理简化一下,咱们能够获得以下代码:数组

$results = [];
    
    foreach ($items as $item) { 
      # $result = $item->email; 
       $results[] = $result; 
    }
    
    return $results;
$results = [];
    
    foreach ($items as $item) { 
      # $result = [ 
      #  'product' => $item->productName, 
      #  'total_value' => $item->quantity * $item->price,
      #  ]; 
      $results[] = $result;
    }
    
    return $results;

咱们如今接近抽象化了,可是中间那两个代码还在防碍着咱们进行下一步操做.咱们须要将這两部分取出来,而后用使得两个例子保持不变的东西来代替他们.app

咱们要作的就是把这两个代码放到匿名函数中,每一个匿名函数会将每一个数组元素做为其参数,而后进行相应的处理而且将其返回.

如下是用匿名函数处理email的实例:

$func = function ($customer) {
        return $customer->email; 
    };

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

    #return $results;

如下用匿名函数的商品库存实例:

$func = function ($item) { 
        return [ 
            'product' => $item->productName,
            'total_value' => $item->quantity * $item->price, 
        ]; 
     };
     
    #$results = [];
    
    #foreach ($items as $item) { 
        $result = $func($item);
         #$results[] = $result; 
    #}
    
    #return $results;

如今咱们看到两个例子中有不少相同的代码咱们能够提取出来重用,若是咱们将其运用到本身的函数中,咱们能够实现一个更高阶的函数叫map();

function map($items, $func)
    { 
         $results = [];
         
         foreach ($items as $item) { 
             $results[] = $func($item); 
         }
         
         return $results;
    }
    
    $customerEmails = map($customers, function ($customer) {
     return $customer->email; 
     });
     
    $stockTotals = map($inventoryItems, function ($item) { 
         return [ 
             'product' => $item->productName,
              'total_value' => $item->quantity * $item->price, 
         ];
     });

Functional Building Blocks(功能构件块)

map()函数是强大的处理数组的高阶函数中的一种,以后的例子中咱们会讲到这部分,可是如今让咱们来深刻了解下基础知识。

Each

Each只是一个foreach循环嵌套一个高阶函数罢了,以下:

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

你或许会问你本身:"为何会很厌烦写这个逻辑?"它隐藏了循环的详细实现(而且咱们讨厌写循环逻辑).

假如PHP没有foreach循环,那each()实现方式就会变成这样:

function each($items, $func) 
    { 
        for ($i = 0; $i < count($items); $i++) { 
             $func($items[$i]); 
        }    
    }

若是是没有foreach,那么就须要把对每一个数组元素的处理进行抽象化.代码就会变成这样:

for ($i = 0; $i < count($productsToDelete); $i++) {         
         $productsToDelete[$i]->delete(); 
    }

把它重写一下,让它变得更富有表达力.

each($productsToDelete, function ($product) {
          $product->delete(); 
    });

一旦你上手了链式功能操做,Each()在使用foreach循环时会有明显的提高,这部份咱们会在以后讲到.

在使用Each()有几件事须要注意下:

  • 若是你想得到集合中的某个元素,你不该该使用Each()

// Bad! Use `map` instead. 
   each($customers, function ($customer) use (&$emails) { 
         $emails[] = $customer->email; 
   });
   // Good! 
   $emails = map($customers, function ($customer) { 
         return $customer->email; 
   });
  • 不像其余的数组处理函数,each不会返回任何值.由此可得,Each适合于执行一些逻辑处理,好比说像'删除商品','装货单','发送邮件',等等.

each($orders, function ($order) { 
         $order->markAsShipped(); 
   });

MAP

咱们在前文屡次提到过map(),可是它是一个很重要的函数,而且须要专门的章节来介绍它.
map()一般用于将一个数组中的全部元素转移到另外一个数组中.将一个数组和匿名函数做为参数,传递给map,map会对数组中的每一个元素用这个匿名进行处理而且将其放到一样大小的新数组中,而后返回这个新数组.

看下map()实现代码:

function map($items, $func) 
    { 
        $result = [];
        
        foreach ($items as $item) { 
            $result[] = $func($item); 
        }
        
       return $result;
   }

记住,新数组中的每一个元素和原始数组中的元素是一一对应的关系。还有要理解map()是如何实现的,想明白:旧数组和新数组的每一个元素之间存在一个映射关系就能够了.

Map对如下这些场景是很是适用的:

  • 从一个对象数组中获取一个字段 ,好比获取顾客的邮件地址.

$emails = map($customers, function ($customer) { 
            return $customer->email; 
     });

Populating an array of objects from raw data, like mapping an array of JSON results into an array of domain objects

$products = map($productJson, function ($productData) {
            return new Product($productData);
      });
  • 改变数组元素的格式,好比价格字段,其单位为"分",那么对其值进行格式化处理.
    (如:1001 ==> 1,001这种格式).

$displayPrices = map($prices, function ($price) { 
             return '$' . number_format($price / 100, 2);
     });

Map vs Each

大部分人会对 "应该使用map"仍是"使用each"犯难.

想下咱们在前文用each作过商品删除的那个例子,你照样能够用map()去实现,而且效果是同样的.

map($productsToDelete, function ($product) { 
         $product->delete(); 
    });

尽管代码能够运行成功,可是在语义上仍是不正确的.咱们不能什么都用map(),由于这段代码会致使建立一个彻底没用处的,元素全为null的数组,那么这就形成了"资源浪费",这是不可取的.

Map是将一个数组转移到另外一个数组中.若是你不是转移任何元素,那么你就不该该使用map.

通常来说,若是知足如下条件你应该使用each而不是map:

  1. 你的回掉函数不会返回任何值.

  2. 你不会对map()返回的数组进行任何处理.

  3. 你只是须要每一个数组的元素执行一些操做.

What's Your GitHub Score?

这儿有一份某人在Reddit分享的面试问题.
GitHub提供一个开放的API用来返回一个用户最近全部的公共活动.响应会以json个返回一个对象数组,以下:

[
    {
      "id": "3898913063",
      "type": "PushEvent",
      "public": true,
      "actor": "adamwathan",
      "repo": "tightenco/jigsaw",
      "payload": { /* ... */ }
    },
    // ...
]

你能够用你的GitHub帐号,试下这个接口:

https://api.github.com/users/{your-username}/events

面试问题是:获取这些事件而且决定一个用户的"GitHubd Score",基于如下规则:

  1. 每一个"PushEvent",5分.

  2. 每一个"CreateEvent",4分.

  3. 每一个"IssueEvent",3分.

  4. 每一个'CommitCommentEvent',2分.

  5. 其余全部的事件都是1分.

Loops and Conditionals (循环和条件)

首先让咱们采用用命令式编程来解决这个问题.

function githubScore($username) 
    { 
    // Grab the events from the API, in the real world you'd probably use 
    // Guzzle or similar here, but keeping it simple for the sake of brevity. 
    $url = "https://api.github.com/users/{$username}/events"; 
    $events = json_decode(file_get_contents($url), true);
    
    // Get all of the event types 
    $eventTypes = [];
    
    foreach ($events as $event) {
        $eventTypes[] = $event['type']; 
    }
    // Loop over the event types and add up the corresponding scores 
    $score = 0;

    foreach ($eventTypes as $eventType) {
        switch ($eventType) { 
            case 'PushEvent':
                $score += 5;
                break; 
            case 'CreateEvent':
                $score += 4;
                break; 
            case 'IssuesEvent':
                $score += 3;
                break; 
            case 'CommitCommentEvent':
                $score += 2;
                break;
            default: 
                $score += 1;
                break;
       }
  }
  return $score;
}

Ok,让咱们来"clean"(清理)下这块代码.

Replace Collecting Loop with Pluck(用pluck替换collection的循环)

首先,让咱们把GitHub events 放到一个collection中.

function githubScore($username) 
    { 
        $url = "https://api.github.com/users/{$username}/events";
-     $events = json_decode(file_get_contents($url), true); 
+     $events = collect(json_decode(file_get_contents($url), true));
     
     // ...
    }

Now,让咱们看下第一次循环:

#function githubScore($username) 
    #{ 
        #$url = "https://api.github.com/users/{$username}/events"; 
        #$events = collect(json_decode(file_get_contents($url), true));
    
        $eventTypes = [];
        
        foreach ($events as $event) { 
            $eventTypes[] = $event['type'];
        }
    
        #$score = 0;
        #foreach ($eventTypes as $eventType) { 
             switch ($eventType) { 
                 case 'PushEvent': 
                     $score += 5;
                      break; 
                      // ... 
             }
         }
    return $score;
}

咱们知道,任什么时候候咱们要转移一个数组的每一个元素到另一个数组,能够用map是吧?在这种状况下,"转移"是很是简单的,咱们甚至可使用pluck,因此咱们把它换掉.

#function githubScore($username) 
    #{ 
       #$url="https://api.github.com/users/{$username}/events";
       #$events = collect(json_decode(file_get_contents($url), true));
        
        $eventTypes = $events->pluck('type');
        
        #$score = 0;
        
        #foreach ($eventTypes as $eventType) { 
            #switch ($eventType) { 
                #case 'PushEvent':
                    #$score += 5; 
                   # break; 
                   # // ... 
           # }
        # }
    #return $score;
    
 #}

嗯,少了四行代码,代码更有表达力了,nice!

Extract Score Conversion with Map

那么switch这块怎么处理呢?

# function githubScore($username) 
   # { 
   #     $url = "https://api.github.com/users/{$username}/events"; 
   #     $events = collect(json_decode(file_get_contents($url), true));
        
   #     $eventTypes = $events->pluck('type');
        
   #     $score = 0;
    
    foreach ($eventTypes as $eventType) { 
        switch ($eventType) { 
            case 'PushEvent': 
                $score += 5; 
                break; 
            case 'CreateEvent':
                 $score += 4;
                 break;
            case 'IssuesEvent':
                  $score += 3;
                  break;
            case 'CommitCommentEvent':
                   $score += 2;
                   break;
            default:
                   $score += 1;
                    break;
            }
       }
    
    return $score;
    
 }

咱们如今要计算全部成绩的总和,可是咱们用的是事件类型的集合(collection).

或许咱们用成绩的集合去计算总成绩会更简单吗?让咱们用map把事件类型转变为成绩,以后饭后该集合的总和.

function githubScore($username) 
    { 
      
      $url ="https://api.github.com/users/{$username}/events"; 
      $events = collect(json_decode(file_get_contents($url), true));
      
      $eventTypes = $events->pluck('type');

      $scores = $eventTypes->map(function ($eventType) { 
          switch ($eventType) { 
              case 'PushEvent':
                  return 5;
              case 'CreateEvent':
                  return 4;
              case 'IssuesEvent':
                  return 3;
              case 'CommitCommentEvent':
                  return 2;
              default:
                  return 1;
            } 
       });
       
    return $scores->sum();
 }

这样看起来好一点了,可是switch这块仍是让人不太舒服.再来.

Replace Switch with Lookup Table("映射表"替换switch)

若是你在开发过程当中碰到相似的switch,那么你彻底能够用数组构造"映射"关系.

#function githubScore($username)
     { 
        $url = "https://api.github.com/users/{$username}/events"; 
        #$events = collect(json_decode(file_get_contents($url), true));
        
        #$eventTypes = $events->pluck('type');
        
        #$scores = $eventTypes->map(function ($eventType) { 
           $eventScores = [ 
               'PushEvent' => 5,
               'CreateEvent' => 4,
               'IssuesEvent' => 3,
               'CommitCommentEvent' => 2,
           ];
           
         return $eventScores[$eventType];
    #});
 
   # return $scores->sum();   
 #}

比起之前用switch,如今用数组找映射关系,使得代码更简洁了.可是如今有一个问题,switch的default给漏了,所以,当要使用数组找关系时,咱们要判断事件类型是否在数组中.

# function githubScore($username)
     #{ 
         // ...
        #$scores = $eventTypes->map(function ($eventType) { 
             #$eventScores = [ 
              #   'PushEvent' => 5,
              #   'CreateEvent' => 4,
              #   'IssuesEvent' => 3,
              #   'CommitCommentEvent' => 2,
             #];
             
             if (! isset($eventScores[$eventType])) { 
                 return 1; 
                }
                
              # return $eventScores[$eventType];
   # });
    
  #  return $scores->sum();
# }

额,如今看起来,好像并不比switch好到哪儿去,不用担忧,但愿就在前方.

Associative Collections(关联数组集合)

Everything is better as a collection, remember?

到目前为止,咱们用的集合都是索引数组,可是collection也给咱们提供了处理关联数组强大的api.

你之前听过"Tell, Don't Ask"原则吗?其主旨就是你要避免询问一个对象关于其自身的问题,以便对你将要处理的对象作出另外一个决定.相反,相反,你应该把这个责任推到这个对象上,因此你能够告诉它须要什么,而不是问它问题.

那说到底,这个原则跟我们例子有什么关系呢?我很happy你能这么问,ok,让咱们再看下那个if判断.

# $eventScores = [ 
    #     'PushEvent' => 5,
    #     'CreateEvent' => 4,
    #     'IssuesEvent' => 3,
    #     'CommitCommentEvent' => 2,
    #];

    if (! isset($eventScores[$eventType])) { 
        return 1;
    }
    
   # return $eventScores[$eventType];

嗯,咱们如今呢就是在问这个关联数组是否存在某个值,存在会怎么样..,不存在怎么样..都有相应的处理.

Collection经过get方法让"Tell, Don't Ask"这个原则变得容易实现,get()有两个参数,第一个参数表明你要找的key,第二个参数是当找不到key时,会返回一个默认值的设置.

若是咱们把$eventScores变成一个Collection,咱们能够把之前的代码重构成这样:

$eventScores = collect([ 
           'PushEvent' => 5,
           'CreateEvent' => 4,
           'IssuesEvent' => 3,
           'CommitCommentEvent' => 2,
    ]);
    
    return $eventScores->get($eventType, 1);

ok,把这部分还原到总代码中:

function githubScore($username)
    { 
        $url = "https://api.github.com/users/{$username}/events"; 
        $events = collect(json_decode(file_get_contents($url), true));
        
        $eventTypes = $events->pluck('type');
        
        $scores = $eventTypes->map(function ($eventType) {
            return collect([ 
                'PushEvent' => 5,
                'CreateEvent' => 4,
                'IssuesEvent' => 3,
                'CommitCommentEvent' => 2,
            ])->get($eventType, 1);
    });
    return $scores->sum();

ok,咱们全部处理简炼成" a single pipeline".(单一管道)

function githubScore($username)
    { 
         $url = "https://api.github.com/users/{$username}/events";
         $events = collect(json_decode(file_get_contents($url), true));
    
    return $events->pluck('type')->map(function ($eventType) {
    return collect([ 
                  'PushEvent' => 5, 
                  'CreateEvent' => 4,
                  'IssuesEvent' => 3,
                   'CommitCommentEvent' => 2,
             ])->get($eventType, 1); 
         })->sum();
    }

Extracting Helper Functions(提取帮助函数)

有的时候,map()函数体内容会占不少行,好比上例中经过事件找成绩这块逻辑.

虽然到如今为止,咱们谈的也比较少,这只是由于咱们使用Collection PipeLine(集合管道)可是并不意味这咱们不用其余编程技巧,好比咱们能够把一些小逻辑写道函数中封装起来.

好比,在本例中,我想把API调用和事件成绩查询放到独立的函数中,代码以下:

function githubScore($username) 
    { 
        return fetchEvents($username)->pluck('type')->map(function ($eventType) { 
        return lookupEventScore($eventType); 
        })->sum(); 
    }
    
    function fetchEvents($username) { 
         $url = "https://api.github.com/users/{$username}/events"; 
         return collect(json_decode(file_get_contents($url), true)); 
    }
    
    function lookupEventScore($eventType) {
       
        return collect([ 
                 'PushEvent' => 5,
                 'CreateEvent' => 4,
                 'IssuesEvent' => 3,
                 'CommitCommentEvent' => 2,
        ])->get($eventType, 1); 
   }

Encapsulating in a Class (封装到一个类)

现代PHPweb应用要获取某人GitHub成绩的典型作法是什么呢?咱们确定不是用一个全局函数来回互相调,对吧? 咱们通常会定义一个带有namespace的类,方法的"封装型"本身定,

class GitHubScore 
    { 
        public static function forUser($username) { 
            return self::fetchEvents($username) 
            ->pluck('type') 
            ->map(function ($eventType) { 
            return self::lookupScore($eventType); })->sum();
         }
         
         
       private static function fetchEvents($username) { 
            $url = "https://api.github.com/users/{$this->username}/events"; 
            return collect(json_decode(file_get_contents($url), true)); 
       }
         
       private static function lookupScore($eventType) { 
           return collect([ 
                     'PushEvent' => 5,
                     'CreateEvent' => 4,
                     'IssuesEvent' => 3,
                     'CommitCommentEvent' => 2,
            ])->get($eventType, 1);
    
     }

有了这个类,GitHubScore::forUser('adamwathan') 便可得到成绩.
这种方法的一个问题是,因为咱们不使用实际的对象,咱们没法跟踪任何状态。 相反,你最终在一些地方传递相同的参数,由于你真的没有任何地方能够存储该数据

这个例子如今看起来没什么问题,可是你能够看到咱们必须传$username给fetchEvents()不然它不知道要获取的是那个用户的huod信息.

class GitHubScore { 
           public static function forUser($username) 
           { 
                return self::fetchEvents($username)
                    ->pluck('type') 
                    ->map(function ($eventType) { 
                    return self::lookupScore($event['type']); })
                     ->sum(); 
            }
            
            
           private static function fetchEvents($username) 
           { 
                $url = "https://api.github.com/users/{$this->username}/events"; 
                
                return collect(json_decode(file_get_contents($url), true)); }
                // ...
    }

This can get ugly pretty fast when you've extracted a handful of small methods that need access to the same data.

像本例这种状况,我通常会建立一个私有属性.
代替掉类中的静态方法,我在第一个静态方法中建立了一个实例,委派全部的任务给这个实例.

class GitHubScore 
    { 
        private $username;

        private function __construct($username) 
        { 
            $this->username = $username; 
        }
        
        public static function forUser($username) 
        { 
            return (new self($username))->score(); 
        }
        
        private function score() 
        { 
            $this->events()
            ->pluck('type')
            ->map(function ($eventType) { 
            return $this->lookupScore($eventType);
             })->sum(); 
         }
         
        private function events() 
        { 
            $url = "https://api.github.com/users/{$this->username}/events"; 
           return collect(json_decode(file_get_contents($url), true)); 
         }
           
        private function lookupScore($eventType) 
        { 
           return collect([ 
                'PushEvent' => 5,
                'CreateEvent' => 4,
                'IssuesEvent' => 3,
                'CommitCommentEvent' => 2,
            ])->get($eventType, 1); 
         }
    }

如今你获得了方便的静态API,可是其内部使用的对象是有它的状态信息.可使你的方法署名能够更简短,很是灵巧!

额,真不容易,从晚上9点干到凌晨3:30,虽然辛苦,可是又巩固了一遍,仍是值得的.2017/04/16 03:34

因为时间有限,未能复查,翻译的不周到的地方,麻烦你留言指出,我再改正,谢谢!

相关文章
相关标签/搜索