Clean Code PHP

Đây là những nguyên lý kỹ thuật phần mềm, được trích từ cuốn sách Clean Code của tác giả Robert C. Martin(thường gọi là Uncle Bob) rất thích hợp cho ngôn ngữ PHP. Tài liệu này không phải là sách hướng dẫn về phong cách viết code, mà là hướng dẫn cách làm thế nào để viết code dễ đọc, dễ sử dụng lại, và dễ cải tiến trong PHP.

Bạn không cần phải tuân theo tất cả các nguyên tắc trong tài liệu này. Đây chỉ đơn giản là những hướng dẫn, nhưng dù sao nó cũng là đúc kết từ nhiều năm kinh nghiệm của tác giả.

** Note: Mình chỉ dịch từ mục 5 trở đi thôi nhé 😆😆😆

1. Đối tượng và kiến trúc dữ liệu

1.1. Sử dụng đối tượng đóng gói

Trong PHP, bạn có thể khai báo public, protectedprivate cho các thuộc tính và phương thức.Sử dụng nó thì bạn có thể kiểm soát được sự thay đổi thuộc tính trong Object.

  • Khi bạn muốn làm nhiều hơn là ngoài việc có được thuộc tính của đối tượng, bạn không phải tìm kiếm và thay đổi quyền khi truy cập vào đối tượng của bạn.
  • Tạo thêm validation đơn giản khi thực hiện set.
  • Đóng gói các thành phần bên trong.
  • Dễ dàng ghi log và xử lý lỗi khi getset.
  • Kế thừa lớp, bạn có thể ghi đè các phương thức mặc định.
  • Bạn có thể lazy load các thuộc tính của object, giả sử nó được lấy từ server. Thêm nữa, nó là thành phần của nguyên tắc Open/Closed

Chưa tốt:

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();

1.2. Tạo đối tượng có chứa thuộc tính hoặc phương thức private/protected

  • Phương thức và thuộc tính public khá nguy hiểm, bởi vì vài dòng code bên ngoài có thể dễ dàng thay đổi chúng và bạn không thể kiểm soát được nó thay đổi những gì. Thay đổi trong lớp là nguy hiểm cho tất cả người dùng class đó.
  • protected cũng nguy hiểm không kém, bởi vì chúng được cấp quyền ở các lớp con. Điều này có nghĩa là sự khác nhau giữa publicprotected chỉ là cơ chế truy cập, nhưng tính đóng gói vẫn giữ nguyên. Thay đổi trong lớp là nguy hiểm cho các lớp con.
  • private sửa đổi đảm bảo rằng code thay đổi chỉ nguy hiểm trong lớp đó Do đó hãy mặc định sử dụng privateprivate/public khi bạn cần truy cập từ lớp bên ngoài.

Chưa tốt:

class Employee
{
 public $name;

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

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

Tốt:

class Employee
{
 private $name;

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

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

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

2. Lớp

2.1. Ưu tiên thành phần hơn kế thừa

Có thể bạn sẽ tự hỏi, khi nào nên sử dụng kế thừa ?. Nó tùy thuộc vào từng vấn đề của bạn, nhưng đây là danh sách hợp lý khi nào thì kiểu kế thừa tốt hơn kiểu thành phần:

  • Kiểu kế thừa đó đại diện cho mối quan hệ is-a không phải quan hệ has-a (Human->Animal vs Human->UserDetail).
  • Bạn cần sử dụng lại code từ lớp cha ((Human)Người có thể chạy như (Animal)Động vật)
  • Bạn muốn khi sử đổi lớp cha thì các lớp có liên quan cũng thay đổi theo.

Chưa tốt:

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:

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(string $ssn, string $salary)
    {
        $this->taxData = new EmployeeTaxData($ssn, $salary);
    }

    // ...
}

2.2. Ưu tiên final class

final nên được sử dụng bất cứ khi nào có thể:

  • Ngăn chặn kế thừa không kiểm soát.
  • Khuyến khích thành phần hơn là kế thừa.
  • Khuyến khích Single Responsibility Pattern.
  • Khuyến khích các developer sử dụng phương thức public thay vì kế thừa lớp để có quyền truy cập vào các phương thức protected.
  • Cho phép bạn thay đổi code của mình mà không làm thay đổi các thành phần liên quan sử dụng lớp của bạn. Điều duy nhất là lớp của bên nên implement một interface và không có phương thức public nào được định nghĩa.

Chưa tốt:

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:

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;
    }

    /**
     * {@inheritdoc}
     */
    public function getColor()
    {
        return $this->color;
    }
}

3. Nguyên tắc 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)

Một lớp và một phương thức nên chỉ có một trách nhiệm và nên chỉ có một trách nhiệm mà thô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)

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 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;
    }
}

function printArea(Rectangle $rectangle): void
{
    $rectangle->setWidth(4);
    $rectangle->setHeight(5);

    // BAD: Will return 25 for Square. Should be 20.
    echo sprintf('%s has area %d.', get_class($rectangle), $rectangle->getArea()).PHP_EOL;
}

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

foreach ($rectangles as $rectangle) {
    printArea($rectangle);
}

Tốt:

interface Shape
{
    public function getArea(): int;
}

class Rectangle implements Shape
{
    private $width = 0;
    private $height = 0;

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

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

class Square implements Shape
{
    private $length = 0;

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

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

function printArea(Shape $shape): void
{
    echo sprintf('%s has area %d.', get_class($shape), $shape->getArea()).PHP_EOL;
}

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

foreach ($shapes as $shape) {
    printArea($shape);
}

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 HumanEmployee implements Employee
{
    public function work(): void
    {
        // ....working
    }

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

class RobotEmployee 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:

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

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

interface Employee extends Feedable, Workable
{
}

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

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

// robot can only work
class RobotEmployee 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.

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();
    }
}

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

Bạn đã bao giờ viết các đoạn code giống nhau nằm ở các phần, module khác nhau của project? bạn đã bao giờ có 2 màn hình giống nhau nhưng lại dùng tới 2 đoạn code để hiển thị 2 màn hình đó? Đừng lặp lại code ở đây là không lặp lại các đoạn code giống nhau, các method thực hiện chức năng như nhau, cố gắng gom chúng lại 1 cách gọn gàng và có thể dùng lại khi cần.

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:

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

Nguồn tham khảo

clean-code-php