Hướng dẫn
Quảng cáo

Cách viết clean code trong PHP

Bài viết này sẽ hướng dẫn các cách viết clean code trong PHP và các ví dụ cụ thể về clean code trong PHP

Việc viết mã nguồn sạch sẽ, trình bày dễ hiểu, định dạng tốt luôn là vấn đề cần quan tâm hàng đầu của những lập trình viên viết code, bài viết này sẽ hướng dẫn các bạn một số cách trình bày code PHP đơn giản, dễ hiểu và quy củ giúp cho việc lập trình trở nên dễ dàng hơn.

Nội dung bài viết

Đặt tên biến

Đặt tên biến có ý nghĩa và có thể đọc hiểu được

Mã gốc kém

declare(strict_types=1);
$ymdstr = $moment->format('y-m-d');

Mã tốt

declare(strict_types=1);
$currentDate = $moment->format('y-m-d');

Thống nhất cách đặt trên cho cùng 1 loại biến

Kém

declare(strict_types=1);
getUserInfo();
getUserData();
getUserRecord();
getUserProfile();

Tốt

declare(strict_types=1);

getUser();

Sử dụng tên có thể tìm kiếm được

Thường thì chúng ta sẽ cần phải đọc, tìm lại code sau 1 khoảng thời gian. Để việc này không mất thời gian thì việc đặt tên cho các hằng số, biến là điều cần thiết.

Kém

declare(strict_types=1);

// 448 có nghĩa là gì?
$result = $serializer->serialize($data, 448);

Tốt

declare(strict_types=1);

$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

Một ví dụ khác

Kém

declare(strict_types=1);

class User
{
    // 7 ở đây ý nghĩa là gì
    public $access = 7;
}

// 4 có ý nghĩa gì?
if ($user->access & 4) {
    // ...
}

// Đoạn mã này thực sự đang muốn làm gì?
$user->access ^= 2;

Tốt

declare(strict_types=1);

class User
{
    public const ACCESS_READ = 1;

    public const ACCESS_CREATE = 2;

    public const ACCESS_UPDATE = 4;

    public const ACCESS_DELETE = 8;

    // Quyền mặc định sẽ là đọc, khởi tạo, cập nhật
    public $access = self::ACCESS_READ | self::ACCESS_CREATE | self::ACCESS_UPDATE;
}

if ($user->access & User::ACCESS_UPDATE) {
    // do edit ...
}

// Hủy bỏ quyền khởi tạo (create)
$user->access ^= User::ACCESS_CREATE;

Sử dụng các biến có ý nghĩa

Kém

declare(strict_types=1);

$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);

saveCityZipCode($matches[1], $matches[2]);

Tạm chấp nhận

declare(strict_types=1);

$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);

[, $city, $zipCode] = $matches;
saveCityZipCode($city, $zipCode);

Tốt

//Đặt tên cho các sub pattern
declare(strict_types=1);

$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(?<city>.+?)\s*(?<zipCode>\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);

saveCityZipCode($matches['city'], $matches['zipCode']);

Tránh việc lồng thẻ quá sâu và return liên tục

Việc thực hiện if else liên tục chỉ làm cho code bạn phức tạp hơn mà thôi

Kém

declare(strict_types=1);

function isShopOpen($day): bool
{
    if ($day) {
        if (is_string($day)) {
            $day = strtolower($day);
            if ($day === 'friday') {
                return true;
            } elseif ($day === 'saturday') {
                return true;
            } elseif ($day === 'sunday') {
                return true;
            }
            return false;
        }
        return false;
    }
    return false;
}

Tốt

declare(strict_types=1);

function isShopOpen(string $day): bool
{
    if (empty($day)) {
        return false;
    }

    $openingDays = ['friday', 'saturday', 'sunday'];

    return in_array(strtolower($day), $openingDays, true);
}

Một ví dụ khác vì việc lồng cấp

Kém

declare(strict_types=1);

function fibonacci(int $n)
{
    if ($n < 50) {
        if ($n !== 0) {
            if ($n !== 1) {
                return fibonacci($n - 1) + fibonacci($n - 2);
            }
            return 1;
        }
        return 0;
    }
    return 'Not supported';
}

Tốt

declare(strict_types=1);

function fibonacci(int $n): int
{
    if ($n === 0 || $n === 1) {
        return $n;
    }

    if ($n >= 50) {
        throw new Exception('Not supported');
    }

    return fibonacci($n - 1) + fibonacci($n - 2);
}

Đừng bắt người đọc phải dịch nghĩa tên biến của bạn

Kém

$l = ['Austin', 'New York', 'San Francisco'];

for ($i = 0; $i < count($l); $i++) {
    $li = $l[$i];
    doStuff();
    doSomeOtherStuff();
    // ...
    // ...
    // ...
    // Wait, what is `$li` for again?
    dispatch($li);
}

Tốt

declare(strict_types=1);

$locations = ['Austin', 'New York', 'San Francisco'];

foreach ($locations as $location) {
    doStuff();
    doSomeOtherStuff();
    // ...
    // ...
    // ...
    dispatch($location);
}

Không thêm ngữ cảnh không cần thiết

Nếu tên class, đối tượng của bạn đã có ý nghĩa rồi, thì không nhất thiết phải lặp lại chúng trong tên biến

Kém

declare(strict_types=1);

class Car
{
    public $carMake;

    public $carModel;

    public $carColor;

    //...
}

Tốt

declare(strict_types=1);

class Car
{
    public $make;

    public $model;

    public $color;

    //...
}

Sử dụng giá trị mặc định cho tham số hàm

Không tốt

Trước đây chúng ta hay dùng như ví dụ dưới đây. Tuy vậy khi gọi hàm này, giá trị truyền vào có thể là NULL. Do đó chưa được tốt lắm!

Ví dụ

function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void
{
    // ...
}

Không tồi

Việc kiểm tra giá trị như ví dụ dưới đây cũng tạm chấp nhận. Tuy vậy chúng ta mất thêm công sức để kiểm tra giá trị của biến trước khi sử dụng.

Ví dụ

function createMicrobrewery($name = null): void
{
    $breweryName = $name ?: 'Hipster Brew Co.';
    // ...
}

Tốt

Hãy sử dụng thêm kiểu dữ liệu tham số truyền vào (type hinting) để chắc chắn dữ liệu truyền vào là chuẩn xác

Như ví dụ dưới đây, giá trị truyền vào không thể là NULL.

Ví dụ

function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void
{
    // ...
}


So sánh

Tham khảo thêm https://www.php.net/manual/en/language.operators.comparison.php

Không tốt

So sánh thông thường sẽ chuyển từ string thành integer

Hàm if dưới đây sẽ return FALSE, mặc dù bản chất phải là TRUE

Ví dụ

declare(strict_types=1);

$a = '42';
$b = 42;

if ($a != $b) {
    // The expression will always pass
}

Tốt

Hãy so sánh cả kiểu dữ liệu của các biến.

Ví dụ

declare(strict_types=1);

$a = '42';
$b = 42;

if ($a !== $b) {
    // The expression is verified
}

Toán tử kiểm tra Null

Toán tử kiểm tra null ?? được giới thiệu từ PHP 7. Toán tử này trả về giá trị biến đầu nếu không null và trả vế biến thứ 2 nếu biến đầu tiên bị null.

Kém

declare(strict_types=1);

if (isset($_GET['name'])) {
    $name = $_GET['name'];
} elseif (isset($_POST['name'])) {
    $name = $_POST['name'];
} else {
    $name = 'nobody';
}

Tốt

declare(strict_types=1);

$name = $_GET['name'] ?? $_POST['name'] ?? 'nobody';

Clean code với Function

Số lượng tham số Function (lý tưởng là 2 hoặc ít hơn)

Việc có số lượng tham số ít sẽ giúp code trông sạch sẽ hơn. Đồng thời việc kiểm tra tính đúng đắn hoặc kiểm tra dữ liệu đầu vào cũng ngắn gọn hơn.

Hàm không có tham số là trường hợp tốt nhất, Một hoặc 2 tham số là khá tốt. Ba tham số thì tạm chấp được. Nhưng nhiều hơn thì bạn nên xem xét tới việc tối ưu lại. Ví dụ như quá nhiều tham số đầu vào có thể chuyển đổi hàm đó về dạng class.

Kém

declare(strict_types=1);

class Questionnaire
{
    public function __construct(
        string $firstname,
        string $lastname,
        string $patronymic,
        string $region,
        string $district,
        string $city,
        string $phone,
        string $email
    ) {
        // ...
    }
}

Tốt

declare(strict_types=1);

class Name
{
    private $firstname;

    private $lastname;

    private $patronymic;

    public function __construct(string $firstname, string $lastname, string $patronymic)
    {
        $this->firstname = $firstname;
        $this->lastname = $lastname;
        $this->patronymic = $patronymic;
    }

    // getters ...
}

class City
{
    private $region;

    private $district;

    private $city;

    public function __construct(string $region, string $district, string $city)
    {
        $this->region = $region;
        $this->district = $district;
        $this->city = $city;
    }

    // getters ...
}

class Contact
{
    private $phone;

    private $email;

    public function __construct(string $phone, string $email)
    {
        $this->phone = $phone;
        $this->email = $email;
    }

    // getters ...
}

class Questionnaire
{
    public function __construct(Name $name, City $city, Contact $contact)
    {
        // ...
    }
}

Tên Function nên nói rõ ý nghĩa của nó

Kém

class Email
{
    //...

    public function handle(): void
    {
        mail($this->to, $this->subject, $this->body);
    }
}

$message = new Email(...);
// What is this? A handle for the message? Are we writing to a file now?
$message->handle();

Tốt

class Email
{
    //...

    public function send(): void
    {
        mail($this->to, $this->subject, $this->body);
    }
}

$message = new Email(...);
// Clear and obvious
$message->send();

 Function chỉ nên có 1 cấp độ trừu tượng

Khi bạn có nhiều hơn nghiệp vụ cần xử lý thì là lúc bạn cần tới việc chia nhỏ để đạt được hiệu quả sử dụng và test.

Kém

declare(strict_types=1);

function parseBetterPHPAlternative(string $code): void
{
    $regexes = [
        // ...
    ];

    $statements = explode(' ', $code);
    $tokens = [];
    foreach ($regexes as $regex) {
        foreach ($statements as $statement) {
            // ...
        }
    }

    $ast = [];
    foreach ($tokens as $token) {
        // lex...
    }

    foreach ($ast as $node) {
        // parse...
    }
}

Không tồi

Mặc dù đã tối ưu nhưng hàm parseBetterPHPAlternative vẫn còn khá phức tạp.

Ví dụ

function tokenize(string $code): array
{
    $regexes = [
        // ...
    ];

    $statements = explode(' ', $code);
    $tokens = [];
    foreach ($regexes as $regex) {
        foreach ($statements as $statement) {
            $tokens[] = /* ... */;
        }
    }

    return $tokens;
}

function lexer(array $tokens): array
{
    $ast = [];
    foreach ($tokens as $token) {
        $ast[] = /* ... */;
    }

    return $ast;
}

function parseBetterPHPAlternative(string $code): void
{
    $tokens = tokenize($code);
    $ast = lexer($tokens);
    foreach ($ast as $node) {
        // parse...
    }
}

Tốt

class Tokenizer
{
    public function tokenize(string $code): array
    {
        $regexes = [
            // ...
        ];

        $statements = explode(' ', $code);
        $tokens = [];
        foreach ($regexes as $regex) {
            foreach ($statements as $statement) {
                $tokens[] = /* ... */;
            }
        }

        return $tokens;
    }
}

class Lexer
{
    public function lexify(array $tokens): array
    {
        $ast = [];
        foreach ($tokens as $token) {
            $ast[] = /* ... */;
        }

        return $ast;
    }
}

class BetterPHPAlternative
{
    private $tokenizer;
    private $lexer;

    public function __construct(Tokenizer $tokenizer, Lexer $lexer)
    {
        $this->tokenizer = $tokenizer;
        $this->lexer = $lexer;
    }

    public function parse(string $code): void
    {
        $tokens = $this->tokenizer->tokenize($code);
        $ast = $this->lexer->lexify($tokens);
        foreach ($ast as $node) {
            // parse...
        }
    }
}

Không sử dụng các biến cờ (biến boolean) để làm tham số

Kém

declare(strict_types=1);

function createFile(string $name, bool $temp = false): void
{
    if ($temp) {
        touch('./temp/' . $name);
    } else {
        touch($name);
    }
}

Tốt

declare(strict_types=1);

function createFile(string $name): void
{
    touch($name);
}

function createTempFile(string $name): void
{
    touch('./temp/' . $name);
}

Tránh tác dụng phụ

Một hàm có tác dụng phụ nếu như nó thực hiện nhiều hơn một nhiệm vụ trong hàm đó. Tác dụng phụ đó có thể như là ghi file, chỉnh sửa biến global, …

Kém

declare(strict_types=1);

// Global variable referenced by the following function.
// If we had another function that used this name, now it'd be an array and it could break it.
$name = 'Ryan McDermott';

function splitIntoFirstAndLastName(): void
{
    global $name;

    $name = explode(' ', $name);
}

splitIntoFirstAndLastName();

var_dump($name);
// ['Ryan', 'McDermott'];

Tốt

declare(strict_types=1);

function splitIntoFirstAndLastName(string $name): array
{
    return explode(' ', $name);
}

$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);

var_dump($name);
// 'Ryan McDermott';

var_dump($newName);
// ['Ryan', 'McDermott'];

Không viết các hàm global

Việc này có thể tạo ra sự xung đột với các hàm cùng tên trong các thư viện khác. Chính vì vậy chúng ta nên hạn chế việc sử dụng các hàm global.

Kém

declare(strict_types=1);

function config(): array
{
    return [
        'foo' => 'bar',
    ];
}

Tốt

declare(strict_types=1);

class Configuration
{
    private $configuration = [];

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

    public function get(string $key): ?string
    {
        // null coalescing operator
        return $this->configuration[$key] ?? null;
    }
}

Khi đó lấy config thông qua class Configuration

Ví dụ

declare(strict_types=1);

$configuration = new Configuration([
    'foo' => 'bar',
]);

Hãy cẩn thận khi sử dụng Singleton

Bởi lẽ Singleton là một anti-pattern với các lý do

  • Pattern này sẽ ẩn đi các phụ thuộc của ứng dụng bên trong mã code của bạn tya vì thông qua các interface.
  • Vi phạm nguyên tắc đơn chức năng trong SOLID vì thực tế là với Singleton tự điều khiển vòng đời của nó.
  • Làm test khó khăn hơn

Chưa tốt

declare(strict_types=1);

class DBConnection
{
    private static $instance;

    private function __construct(string $dsn)
    {
        // ...
    }

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    // ...
}

$singleton = DBConnection::getInstance();

Tốt hơn

declare(strict_types=1);

class DBConnection
{
    public function __construct(string $dsn)
    {
        // ...
    }

    // ...
}

Khi này, bạn tạo đối tượng của DBConnection và sử dụng

Ví dụ

declare(strict_types=1);

$connection = new DBConnection($dsn);

Đóng gói các điều kiện

Kém

declare(strict_types=1);

if ($article->state === 'published') {
    // ...
}

Tốt

declare(strict_types=1);

if ($article->isPublished()) {
    // ...
}

Loại bỏ các điều kiện phủ định

Kém

declare(strict_types=1);

function isDOMNodeNotPresent(DOMNode $node): bool
{
    // ...
}

if (! isDOMNodeNotPresent($node)) {
    // ...
}

Tốt

declare(strict_types=1);

function isDOMNodePresent(DOMNode $node): bool
{
    // ...
}

if (isDOMNodePresent($node)) {
    // ...
}

Tránh câu lệnh rẽ nhánh liên tục

Thay vì sử dụng câu lệnh if kiểm tra liên tục, bạn có thể sử dụng tính đa hình để làm việc đó.

Kém

declare(strict_types=1);

class Airplane
{
    // ...

    public function getCruisingAltitude(): int
    {
        switch ($this->type) {
            case '777':
                return $this->getMaxAltitude() - $this->getPassengerCount();
            case 'Air Force One':
                return $this->getMaxAltitude();
            case 'Cessna':
                return $this->getMaxAltitude() - $this->getFuelExpenditure();
        }
    }
}

Tốt

declare(strict_types=1);

interface Airplane
{
    // ...

    public function getCruisingAltitude(): int;
}

class Boeing777 implements Airplane
{
    // ...

    public function getCruisingAltitude(): int
    {
        return $this->getMaxAltitude() - $this->getPassengerCount();
    }
}

class AirForceOne implements Airplane
{
    // ...

    public function getCruisingAltitude(): int
    {
        return $this->getMaxAltitude();
    }
}

class Cessna implements Airplane
{
    // ...

    public function getCruisingAltitude(): int
    {
        return $this->getMaxAltitude() - $this->getFuelExpenditure();
    }
}

Tránh các câu lệnh kiểm tra kiểu dữ liệu

PHP là ngôn ngữ không kiểu dữ liệu, mặc dù điều này được cải thiện ở các phiên bản PHP mới. Điều này có nghĩa là các function có thể nhận bất kì kiểu dữ liệu nào. Để giải quyết vấn đề này chúng ta có thể giải quyết bằng cách sử dụng tính chất kế thừa, đa hình.

Kém

declare(strict_types=1);

function travelToTexas($vehicle): void
{
    if ($vehicle instanceof Bicycle) {
        $vehicle->pedalTo(new Location('texas'));
    } elseif ($vehicle instanceof Car) {
        $vehicle->driveTo(new Location('texas'));
    }
}

Tốt

declare(strict_types=1);

function travelToTexas(Vehicle $vehicle): void
{
    $vehicle->travelTo(new Location('texas'));
}

Đối với các kiểu dữ liệu nguyên thủy như int, array, string… thì việc sử dụng tính đa hình là không thể. Do vậy hãy chỉ đích danh kiểu dữ liệu của biến.

Kém

declare(strict_types=1);

function combine($val1, $val2): int
{
    if (! is_numeric($val1) || ! is_numeric($val2)) {
        throw new Exception('Must be of type Number');
    }

    return $val1 + $val2;
}

Tốt

declare(strict_types=1);

function combine(int $val1, int $val2): int
{
    return $val1 + $val2;
}

Xóa bỏ các dead code

Dead code cũng tệ như việc duplicate code vậy. Hãy loại bỏ chúng!

Kém

declare(strict_types=1);

function oldRequestModule(string $url): void
{
    // ...
}

function newRequestModule(string $url): void
{
    // ...
}

$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');

Tốt

declare(strict_types=1);

function requestModule(string $url): void
{
    // ...
}

$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');

Các đối tượng và cấu trúc dữ liệu

Trong PHP, bạn sử dụng public, private, protected để điều khiển quyền truy cập vào một đối tượng.

Kém

declare(strict_types=1);

class BankAccount
{
    public $balance = 1000;
}

$bankAccount = new BankAccount();

// Buy shoes...
$bankAccount->balance -= 100;

Tốt

class BankAccount
{
    private $balance;

    public function __construct(int $balance = 1000)
    {
      $this->balance = $balance;
    }

    public function withdraw(int $amount): void
    {
        if ($amount > $this->balance) {
            throw new \Exception('Amount greater than available balance.');
        }

        $this->balance -= $amount;
    }

    public function deposit(int $amount): void
    {
        $this->balance += $amount;
    }

    public function getBalance(): int
    {
        return $this->balance;
    }
}

$bankAccount = new BankAccount();

// Buy shoes...
$bankAccount->withdraw($shoesPrice);

// Get balance
$balance = $bankAccount->getBalance();

Một ví dụ khác

Kém

declare(strict_types=1);

class Employee
{
    public $name;

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

$employee = new Employee('John Doe');
// Employee name: John Doe
echo 'Employee name: ' . $employee->name;

Tốt

declare(strict_types=1);

class Employee
{
    private $name;

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

    public function getName(): string
    {
        return $this->name;
    }
}

$employee = new Employee('John Doe');
// Employee name: John Doe
echo 'Employee name: ' . $employee->getName();

Classes

Ưu tiên sử dụng Composition hơn là Kế thừa.

Bạn nên xác định xem khi nào nên sử dụng kế thừa, khi nào nên sử dụng Composition. Trong mỗi hoàn cảnh khác khác thì các cách này sẽ tạo ra lợi ích nhất định.

Việc kế thừa sẽ có những lợi ích như:

  • Kế thừa thể hiện được mối quan hệ “is – a” và không phải là mối quan hệ “has – a” (Hunmain -> Animal với User -> UserDetails)
  • Sử dụng code từ các lớp cha.
  • Thay đổi code ở lớp dẫn xuất thì toàn bộ các lớp kế thừa đều có tác dụng

Tuy vậy Composition sẽ có hiệu quả hơn trong trường hợp ứng dụng có độ phức tạp cao hơn. Hãy tưởng tượng nếu số lượng class kế thừa lớn, sẽ làm phức tạp ứng dụng, giảm hiệu năng…

Kém

declare(strict_types=1);

class Employee
{
    private $name;

    private $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    // ...
}

// Bad because Employees "have" tax data.
// EmployeeTaxData is not a type of Employee

class EmployeeTaxData extends Employee
{
    private $ssn;

    private $salary;

    public function __construct(string $name, string $email, string $ssn, string $salary)
    {
        parent::__construct($name, $email);

        $this->ssn = $ssn;
        $this->salary = $salary;
    }

    // ...
}

Tốt

declare(strict_types=1);

class EmployeeTaxData
{
    private $ssn;

    private $salary;

    public function __construct(string $ssn, string $salary)
    {
        $this->ssn = $ssn;
        $this->salary = $salary;
    }

    // ...
}

class Employee
{
    private $name;

    private $email;

    private $taxData;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function setTaxData(EmployeeTaxData $taxData): void
    {
        $this->taxData = $taxData;
    }

    // ...
}

Tránh fluent interface

Fluent Interface là một API hướng đối tượng nhằm mục đích cải thiện khả năng đọc của mã nguồn bằng cách sử dụng Method chaining.

Trong một số ngữ cảnh, thường là khi xây dựng object nơi mà pattern này giảm tính rườm rà của code (ví dụ PHPUnit Mock Builder hoặc Doctrine Query Builder), sẽ gây ra một số thiệt hại như sau:

  • Phá vỡ Encapsulation
  • Phá vỡ Decorators
  • Khó tạo mock hơn trong test suite
  • Khiến cho khó đọc "sự khác nhau giữa các file" hơn khi commit code

Kém

declare(strict_types=1);

class Car
{
    private $make = 'Honda';

    private $model = 'Accord';

    private $color = 'white';

    public function setMake(string $make): self
    {
        $this->make = $make;

        // NOTE: Returning this for chaining
        return $this;
    }

    public function setModel(string $model): self
    {
        $this->model = $model;

        // NOTE: Returning this for chaining
        return $this;
    }

    public function setColor(string $color): self
    {
        $this->color = $color;

        // NOTE: Returning this for chaining
        return $this;
    }

    public function dump(): void
    {
        var_dump($this->make, $this->model, $this->color);
    }
}

$car = (new Car())
    ->setColor('pink')
    ->setMake('Ford')
    ->setModel('F-150')
    ->dump();

Tốt

declare(strict_types=1);

class Car
{
    private $make = 'Honda';

    private $model = 'Accord';

    private $color = 'white';

    public function setMake(string $make): void
    {
        $this->make = $make;
    }

    public function setModel(string $model): void
    {
        $this->model = $model;
    }

    public function setColor(string $color): void
    {
        $this->color = $color;
    }

    public function dump(): void
    {
        var_dump($this->make, $this->model, $this->color);
    }
}

$car = new Car();
$car->setColor('pink');
$car->setMake('Ford');
$car->setModel('F-150');
$car->dump();

Nên sử dụng final class

Từ khóa final nên sử dụng khi có thể, vì:

  • Ngăn chặn việc kế thừa không thể kiểm soát
  • Khuyến khích sử dụng Composition thay vì kế thừa
  • Khuyến khích nguyên tắc đơn chức năng trong Solid
  • Khuyến khích các lập trình viên khác tận dụng sử dụng các hàm public thay vì cố gắng mở rộng class và thiệp vào class đó
  • Bạn có thể thay đổi code của mình mà ít ảnh hưởng đến ứng dụng – các phần đang sử dụng class của bạn.

Điều kiện để triển khai là, bạn nên implement một 1 interface và không có thêm public method nào khác.

Kém

declare(strict_types=1);

final class Car
{
    private $color;

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

    /**
     * @return string The color of the vehicle
     */
    public function getColor()
    {
        return $this->color;
    }
}

Tốt

declare(strict_types=1);

interface Vehicle
{
    /**
     * @return string The color of the vehicle
     */
    public function getColor();
}

final class Car implements Vehicle
{
    private $color;

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

    public function getColor()
    {
        return $this->color;
    }
}

SOLID

SOLID là từ viết tắt được đưa ra bởi Michael Feathers cho 5 nguyên lý đầu tiên của Robert Martin, 5 nguyên tắc cơ bản của lập trình hướng đối tượng.

  • S: Nguyên lý trách nhiệm duy nhất (SRP)
  • O: Nguyên lý Đóng/Mở (OCP)
  • L: Nguyên lý thay thế Liskov (LSP)
  • I: Nguyên lý phân tách interface (ISP)
  • D: Nguyên lý đảo ngược dependencies (DIP)

Nguyên lý trách nhiệm duy nhất (SRP)

Như đã đề cập trong cuốn Clean Code, "Không nên có nhiều hơn một lý do để thay đổi class". Viết một class với thật nhiều chức năng thì quá sướng. Vấn đề là class không có khái niệm liên kết và nó có khá nhiều lý do để thay đổi. Nếu quá nhiều chức năng trong một class thì khi thay đổi gì đó mình không biết được hết những ảnh hưởng của nó đến các chức năng khác trong các module liên quan.

Chưa tốt:

class UserSettings
{
    private $user;

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

    public function changeSettings(array $settings): void
    {
        if ($this->verifyCredentials()) {
            // ...
        }
    }

    private function verifyCredentials(): bool
    {
        // ...
    }
}

Tốt:

class UserAuth 
{
    private $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }
    
    public function verifyCredentials(): bool
    {
        // ...
    }
}

class UserSettings 
{
    private $user;
    private $auth;

    public function __construct(User $user) 
    {
        $this->user = $user;
        $this->auth = new UserAuth($user);
    }

    public function changeSettings(array $settings): void
    {
        if ($this->auth->verifyCredentials()) {
            // ...
        }
    }
}

Nguyên lý Đóng/Mở (OCP)

Như đã đề cập bởi Bertrand Meyer, "thực thể phần mềm (lớp, modules, hàm, etc...) nên cho phép mở rộng, nhưng không cho phép sửa đổi." Điều đó có nghĩa là gì? Nguyên lý này đơn giản là nên cho phép người dùng thêm mới mà không được thay đổi code hiện tại.

Chưa tốt:

abstract class Adapter
{
    protected $name;

    public function getName(): string
    {
        return $this->name;
    }
}

class AjaxAdapter extends Adapter
{
    public function __construct()
    {
        parent::__construct();

        $this->name = 'ajaxAdapter';
    }
}

class NodeAdapter extends Adapter
{
    public function __construct()
    {
        parent::__construct();

        $this->name = 'nodeAdapter';
    }
}

class HttpRequester
{
    private $adapter;

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

    public function fetch(string $url): Promise
    {
        $adapterName = $this->adapter->getName();

        if ($adapterName === 'ajaxAdapter') {
            return $this->makeAjaxCall($url);
        } elseif ($adapterName === 'httpNodeAdapter') {
            return $this->makeHttpCall($url);
        }
    }

    private function makeAjaxCall(string $url): Promise
    {
        // request and return promise
    }

    private function makeHttpCall(string $url): Promise
    {
        // request and return promise
    }
}

Tốt:

interface Adapter
{
    public function request(string $url): Promise;
}

class AjaxAdapter implements Adapter
{
    public function request(string $url): Promise
    {
        // request and return promise
    }
}

class NodeAdapter implements Adapter
{
    public function request(string $url): Promise
    {
        // request and return promise
    }
}

class HttpRequester
{
    private $adapter;

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

    public function fetch(string $url): Promise
    {
        return $this->adapter->request($url);
    }
}

Nguyên lý thay thế Liskov (LSP)

Nguyên lý này được định nghĩa như sau "Nếu S là phụ thuộc của T, thì object của T có thể được thay thế bởi object của S (nghĩa là object của S có thể thay thế object của T) mà không làm thay đổi các thuộc tính của chương trình(tính đúng đắn, công việc thực hiện,...)"

Để dễ hiểu hơn, nếu bạn có một class cha và một class con, sau đó class cha và class con có thể được sử dụng hoán đổi cho nhau mà không sai kết quả trả về. Có thể vẫn còn khó hiểu, hãy xem ví dụ cơ bản Square-Rectangle bên dưới.

Trong toán học, hình vuông là hình chữ nhật, nhưng nếu bạn sử dụng quan hệ "is-a" qua kế thừa, bạn sẽ gặp rắc rối.

Chưa tốt:

class Rectangle
{
    protected $width = 0;
    protected $height = 0;

    public function render(int $area): void
    {
        // ...
    }

    public function setWidth(int $width): void
    {
        $this->width = $width;
    }

    public function setHeight(int $height): void
    {
        $this->height = $height;
    }

    public function getArea(): int
    {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle
{
    public function setWidth(int $width): void
    {
        $this->width = $this->height = $width;
    }

    public function setHeight(int $height): void
    {
        $this->width = $this->height = $height;
    }
}

/**
 * @param Rectangle[] $rectangles
 */
function renderLargeRectangles(array $rectangles): void
{
    foreach ($rectangles as $rectangle) {
        $rectangle->setWidth(4);
        $rectangle->setHeight(5);
        $area = $rectangle->getArea(); // Lỗi rồi: Đoạn này sẽ trả về 25, nhưng 20 mới là kết quả đúng.
        $rectangle->render($area);
    }
}

$rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles($rectangles);

Tốt:

abstract class Shape
{
    abstract public function getArea(): int;

    public function render(int $area): void
    {
        // ...
    }
}

class Rectangle extends Shape
{
    private $width;
    private $height;

    public function __construct(int $width, int $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function getArea(): int
    {
        return $this->width * $this->height;
    }
}

class Square extends Shape
{
    private $length;

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

    public function getArea(): int
    {
        return pow($this->length, 2);
    }
}

/**
 * @param Rectangle[] $rectangles
 */
function renderLargeRectangles(array $rectangles): void
{
    foreach ($rectangles as $rectangle) {
        $area = $rectangle->getArea(); 
        $rectangle->render($area);
    }
}

$shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeRectangles($shapes);

Nguyên lý phân tách interface (ISP)

ISP đề cập rằng "Không nên ép người dùng phải phụ thuộc vào interface mà họ không sử dụng."

Để hiểu ý nghĩa của nguyên tắc này, hãy nhìn vào những class yêu cầu một số lượng lớn các object cần phải inject vào để sử dụng. Không yêu cầu người dùng phải inject số lượng lớn các tùy chọn là một lợi thế, bởi vì hầu hết chúng không cần thiết. Hãy coi chúng là tùy chọn(có thể không dùng) để giúp cho interface bớt phình to.

Chưa tốt

interface Employee
{
    public function work(): void;

    public function eat(): void;
}

class Human implements Employee
{
    public function work(): void
    {
        // ....working
    }

    public function eat(): void
    {
        // ...... eating in lunch break
    }
}

class Robot implements Employee
{
    public function work(): void
    {
        //.... working much more
    }

    public function eat(): void
    {
        //.... robot can't eat, but it must implement this method
    }
}

Tốt:

Không phải tất cả worker đều là employee, nhưng employee là một worker.

Ví dụ

interface Workable
{
    public function work(): void;
}

interface Feedable
{
    public function eat(): void;
}

interface Employee extends Feedable, Workable
{
}

class Human implements Employee
{
    public function work(): void
    {
        // ....working
    }

    public function eat(): void
    {
        //.... eating in lunch break
    }
}

// robot can only work
class Robot implements Workable
{
    public function work(): void
    {
        // ....working
    }
}

Nguyên lý đảo ngược dependencies (DIP)

Nguyên lý này đề cập 2 vấn đề cơ bản:

  • Module cấp cao không nên phụ thuộc vào module cấp thấp. Cả hai nên phụ thuộc vào abstract.
  • Abstract không nên phụ thuộc vào chi tiết, mà phải ngược lại.

Hơi khó hiểu một chút đúng không? Nhưng nếu bạn làm việc với PHP frameworks (ví dụ Symfony), bạn sẽ thấy nguyên tắc này được áp dụng trên Dependency Injection(DI). Một lợi ích lớn của việc này là chúng giảm sự trùng lặp giữa các modules. Trùng lặp thì tất nhiên Chưa tốt vì chúng khiến code khó refactor.

Chưa tốt:

class Employee
{
    public function work(): void
    {
        // ....working
    }
}

class Robot extends Employee
{
    public function work(): void
    {
        //.... working much more
    }
}

class Manager
{
    private $employee;

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

    public function manage(): void
    {
        $this->employee->work();
    }
}

Tốt:

interface Employee
{
    public function work(): void;
}

class Human implements Employee
{
    public function work(): void
    {
        // ....working
    }
}

class Robot implements Employee
{
    public function work(): void
    {
        //.... working much more
    }
}

class Manager
{
    private $employee;

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

    public function manage(): void
    {
        $this->employee->work();
    }
}

Nguyên lý đừng lặp lại chính mình (DRY)

Tìm hiểu thêm về nguyên lý DRY

Tốt nhất nên chống lặp code ngay khi có thể. Vì lặp code không tốt tí nào, khi bạn muốn thay đổi logic bạn cần phải sửa nhiều chỗ.

Hãy tưởng tượng bạn đang vận hành một nhà hàng và bạn theo dõi lượng hàng tồn kho của bạn: cà chua, hành, tỏi, gia vị,... Nếu bạn có nhiều danh sách để quản lý chúng, bạn cần cập nhật tất cả các danh sách đó mỗi khi bạn bán một đĩa thức ăn. Nhưng nếu như bạn chỉ có 1 danh sách, thì chỉ cần cập nhật ở một nơi!

Thỉnh thoảng vẫn có lặp code bởi vì bạn có hai hoặc nhiều hơn những thứ khác nhau, có nhiều điểm chung, nhưng sự khác nhau giữa chúng buộc bạn phải chia ra 2 hàm làm rất nhiều việc. Để xóa bỏ lặp code, cần tạo ra một abstract có thể xử lý sự khác biệt giữa chúng với chỉ 1 hàm/module/lớp.

Tạo ra được abstract tốt rất quan trọng và khó, đó là lý do tại sao bạn nên dựa theo các nguyên lý SOLID được đưa ra tại mục Lớp. Abstract củ chuối có thể sẽ tệ hại hơn là lặp code, hãy cẩn thận! Nếu có thể tạo một abstract tốt, hãy tạo nó! Đừng lặp lại code, nếu không bạn sẽ phải rất cực khổ mỗi khi muốn sửa đổi gì đó.

Chưa tốt:

function showDeveloperList(array $developers): void
{
    foreach ($developers as $developer) {
        $expectedSalary = $developer->calculateExpectedSalary();
        $experience = $developer->getExperience();
        $githubLink = $developer->getGithubLink();
        $data = [
            $expectedSalary,
            $experience,
            $githubLink
        ];

        render($data);
    }
}

function showManagerList(array $managers): void
{
    foreach ($managers as $manager) {
        $expectedSalary = $manager->calculateExpectedSalary();
        $experience = $manager->getExperience();
        $githubLink = $manager->getGithubLink();
        $data = [
            $expectedSalary,
            $experience,
            $githubLink
        ];

        render($data);
    }
}

Tốt:

function showList(array $employees): void
{
    foreach ($employees as $employee) {
        $expectedSalary = $employee->calculateExpectedSalary();
        $experience = $employee->getExperience();
        $githubLink = $employee->getGithubLink();
        $data = [
            $expectedSalary,
            $experience,
            $githubLink
        ];

        render($data);
    }
}

Rất tốt:

Sử dụng một phiên bản gọn hơn

Ví dụ

function showList(array $employees): void
{
    foreach ($employees as $employee) {
        render([
            $employee->calculateExpectedSalary(),
            $employee->getExperience(),
            $employee->getGithubLink()
        ]);
    }
}

Để đảm bảo có được một ứng dụng web sạch sẽ và tốt bạn nên áp dụng các gợi ý viết code sạch trong PHP trên đây. Mặc dù các tiêu chí này không phải là bắt buộc nhưng là điều cần thiết trước khi phát triển ứng dụng bằng PHP.

Bài viết tham khảo: clean-code-php

Bài viết này đã giúp ích cho bạn?

Bài viết mới

Advertisements