Áp dụng Funtional Programming với PHP

Functional PHP? Vâng, như các bạn cũng đã biết thì PHP không phải là một ngôn ngữ hướng function. Nhưng các kỹ thuật hướng function có thể được sử dụng để cải thiện code của chúng ta: giúp code dễ đọc hơn, dễ dàng bảo trì,…

Trong nhiều năm, PHP được viết dưới dạng hướng thủ tục, tất cả trong 1 file với các function mọi nơi. Sau phiên bản 5.*, các ứng dụng được viết bằng mô hình hướng đối tượng. Nhưng một vài thời điểm, chúng ta nghĩ rằng PHP trong mô hình hướng function. Tất nhiên PHP không phải là một ngôn ngữ hướng function và bài viết này cũng ko so sánh cũng như chống lại bất cứ mô hình nào cả. Hãy cứ thử để áp dụng một cách tốt nhất các loại mô hình vào công việc cũng như học tập.

Do thực tế là PHP là một môi trường OO khá “cổ điển”, chúng tôi đề xuất một cách tiếp cận khác gọi đó là dùng 1 cách hợp nhất. Cách tiếp cận này của chúng ta đi theo hướng giống như cách chúng ta làm trong các ngôn ngữ đa mô hình như là Scala, Kotlin hay là Typescript: điều mà chúng ta sẽ gọi là “Object-Functional “.

Chúng tôi vẫn tạo các lớp và các đối tượng nhưng sử dụng một tư duy chức năng. Thay vì nói cho máy tính biết cách giải quyết các vấn đề (hay còn gọi là lập trình bắt buộc), hãy bắt đầu nói với máy tính những gì chúng ta muốn đạt được (lập trình khai báo).

Functional programming & Functional PHP

Tóm lại, lập trình hướng chức năng:

  • Tránh sự đột biến state : Một khi một đối tượng được tạo ra, không bao giờ thay đổi. Không có các biến khi chúng ta sử dụng trong PHP.
  • Function như các giá trị hạng nhất: Các function có thể được sử dụng làm đầu vào hoặc đầu ra của các function khác cho phép function kết hợp. Chúng không phải là các hàm như chúng ta biết trong PHP. Chúng giống như các hàm toán học: cùng một đầu vào tạo ra luôn cùng một đầu ra (cho dù nó được gọi bao nhiêu lần). Chúng được gọi là các hàm thuần túy.

Hàm thuần túy và không thuần túy

  • Hàm thuần túy
    • Không dùng các biến toàn cục, giá trị không truyền bởi tham chiếu.
    • Không có các tác dụng phụ.
    • Không sử dụng bất cứ loại vòng lặp: for, foreach, while,…
  • Hàm không thuần túy
    • Gây ra đột biến cho state toàn cục.
    • Có thể sửa đổi các tham số đầu vào của họ.
    • Có thể throw exceptions.
    • Có thể thực hiện bất kỳ hoạt động I/O nào: với các tài nguyên bên ngoài như cơ sở dữ liệu, mạng, hệ thống tệp.
    • Có thể tạo ra kết quả khác nhau ngay cả với các tham số đầu vào giống nhau.
    • Có thể có tác dụng phụ.

Cùng hướng function trở thành thành bạn nào

Hãy cùng xem một số ví dụ thực tế về hướng function trong PHP:

Tránh các biến tạm

Không có các biến tạm thời trong code của chúng ta, chúng ta tránh có state cục bộ có thể tạo ra kết quả không mong muốn.

Trong ví dụ sau, chúng ta giữ trạng thái của nó cho đến cuối cùng khi return:

function isPositive(int $number) {
    if ($number > 0) {
        $status = true;
    } else {
        $status = false;
    }
    return $status;
}

Chúng ta có thể loại bỏ các biến tạm thời trả lại trực tiếp status:

function isPositive(int $number) {
    if ($number > 0) {
        return true;
    }
    return false;
}

Bước cuối cùng: loại bỏ if. Nó cuối cùng cũng trở thành hướng function rồi:

function isPositive(int $number) {
    return $number > 0;
}

Function nhỏ

  • Nên chỉ có trách nhiệm duy nhất: chỉ làm một việc.
  • Nó cũng giúp chúng ta dễ viết test hơn.
function sum(int $number1, int $number2) {
    return $number1 + $number2;
}

echo sum(sum(3, 4), sum(5, 5));

Hãy chú ý rằng chúng ta có thể thay đổi sum(3, 4) cho 7 và kết quả vẫn sẽ giống nhau:

echo sum(7, sum(5, 5));

Điều này được gọi là tính tham chiếu minh bạch: một chức năng có thể được thay thế bằng kết quả của nó và kết quả cuối cùng hoàn toàn không thay đổi.

Loại bỏ state

Điều này có thể khá khó khăn khi mà chúng ta đã quen viết theo một cách bắt buộc. Trong ví dụ này, hàm sẽ tính toán tích của tất cả các giá trị trong một mảng. Để làm như vậy, chúng ta sẽ sử dụng mảng và một vòng lặp để lặp qua nó.

function productImperative(array $data) {
    if (empty($data)) {
        return 0;
    }
    $total = 1;
    $i = 0;
    while ($i < count($data)) {
        $total *= $data[$i];
        $i++;
    }
    return $total;
}

Trong những trường hợp như thế này, khi có một số hành động lặp đi lặp lại, trạng thái có thể được loại bỏ bằng đệ quy:

function product(array $data) {
    if (empty($data)) {
        return 0;
    }
    if (count($data) == 1) {
        return $data[0];
    }
    return array_pop($data) * product($data);
}

Tất nhiên, ví dụ này khá đơn giản và sẽ tốt hơn nếu giải quyết nó thông qua việc sử dụng array_reduce:

echo array_reduce([5, 3, 2], function($total, $item) {
   return $total * $item;
}, 1);

Không sử dụng vòng lặp để xử lý các array

Ví dụ: Chúng tôi nhận được danh sách người dùng từ cơ sở dữ liệu và cần trả về model mà ứng dụng của chúng ta mong đợi.

Như bình thường, chúng ta sẽ làm như sau: dùng foreach và nói cho máy tính biết phải làm gì.

function getUsers() {
    return [
        ["firstname" => "john", "surname1" => "doe", "location" => "Barcelona", "numpets" => 2],
        ["firstname" => "david", "surname1" => "ee", "location" => "Girona", "numpets" => 10],
        ["firstname" => "jane", "surname1" => "qwerty", "location" => "Barcelona", "numpets" => 1],
    ];
}

function findUsers()
{
    $users = getUsers();
    if (empty($users)) {
        return false;
    }
    $usersDTO = [];
    foreach ($users as $user) {
        $usersDTO[] = new UserDTO($user);
    }
    return $usersDTO;
}

Phong cách hướng function thì sẽ như sau:

function findUsersMap()
{
    return array_map("convertUser", getUsers());
}

function convertUser(array $user) {
    return new UserDTO($user);
}

Chúng ta sẽ sử dụng array_map thay vì foreach và tạo một hàm thuần túy mà mục đích duy nhất của nó là chuyển đổi định dạng của người dùng. Phiên bản này dễ đọc hơn nhiều so với phiên bản trước.

Filter

Lặp lại qua một mảng nhưng chỉ trả về những kết quả vượt qua một số điều kiện.

Bây giờ chúng tôi có danh sách người dùng, chúng tôi muốn chỉ hiển thị những người sống ở Barcelona. Một lần nữa, chúng ta lặp qua mảng và kiểm tra từng thành phố của họ.

function getUsersFromBcn(array $users) {
    $bcnUsers = [];
    foreach ($users as $user) {
        if ($user->getCity() == "Barcelona") {
            $bcnUsers[] = $user;
        }
    }
    return $bcnUsers;
}

Cùng xem hướng function sẽ làm nào:

function getUsersFromBcn(array $users) {
    return array_filter($users, "isFromBcn");
}

Thấy không code này đơn giản hơn và dễ dàng hơn để test mà không cần các biến tạm thời cũng như foreach + if.

Reduce 

Bây giờ, chúng tôi muốn tính trung bình của vật nuôi trong thành phố Barcelona. Hãy cùng nhóm người dùng Barcelona lặp lại và tính toán số lượng thú cưng:

function getAvgPets(array $users) {
    $numPets = 0;
    foreach ($users as $user) {
        $numPets += $user->getPets();
    }
    return $numPets / count($users);
}

Hoặc chúng ta có thể tổng hợp số lượng vật nuôi bằng cách:

function getAvgPets(array $users) {
    return array_reduce($users, "getTotalPets", 0) / count($users);
}

function getTotalPets($total, UserDTO $user) {
    return $total + $user->getPets();
}

Sử dụng pipelines

Hãy để nhóm tất cả các điều kiện cùng nhau:

echo getAvgPetsReduce(getUsersFromBcn(findUsers()));

Nếu chúng ta có nhiều điều kiện, chúng ta có thể kết thúc bằng một câu rất dài có thể rất khó đọc. May mắn thay, có một số thư viện tốt để sử dụng dạng pipeline.

Laravel collection

Framework Laravel có một thư viện tuyệt vời để làm việc với các mảng, được gọi là các collection. Nó có thể được sử dụng bên ngoài Laravel, có thể cài đặt thông qua composer.

composer require tightenco/collect

Sử dụng thư viện này, các đối tượng là bất biến và mã rất dễ đọc. Sử dụng ví dụ của chúng tôi về mức trung bình của vật nuôi ở Barcelona sẽ là:

$collection = collect(getUsers());
echo $collection->map("convertUser")
                ->filter('isFromBcn')
                ->map("getListPets")
                ->average();

Thứ tự của các hành động rất rõ ràng.

  1. Chuyển đổi người dùng
  2. Chỉ lọc những người từ Barcelona
  3. Lấy danh sách vật nuôi trên mỗi người dùng
  4. Lấy mức trung bình

Kết luận

Chúng tôi biết, PHP không phải là 100% hướng chức năng. Và thay đổi cách chúng ta lập trình theo cách thức hướng chức năng là không phải là một nhiệm vụ dễ dàng. Nhưng chúng ta có thể bắt đầu áp dụng một số cách tiếp cận đơn giản này để làm cho code của chúng ta đơn giản hơn, dễ đọc hơn nhiều, có thể test và với ít tác dụng phụ hơn.

Bài viết dịch từ: https://apiumhub.com/tech-blog-barcelona/functional-php/