[Collection] Transforming Data, Introducing Collections

….

Trở về ví dụ đầu tiên, lấy danh sách email của users

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getUserEmails($users)
{
$emails = [];

for ($i = 0; $i < count($users); $i++) {
$user = $users[$i];

if ($user->email !== null) {
$emails[] = $user->email;
}
}

return $emails;
}

Hay nhìn như kiểu này
1
SELECT email FROM users WHERE email IS NOT NULL

Đối với những người mới bắt đầu, chựa chọn foreach dễ dàng hơn

1
2
3
4
5
6
7
8
9
10
11
12
function getUserEmails($users)
{
$emails = [];

foreach ($users as $user) {
if ($user->email !== null) {
$emails[] = $user->email;
}
}

return $emails;
}

Nếu không kiểm tra null trong email, việc dùng map là phù hợp.

Đặc điểm của map là áp dụng chuyển đổi mỗi phần tử của mảng, bởi vậy nó cũng trả về 1 mảng với size giống với mảng gốc. Hàm getUserEmails chỉ có nghĩa là trả về tập con (subset) của items, những item có trường $email khác null.

Khi tạo subset, chúng ta sẽ sử dụng filter, nhưng filter cũng không đúng ở đây.

Filter có nghĩa là trả cho bạn tất cả các phần tử thỏa mãn 1 vài điều kiện, trong trường hợp này là email not null. Vấn đề ở đây là filter là chúng trả về cho chúng ta users có email chứ không phải là địa chỉ email của họ

Thinking in Steps

Vấn đề chúng đề đang phải đối mặt là chúng ta đang cố gắng làm quá nhiều thứ cùng 1 lúc. Khi bạn đang ở tình trạng này, thứ đầu tiên bạn làm là cố gắng cho “I can’t because …” vào “Nếu tôi có thể …” (I could if …).

Ví dụ:

Tôi không thể map vì nó sẽ được áp dụng cho mỗi users, không chỉ với users có email


Trở thành:

Tôi có thể sử dụng map nếu tôi chỉ làm việc với những users có email


Well, lấy những users với email nghe thôi đã thấy ta có thể sử dụng với filter
1
2
3
$usersWithEmails = filter($users, function ($user) {
return $user->email !== null;
});

Và giờ khi đã chúng ta chỉ làm việc với users có email rồi, chúng ta có thể xóa điều kiện ra khỏi lòng lặp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getUserEmails($users)
{
$usersWithEmails = filter($users, function ($user) {
return $user->email !== null;
});

$emails = [];

foreach ($usersWithEmails as $user) {
$emails[] = $user->email;
}

return $emails;
}

Rồi nha, một khi điều kiện đã không còn, chúng ta sẽ thay thế nó với map
1
2
3
4
5
6
7
8
9
10
11
12
function getUserEmails($users)
{
$usersWithEmails = filter($users, function ($user) {
return $user->email !== null;
});

$emails = map($usersWithEmails, function ($user) {
return $user->email;
});

return $emails;
}

Chúng ta đã giải quyết xong vấn đề bằng cách tách biệt vào 1 toán tử riêng biệt, mỗi cái phụ trách 1 phần rõ ràng

Khi code của bạn trở nên phức tạp hơn, việc tách biệt chia nhỏ mọi thứ như thế này bắt đầu hiệu quả bởi vì khi debug sẽ debug được các hoạt động độc lập, đơn giản hơn nhiều khi gỡ lỗi với các toán tử phức tạp

The Problem with Primitives (Vấn đề với kiểu dữ liệu nguyên thủy)

Cái mà chúng ta cần cải thiện nữa loại bỏ đi các biến trung gian $usersWithEmails$email

Loại bỏ $email thì rất đơn giản, chúng ta chỉ cần trả về trực tiếp:

1
2
3
4
5
6
7
8
9
10
function getUserEmails($users)
{
$usersWithEmails = filter($users, function ($user) {
return $user->email !== null;
});

return map($usersWithEmails, function ($user) {
return $user->email;
});
}

Nhưng đoạn code bắt đầu khó hiểu 1 chút nếu bạn sử dụng biến $usersWithEmail cùng dòng như vậy
1
2
3
4
5
6
7
8
function getUserEmails($users)
{
return map(filter($users, function ($user) {
return $user->email !== null;
}), function ($user) {
return $user->email;
});
}

What? Nó không dễ để đọc. Nó nhìn thậm chí còn tồi tệ hơn nếu bạn sử dụng PHP’s build-in array functions vì chúng có thứ tự tham số không trực quan
1
2
3
4
5
6
7
8
function getUserEmails($users)
{
return array_map(function ($user) {
return $user->email;
}, array_filter($users, function ($user) {
return $user->email !== null;
}));
}

Lý do code khó hiểu là vì nó phải được đọc “inside-out” (đọc từ trong ra ngoài)

Vấn đề giống nhau gặp phải phát sinh khi làm việc với string trong PHP. Ví dụ bạn có đoạn code chuyển đổi từ snake_case sang camelCase

1
2
3
4
5
$camelString = lcfirst(
str_replace(' ', '',
ucwords(str_replace('_', ' ', $snakeString))
)
);

Mất một lúc để load não. Vì strings và array là kiểu dữ liệu nguyên thủy, chúng ta phải thực hiện toán tử với nó từ bên ngoài bằng cách truyền chúng dưới dạng tham số vào các hàm khác (kiểu phải thực hiện ở đâu đó rồi mới truyền vào được chứ không thể thực hiện trong hàm chính luôn ý). Đó là những gì dẫn đến “inside-out”, nơi bạn cần đếm các bước liên kết để xem chuyện gì xảy ra đầu tiên với biến của mình.

Thay thế chỗ inside-out code kia bằng ví dụ sau đây?

1
2
3
4
$camelString = $snakeString->replace('_', ' ')
->ucwords()
->replace(' ', '')
->lcfirst();

Có phải dễ hiểu hơn rất nhiều đúng ko?

Điểm khác biệt ở đây là chúng ta xem $snakeString như là 1 object thay vì giá trị kiểu nguyên thủy. Bằng cách gọi các phương thức trực tiếp thay vì truyền nó thông qua các tham số, bạn chỉ cần đọc code từ trái qua phải với các toán tử xuất hiện đã được sắp xếp

Arrays as Objects

Hãy tưởng tượng trong giây lát, cách chúng ta biến array thành Object trong hàm getUserEmails function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getUserEmails($users)
{
- $usersWithEmails = filter($users, function ($user) {
+ $usersWithEmails = $users->filter(function ($user) {
return $user->email !== null;
});

- $emails = map($usersWithEmails, function ($user) {
+ $emails = $usersWithEmails->map(function ($user) {
return $user->email;
});

return $emails;
}

Sự khác biệt lúc này là $usersWithEmails outside trong lời gọi map của chúng ta thay về inside. Bây giờ chúng ta chúng ta có thể inline nó, filter trước khi map, code của chúng ta được đọc từ trái qua phải mà không cần inside.
1
2
3
4
5
6
7
8
function getUserEmails($users)
{
return $users->filter(function ($user) {
return $user->email !== null;
})->map(function ($user) {
return $user->email;
});
}

Bạn có thấy thú vị không? Kiểu lập trình này được gọi chung là collection pipeline và chúng ta hoàn toàn có thể làm điều này trong PHP.

Giới thiệu về 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.

Một collection là 1 object “bó” lại 1 mảng và cho phép chúng ta thực hiện các toán tử được gọi là các methods với collection thay vì truyền mảng vào mảng.

Đây là Collection class đơn giản chỉ hỗ trợ map và filter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Collection
{
protected $items;

public function __construct($items)
{
$this->items = $items;
}

public function map($callback)
{
return new static(array_map($callback, $this->items));
}

public function filter($callback)
{
return new static(array_filter($this->items, $callback));
}

public function toArray()
{
return $this->items;
}
}

Để lấy dánh sách email của $user theo ví dụ trên chúng ta có thể dùng Collection
1
2
3
4
5
6
7
8
function getUserEmails($users)
{
return (new Collection($users))->filter(function ($user) {
return $user->email !== null;
})->map(function ($user) {
return $user->email;
})->toArray();
}

Phương thức liên kết sau hàm tạo có thể nhìn có 1 chút lộn xộn vì vậy tôi sẽ thường xuyên tạo hàm tạo có tên để rõ ràng mọi thứ
1
2
3
4
public static function make($items)
{
return new static($items);
}

Sử dụng hàm tạo có tên giúp chúng ta tiết kiệm được các dấu ngoặc đơn và nhìn gọn hơn
1
2
3
4
5
6
7
8
9
function getUserEmails($users)
{
- return (new Collection($users))->filter(function ($user) {
+ return Collection::make($users)->filter(function ($user) {
return $user->email !== null;
})->map(function ($user) {
return $user->email;
})->toArray();
}

Một chú ý về khả năng biến đổi

Bạn phải chú ý rằng trong các ví dụ tính đến thời điểm hiện tại, chúng ta áp dụng các toán tử với mảng và chúng ta luôn trả về mảng mới, chúng ta thực sự không thay đổi gì trong mảng gốc.

Điều này rõ ràng với các thực thi Collection ở dưới khi mà ta trả về new static với cả map và filter

So sánh điều đó với các thực hiện này, tahy vì trả về new collection, chúng ta sẽ trả về thuộc tinh $item thay thế

1
2
3
4
5
public function map($callback)
{
$this->items = array_map($callback, $this->items);
return $this;
}

Điều này có vẻ không phải là một sự khác biệt lớn, nhưng nó có thể gây ra các lỗi làm tan chảy não, ma quái hành động ở khoảng cách sẽ loại bỏ tất cả “niềm vui” ra khỏi lập trình.
=> điều này có thể hack não và gây bug đó :D

Code sẽ như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
$employees = new Collection([
['name' => 'Mary', 'email' => 'mary@example.com', 'salaried' => true],
['name' => 'John', 'email' => 'john@example.com', 'salaried' => false],
['name' => 'Kelly', 'email' => 'kelly@example.com', 'salaried' => true],
]);

$employeeEmails = $employees->map(function ($employee) {
return $employee['email'];
});

$salariedEmployees = $employees->filter(function ($employee) {
return $employee['salaried'];
});

Bạn có thể phát hiện ra bug?

Hãy nhìn map. Khi chúng ta đang cố filter danh sách nhân viên thì chúng ta thực tế lại đang lọc danh sách email của họ. Rất tiếc

Quacking Like… an Array?

Phần này nói về các interface để tương tác với Collection như 1 mảng nhé

Collection đơn giản cho đến giờ khá gọn, nhưng hơi khó chịu 1 chút khi phải liên tục chuyển đổi dữ liệu qua lại giữa collection và array.

Thật sự tuyệt vời nếu chúng ta có thể xây dựng collection, cái mà chúng ta có thể sử dụng thay cho mảng mà không cần hệ thống nhận thấy, và rất mắn, PHP được được điều đó (hầu hếu, đa số) thông qua 1 interfaces sau

ArrayAccess

Một trong những chức năng đặc biệt của mảng là bạn có thể lấy phần tử bằng cách xác định offset thông qua dấu []

1
2
3
$items = [1, 2, 3];
echo $items[2];
// => 3

Nếu bạn thử làm nó với Collection, nó sẽ lỗi. Chúng ta có thể thêm nó thông qua interface ArrayAccess (xem thêm trong sách - 43)

Countable

Interface tương tự count()

IteratorAggregate

Interface cho vòng lặp

The Golden Rule of Collection Programming: Nguyên tắc vàng của lập trình collection

Never use a foreach loop outside of a collection!

Mỗi khi bạn sử dụng vòng lặp foreach, bạn đang làm cái qué gì đó và tôi hứa với bạn rằng “cái qué gì đó” đã có tên:

  • Cần vòng lặp trên 1 mảng để thực hiện 1 vài hành động với mỗi item và nhét kết quả vào mảng khác? Bạn không cần lặp, bạn cần map
  • Bạn cần vòng lặp qua 1 mảng để loại bỏ 1 vài phần tử không trùng với điều kiện. Bạn không cần vòng lặp, bạn cần filter
  • Pipeline programming (lập trình đường ống) là về các toán tử ở mức độ cao hơn. Thay vì làm điều gì đó với items trong collection, bạn hãy làm tự làm nó với collection
    => Câu này khó hiểu quá, huhu
  • Map it, filter it, reduce it, sum it, zip it, reverse it, transpose it, flatten it, group it, count it, chunk it, sort it, slice it, search it; if you can do it with a foreach loop, you can do it with a collection method.
  • Mỗi khi bạn nâng cấp mảng từ các kiểu nguyên thủy lên thành object, cái mà có các hành vi, thì không có lý do gì để sử dụng vòng lặp bên ngoài chính collection đó và tôi sẽ chứng minh điều đó với bạn.
  • Kể từ thời điểm này, bạn sẽ không nhìn thấy 1 vòng foreach đơn nào trừ khi nó bên trong 1 collection method nữa.

Let the games begin!

Author

Ming

Posted on

2019-12-27

Updated on

2021-04-10

Licensed under

Comments