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

因爲時間有限,未能複查,翻譯的不周到的地方,麻煩你留言指出,我再改正,謝謝!

相關文章
相關標籤/搜索