Vấn đề Clean Code đã được nhắc tới từ trong cuốn sách cùng tên của C. Martin. Cuốn sách này cũng được Code từ tâm nhắc đến trong các bài viết trước đây. Các bạn có thể tham khảo chuỗi bài Clean code tiếng việt.
Trong bài viết này sẽ đi cụ thể hơn về Clean Code với PHP. Bài viết được trên bài viết gốc tại https://github.com/jupeter/clean-code-php
Mã 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');
Kém
declare(strict_types=1); getUserInfo(); getUserData(); getUserRecord(); getUserProfile();
Tốt
declare(strict_types=1); getUser();
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;
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']);
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); }
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; //... }
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!
function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void { // ... }
Không tồi
Việc check 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 để check giá trị của biến trước khi sử dụng.
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.
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
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.
declare(strict_types=1); $a = '42'; $b = 42; if ($a !== $b) { // The expression is verified }
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';
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) { // ... } }
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();
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.
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... } } }
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); }
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 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'];
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
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
declare(strict_types=1); $connection = new DBConnection($dsn);
Kém
declare(strict_types=1); if ($article->state === 'published') { // ... }
Tốt
declare(strict_types=1); if ($article->isPublished()) { // ... }
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)) { // ... }
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(); } }
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; }
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');
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();
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; } // ... }
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.
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();
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; } }
Để đả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 ý Clean code 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.
Ngoài các tiêu chí clean code trong php này bạn nên tuân thủ theo nguyên tắc SOLID để đảm bảo chương trình được tối ưu và đạt hiệu quả tốt khi phát triển và vận hành. Trong suốt quá trình phát triển nên hạn chế tối đa việc lặp lại code không cần thiết. Điều này còn biết đến với tên là Nguyên tắc DRY (Dont Repeat yourself)!
Bình luận: