摘要:獲取試讀文章高階函數高階函數就是參數為可以為,并且返回值也可為的函數。比方說,我們現在有顧客名單,但我們需要得到他們的郵箱地址我們現在不用高階函數用一個來實現它,代碼如下。
《Refactoring To Collection》
獲取試讀文章:https://adamwathan.me/refactoring-to-collections/#sample高階函數
高階函數就是參數為可以為function,并且返回值也可為function的函數。我們舉一個用高階函數實現數據庫事務的例子.代碼如下:
public function transaction($func) { $this->beginTransaction(); try { $result = $func(); $this->commitTransaction(); } catch (Exception $e) { $this->rollbackTransaction(); throw $e; } return $result; }
看下它的使用:
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"屬性。
# $customerEmails = []; #foreach ($customers as $customer) { $email = $customer->email; #$customerEmails[] = $email; #} #return $customerEmails;
在第二個例子中,我們用$item中的幾個字段創建了一個新的關聯數組.
# $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;
我們現在接近抽象化了,但是中間那兩個代碼還在防礙著我們進行下一步操作.我們需要將這兩部分取出來,然后用使得兩個例子保持不變的東西來代替他們.
我們要做的就是把這兩個代碼放到匿名函數中,每個匿名函數會將每個數組元素作為其參數,然后進行相應的處理并且將其返回.
以下是用匿名函數處理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()函數是強大的處理數組的高階函數中的一種,之后的例子中我們會講到這部分,但是現在讓我們來深入了解下基礎知識。
EachEach只是一個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:
你的回掉函數不會返回任何值.
你不會對map()返回的數組進行任何處理.
你只是需要每個數組的元素執行一些操作.
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",基于以下規則:
每個"PushEvent",5分.
每個"CreateEvent",4分.
每個"IssueEvent",3分.
每個"CommitCommentEvent",2分.
其他所有的事件都是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
由于時間有限,未能復查,翻譯的不周到的地方,麻煩你留言指出,我再改正,謝謝!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/22770.html
摘要:注本文是翻譯寫的關于調試技巧,讀完以后很實用,分享給大家閱讀過程中,翻譯有錯誤的希望大家指正原文鏈接最近我一直在使用的,如果你還不了解,我簡單說下一個集合就是一個功能強大的數組有很多強大處理其內部數據的函數但是唯一讓我頭疼的地方是如何調試的 注:本文是翻譯Freek Van der Herten寫的關于Collection調試技巧,,讀完以后很實用,分享給大家.閱讀過程中,翻譯有錯誤的...
摘要:前端日報精選大前端公共知識梳理這些知識你都掌握了嗎以及在項目中的實踐深入貫徹閉包思想,全面理解閉包形成過程重溫核心概念和基本用法前端學習筆記自定義元素教程阮一峰的網絡日志中文譯回調是什么鬼掘金譯年,一個開發者的好習慣知乎專 2017-06-23 前端日報 精選 大前端公共知識梳理:這些知識你都掌握了嗎?Immutable.js 以及在 react+redux 項目中的實踐深入貫徹閉包思...
摘要:為了消除重復,可以將查找算法與比較準則這兩個變化方向進行分離。此刻,查找算法的方法名也應該被重命名,使其保持在同一個抽象層次上。結構性重復和存在結構型重復,需要進一步消除重復。 Refactoring to DSL OO makes code understandable by encapsulating moving parting, but FP makes code unders...
摘要:前端日報精選開發常見問題集錦前端碼農的自我修養虛擬內部是如何工作的譯知乎專欄并不慢,只是你使用姿勢不對一份優化指南掘金老司機帶你秒懂內存管理第一部中文免費公開課前端面試的大關鍵點,你到了嗎知乎專欄高效開發與設計姐的圖片二三 2017-07-19 前端日報 精選 VueJS 開發常見問題集錦 - 前端碼農的自我修養 - SegmentFault虛擬 DOM 內部是如何工作的?[譯]Hig...
摘要:例如中的操作數據庫時,先取得數據庫的連接,操作數據后確保釋放連接當操作文件時,先打開文件流,操作文件后確保關閉文件流。例如遍歷文件中所有行,并替換制定模式為其他的字符串。使用實現行為的參數化。 OO makes code understandable by encapsulating moving parting, but FP makes code understandable by...
閱讀 1378·2021-09-24 10:26
閱讀 1689·2019-08-30 14:14
閱讀 2098·2019-08-29 16:54
閱讀 363·2019-08-29 14:09
閱讀 1468·2019-08-29 12:55
閱讀 922·2019-08-28 18:13
閱讀 1573·2019-08-26 13:39
閱讀 2560·2019-08-26 11:43