[Collection] Practice 4: What's Your GitHub Score?
Đây là 1 trong những câu hỏi trong cuộc phỏng vấn vào Reddit.
GitHub cung cấp 1 public API trả về hoạt động public của người dùng gần đây. JSON respone này cho bạn 1 mảng các objects có dạng sau:1
2
3
4
5
6
7
8
9
10
11[
{
"id": "3898913063",
"type": "PushEvent",
"public": true,
"actor": "adamwathan",
"repo": "tightenco/jigsaw",
"payload": { /* ... */ }
},
// ...
]
Nhiệm vụ của bạn là tính điểm GitHub của 1 user dựa vào điều kiện sau1
2
3
4
51. Each PushEvent is worth 5 points.
2. Each CreateEvent is worth 4 points.
3. Each IssuesEvent is worth 3 points.
4. Each CommitCommentEvent is worth 2 points.
5. All other events are worth 1 point.
Loops and Conditionals
1 | function githubScore($username) |
Nhìn hơi tù nhỉ, refactor nào :D
Replace Collecting Loop with Pluck
Chắc hẳn chúng ta đã nhìn ra ngay đoạn lấy trường type hoàn toàn có thể thay thế bằng pluck
rồi nhỉ1
2
3
4
5
6
7
8
9/**
// Get all of the event types
$eventTypes = [];
foreach ($events as $event) {
$eventTypes[] = $event['type'];
}
**/
$eventTypes = $events->pluck('type');
Bây giờ cần xử lý đoạn này:
1 | $eventTypes = $events->pluck('type'); |
Extract Score Conversion with Map
Đây rõ ràng là ánh xạ điểm số cho mỗi event rồi, dùng map
là phù hợp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15$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();
Vẫn còn 1 vấn đề bẩn là switch thực sự cần được rút ngắn lại. Nào giải quyết nó thôi
Replace Switch with Lookup Table
Mỗi khi bạn có 1 switch như vậy, bạn có thể thay thế nó với 1 mảng và case
sẽ trở thành key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function 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();
}
Cảm giác code đã được dọn dẹp bởi vì nhìn vào điểm cho mỗi sự kiện trở nên rất tự nhiên về những gì chúng ta đang cố gắng làm với 1 biểu thức điều kiện như switch.
Hình như vấn đề chúng ta quên mất default case, tất cả các trường hợp không phải sự kiện trên sẽ trả về 1. Cái này thì có thể bổ sung như sau:
1 | function githubScore($username) |
Đột nhiên, điều này không thực sự có vẻ tốt hơn nhiều so với khối switch
, nhưng đừng sợ hãi, chúng ta vẫn còn hi vọng :D
Associative Collections: Liên kết collections
Everything is better as a collection, remember?
Cho đến giờ thì chúng ta chỉ sử dụng collection cho các mảng số truyền thống nhưng collection cho chúng ta nhiều sức mạnh hơn khi làm việc với tập hợp mảng
“Tell, Don’t Ask” Principle!!!
Bạn đã bao giờ nghe về nguyên lý “Nói, đừng có hỏi”. Ý tưởng chung là bạn nên tránh việc hỏi 1 object 1 câu hỏi về chính nó để quyết định khác về điều gì đó bạn đang định làm với object này. Thay vào đó, bạn nên đẩy trách nhiệm vào object này, vì vậy bạn có thể nói với nó cái bạn cần thay vì hỏi nó đầu tiên
Nó thì liên quan gì đến ví dụ này. Hãy nhìn lại nhé1
2
3
4
5
6
7
8
9
10
11
12$eventScores = [
'PushEvent' => 5,
'CreateEvent' => 4,
'IssuesEvent' => 3,
'CommitCommentEvent' => 2,
];
if (! isset($eventScores[$eventType])) {
return 1;
}
return $eventScores[$eventType];
Bạn đang sử dụng một bảng để tra cứ nếu nó có 1 giá trị chắc chắn và nếu nó không có giá trị chắc chắn thì nó sẽ trả về giá trị mặc định
Collection giúp chúng ta áp dụng nguyên tắc “Tell, Don’t Ask” để giải quyết vấn đề với get()
method, cái mà sẽ nhận vào key và tra cứu và 1 giá trị mặc định sẽ được trả về nếu nó không tồn tại.1
2
3
4
5
6
7
8$eventScores = collect([
'PushEvent' => 5,
'CreateEvent' => 4,
'IssuesEvent' => 3,
'CommitCommentEvent' => 2,
]);
return $eventScores->get($eventType, 1);
Tập hợp lại nào:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function 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
Dường như body của toán tử map vẫn còn nhiều dòng cho việc tra cứu điểm cho sự kiện ở đây.
Chúng tôi chưa nói nhiều từ trước, nhưng vì chúng ta đang làm việc với collection pipelines không có nghĩa là chúng ta nên ném những good practice khác ra ngoài như việc để logic ra các hàm nhỏ.
Trong trường hợp của chúng ta là hàm call API và hàm kiểm tra điểm có thể tách ra:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function 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
Tạo 1 class nếu cần thiết
Tổng kết
- Xử lý switch case với collection. Thông qua lookup Table
- Nguyên tắc “Tell, Don’t Ask” để làm việc giá trị mặc định.
[Collection] Practice 4: What's Your GitHub Score?
http://yoursite.com/2019/12/27/Collection-Practice-4-What-s-Your-GitHub-Score/