Вступ до об'єктно-орієнтованого програмування

Термін “ООП” розшифровується як об'єктно-орієнтоване програмування – спосіб організації та структурування коду. ООП дозволяє розглядати програму як набір об'єктів, які взаємодіють між собою, а не як послідовність команд і функцій.

В ООП “об'єкт” – це одиниця, яка містить дані та функції, що оперують цими даними. Об'єкти створюються на основі “класів”, які можна розуміти як схеми або шаблони для об'єктів. Маючи клас, ми можемо створити його “екземпляр”, тобто конкретний об'єкт, створений на основі цього класу.

Давайте розглянемо, як можна створити простий клас в PHP. При визначенні класу ми використовуємо ключове слово “class”, за яким слідує ім'я класу, а потім фігурні дужки, в які вкладаються функції класу (звані “методами”) і змінні класу (звані “властивостями” або “атрибутами”):

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

У цьому прикладі ми створили клас з іменем Car з однією функцією (або “методом”) honk.

Кожен клас повинен вирішувати лише одне основне завдання. Якщо клас виконує занадто багато завдань, може бути доцільно розділити його на менші, спеціалізовані класи.

Класи зазвичай зберігаються в окремих файлах, щоб упорядкувати код і полегшити навігацію по ньому. Ім'я файлу має відповідати імені класу, тому для класу Car ім'я файлу буде Car.php.

Називаючи класи, варто дотримуватися конвенції “PascalCase”, тобто кожне слово в назві починається з великої літери, без підкреслень або інших роздільників. Методи та властивості слідують конвенції “camelCase”, тобто починаються з малої літери.

Деякі методи в PHP відіграють особливу роль і мають префікс __ (два підкреслення). Одним з найважливіших спеціальних методів є “конструктор”, позначений як __construct. Конструктор – це метод, який автоматично викликається при створенні нового екземпляра класу.

Ми часто використовуємо конструктор для встановлення початкового стану об'єкта. Наприклад, при створенні об'єкта, що представляє людину, ви можете використовувати конструктор, щоб встановити її вік, ім'я або інші атрибути.

Давайте подивимося, як використовувати конструктор в PHP:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Outputs: 25

У цьому прикладі клас Person має властивість $age і конструктор, який встановлює цю властивість. Метод howOldAreYou() надає доступ до віку людини.

Ключове слово new використовується для створення нового екземпляра класу. У наведеному вище прикладі ми створили нову особу віком 25 років.

Ви також можете встановити значення за замовчуванням для параметрів конструктора, якщо вони не вказані при створенні об'єкта. Наприклад:

class Person
{
	private $age;

	function __construct($age = 20)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // if no argument is passed, parentheses can be omitted
echo $person->howOldAreYou(); // Outputs: 20

У цьому прикладі, якщо не вказати вік при створенні об'єкта Person, буде використано значення за замовчуванням 20.

Нарешті, визначення властивості з її ініціалізацією через конструктор можна скоротити і спростити таким чином:

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

Простори імен

Простори імен дозволяють нам організовувати і групувати пов'язані класи, функції і константи, уникаючи конфліктів імен. Ви можете уявити їх як папки на комп'ютері, де кожна папка містить файли, пов'язані з певним проектом або темою.

Простори імен особливо корисні у великих проектах або при використанні сторонніх бібліотек, де можуть виникати конфлікти імен класів.

Уявіть, що у вашому проекті є клас з іменем Car, і ви хочете розмістити його у просторі імен Transport. Ви можете зробити це таким чином:

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

Якщо ви хочете використати клас Car в іншому файлі, вам потрібно вказати, з якого простору імен походить клас:

$car = new Transport\Car;

Для спрощення ви можете вказати на початку файлу, який саме клас з певного простору імен ви хочете використовувати, що дозволить вам створювати екземпляри без зазначення повного шляху:

use Transport\Car;

$car = new Car;

Спадщина

Спадкування – це інструмент об'єктно-орієнтованого програмування, який дозволяє створювати нові класи на основі існуючих, успадковувати їх властивості та методи, а також розширювати або перевизначати їх за потреби. Спадкування забезпечує повторне використання коду та ієрархію класів.

Простіше кажучи, якщо ми маємо один клас і хочемо створити інший, похідний від нього, але з деякими змінами, ми можемо “успадкувати” новий клас від початкового.

У PHP успадкування реалізується за допомогою ключового слова extends.

Наш клас Person зберігає інформацію про вік. Ми можемо створити ще один клас, Student, який розширює Person і додає інформацію про галузь навчання.

Давайте розглянемо приклад:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		echo 'Age of student: ', $this->howOldAreYou();
		echo 'Field of study: ', $this->fieldOfStudy;
	}
}

$student = new Student(20, 'Computer Science');
$student->printInformation();

Як працює цей код?

  • Ми використали ключове слово extends для розширення класу Person, тобто клас Student успадковує всі методи та властивості від Person.
  • Ключове слово parent:: дозволяє нам викликати методи з батьківського класу. У цьому випадку ми викликали конструктор з класу Person перед тим, як додати власну функціональність до класу Student.

Спадкування призначене для ситуацій, коли між класами існує відношення “є”. Наприклад, Student є Person. Кіт – це тварина. Це дозволяє нам у випадках, коли ми очікуємо один об'єкт (наприклад, “Людина”) в коді, використовувати замість нього похідний об'єкт (наприклад, “Студент”).

Важливо розуміти, що основною метою успадкування не є запобігання дублюванню коду. Навпаки, зловживання успадкуванням може призвести до створення складного і важкого для супроводу коду. Якщо між класами немає відношення “є”, то замість успадкування слід розглядати композицію.

Композиція

Композиція – це техніка, коли замість того, щоб успадковувати властивості та методи з іншого класу, ми просто використовуємо його екземпляр у своєму класі. Це дозволяє об'єднати функціональність і властивості декількох класів без створення складних структур успадкування.

Наприклад, у нас є клас Engine і клас Car. Замість того, щоб сказати “Автомобіль – це двигун”, ми скажемо “Автомобіль має двигун”, що є типовим відношенням композиції.

class Engine
{
	function start()
	{
		echo "Engine is running.";
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo "The car is ready to drive!";
	}
}

$car = new Car;
$car->start();

Тут Car не має всіх властивостей і методів класу Engine, але має доступ до нього через властивість $engine.

Перевагою композиції є більша гнучкість дизайну та краща адаптивність до майбутніх змін.

Видимість

У PHP ви можете визначити “видимість” для властивостей, методів і констант класу. Видимість визначає, де ви можете отримати доступ до цих елементів.

  1. Публічний: Якщо елемент позначено як public, це означає, що ви можете отримати до нього доступ з будь-якого місця, навіть за межами класу.
  2. Protected: Елемент, позначений як protected, доступний тільки в межах класу та всіх його нащадків (класів, що успадковуються від нього).
  3. Приватний: Якщо елемент позначено як private, ви можете отримати доступ до нього лише з класу, в якому його було визначено.

Якщо ви не вкажете видимість, PHP автоматично встановить її на public.

Давайте розглянемо приклад коду:

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		echo $this->privateProperty;    // Works
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Works
// echo $object->protectedProperty;   // Throws an error
// echo $object->privateProperty;     // Throws an error

Продовження успадкування класів:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		// echo $this->privateProperty;   // Throws an error
	}
}

У цьому випадку метод printProperties() у класі ChildClass може отримати доступ до загальнодоступних та захищених властивостей, але не може отримати доступ до приватних властивостей батьківського класу.

Дані та методи повинні бути максимально прихованими і доступними лише через визначений інтерфейс. Це дозволяє змінювати внутрішню реалізацію класу, не впливаючи на решту коду.

Заключне ключове слово

У PHP ми можемо використовувати ключове слово final, якщо ми хочемо запобігти успадкуванню або перевизначенню класу, методу або константи. Коли клас позначено як final, він не може бути розширений. Коли метод позначено як final, його не можна перевизначити в підкласі.

Усвідомлення того, що певний клас або метод більше не буде модифіковано, дозволяє нам легше вносити зміни, не турбуючись про потенційні конфлікти. Наприклад, ми можемо додати новий метод, не боячись, що у нащадка вже може бути метод з такою ж назвою, що призведе до колізії. Або ми можемо змінити параметри методу, знову ж таки без ризику спричинити неузгодженість з перевизначеним методом у нащадка.

final class FinalClass
{
}

// The following code will throw an error because we cannot inherit from a final class.
class ChildOfFinalClass extends FinalClass
{
}

У цьому прикладі спроба успадкувати від кінцевого класу FinalClass призведе до помилки.

Статичні властивості та методи

Коли ми говоримо про “статичні” елементи класу в PHP, ми маємо на увазі методи і властивості, які належать самому класу, а не конкретному екземпляру класу. Це означає, що вам не потрібно створювати екземпляр класу, щоб отримати до них доступ. Замість цього ви викликаєте або отримуєте доступ до них безпосередньо через ім'я класу.

Майте на увазі, що оскільки статичні елементи належать класу, а не його екземплярам, ви не можете використовувати псевдо-змінну $this всередині статичних методів.

Використання статичних властивостей призводить до заплутаного коду, повного підводних каменів, тому ви ніколи не повинні використовувати їх, і ми не будемо показувати приклад тут. З іншого боку, статичні методи корисні. Ось приклад:

class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

	public static function subtract($a, $b)
	{
		return $a - $b;
	}
}

// Using the static method without creating an instance of the class
echo Calculator::add(5, 3); // Output: 8
echo Calculator::subtract(5, 3); // Output: 2

У цьому прикладі ми створили клас Calculator з двома статичними методами. Ми можемо викликати ці методи безпосередньо, не створюючи екземпляр класу за допомогою оператора ::. Статичні методи особливо корисні для операцій, які не залежать від стану конкретного екземпляра класу.

Константи класу

У класах ми маємо можливість визначати константи. Константи – це значення, які ніколи не змінюються під час виконання програми. На відміну від змінних, значення константи залишається незмінним.

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): int
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Output: 4

У цьому прикладі ми маємо клас Car з константою NumberOfWheels. При зверненні до константи всередині класу ми можемо використовувати ключове слово self замість імені класу.

Інтерфейси об'єктів

Об'єктні інтерфейси діють як “контракти” для класів. Якщо клас реалізує об'єктний інтерфейс, він повинен містити всі методи, які визначає інтерфейс. Це чудовий спосіб гарантувати, що певні класи дотримуються одного “контракту” або структури.

У PHP інтерфейси визначаються за допомогою ключового слова interface. Всі методи, визначені в інтерфейсі, є загальнодоступними (public). Коли клас реалізує інтерфейс, він використовує ключове слово implements.

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Meow';
	}
}

$cat = new Cat;
$cat->makeSound();

Якщо клас реалізує інтерфейс, але не всі очікувані методи визначені, PHP видасть помилку. Клас може реалізовувати декілька інтерфейсів одночасно, що відрізняється від успадкування, де клас може успадковувати тільки від одного класу.

Абстрактні класи

Абстрактні класи слугують базовими шаблонами для інших класів, але ви не можете створювати їхні екземпляри безпосередньо. Вони містять суміш повних методів та абстрактних методів, які не мають визначеного змісту. Класи, які успадковують абстрактні класи, повинні надавати визначення для всіх абстрактних методів батьківського класу.

Ми використовуємо ключове слово abstract для визначення абстрактного класу.

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'This is a regular method';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'This is the implementation of the abstract method';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

У цьому прикладі ми маємо абстрактний клас з одним звичайним і одним абстрактним методом. Потім у нас є клас Child, який успадковується від AbstractClass і надає реалізацію для абстрактного методу.

Перевірка типів

У програмуванні дуже важливо переконатися, що дані, з якими ми працюємо, мають правильний тип. У PHP є інструменти, які забезпечують таку впевненість. Перевірка того, що дані мають правильний тип, називається “перевіркою типу”.

Типи, з якими ми можемо зіткнутися в PHP:

  1. Базові типи: До них відносяться int (цілі числа), float (числа з плаваючою комою), bool (логічні значення), string (рядки), array (масиви) і null.
  2. Класи: Коли ми хочемо, щоб значення було екземпляром певного класу.
  3. Інтерфейси: Визначає набір методів, які клас повинен реалізувати. Значення, яке відповідає інтерфейсу, повинно мати ці методи.
  4. Змішані типи: Ми можемо вказати, що змінна може мати декілька дозволених типів.
  5. Немає значення: Цей спеціальний тип вказує на те, що функція або метод не повертає жодного значення.

Давайте подивимося, як модифікувати код для включення типів:

class Person
{
	private int $age;

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

	public function printAge(): void
	{
		echo "This person is " . $this->age . " years old.";
	}
}

/**
 * A function that accepts a Person object and prints the person's age.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

Таким чином, ми гарантуємо, що наш код очікує і працює з даними правильного типу, що допоможе нам запобігти потенційним помилкам.

Порівняння та ідентифікація

У PHP ви можете порівнювати об'єкти двома способами:

  1. Порівняння значень ==: Перевіряє, чи об'єкти належать до одного класу і мають однакові значення у своїх властивостях.
  2. Ідентифікація ===: Перевіряє, чи це той самий екземпляр об'єкта.
class Car
{
	public string $brand;

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

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, because they have the same value
var_dump($car1 === $car2);  // false, because they are not the same instance
var_dump($car1 === $car3);  // true, because $car3 is the same instance as $car1

Оператор instanceof

Оператор instanceof дозволяє визначити, чи є даний об'єкт екземпляром певного класу, нащадком цього класу або чи реалізує він певний інтерфейс.

Уявімо, що у нас є клас Person і ще один клас Student, який є нащадком класу Person:

class Person
{
	private int $age;

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

class Student extends Person
{
	private string $major;

	public function __construct(int $age, string $major)
	{
		parent::__construct($age);
		$this->major = $major;
	}
}

$student = new Student(20, 'Computer Science');

// Check if $student is an instance of the Student class
var_dump($student instanceof Student);  // Output: bool(true)

// Check if $student is an instance of the Person class (because Student is a descendant of Person)
var_dump($student instanceof Person);   // Output: bool(true)

З результатів видно, що об'єкт $student вважається екземпляром класів Student та Person.

Зручні інтерфейси

“Вільний інтерфейс” – це техніка в ООП, яка дозволяє об'єднувати методи в ланцюжок в одному виклику. Це часто спрощує і робить код зрозумілішим.

Ключовим елементом вільного інтерфейсу є те, що кожен метод у ланцюжку повертає посилання на поточний об'єкт. Це досягається за рахунок використання return $this; в кінці методу. Цей стиль програмування часто асоціюється з методами, що називаються “сеттерами”, які встановлюють значення властивостей об'єкта.

Давайте подивимося, як може виглядати зручний інтерфейс для надсилання електронних листів:

public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Hello, this is a message.')
		  ->send();
}

У цьому прикладі методи setFrom(), setRecipient() і setMessage() використовуються для встановлення відповідних значень (відправник, одержувач, зміст повідомлення). Після встановлення кожного з цих значень методи повертають поточний об'єкт ($email), що дозволяє нам підключити інший метод після нього. Нарешті, ми викликаємо метод send(), який власне і надсилає лист.

Завдяки вільним інтерфейсам ми можемо писати код, який є інтуїтивно зрозумілим і легко читається.

Копіювання за допомогою clone

У PHP ми можемо створити копію об'єкта за допомогою оператора clone. Таким чином, ми отримуємо новий екземпляр з ідентичним вмістом.

Якщо при копіюванні об'єкта нам потрібно змінити деякі його властивості, ми можемо визначити в класі спеціальний метод __clone(). Цей метод автоматично викликається при клонуванні об'єкта.

class Sheep
{
	public string $name;

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

	public function __clone()
	{
		$this->name = 'Clone of ' . $this->name;
	}
}

$original = new Sheep('Dolly');
echo $original->name . "\n";  // Outputs: Dolly

$clone = clone $original;
echo $clone->name . "\n";     // Outputs: Clone of Dolly

У нашому прикладі є клас Sheep з однією властивістю $name. Коли ми клонуємо екземпляр цього класу, метод __clone() гарантує, що ім'я клонованої вівці отримає префікс “Clone of”.

Властивості

Риси в PHP – це інструмент, який дозволяє обмінюватися методами, властивостями і константами між класами і запобігає дублюванню коду. Ви можете думати про них як про механізм “копіювання і вставки” (Ctrl-C і Ctrl-V), де вміст трейту “вставляється” в класи. Це дозволяє повторно використовувати код без необхідності створювати складні ієрархії класів.

Давайте розглянемо простий приклад використання трейтів в PHP:

trait Honking
{
	public function honk()
	{
		echo 'Beep beep!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // Outputs 'Beep beep!'

$truck = new Truck;
$truck->honk(); // Also outputs 'Beep beep!'

У цьому прикладі у нас є трейт з іменем Honking, який містить один метод honk(). Потім у нас є два класи: Car і Truck, обидва з яких використовують трейта Honking. У результаті обидва класи “мають” метод honk(), і ми можемо викликати його в об'єктах обох класів.

Трейти дозволяють легко і ефективно обмінюватися кодом між класами. Вони не входять в ієрархію успадкування, тобто $car instanceof Honking поверне false.

Винятки

Винятки в ООП дозволяють нам обробляти та керувати помилками, які можуть виникнути під час виконання нашого коду. По суті, це об'єкти, призначені для запису помилок або неочікуваних ситуацій у вашій програмі.

У PHP для цих об'єктів є вбудований клас Exception. Він має кілька методів, які дозволяють отримати більше інформації про виняток, наприклад, повідомлення про помилку, файл, рядок, в якому виникла помилка, тощо.

Коли виникає проблема, ми можемо “згенерувати” виключення (використовуючи throw). Якщо ми хочемо “перехопити” і обробити цей виняток, ми використовуємо блоки try і catch.

Давайте подивимося, як це працює:

try {
	throw new Exception('Message explaining the reason for the exception');

	// This code won't execute
	echo 'I am a message that nobody will read';

} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}

Важливо зазначити, що виключення може бути згенероване і глибше, під час виклику інших методів.

Для одного блоку try можна вказати декілька блоків catch, якщо ви очікуєте різні типи винятків.

Ми також можемо створити ієрархію винятків, де кожен клас винятків успадковує попередній. Як приклад, розглянемо простий банківський додаток, який дозволяє вносити та знімати кошти:

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

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

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('Not enough funds in the account.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Daily withdrawal limit exceeded.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'An error occurred during the operation.';
}

У цьому прикладі важливо звернути увагу на порядок розташування блоків catch. Оскільки всі винятки успадковуються від BankingException, якби цей блок був першим, то всі винятки були б перехоплені в ньому без того, щоб код дійшов до наступних блоків catch. Тому важливо, щоб більш специфічні винятки (тобто ті, що успадковуються від інших) були вище в порядку блоків catch, ніж їхні батьківські винятки.

Кращі практики

Після того, як ви засвоїли основні принципи об'єктно-орієнтованого програмування, дуже важливо зосередитися на найкращих практиках ООП. Вони допоможуть вам писати код, який буде не лише функціональним, але й читабельним, зрозумілим та легко підтримуваним.

  1. **Розподіл обов'язків: Кожен клас повинен мати чітко визначену відповідальність і вирішувати лише одну основну задачу. Якщо клас робить занадто багато речей, може бути доцільно розділити його на менші, спеціалізовані класи.
  2. Інкапсуляція: Дані та методи повинні бути максимально приховані і доступні лише через визначений інтерфейс. Це дозволяє змінювати внутрішню реалізацію класу, не впливаючи на решту коду.
  3. Ін'єкція залежностей: Замість того, щоб створювати залежності безпосередньо всередині класу, ви повинні “впорскувати” їх ззовні. Для більш глибокого розуміння цього принципу ми рекомендуємо прочитати главу про ін'єкцію залежностей.
версію: 4.0