Абстрактні заняття, частина 1 – Абстрагування поведінки
Приблизно місяць тому я писав про один із стовпів об’єктно-орієнтованого програмування (зокрема про абстракцію). У дописі я визначив абстракцію наступним чином:
Замість цього ми будемо абстрагувати ідеї в їхніх класах. І тут є ключова ідея: клас має представляти іменник.
І хоча це все ще вірно, ідея абстрактних класів є чимось іншим в об’єктно-орієнтованому програмуванні.
Звучить заплутано, правда? Це:
- на одному рівні ми маємо абстракцію, яка визначається як ідея, що ми беремо ідею та представляємо її в класі,
- на іншому рівні ми маємо абстрактні класи, які використовуються для визначення функцій, які мають реалізовувати підкласи.
І якщо це не досить заплутано, ми змішуємо це з інтерфейсами, які забезпечують класи реалізації контракту, які мають слідувати, а потім змішуємо це з абстрактними класами, які визначають методи, які також повинні бути реалізовані, але також можуть реалізовувати власні методи.
Ще заплуталися? Нічого страшного. Суть наступних трьох дописів полягає в наступному:
- Визначте, що таке абстрактні класи,
- Опишіть різні абстрактні класи та інтерфейси,
- Допоможіть вирішити, коли ви хочете використовувати одне замість іншого.
З огляду на це, ось вся ідея абстрактних класів.
Абстрагування поведінки
Перш за все, є різниця в абстракції та абстрактних класах. Перше відноситься до ідеї представлення чогось у програмуванні; останнє відноситься до фактичного способу написання коду.
І один із найкращих способів, які я знайшов, щоб подумати про абстрактні класи в об’єктно-орієнтованому програмуванні, це подумати про них так:
Абстрактні класи є заміною реалізації.
Можливо, ще один спосіб думати про них як про заповнювачі. Зрештою, вони забезпечують поведінку, яку повинні реалізувати підкласи.
Чим це відрізняється від інтерфейсу? Пам’ятайте, що інтерфейс визначає сигнатуру для функції (ім’я функції, її аргументи та модифікатори видимості), яку має реалізувати клас.
Абстракція, з іншого боку, забезпечує заміну реалізації, яку повинен реалізувати підклас. Але, можливо, це найкраще продемонстровано за допомогою коду.
Абстракція на практиці
Припустімо, що ви працюєте над проектом і виявили, що у вас є функціональність, яка існує в кількох місцях. Окрім порушення загальної ідеї DRY, він також має потенціал бути місцем, де ви можете абстрагувати функціональність у базовий клас і використовувати його знову.
Давайте розглянемо це в контексті видавничої системи. WordPress не обов’язково реалізує це так, але він використовує ідею, з якою ми знайомі: таксономії.
Згадайте, що в WordPress є теги та категорії. Існують тонкі відмінності між ними (наприклад, ієрархічний чи ні), але вони також мають схожі атрибути, наприклад ім’я та слаг.
Абстракція таксономії
Отже, ми можемо почати з написання абстрактного класу таксономії, який абстрагує спільну функціональність у власний клас.
<?php
abstract class Taxonomy
{
private $taxonomyName;
private $taxonomySlug;
public function __construct($name) {
$this->taxonomyName = $name;
$this->taxonomySlug = strtolower(str_ireplace(' ', '-', $this->taxonomyName));
}
public function getName() {
return $this->taxonomyName;
}
public function getSlug() {
return $this->taxonomySlug;
}
abstract protected function isHierarchical();
abstract protected function isCategory();
abstract protected function isTag();
}
У наведеному вище коді ви побачите, що я зробив наступне:
- оголосив абстрактний клас
- визначено кілька атрибутів, які будуть встановлені в конструкторі
- забезпечив реалізацію кількох громадських функцій,
- додано кілька захищених методів.
Ключовим моментом у цьому класі є те, що будь-який клас, який реалізує цей абстрактний клас, автоматично матиме функціональність, визначену в конструкторі, функції getName і функції getSlug.
Однак вони не матимуть реалізації абстрактних функцій. Це те, що залишилося реалізувати підкласами (про що я зараз поділюся).
Конкретна таксономія: тег
Тепер, коли ми маємо визначений абстрактний клас, можна реально реалізувати абстракцію. наприклад :
<?php
class Tag extends Taxonomy
{
protected function isHierarchical() {
return false;
}
protected function isCategory() {
return $this->isHierarchical;
}
protected function isTag() {
return !$this->isHierarchical;
}
}
Зверніть увагу на те, що в наведеному вище коді все, що робить клас, це реалізація абстрактних функцій, визначених у абстрактному класі (що вказується функцією extends у визначенні класу).
Пізніше в цій статті я розповім, як протестувати цей код, але зауважте, що вищезазначене пропонує не лише функціональність, яку ви бачите, але й функціональність класу Taxonomy.
Конкретна таксономія: категорія
Перш ніж поглянути на це в дії, я також хочу визначити категорію. Це включатиме код, який реалізує функції з абстрактного класу, а також власні функції.
<?php
class Category extends Taxonomy
{
private $parentId = -1;
protected function isHierarchical() {
return true;
}
protected function isCategory() {
return $this->isHierarchical;
}
protected function isTag() {
return !$this->isHierarchical;
}
public function setParentId($parentId) {
$this->parentId = $parentId;
}
public function getParentId() {
return $this->parentId;
}
}
Тут у нас є все, що приходить із класом Taxonomy, але ми також визначили нашу властивість для його батьківського ідентифікатора та методів getter і setter. Хоча в цьому випадку це тривіально, воно показує, як можуть функціонувати категорії, які є ієрархічними.
Крім того, якщо категорія не має батьківського елемента, тоді для ідентифікатора встановлюється значення -1, що полегшує запис для автоматизованого тестування або навіть перевірку наявності батьківського елемента.
Побачити це в дії
Щоб продемонструвати весь цей код, у мене є суть, яка містить увесь код в одному файлі. Як найкращу практику, я не рекомендую цього. Натомість кожен клас має зберігатися у власному файлі, і кожен клас має належати до простору імен.
Але оскільки це виключно для демонстраційних цілей, цього достатньо.
<?php
abstract class Taxonomy
{
private $taxonomyName;
private $taxonomySlug;
public function __construct($name) {
$this->taxonomyName = $name;
$this->taxonomySlug = strtolower(str_ireplace(' ', '-', $this->taxonomyName));
}
public function getName() {
return $this->taxonomyName;
}
public function getSlug() {
return $this->taxonomySlug;
}
abstract protected function isHierarchical();
abstract protected function isCategory();
abstract protected function isTag();
}
/*--*/
class Tag extends Taxonomy
{
protected function isHierarchical() {
return false;
}
protected function isCategory() {
return $this->isHierarchical;
}
protected function isTag() {
return !$this->isHierarchical;
}
}
/*--*/
class Category extends Taxonomy
{
private $parentId = -1;
protected function isHierarchical() {
return true;
}
protected function isCategory() {
return $this->isHierarchical;
}
protected function isTag() {
return !$this->isHierarchical;
}
public function setParentId($parentId) {
$this->parentId = $parentId;
}
public function getParentId() {
return $this->parentId;
}
}
/*- Tag Demo ----------------------------*/
$tag = new Tag('Acme Tag');
echo $tag->getName();
echo $tag->getSlug();
/*- Category Demo -----------------------*/
$category = new Category('Acme Category');
echo $category->getName();
echo $category->getSlug();
echo $category->getParentId();
$category->setParentId(100);
echo $category->getparentId();
Коли ви запускаєте це в консолі, ви повинні побачити щось на кшталт наступного результату:
Можливо, вам знадобиться додати кілька операторів echo, щоб переконатися, що він створює нові рядки, але це залежить від вас.
А як щодо інтерфейсів?
Отже, на цьому етапі:
- ми маємо робоче визначення того, що таке абстрактні класи,
- у нас є приклад того, як виглядають абстрактні класи,
- і ми маємо робочу демонстрацію того, як вони можуть працювати.
Далі я глибше занурюся в обговорення відмінностей між абстрактними класами та інтерфейсами, коли ви можете використовувати один над іншим або коли ви можете використовувати їх у поєднанні один з одним.