- Вступ
- Змінні
- Використовуйте значущі й вимовні імена змінних
- Для одного типу змінних використовуйте єдиний словник
- Використовуйте імена, за якими зручно шукати (частина 1)
- Використовуйте імена, за якими зручно шукати (частина 2)
- Використовуйте пояснювальні змінні
- Уникайте глибоких вкладень (частина 1)
- Уникайте глибоких вкладень (частина 2)
- Уникайте ментального зіставлення
- Не додавайте непотрібний контекст
- Використовуйте аргументи за замовчуванням замість коротких або умовних
- Порівняння
- Функції
- Аргументи функції (ідеально 2 або менше)
- Назви функцій мають вказувати на їх призначення
- Функції повинні мати лише один рівень абстракції
- Не використовуйте прапорці як параметри функцій
- Уникайте побічних ефектів
- Не записуйте до глобальних функцій
- Не використовуйте патерн Одинак
- Інкапсулюйте умовні конструкції
- Уникайте умовних конструкцій із запереченням
- Уникайте умовних конструкцій
- Уникайте перевірки типів (частина 1)
- Уникайте перевірки типів (частина 2)
- Вилучіть мертвий код
- Об'єкти та структури даних
- Класи
- SOLID
- Не повторюйте себе (DRY)
Принципи розробки програмного забезпечення з книги Роберта К. Мартіна Clean Code адаптований для PHP. Це не керівництво по стилю. Це посібник по створенню читабельного, повторно використовуваного та придатного до рефакторингу програмного забезпечення на PHP.
Не обов'язково дотримуватися кожного принципу, викладеного тут, і ще менше з них будуть прийняті всіма. Це лише рекомендації, але вони були сформовані протягом багатьох років колективного досвіду авторами "Чистого коду".
На основі clean-code-javascript
Більшість прикладів у цій статті працюють з PHP 7+.
Погано:
$ymdstr = $moment->format('y-m-d');
Добре:
$currentDate = $moment->format('y-m-d');
Погано:
getUserInfo();
getUserData();
getUserRecord();
getUserProfile();
Добре:
getUser();
Ми читаємо більше коду, ніж пишемо. Важливо, щоб код, який ми пишемо, був читабельним та здатним до пошуку. Надавайте своїм іменам зрозумілі назви, які можна легко знайти.
Погано:
// What the heck is 448 for?
$result = $serializer->serialize($data, 448);
Добре:
$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
Погано:
class User
{
// What the heck is 7 for?
public $access = 7;
}
// What the heck is 4 for?
if ($user->access & 4) {
// ...
}
// What's going on here?
$user->access ^= 2;
Добре:
class User
{
public const ACCESS_READ = 1;
public const ACCESS_CREATE = 2;
public const ACCESS_UPDATE = 4;
public const ACCESS_DELETE = 8;
// User as default can read, create and update something
public $access = self::ACCESS_READ | self::ACCESS_CREATE | self::ACCESS_UPDATE;
}
if ($user->access & User::ACCESS_UPDATE) {
// do edit ...
}
// Deny access rights to create something
$user->access ^= User::ACCESS_CREATE;
Погано:
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches[1], $matches[2]);
Не погано:
Це краще, але ми все ще сильно залежні від regex.
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);
[, $city, $zipCode] = $matches;
saveCityZipCode($city, $zipCode);
Добре:
Зменшуйте залежність від регулярних виразів, надаючи імена підшаблонам.
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(?<city>.+?)\s*(?<zipCode>\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches['city'], $matches['zipCode']);
Занадто багато умовних операторів if-else можуть зробити ваш код важким для розуміння. Явне краще, ніж неявне.
Погано:
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;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
}
Добре:
function isShopOpen(string $day): bool
{
if (empty($day)) {
return false;
}
$openingDays = [
'friday', 'saturday', 'sunday'
];
return in_array(strtolower($day), $openingDays, true);
}
Погано:
function fibonacci(int $n)
{
if ($n < 50) {
if ($n !== 0) {
if ($n !== 1) {
return fibonacci($n - 1) + fibonacci($n - 2);
} else {
return 1;
}
} else {
return 0;
}
} else {
return 'Not supported';
}
}
Добре:
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);
}
Не змушуйте читача вашого коду перекладати значення змінної. Явне краще, ніж неявне.
Погано:
$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);
}
Добре:
$locations = ['Austin', 'New York', 'San Francisco'];
foreach ($locations as $location) {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch($location);
}
Якщо ім'я вашого класу/об'єкта є змістовним і зрозумілим, не повторюйте це в імені змінної.
Погано:
class Car
{
public $carMake;
public $carModel;
public $carColor;
//...
}
Добре:
class Car
{
public $make;
public $model;
public $color;
//...
}
Погано:
Це погано тому, що $breweryName
може бути NULL
.
function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void
{
// ...
}
Не погано:
Ця думка більш зрозуміла, ніж попередня версія, бо краще контролює значення змінної.
function createMicrobrewery($name = null): void
{
$breweryName = $name ?: 'Hipster Brew Co.';
// ...
}
Добре:
Ви можете використовувати type hinting
і бути упевненим, що $breweryName
не буде NULL
.
function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void
{
// ...
}
Використовуйте ідентичне порівняння
Погано:
Просте порівняння перетворить рядок на ціле число.
$a = '42';
$b = 42;
if ($a != $b) {
// The expression will always pass
}
Порівняння $a != $b
повертає FALSE
, але насправді це TRUE
! Рядок "42"
відрізняється від цілого числа 42
.
Добре:
Ідентичне порівняння порівнює тип і значення.
$a = '42';
$b = 42;
if ($a !== $b) {
// The expression is verified
}
Порівняння $a !== $b
поверне TRUE
.
До PHP 8.0.0, якщо рядок (string) порівнювався з числом або рядком, що містить число, то рядок (string) перетворювалося на число перед виконанням порівняння. Це могло призвести до несподіваних результатів, які можна побачити на наступному прикладі.
<?php
var_dump(0 == "a");
var_dump("1" == "01");
var_dump("10" == "1e1");
var_dump(100 == "1e2");
switch ("a") {
case 0:
echo "0";
break;
case "a":
echo "a";
break;
}
?>
Результати порівняння:
//PHP 7
bool(true)
bool(true)
bool(true)
bool(true)
0
//PHP 8
bool(false)
bool(true)
bool(true)
bool(true)
a
Null об'єднання є новим оператором введено в PHP 7.
Оператор злиття з перевіркою на NULL ??
був доданий як синтаксичний цукор для звичайного випадку, коли потрібно
використовувати тернарний оператор разом з isset()
. Він повертає свій перший операнд, якщо він існує і не є null
;
в іншому випадку він повертає свій другий операнд.
Погано:
if (isset($_GET['name'])) {
$name = $_GET['name'];
} elseif (isset($_POST['name'])) {
$name = $_POST['name'];
} else {
$name = 'nobody';
}
Добре:
$name = $_GET['name'] ?? $_POST['name'] ?? 'nobody';
Limiting the amount of function parameters is incredibly important because it makes testing your function easier. Having more than three leads to a combinatorial explosion where you have to test tons of different cases with each separate argument.
Zero arguments is the ideal case. One or two arguments is ok, and three should be avoided. Anything more than that should be consolidated. Usually, if you have more than two arguments then your function is trying to do too much. In cases where it's not, most of the time a higher-level object will suffice as an argument.
Погано:
class Questionnaire
{
public function __construct(
string $firstname,
string $lastname,
string $patronymic,
string $region,
string $district,
string $city,
string $phone,
string $email
) {
// ...
}
}
Добре:
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)
{
// ...
}
}
Погано:
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();
Добре:
class Email
{
//...
public function send(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// Clear and obvious
$message->send();
Коли у вас є більше одного рівня абстракції, ваша функція зазвичай робить забагато. Розбиття функцій призводить до повторного використання та полегшує тестування.
Погано:
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...
}
}
Погано too:
Ми виконали деяку функціональність, але функція parseBetterPHPAlternative()
все ще дуже складна і не піддається тестуванню.
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...
}
}
Добре:
Найкращим рішенням є вилучення залежностей від функції parseBetterPHPAlternative()
.
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...
}
}
}
Булеві параметри говорять користувачеві, що функція робить більше, ніж одну річ. Функції повинні робити одну річ. Розбийте вашу функцію на кілька, якщо вони йдуть різними шляхами коду, залежно від булевого значення.
Погано:
function createFile(string $name, bool $temp = false): void
{
if ($temp) {
touch('./temp/'.$name);
} else {
touch($name);
}
}
Добре:
function createFile(string $name): void
{
touch($name);
}
function createTempFile(string $name): void
{
touch('./temp/'.$name);
}
Функція викликає побічний ефект, якщо вона робить щось, крім приймання значення та повернення іншого значення або значень. Побічні ефекти можуть включати запис до файлу, зміну глобальної змінної або випадкове перерахування всіх ваших грошей незнайомцю.
Звичайно, у програмі потрібно мати побічні ефекти. Наприклад, як у попередньому прикладі, вам може знадобитися запис до файлу. Важливо централізувати те, що ви робите. Не створюйте декілька функцій і класів, що записують до певного файлу. Має бути лише один сервіс, що це робить.
Основна ідея полягає у тому, щоб уникати поширених помилок, таких як спільний стан між об'єктами без будь-якої структури, використання змінних типів даних, які можуть бути змінені будь-чим та невідцентроване розміщення побічних ефектів. Якщо ви можете досягти цього, ви будете щасливішими за більшість інших програмістів.
Погано:
// 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'];
Добре:
function splitIntoFirstAndLastName(string $name): array
{
return explode(' ', $name);
}
$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);
var_dump($name); // 'Ryan McDermott';
var_dump($newName); // ['Ryan', 'McDermott'];
Забруднення глобальних змінних є поганою практикою у багатьох мовах програмування, оскільки ви можете зіткнутися з іншою
бібліотекою, і користувач вашого API не буде попереджений, поки не отримає виняток у продакшені.
Подумаймо про приклад: що, якщо вам потрібен масив конфігурації? Ви можете написати глобальну функцію, наприклад,
config()
, але це може зіткнутися з іншою бібліотекою, яка також намагатиметься зробити те саме.
Погано:
function config(): array
{
return [
'foo' => 'bar',
];
}
Добре:
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;
}
}
Завантажте конфігурацію та створіть екземпляр класу Configuration
.
$configuration = new Configuration([
'foo' => 'bar',
]);
І тепер вам потрібно використовувати екземпляр класу Configuration
у своєму додатку.
Сінґлтон є анти-патерном. За перефразованими словами Брайана Баттона:
- Зазвичай він використовуються як глобальний екземпляр. Чому це Погано? Тому що ви приховуєте залежності вашої програми в вашому коді, замість того, щоб викладати їх через інтерфейси. Робити щось глобальним, щоб уникнути передачі даних по всій програмі, є запахом коду.
- Він порушує принцип єдиного обов'язку, оскільки керують своїм власним створенням та життєвим циклом.
- Він неодмінно призводить до зв'язності. Це ускладнює їх емуляцію під час тестування, в багатьох випадках.
- Він зберігає стан упродовж життєвого циклу програми. Це також впливає на тестування, оскільки може статися так, що тести потребують послідовного запуску, що не допустимо для юніт-тестів. Чому? Тому що кожен юніт-тест має бути незалежним від інших.
Також Misko Hevery висловлює думку про корінь проблеми.
Погано:
class DBConnection
{
private static $instance;
private function __construct(string $dsn)
{
// ...
}
public static function getInstance(): DBConnection
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
// ...
}
$singleton = DBConnection::getInstance();
Добре:
class DBConnection
{
public function __construct(string $dsn)
{
// ...
}
// ...
}
Створіть екземпляр класу DBConnection
та налаштуйте його з допомогою DSN.
$connection = new DBConnection($dsn);
Тепер вам потрібно використовувати екземпляр класу DBConnection
у своєму додатку.
Погано:
if ($article->state === 'published') {
// ...
}
Добре:
if ($article->isPublished()) {
// ...
}
Погано:
function isDOMNodeNotPresent(\DOMNode $node): bool
{
// ...
}
if (!isDOMNodeNotPresent($node))
{
// ...
}
Добре:
function isDOMNodePresent(\DOMNode $node): bool
{
// ...
}
if (isDOMNodePresent($node)) {
// ...
}
Це здається неможливим завданням. Почувши це вперше, більшість людей кажуть: "як я повинен щось зробити без оператора
if
?" Відповідь полягає в тому, що в багатьох випадках можна використовувати поліморфізм, щоб досягти тієї ж самої мети.
Друге запитання зазвичай таке: "добре, це чудово, але навіщо мені це потрібно?" Відповідь полягає в попередньому концепті
чистого коду, який ми вивчили: функція повинна робити тільки одну річ. Коли в класах та функціях є оператори if
, ви
повідомляєте своїм користувачам, що ваша функція робить більше однієї речі. Пам'ятайте, робіть тільки одну річ.
Погано:
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();
}
}
}
Добре:
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 не строго типізована мова, що означає, що ваші функції можуть приймати будь-який тип аргументу. Іноді ця свобода може виявитись проблемою, і з'являється спокуса робити перевірку типів у ваших функціях. Але існує багато способів, які допоможуть уникнути цього. Перш за все потрібно забезпечити стійкий API.
Погано:
function travelToTexas($vehicle): void
{
if ($vehicle instanceof Bicycle) {
$vehicle->pedalTo(new Location('texas'));
} elseif ($vehicle instanceof Car) {
$vehicle->driveTo(new Location('texas'));
}
}
Добре:
function travelToTexas(Vehicle $vehicle): void
{
$vehicle->travelTo(new Location('texas'));
}
Якщо ви працюєте з базовими примітивними типами даних, такими як рядки, цілі числа та масиви, і використовуєте PHP 7+ та не можете використовувати поліморфізм, але все ще відчуваєте потребу в перевірці типів, вам слід розглянути використання оголошення типу або строгого режиму. Це надає вам статичну типізацію на основі стандартного синтаксису PHP. Проблема з ручною перевіркою типів полягає в тому, що це вимагає так багато додаткового коду, що псевдо "типобезпека", яку ви отримуєте, не виправдовує втраченої зрозумілості коду. Тримайте свій PHP код чистим, пишіть добрі тести та проводьте добре код-ревю. Інакше, робіть все це, але з використанням строгої типізації PHP або строгого режиму.
Погано:
function combine($val1, $val2): int
{
if (!is_numeric($val1) || !is_numeric($val2)) {
throw new \Exception('Must be of type Number');
}
return $val1 + $val2;
}
Добре:
function combine(int $val1, int $val2): int
{
return $val1 + $val2;
}
Мертвий код так само погано, як і дубльований код. Немає причин залишати його в своєму кодові. Якщо він не використовується, видаліть його! Він все ще буде збережений в історії версій, якщо вам все ще потрібно буде до нього звернутися.
Погано:
function oldRequestModule(string $url): void
{
// ...
}
function newRequestModule(string $url): void
{
// ...
}
$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
Добре:
function requestModule(string $url): void
{
// ...
}
$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
У PHP ви можете використовувати ключові слова public
, protected
та private
для методів. Використовуючи їх,
ви можете контролювати зміну властивостей об'єкта.
- Коли вам потрібно більше, ніж лише отримання властивості об'єкта, вам не потрібно шукати й змінювати кожний метод доступу в вашій кодовій базі.
- Дозволяє легко додавати валідацію під час виклику
set
. - Інкапсулює внутрішнє представлення об'єкта.
- Легко додати логування та обробку помилок під час отримання та встановлення властивостей.
- Наслідуючи цей клас, ви можете перевизначити функціональність за замовчуванням.
- Ви можете ліниво завантажувати властивості вашого об'єкта, наприклад, отримуючи їх з сервера.
Крім того, це частина принципу Open/Closed.
Погано:
class BankAccount
{
public $balance = 1000;
}
$bankAccount = new BankAccount();
// Buy shoes...
$bankAccount->balance -= 100;
Добре:
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();
public
модифікатор для методів та властивостей є найбільш небезпечними для змін, оскільки зовнішній код може легко розраховувати на них і ви не можете контролювати, на що розраховує код. Модифікації в класі є небезпечними для всіх користувачів класу.protected
модифікатор так само небезпечний, як іpublic
, оскільки він доступний в області будь-якого дочірнього класу. Це практично означає, що різниця міжpublic
таprotected
полягає лише у механізмі доступу, але гарантія інкапсуляції залишається тією ж самою. Модифікації в класі є небезпечними для всіх нащадків класу.private
модифікатор гарантує, що код є небезпечним для змін лише в межах одного класу (ви захищені від модифікацій ззовні й не матимете ефекту Jenga).
Отже, за замовчуванням використовуйте private
, а public/protected
- коли вам потрібно надати доступ зовнішнім класам.
Для отримання додаткової інформації ви можете прочитати пост у блозі на цю тему, написаний Fabien Potencier.
Погано:
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
Добре:
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
Як зазначається у відомій книзі Design Patterns від "Банди Чотирьох", варто віддавати перевагу композиції перед успадкуванням, якщо це можливо. Є багато добрих причин для використання успадкування і багато добрих причин для використання композиції. Основна мета цього правила полягає в тому, що якщо ваш розум інстинктивно налаштований на успадкування, спробуйте подумати, чи може композиція краще описати вашу проблему. У деяких випадках вона може бути кращим рішенням.
You might be wondering then, "when should I use inheritance?" It depends on your problem at hand, but this is a decent list of when inheritance makes more sense than composition:
Ви, можливо, ставите собі питання: "коли ж мені варто використовувати наслідування?" Це залежить від вашої конкретної проблеми, але ось список випадків, коли наслідування є більш доцільним, ніж композиція:
- Ваше наслідування представляє "є" відношення, а не "має" відношення (Human->Animal проти User->UserDetails).
- Ви можете використовувати код з базових класів (Людина може рухатись як будь-яка тварина).
- Ви хочете внести глобальні зміни в похідні класи, змінивши базовий клас (зміна калорійної витрати всіх тварин під час руху).
Погано:
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;
}
// ...
}
Добре:
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)
{
$this->taxData = $taxData;
}
// ...
}
Fluent interface це об'єктноорієнтований API, який має на меті покращити читабельність вихідного коду за допомогою використання Ланцюжкових методів.
Хоча є деякі контексти, часто об'єкти-будівники, де цей шаблон зменшує об'єм коду (наприклад, PHPUnit Mock Builder чи Doctrine Query Builder), частіше це пов'язано з деякими витратами:
- Порушує Encapsulation.
- Порушує Decorators.
- Складніше імітувати в тестах.
- Робить відмінності комітів важчими для читання.
Для отримання додаткової інформації ви можете прочитати повний текст статті на цю тему написав Marco Pivetta.
Погано:
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();
Добре:
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();
За можливості слід використовувати final
::
- Воно запобігає неконтрольованому ланцюжку успадкування.
- Воно сприяє композиції над успадкуванням. composition.
- Воно підтримує принцип єдиного обов'язку (SRP) Single Responsibility Pattern.
- Воно спонукає розробників використовувати ваші публічні методи замість успадкування класу для отримання доступу до захищених методів.
- Воно дозволяє вам змінювати свій код без будь-яких помилок у додатках, які використовують ваш клас.
Єдине обмеження полягає в тому, що ваш клас має реалізувати інтерфейс і не повинен мати інших публічних методів.
Для додаткової інформації можна прочитати статтю автора Marco Pivetta (Ocramius).
Погано:
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;
}
}
Добре:
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;
}
}
SOLID це мнемонічний абревіатурний акронім, який був запропонований Майклом Фезерсом для перших п'яти принципів, названих Робертом Мартіном. Ці принципи становлять п'ять основних принципів об'єктноорієнтованого програмування та проєктування.
- S: Принцип єдиного обов'язку (SRP)
- O: Принцип відкритості/закритості (OCP)
- L: Принцип підстановки Барбари Лісков (LSP)
- I: Принцип розділення інтерфейсу (ISP)
- D: Принцип інверсії залежності (DIP)
Як зазначено в "Clean Code", "Ніколи не повинно бути більше однієї причини для зміни класу". Дуже привабливо наповнити клас багатьма функціональностями, як у випадку, коли ви можете взяти з собою тільки одну валізу на рейс. Проблема полягає в тому, що ваш клас не буде концептуально згуртованим, і у нього буде багато причин для зміни. Мінімізація кількості випадків, коли потрібно змінити клас, є важливою. Це важливо, оскільки якщо в одному класі занадто багато функціональності, і ви змінюєте частину коду, то може бути важко зрозуміти, як це вплине на інші залежні модулі в вашому коді.
Погано:
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
{
// ...
}
}
Добре:
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()) {
// ...
}
}
}
Як зазначено Бертраном Майєром, "програмні сутності (класи, модулі, функції тощо) мають бути відкриті для розширення, але закриті для змін". Але що це означає? Основна ідея цього принципу полягає в тому, що ви маєте дозволити користувачам додавати нові функціональності без зміни наявного коду.
Погано:
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
}
}
Добре:
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);
}
}
Це насправді дуже простий концепт, названий досить жахливою термінологією. Формально це визначається як "Якщо S є підтипом T, то об'єкти типу T можуть бути замінені об'єктами типу S (тобто об'єкти типу S можуть заміщувати об'єкти типу 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);
// Погано: 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);
}
Добре:
Найкращим рішенням є розділення чотирикутників та виділення більш загального субтипу для обох фігур.
Попри видиму схожість квадрата та прямокутника, вони різні. Квадрат має багато спільного з ромбом, а прямокутник - з паралелограмом, але вони не є підтипами один одного. Квадрат, прямокутник, ромб та паралелограм - це окремі фігури з власними властивостями, хоча вони й схожі.
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);
}
Принцип ISP стверджує, що "Клієнти не повинні залежати від інтерфейсів, які вони не використовують."
Добрим прикладом, який демонструє цей принцип, є класи, які вимагають великих об'єктів налаштувань. Не вимагати від клієнтів налаштовувати великі кількості параметрів корисно, оскільки більшість часу вони не потребують всіх цих налаштувань. Якщо зробити їх необов'язковими, то це допоможе запобігти виникненню "товстого інтерфейсу".
Погано:
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
}
}
Добре:
Не кожен працівник є співробітником, але кожен співробітник є працівником. Термін "працівник" є більш загальним і може містити різні типи праці, включаючи фізичну працю та роботу на самозайнятій основі. Водночас термін "співробітник" зазвичай використовується для опису тих, хто працює на певну компанію або організацію і зазвичай має певний договір про працю або угоду.
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
}
}
Принцип заявляє про дві суттєві речі:
- Модулі високого рівня не повинні залежати від модулів нижчого рівня. Обидва повинні залежати від абстракцій.
- Абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.
Це може бути важко зрозуміти на початку, але якщо ви працювали з фреймворками PHP (наприклад, Symfony), ви бачили реалізацію цього принципу у вигляді Dependency Injection (DI). Хоча це не тотожні концепції, DIP запобігає високорівневим модулям знати деталі низькорівневих модулів та налаштовувати їх. Це можливо завдяки DI. Велика перевага полягає в тому, що це зменшує зв'язок між модулями. Зв'язність є дуже поганою практикою розробки, оскільки це ускладнює рефакторинг вашого коду.
Погано:
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();
}
}
Добре:
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();
}
}
Намагайтеся дотримуватися принципу DRY (не повторюйся).
Робіть все можливе, щоб уникнути дублювання коду. Дублювання коду - це погано, оскільки це означає, що потрібно змінювати логіку в більш ніж одному місці.
Уявіть, що ви ведете ресторан і відстежуєте свій інвентар: всі ваші помідори, цибуля, часник, спеції тощо. Якщо у вас є кілька списків для цього, то всі вони повинні бути оновлені, коли ви подаєте страву з помідорами. Якщо ж у вас є лише один список, то потрібно оновлювати тільки одне місце!
Часто дублювання коду виникає тому, що у вас є два або більше незначно різних елементів, які мають багато спільного, але їхні відмінності змушують вас мати дві або більше окремих функцій, які виконують багато однакових дій. Видалення дубльованого коду означає створення абстракції, яка може обробляти цей набір різних елементів за допомогою однієї функції/модуля/класу.
Важливо правильно визначити абстракцію, тому вам слід дотримуватися принципів SOLID, наведених у розділі Класи. Погані абстракції можуть бути гіршими за дубльований код, тому будьте обережні! Проте, якщо ви можете створити хорошу абстракцію, то зробіть це! Не повторюйте себе, інакше вам доведеться оновлювати кілька місць кожного разу, коли ви хочете змінити щось одне.
Погано:
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);
}
}
Добре:
function showList(array $employees): void
{
foreach ($employees as $employee) {
$expectedSalary = $employee->calculateExpectedSalary();
$experience = $employee->getExperience();
$githubLink = $employee->getGithubLink();
$data = [
$expectedSalary,
$experience,
$githubLink
];
render($data);
}
}
Very Добре:
Краще використовувати компактну версію коду.
function showList(array $employees): void
{
foreach ($employees as $employee) {
render([
$employee->calculateExpectedSalary(),
$employee->getExperience(),
$employee->getGithubLink()
]);
}
}
Також доступно на інших мовах:
- 🇺🇦 Ukrainian:
- 🇨🇳 Chinese:
- 🇷🇺 Russian:
- 🇪🇸 Spanish:
- 🇧🇷 Portuguese:
- 🇹🇭 Thai:
- 🇫🇷 French:
- 🇻🇳 Vietnamese:
- 🇰🇷 Korean:
- 🇹🇷 Turkish:
- 🇮🇷 Persian:
- 🇧🇩 Bangla:
- 🇪🇬 Arabic:
- 🇯🇵 Japanese: