Наразі вийшов серпневий випуск PHP Annotated з коментарями JetBrains із самими цікавими подіями, що відбулися в світі PHP за останній місяць. Що ж там відбулося?
- Було випущено PHP 8.2 Beta 3. А вже першого вересня очікуємо PHP 8.2 RC1. Про все нове, що там є, будемо писати далі на каналі (там буде і про Deprecated dynamic properties, і про новий тип true, і про readonly класи, тощо).
Якщо ще не дійшли до 8.2, то треба оновитися до PHP 8.0.22 або до PHP 8.1.9 відповідно. Якщо ви ще застряли на старих версіях PHP, то гляньте відео від Brent Roose, там він розказує нащо взагалі треба оновлюватися.
- Вийшов Composer 2.4 Там зʼявилися аудит залежностей задля безпеки, автозавершення bash і багато інших дрібніших доповнень. Загалом, composer bump й composer audit можуть стати у нагоді
- Вийшов deployphp/deployer 7.0.0 — це інструмент розгортання, написаний на PHP із підтримкою популярних фреймворків. У 7 версії зʼявилися нові рецепти забезпечення - це така штука, що може автоматично встановити та налаштувати будь-який VPS для запуску вашого застосунку. Він може встановить веб-сервер, SSL, PHP, Redis, node, тощо.
- У Codeception теж мажорний реліз: 5.0.0. Там зʼявилася підтримка атрибутів, PHP 8, шардінг та інші нові функції
- Вийшла нова версія swoole 5.0.0 - розширеня для паралельної роботи. Тепер там є покращена система типів, додані типи для параметрів і значень, що повертаються, оптимізована обробка помилок та інші вдосконалення та доповнення.
- Випущено PhpStorm 2022.2 Це велике оновлення забезпечує підтримку Mockery і Rector, розширену підтримку дженериків і переліків, покращення налагоджувача та HTTP клієнта тощо.
#новини
- Було випущено PHP 8.2 Beta 3. А вже першого вересня очікуємо PHP 8.2 RC1. Про все нове, що там є, будемо писати далі на каналі (там буде і про Deprecated dynamic properties, і про новий тип true, і про readonly класи, тощо).
Якщо ще не дійшли до 8.2, то треба оновитися до PHP 8.0.22 або до PHP 8.1.9 відповідно. Якщо ви ще застряли на старих версіях PHP, то гляньте відео від Brent Roose, там він розказує нащо взагалі треба оновлюватися.
- Вийшов Composer 2.4 Там зʼявилися аудит залежностей задля безпеки, автозавершення bash і багато інших дрібніших доповнень. Загалом, composer bump й composer audit можуть стати у нагоді
- Вийшов deployphp/deployer 7.0.0 — це інструмент розгортання, написаний на PHP із підтримкою популярних фреймворків. У 7 версії зʼявилися нові рецепти забезпечення - це така штука, що може автоматично встановити та налаштувати будь-який VPS для запуску вашого застосунку. Він може встановить веб-сервер, SSL, PHP, Redis, node, тощо.
- У Codeception теж мажорний реліз: 5.0.0. Там зʼявилася підтримка атрибутів, PHP 8, шардінг та інші нові функції
- Вийшла нова версія swoole 5.0.0 - розширеня для паралельної роботи. Тепер там є покращена система типів, додані типи для параметрів і значень, що повертаються, оптимізована обробка помилок та інші вдосконалення та доповнення.
- Випущено PhpStorm 2022.2 Це велике оновлення забезпечує підтримку Mockery і Rector, розширену підтримку дженериків і переліків, покращення налагоджувача та HTTP клієнта тощо.
#новини
Поговоримо трохи про те, як користуватися ООП. Одним із столпів ооп є інкапсуляція, тобто огородження внутрішніх деталей реалізації об'єкта. Розберемо на прилакді умови:
Погано:
Добре:
Тут, завдяки тому, що ми перенесли логіку перевірки статусу статті в сам клас статті, тобто інкапсулювали цю логіку, ми прибрали залежність нашого коду від магічного рядку
Погано:
if ($article->state === 'published') {
// ...
}
Добре:
if ($article->isPublished()) {
// ...
}
Тут, завдяки тому, що ми перенесли логіку перевірки статусу статті в сам клас статті, тобто інкапсулювали цю логіку, ми прибрали залежність нашого коду від магічного рядку
'published'
Ще трохи про назви змінних та методів/функцій
Уникайте негативних умовних слів
Погано:
Добре:
Уникайте негативних умовних слів
Погано:
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();
}
}
class AirPlaneFactory {
// ...
create(string $type): Airplane
{
// ...
}
}
$factory = new AirPlaneFactory();
// ...
$plane = $factory->create('777');
// ...
Уникайте перевірки типу (частина 1)
PHP є нетиповим, що означає, що ваші функції можуть приймати будь-які типи аргументів. Іноді вас кусає ця свобода, і виникає спокуса виконати перевірку типів у своїх функціях. Є багато способів уникнути цього. Перше, що слід робити, це робити ваши класи консистентними:
Погано:
Отже, й ваш ровер і авто можуть їхати і вони обидва є засобами пересування. Тожбо, можно виділити окремий інтерфейс
Добре:
PHP є нетиповим, що означає, що ваші функції можуть приймати будь-які типи аргументів. Іноді вас кусає ця свобода, і виникає спокуса виконати перевірку типів у своїх функціях. Є багато способів уникнути цього. Перше, що слід робити, це робити ваши класи консистентними:
Погано:
function travelToTexas($vehicle): void
{
if ($vehicle instanceof Bicycle) {
$vehicle->pedalTo(new Location('texas'));
} elseif ($vehicle instanceof Car) {
$vehicle->driveTo(new Location('texas'));
}
}
Отже, й ваш ровер і авто можуть їхати і вони обидва є засобами пересування. Тожбо, можно виділити окремий інтерфейс
Vehicle
й реалізувати класи Bicycle і Car як імплементації цього інтерфейсу. В вашому же коді зробити так:Добре:
function travelToTexas(Vehicle $vehicle): void
{
$vehicle->travelTo(new Location('texas'));
}
Уникайте перевірки типу (частина 2)
Якщо ви працюєте з базовими примітивними значеннями, такими як рядки, цілі числа та масиви, використовуєте PHP 7+ і не можете використовувати поліморфізм у повній мірі, але все одно відчуваєте потребу у перевірці типу, вам слід розглянути декларацію типу або суворий режим. Він надає вам статичний тип на додачу до стандартного синтаксису PHP. Проблема з перевіркою типу вручну полягає в тому, що для її виконання знадобиться писати забагато зайвого коду і ви не отримуєте ту якість, яка би компенсувала втрачену читабельность. Тримайте свій PHP чистим, пишіть хороші тести та добре перевіряйте код. В іншому випадку виконайте все це, але з декларацією строгого типу PHP або суворим режимом.
Погано:
Добре:
Якщо ви працюєте з базовими примітивними значеннями, такими як рядки, цілі числа та масиви, використовуєте 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;
}
php.org.ua
language.types.declarations | Php.org.ua документація з PHP Українською
Php.org.ua - документація з PHP Українською.
Видалити мертвий код
Це трохи незвичайно, але мертвий код так само шкідливий, як і повторюваний код. Немає причин зберігати його у своїй кодовій базі. Якщо він не викликається, позбудьтеся його! Не треба його коментувати в надії, що завтра він знадобиться: якщо буде потрібен ви зможете знайти його в історії гіта (ви ж користуєтесь гітом, правда?)
Погано:
Добре:
Це трохи незвичайно, але мертвий код так само шкідливий, як і повторюваний код. Немає причин зберігати його у своїй кодовій базі. Якщо він не викликається, позбудьтеся його! Не треба його коментувати в надії, що завтра він знадобиться: якщо буде потрібен ви зможете знайти його в історії гіта (ви ж користуєтесь гітом, правда?)
Погано:
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 (про нього поговоремо трохи пізніше).
Погано:
Добре:
Використовуйте інкапсуляцію об'єктів
У PHP ви можете задати `public`, `protected` і `private` ключові слова для методів. Використовуючи їх, ви можете керувати зміною властивостей об’єкта.
- Якщо ви хочете зробити більше, крім отримання властивості об’єкта, вам не потрібно буде шукати та змінювати кожен засіб доступу у вашій кодовій базі.
- Спрощує додавання перевірки під час виконання `set`.
- Інкапсулює внутрішнє представлення.
- Легко додати журналювання та обробку помилок під час
get
та 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('Щось грошей у вас не вистачає...');
}
$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();
Зробіть так, щоб об’єкти мали private/protected, а не лише public члени
* Методи та властивості
*
*
Тому використовуйте `
Погано:
Добре:
* Методи та властивості
public
найбільш небезпечні для змін, тому що деякий зовнішній код може легко покладатися на них, і ви не можете контролювати, який код покладається на них. Модифікації в класі небезпечні для всіх користувачів класу.*
protected
модифікатор є таким же небезпечним, як і public, оскільки вони доступні в межах будь-якого дочірнього класу. Це фактично означає, що різниця між public
і protected
полягає лише в механізмі доступу, але гарантія інкапсуляції залишається незмінною. Модифікації в класі небезпечні для всіх нащадкових класів.*
private
модифікатор гарантує, що код є небезпечним для модифікації лише в межах одного класу (ви безпечні для модифікацій, і ви не матимете Ефекту Дженги).Тому використовуйте `
private
` за умовчанням і `public/protected
`, коли вам потрібно надати доступ для зовнішніх класів. Для отримання додаткової інформації ви можете прочитати у блозі на цю тему, написаний Фаб’єном Потенсьє (цей дядька розробив фреймворк Symfony).Погано:
class Employee
{
public $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
$employee = new Employee('Гриць Опанасенко');
// Імʼя: Гриць Опанасенко
echo 'Імʼя: ' . $employee->name;
Добре:
class Employee
{
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
}
$employee = new Employee('Гриць Опанасенко');
// Імʼя: Гриць Опанасенко
echo 'Імʼя: ' . $employee->getName();
Віддавайте перевагу композиції над успадкуванням
Як відомо в Design Patterns від Gang of Four, ви повинні віддавати перевагу композиції над успадкуванням, де це можливо. Є багато вагомих причин використовувати успадкування та багато вагомих причин використовувати композицію. Основний момент цієї сентенції полягає в тому, що ящко ваш розум інстинктивно шукає спадщину, спробуйте подумати, чи могла б краще композиція моделювати вашу проблему. У деяких випадках може. Можливо, у вас виникне запитання: "Коли я маю використовувати успадкування?" Це залежить від вашої проблеми, але ось невиличкий список випадків, коли успадкування має більше сенсу, ніж композиція:
1. Ваше успадкування представляє зв’язок «є-є», а не «має» (Людина->Тварина проти Користувач->Відомості про користувача).
2. Ви можете повторно використовувати код з базових класів (Люди можуть рухатися, як і всі тварини).
3. Ви хочете внести глобальні зміни в похідні класи, змінивши базовий клас. (Наприклад, всіх тварин характерно, що вони витрачають калорії, коли вони рухаються).
Погано:
Краще:
Як відомо в Design Patterns від Gang of Four, ви повинні віддавати перевагу композиції над успадкуванням, де це можливо. Є багато вагомих причин використовувати успадкування та багато вагомих причин використовувати композицію. Основний момент цієї сентенції полягає в тому, що ящко ваш розум інстинктивно шукає спадщину, спробуйте подумати, чи могла б краще композиція моделювати вашу проблему. У деяких випадках може. Можливо, у вас виникне запитання: "Коли я маю використовувати успадкування?" Це залежить від вашої проблеми, але ось невиличкий список випадків, коли успадкування має більше сенсу, ніж композиція:
1. Ваше успадкування представляє зв’язок «є-є», а не «має» (Людина->Тварина проти Користувач->Відомості про користувача).
2. Ви можете повторно використовувати код з базових класів (Люди можуть рухатися, як і всі тварини).
3. Ви хочете внести глобальні зміни в похідні класи, змінивши базовий клас. (Наприклад, всіх тварин характерно, що вони витрачають калорії, коли вони рухаються).
Погано:
class Employee
{
private $name;
private $email;
public function __construct(string $name, string $email)
{
$this->name = $name;
$this->email = $email;
}
// ...
}
// Погано, тому що співробітники "мають" податкові дані.
// EmployeeTaxData не є типом класу 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): void
{
$this->taxData = $taxData;
}
// ...
}
Wikipedia
Design Patterns
1994 software engineering book
Уникайте fluent інтерфейсів
[Fluent-інтерфейс — це об’єктно-орієнтований API, спрямований на покращення читабельності вихідного коду за допомогою з’єднання методів.
Хоча можуть бути деякі контексти (часто об’єкти конструктора) де цей шаблон зменшує багатослівність коду (наприклад, PHPUnit Mock Builder
або Doctrine Query Builder), але частіше це коштує певних витрат:
1. Розриває інкапсуляцію.
2. Розриває декоратори.
3. Важче імітувати у наборі тестів.
4. Робить важчими для читання різницю в git комітів.
Для отримання додаткової інформації ви можете прочитати повний допис у блозі на цю тему, написаний Марко Піветта.
Погано:
Добре:
[Fluent-інтерфейс — це об’єктно-орієнтований API, спрямований на покращення читабельності вихідного коду за допомогою з’єднання методів.
Хоча можуть бути деякі контексти (часто об’єкти конструктора) де цей шаблон зменшує багатослівність коду (наприклад, PHPUnit Mock Builder
або Doctrine Query Builder), але частіше це коштує певних витрат:
1. Розриває інкапсуляцію.
2. Розриває декоратори.
3. Важче імітувати у наборі тестів.
4. Робить важчими для читання різницю в git комітів.
Для отримання додаткової інформації ви можете прочитати повний допис у блозі на цю тему, написаний Марко Піветта.
Погано:
class Car
{
private $make = 'Honda';
private $model = 'Accord';
private $color = 'white';
public function setMake(string $make): self
{
$this->make = $make;
// Ось так і роблять Fluent-інтерфейс
return $this;
}
public function setModel(string $model): self
{
$this->model = $model;
// Ось так і роблять Fluent-інтерфейс
return $this;
}
public function setColor(string $color): self
{
$this->color = $color;
// Ось так і роблять Fluent-інтерфейс
return $this;
}
public function dump(): void
{
var_dump($this->make, $this->model, $this->color);
}
}
$car = (new Car())
->setColor('чорний')
->setMake('Каділак')
->setModel('Escalade')
->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('чорний');
$car->setMake('Каділак');
$car->setModel('Escalade');
$car->dump();
Віддавайте перевагу final класам
Ключове слово
1. Воно запобігає неконтрольованому ланцюжку успадкування.
2. Воно заохочує композицію.
3. Воно заохочує Принцип єдиної відповідальності.
4. Це заохочує розробників використовувати ваші
5. Це дозволяє змінювати ваш код, не порушуючи програми, які використовують ваш клас. Єдина умова полягає в тому, що ваш клас має реалізовувати інтерфейс, а інші
Погано:
Добре:
Ключове слово
final
слід використовувати, коли це можливо: 1. Воно запобігає неконтрольованому ланцюжку успадкування.
2. Воно заохочує композицію.
3. Воно заохочує Принцип єдиної відповідальності.
4. Це заохочує розробників використовувати ваші
public
методи замість того, щоб розширювати клас, щоб отримати доступ до protected
методів. 5. Це дозволяє змінювати ваш код, не порушуючи програми, які використовують ваш клас. Єдина умова полягає в тому, що ваш клас має реалізовувати інтерфейс, а інші
public
методи не визначені. Для отримання додаткової інформації ви можете прочитати допис у блозі на цю тему, написаний Марко Піветта (Окраміус).Погано:
final class Car
{
private $color;
public function __construct($color)
{
$this->color = $color;
}
public function getColor()
{
return $this->color;
}
}
Добре:
interface Vehicle
{
public function getColor();
}
final class Car implements Vehicle
{
private $color;
public function __construct($color)
{
$this->color = $color;
}
public function getColor()
{
return $this->color;
}
}
ocramius.github.io
When to declare classes final
Declaring classes as final enhances our code quality and abstraction dramatically, but is it always correct?
Немає жодного гарного програміста, котрий би на знав про основні принципи програмування. Один із таких принципів це SOLID — це мнемонічний акронім, введений Майклом Фізерсом для перших п’яти принципів, названих Робертом Мартіном, що означало п’ять основних принципів об’єктно-орієнтованого програмування та проектування.
* S: Принцип єдиної відповідальності (SRP)
* O: Принцип відкритості/закритості (OCP)
* L: Принцип заміни Ліскова (LSP)
* I: Принцип сегрегації інтерфейсу (ISP)
* D: Принцип інверсії залежностей (DIP)
* S: Принцип єдиної відповідальності (SRP)
* O: Принцип відкритості/закритості (OCP)
* L: Принцип заміни Ліскова (LSP)
* I: Принцип сегрегації інтерфейсу (ISP)
* D: Принцип інверсії залежностей (DIP)
Одразу розглянемо перший принцип:
Принцип єдиної відповідальності (SRP)
Як зазначено в Чистому коді, "ніколи не повинно бути більше однієї причини для зміни класу". Це спокуса запакувати клас із великою кількістю функціональних можливостей. Це як коли ви можете взяти лише одну валізу в літак й хочете напхати туди всього й побільше. Проблема полягає в тому, що ваш клас не буде концептуально згуртованим, і це дасть йому багато причин для змін. Важливо звести до мінімуму кількість можливих змін класу. Це важливо, оскільки якщо забагато функціональних можливостей міститься в одному класі, і ви модифікуєте лише його частину, може бути важко зрозуміти, як це вплине на інші залежні модулі у вашій кодовій базі.
Погано:
Краще:
Принцип єдиної відповідальності (SRP)
Як зазначено в Чистому коді, "ніколи не повинно бути більше однієї причини для зміни класу". Це спокуса запакувати клас із великою кількістю функціональних можливостей. Це як коли ви можете взяти лише одну валізу в літак й хочете напхати туди всього й побільше. Проблема полягає в тому, що ваш клас не буде концептуально згуртованим, і це дасть йому багато причин для змін. Важливо звести до мінімуму кількість можливих змін класу. Це важливо, оскільки якщо забагато функціональних можливостей міститься в одному класі, і ви модифікуєте лише його частину, може бути важко зрозуміти, як це вплине на інші залежні модулі у вашій кодовій базі.
Погано:
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()) {
// ...
}
}
}
Open/Closed принцип (OCP)
Як стверджував Бертран Мейєр, «програмні сутності (класи, модулі, функції тощо) мають бути відкритими для розширення, але закритими для модифікації». Що це означає? Цей принцип в основному стверджує, що ви повинні дозволити користувачам додавати нові функції, не змінюючи існуючий код.
Погано
Добре:
Як стверджував Бертран Мейєр, «програмні сутності (класи, модулі, функції тощо) мають бути відкритими для розширення, але закритими для модифікації». Що це означає? Цей принцип в основному стверджує, що ви повинні дозволити користувачам додавати нові функції, не змінюючи існуючий код.
Погано
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
{
// ...
}
private function makeHttpCall(string $url): Promise
{
// ...
}
}
Добре:
interface Adapter
{
public function request(string $url): Promise;
}
class AjaxAdapter implements Adapter
{
public function request(string $url): Promise
{
// ...
}
}
class NodeAdapter implements Adapter
{
public function request(string $url): Promise
{
// ...
}
}
class HttpRequester
{
private $adapter;
public function __construct(Adapter $adapter)
{
$this->adapter = $adapter;
}
public function fetch(string $url): Promise
{
return $this->adapter->request($url);
}
}
Принцип заміни Ліскова (LSP)
Це страшний термін для дуже простого поняття. Це формально визначено так: «Якщо S є підтипом T, тоді об’єкти типу T можна замінити об’єктами типу S (тобто об’єкти типу S можуть замінити об’єкти типу T) без зміни будь-яких бажаних властивостей цієї програми (правильність, виконання завдання тощо).
Це все ще страшніше визначення, тож ось найкраще пояснення цього: якщо у вас є батьківський клас і дочірній клас, тоді базовий клас і дочірній клас можна використовувати як взаємозамінні без отримання неправильних результатів. Це все ще може заплутати, тому давайте подивимося на класичний приклад квадрата-прямокутника. З математичної точки зору квадрат — це прямокутник, але якщо ви моделюєте його за допомогою зв’язку «is-a» через успадкування, ви тут отримуєте проблему.
Погано:
Добре:
Найкращий спосіб — розділити чотирикутники та виділити більш загальний підтип для обох фігур. Незважаючи на зовнішню подібність квадрата та прямокутника, вони різняться. Квадрат має багато спільного з ромбом, а прямокутник з паралелограмом, але вони не є підвидами. Квадрат, прямокутник, ромб і паралелограм — окремі фігури зі своїми властивостями, хоча й схожими.
Це страшний термін для дуже простого поняття. Це формально визначено так: «Якщо S є підтипом T, тоді об’єкти типу T можна замінити об’єктами типу S (тобто об’єкти типу S можуть замінити об’єкти типу T) без зміни будь-яких бажаних властивостей цієї програми (правильність, виконання завдання тощо).
Це все ще страшніше визначення, тож ось найкраще пояснення цього: якщо у вас є батьківський клас і дочірній клас, тоді базовий клас і дочірній клас можна використовувати як взаємозамінні без отримання неправильних результатів. Це все ще може заплутати, тому давайте подивимося на класичний приклад квадрата-прямокутника. З математичної точки зору квадрат — це прямокутник, але якщо ви моделюєте його за допомогою зв’язку «is-a» через успадкування, ви тут отримуєте проблему.
Погано:
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);
// Ось тут буде 25 для прямокутника, коли має ж бути 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);
}
Продовжуємо про Принципи SOLID
Принцип сегрегації інтерфейсу (ISP)
ISP заявляє, що «клієнти не повинні бути змушені залежати від інтерфейсів, які вони не використовують». Хорошим прикладом для демонстрації цього принципу є класи, які вимагають великих об’єктів налаштуваннь. Не вимагати від клієнтів величезної кількості варіантів налаштувань вигідно, тому що здебільшого їм не знадобляться всі параметри. Якщо зробити їх необов’язковими, це допоможе уникнути «жирного інтерфейсу».
Погано:
Добре:
Не кожен робітник є працівником, але кожен працівник є робітником.
Принцип сегрегації інтерфейсу (ISP)
ISP заявляє, що «клієнти не повинні бути змушені залежати від інтерфейсів, які вони не використовують». Хорошим прикладом для демонстрації цього принципу є класи, які вимагають великих об’єктів налаштуваннь. Не вимагати від клієнтів величезної кількості варіантів налаштувань вигідно, тому що здебільшого їм не знадобляться всі параметри. Якщо зробити їх необов’язковими, це допоможе уникнути «жирного інтерфейсу».
Погано:
interface Employee
{
public function work(): void;
public function eat(): void;
}
class HumanEmployee implements Employee
{
public function work(): void
{
// працює
}
public function eat(): void
{
// хоче їсти в обідню перерву
}
}
class RobotEmployee implements Employee
{
public function work(): void
{
// працює набагато більше
}
public function eat(): void
{
// робот не може їсти, але він повинен реалізувати цей метод
}
}
Добре:
Не кожен робітник є працівником, але кожен працівник є робітником.
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
{
// працює
}
public function eat(): void
{
// їсть
}
}
// робот може лише працювати
class RobotEmployee implements Workable
{
public function work(): void
{
// працює
}
}
Залишився лише один принцип із SOLID, який ми ще не розглянули.
Принцип інверсії залежностей (DIP)
Цей принцип стверджує дві важливі речі:
1. Модулі високого рівня не повинні залежати від модулів низького рівня. Обидва мають залежати від абстракцій.
2. Абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.
Спочатку це може бути важко зрозуміти, але якщо ви працювали з фреймворками PHP (наприклад, Symfony), ви бачили реалізацію цього принципу у формі ін’єкції залежностей (DI). Хоча це не ідентичні концепції, DIP не дозволяє модулям високого рівня знати подробиці своїх модулів низького рівня та переглядати їх. Це можна зробити за допомогою DI. Величезною перевагою цього є те, що він зменшує сполучення між модулями. Зв’язування є дуже поганою схемою розробки, оскільки вона ускладнює рефакторинг коду. Тож
Розглянемо це на прикладі наших вже знайомих робітників:
Погано:
Добре:
Принцип інверсії залежностей (DIP)
Цей принцип стверджує дві важливі речі:
1. Модулі високого рівня не повинні залежати від модулів низького рівня. Обидва мають залежати від абстракцій.
2. Абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.
Спочатку це може бути важко зрозуміти, але якщо ви працювали з фреймворками PHP (наприклад, Symfony), ви бачили реалізацію цього принципу у формі ін’єкції залежностей (DI). Хоча це не ідентичні концепції, DIP не дозволяє модулям високого рівня знати подробиці своїх модулів низького рівня та переглядати їх. Це можна зробити за допомогою DI. Величезною перевагою цього є те, що він зменшує сполучення між модулями. Зв’язування є дуже поганою схемою розробки, оскільки вона ускладнює рефакторинг коду. Тож
Розглянемо це на прикладі наших вже знайомих робітників:
Погано:
class Employee
{
public function work(): void
{
// працює
}
}
class Robot extends Employee
{
public function work(): void
{
// працює ще більше
}
}
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
{
// працює
}
}
class Robot implements Employee
{
public function work(): void
{
// працює ще більше
}
}
class Manager
{
private $employee;
// Employee в данному випадку - інтерфейс який реалізує і робот і людина
public function __construct(Employee $employee)
{
$this->employee = $employee;
}
public function manage(): void
{
$this->employee->work();
}
}
Окрім вже знайомих вам принципів SOLID є ще деякі принципи программування, котрих краще притримуватись.
Не повторюйся (DRY)
Намагайтеся дотримуватися принципу [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). Зробіть усе можливе, щоб уникнути дублювання коду. Дубльований код — це погано, оскільки це означає, що є більше ніж одне місце, щоб щось змінити якщо, вам потрібно змінити певну логіку. Уявіть, що ви керуєте рестораном і відстежуєте свій інвентар: усі ваші помідори, цибуля, часник, спеції тощо. Якщо у вас є кілька списків, у яких ви зберігаєте наявність продуктів, усі вони мають оновлюватися, коли ви подаєте страву з помідорами. А якщо у вас є лише один список, то є лише одне місце для оновлення!
Часто ви маєте дублікат коду, тому що у вас є дві або більше різних речей, які мають багато спільного, але їхні відмінності змушують вас мати дві або більше окремих функцій, які виконують майже однакові речі. Вони можуть навіть бути привʼязани до іншого контексту.
Видалення повторюваного коду означає створення абстракції, яка може обробляти набор з різних речей лише за допомогою однієї функції/модуля/класу. Правильна абстракція має вирішальне значення, тому ви повинні дотримуватися принципів SOLID, описаних раніше.
Погані абстракції можуть бути гіршими за повторюваний код, тому будьте обережні! Іншими словами, якщо ви можете зробити хорошу абстракцію - зробіть її, інакше ви побачите, що оновлюєте кілька місць щоразу, коли захочете змінити щось одне! Тожбо, не повторюйтеся в рамках однієї абстракції.
Погано:
Добре:
**Ще краще:**
Краще використовувати компактну версію коду. Але слід помʼятати, що код має бути читабельнми. Якщо у вас став вибір компактність проти читабельності, то завжди пишіть читабельний код.
Не повторюйся (DRY)
Намагайтеся дотримуватися принципу [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). Зробіть усе можливе, щоб уникнути дублювання коду. Дубльований код — це погано, оскільки це означає, що є більше ніж одне місце, щоб щось змінити якщо, вам потрібно змінити певну логіку. Уявіть, що ви керуєте рестораном і відстежуєте свій інвентар: усі ваші помідори, цибуля, часник, спеції тощо. Якщо у вас є кілька списків, у яких ви зберігаєте наявність продуктів, усі вони мають оновлюватися, коли ви подаєте страву з помідорами. А якщо у вас є лише один список, то є лише одне місце для оновлення!
Часто ви маєте дублікат коду, тому що у вас є дві або більше різних речей, які мають багато спільного, але їхні відмінності змушують вас мати дві або більше окремих функцій, які виконують майже однакові речі. Вони можуть навіть бути привʼязани до іншого контексту.
Видалення повторюваного коду означає створення абстракції, яка може обробляти набор з різних речей лише за допомогою однієї функції/модуля/класу. Правильна абстракція має вирішальне значення, тому ви повинні дотримуватися принципів 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);
}
}
**Ще краще:**
Краще використовувати компактну версію коду. Але слід помʼятати, що код має бути читабельнми. Якщо у вас став вибір компактність проти читабельності, то завжди пишіть читабельний код.
function showList(array $employees): void
{
foreach ($employees as $employee) {
render([
$employee->calculateExpectedSalary(),
$employee->getExperience(),
$employee->getGithubLink()
]);
}
}