Skip to content

9. Трейти

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

9.1. Дерево скриптів

Image

9.2. Включення риси до класу

Скрипт [traits-01.php] демонструє базове використання риси в класі:


<?php

class Class04 {
  // атрибут
  private $name;

  // конструктор
  public function __construct(string $name) {
    $this->name = $name;
  }

  // методи getter та setter
  public function getName(): string {
    return $this->name;
  }

  public function setName(string $name): void {
    $this->name = $name;
  }

}

trait Trait04 {
  // атрибут
  private $name;

  // методи getter та setter
  public function getName(): string {
    return $this->name;
  }

  public function setName(string $name): void {
    $this->name = $name;
  }

}

class Class05 {
  // включення риси
  use Trait04;

  // конструктор
  public function __construct(string $name) {
    $this->name = $name;
  }

}

// тест --------------
$class04 = new Class04("Tim");
$class05 = new Class05("Burton");
print $class04->getName() . "\n";
print $class05->getName() . "\n";
// виведення двох класів
print_r($class04);
print_r($class05);

Коментарі до коду

  • рядки 3–21: визначення класу [Class04] з атрибутом, методами get/set та конструктором;
  • рядки 23–36: беремо код з [Class04] без його конструктора та переносимо його в трайт [Trait04] без змін. Конструктор не переносимо, оскільки трайт не підлягає інстанціюванню;
  • рядок 23: саме ключове слово [trait] робить [Trait04] рисою, а не класом;
  • рядки 38–45: визначається клас [Class05], який переймає код риси [Trait04] (рядок 40) і додає до нього конструктор (рядки 43–45), ідентичний конструктору класу [Class04], щоб зробити клас інстанційованим;
  • рядок 40: саме ключове слово [use] дозволяє включити рис у клас;
  • рядки 50–56: тести показують, що класи [Class04] та [Class05] працюють однаково;

Результати

Tim
Burton
Class04 Object
(
    [name:Class04:private] => Tim
)
Class05 Object
(
    [name:Class05:private] => Burton
)

Результати у рядках 3–10 показують, що класи [Class04] та [Class05] мають однаковий вміст;

Висновок

Використання інструкції [use Trait] у класі еквівалентно включенню коду [Trait] до класу.

9.3. Використання одного й того самого «тракту» в різних класах

Однією з головних переваг «трасу» є можливість повторного використання одного й того самого коду (атрибутів та методів) у різних класах. Однак ми побачимо, що того самого результату можна досягти, використовуючи прості класи.

Спільне використання trait між класами проілюстровано наступним скриптом [trait-02.php]:


<?php

trait Trait01 {
  // атрибут
  private $id = 0;

  // метод
  public function doSomething() {
    print "Trait01::doSomething… ($this->id)\n";
  }

}

class Class02 {
  // включення Trait01
  use Trait01 {
    // метод [Trait01::doSomething] доступний
    // у класі під назвою [doSomethingInTrait]
    Trait01::doSomething as doSomethingInTrait;
  }

  // метод, властивий класу
  public function doSomething(): void {
    // атрибут id
    $this->id += 10;
    // використання методу Trait01
    $this->doSomethingInTrait();
    // локальне відображення
    print "Class02->doSomething\n";
  }

}

class Class03 {
  // включення методу Trait01
  use Trait01;

  // локальний метод класу
  public function doSomethingElse(): void {
    // атрибут id
    $this->id += 10;
    // використання методу Trait01
    $this->doSomething();
    // локальне відображення
    print "Class03->doSomethingElse\n";
  }

}

// test01 ----------------
function test01(): void {
  $class02 = new Class02();
  $class03 = new Class03();
  $class02->doSomething();
  $class03->doSomethingElse();
}

// test01
print "test01-----------------\n";
test01();

Коментарі

  • рядки 3–10: елемент, що визначає атрибут (рядок 5) та метод (рядки 8–10).
  • Трейт [Trait01] вводиться у два класи: [Class02] (рядки 14–32) та [Class03] (рядки 34–46).
  • рядки 16–20: введення [Trait01] у [Class02];
  • рядок 19 призначений для вирішення конфлікту: класи [Trait01] та [Class02] мають метод з назвою [doSomething]. Слід передбачити два випадки:
    • метод [Class02::doSomething] викликається ззовні класу. У цьому випадку метод [Class02::doSomething] має пріоритет над методом [Trait01::doSomething], і саме він викликається;
    • метод [Class02::doSomething] викликається зсередини класу. У цьому випадку виникає конфлікт: інтерпретатор PHP не знає, який метод викликати;

У рядку 19 метод [Trait01::doSomething] перейменовано на [doSomethingInTrait]. Таким чином, усередині [Class02] будуть використовуватися такі позначення:

  • [doSomethingInTrait] для виклику методу [Trait01::doSomething];
  • [doSomething] для виклику методу [Class02::doSomething];
  • рядки 25, 27: клас [Class02] використовує атрибут і метод класу [Trait01] так, ніби вони є його власними;
  • рядки 34–48: клас [Class03] ідентичний класу [Class02]. Включення класу [Trait01] тут простіше, оскільки немає конфлікту між методами класів [Trait01] та [Class03];

Результати

1
2
3
4
5
test01-----------------
Trait01::doSomething… (10)
Class02->doSomething
Trait01::doSomething… (10)
Class03->doSomethingElse

Слід зауважити, що ознака [Trait01] не є спільною для класів [Class02] та [Class03]. Таким чином,атрибут [Trait01::i], завдяки включенню [Trait01] до класів [Class02] та [Class03], перетворюється на два різних атрибути: [Class02::i] та [Class03::i]. Про це свідчать рядки 2 та 4 результатів. Якби атрибут [Trait01::i] був спільним для класів [Class02] та [Class03], у рядку 4 було б 20 замість 10.

Скрипт [trait-03.php] показує, що можна отримати той самий результат, використовуючи клас замість ознаки:


<?php

// клас, що замінює рис
class Class01 {
  // атрибут
  private $id = 0;

  // сеттер
  public function setId(int $id) {
    $this->id = $id;
  }

  // геттер
  public function getId(): int {
    return $this->id;
  }

  // метод
  public function doSomething(): void {
    print "Class01::doSomething… ($this->id)\n";
  }

}

class Class02 {
  // включення Class01
  private $class01;

  // сетер
  public function setClass01(Class01 $class01) {
    $this->class01 = $class01;
  }

  // метод, властивий класу
  public function doSomething(): void {
    // зміна атрибута Class01
    $id = $this->class01->getId();
    $id += 10;
    $this->class01->setId($id);
    // використання методу класу Class01
    $this->class01->doSomething();
    // локальне відображення
    print "Class02->doSomething\n";
  }

}

class Class03 {
  // включення Class01
  private $class01;

  // сеттер
  public function setClass01(Class01 $class01) {
    $this->class01 = $class01;
  }

  // локальний метод класу
  public function doSomethingElse(): void {
    // зміна атрибута класу Class01
    $id = $this->class01->getId();
    $id += 10;
    $this->class01->setId($id);
    // використання методу класу Class01
    $this->class01->doSomething();
    // локальне відображення
    print "Class03->doSomethingElse\n";
  }

}

// test01 ----------------
function test01(): void {
  // два об’єкти
  $class02 = new Class02();
  $class03 = new Class03();
  // звернуться до двох різних екземплярів [Class01]
  $class02->setClass01(new Class01());
  $class03->setClass01(new Class01());
  // перевірка
  $class02->doSomething();
  $class03->doSomethingElse();
}

// test02 ----------------
function test02(): void {
  // спільний екземпляр [Class01]
  $class01 = new Class01();
  // два об’єкти
  $class02 = new Class02();
  $class03 = new Class03();
  // отримають доступ до одного й того ж екземпляра [Class01]
  $class02->setClass01($class01);
  $class03->setClass01($class01);
  // перевірка
  $class02->doSomething();
  $class03->doSomethingElse();
}

// test01
print "test01-----------------\n";
test01();
// тест02
print "test02-----------------\n";
test02();

Коментарі

  • рядки 4–23: клас [Class01] замінює код [Trait01]. Код [Trait01] був включений до коду класів [Class02] та [Class03]. У даному випадку це не так. У класи [Class02] та [Class03] буде вставлено посилання на код класу [Class01]. Це означає, що атрибути класу [Class01] будуть зовнішніми щодо коду класів [Class02] та [Class03]. Оскільки в даному випадку атрибут [id] є приватним (рядок 6), необхідно передбачити геттер (рядки 14–16) та сеттер (рядки 9–11). Це перша відмінність від риси: потрібно створити код для доступу до приватних атрибутів класу. Можна було б змінити видимість атрибута [id] на [public], але робити це ніколи не рекомендується. Використання сетера для встановлення значення атрибута дозволяє перевірити його правильність;
  • рядок 27: включення в код [Class02] посилання на клас [Class01]. Оскільки цей атрибут є приватним, нам потрібно створити сеттер (рядки 30–32) для його ініціалізації;
  • рядки 37–41: для будь-якого використання коду [Class01] нам потрібно використовувати атрибут [$this→class01];
  • рядки 48–69: клас [Class03] є клоном класу [Classe02], за винятком того, що його метод має іншу назву;
  • рядки 72–82: перший тест. Він полягає у введенні в класи [Class02] та [Class03] двох різних екземплярів класу [Class01] (рядки 77 та 78);
  • рядки 85–97: другий тест вводить у класи [Class02] та [Class03] один і той самий екземпляр класу [Class01] (рядки 97, 92, 93);
  • рядки 100–101: виконання тесту [test01];
  • рядки 103–104: виконання тесту [test02];

Результати

test01-----------------
Class01::doSomething… (10)
Class02->doSomething
Class01::doSomething… (10)
Class03->doSomethingElse
test02-----------------
Class01::doSomething… (10)
Class02->doSomething
Class01::doSomething… (20)
Class03->doSomethingElse

Коментарі до результатів

  • рядки 2–5: отримано ті самі результати, що й у випадку з об’єктом [Trait01]. З цього можна зробити висновок, що використання трейсу тут не є обов’язковим, але воно дозволяє скоротити код, оскільки не потрібно використовувати методи для доступу до атрибутів трейсу: вони є невід’ємною частиною коду, в який було вбудовано трейс;
  • рядки 7–10: оскільки ми ввели одне й те саме посилання на [Class01] у класи [Class02] та [Class03], атрибут [Class01::id] став спільним для обох класів. Саме тому у рядку 9 результатів відображається 20 замість 10 із використанням трейсу. З цього можна зробити висновок, що якщо атрибути трейсу мають бути спільними для класів, то трейс не можна використовувати, і в такому разі слід застосовувати клас;

9.4. Об’єднання методів у трет

У попередньому прикладі трайт містив атрибути та методи. Тут ми розглянемо випадок, коли він містить лише методи. У цьому випадку трайт нагадує факторизацію методів, які потім можна використовувати в різних класах. Оскільки трайт тут не має атрибутів, ми розглянемо випадок, коли методи, які він об’єднує, працюють виключно з параметрами, що їм передаються. Насправді це не є обов’язковим: трейт може оперувати атрибутом [$this→attribut1], не маючи цього атрибута. У такому разі класи, які використовують цей трейт, повинні надати атрибут [$this→attribut1].

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

Використання трейту проілюстровано скриптом [trait-04.php], який використовує трейт із попереднього прикладу, видаливши з нього всі атрибути:


<?php

trait Trait01 {

  // метод для спільного використання
  public function doSomething() {
    print "Trait01::doSomething ….\n";
  }

}

class Class02 {
  // включення Trait01
  use Trait01 {
    // метод [Trait01::doSomething] доступний
    // у класі під назвою [doSomethingInTrait]
    Trait01::doSomething as doSomethingInTrait;
  }

  public function doSomething(): void {
    // виклик методу Trait01
    $this->doSomethingInTrait();
    // локальне відображення
    print "Class02->doSomething\n";
  }

}

class Class03 {
  // включення методу Trait01
  use Trait01;

  // локальний метод класу
  public function doSomethingElse(): void {
    // виклик методу Trait01
    $this->doSomething();
    // локальне відображення
    print "Class03->doSomethingElse\n";
  }

}

// тест ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();

Коментарі

  • рядки 3–10: трайт [Trait01] більше не має атрибутів;
  • рядки 14–18: включення [Trait01] у [Class02]. У рядку 22 використовується метод [Trait01];
  • рядок 31: включення [Trait01] у [Class03]. Метод [Trait01] використовується у рядку 36;

Результати

1
2
3
4
Trait01::doSomething ….
Class02->doSomething
Trait01::doSomething ….
Class03->doSomethingElse

У цьому випадку об’єкт [Trait01] можна легко замінити на клас. Це продемонстровано у наступному скрипті [trait-05.php]:


<?php

abstract class Class01 {

  // статичний метод для спільного використання
  public static function doSomething() {
    print "Class01::doSomething ….\n";
  }

}

class Class02 {

  public function doSomething(): void {
    // виклик методу класу Class01
    Class01::doSomething();
    // локальне відображення
    print "Class02->doSomething\n";
  }

}

class Class03 {

  // локальний метод класу
  public function doSomethingElse(): void {
    // виклик методу класу Class01
    Class01::doSomething();
    // локальне відображення
    print "Class03->doSomethingElse\n";
  }

}

// тест ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();

Коментарі

  • рядки 3–10: риса [Trait01] замінюється абстрактним класом [Class01], усі методи якого оголошені статичними. Клас оголошено абстрактним виключно для того, щоб запобігти його інстанціюванню. Ми хотіли б також написати [final], щоб запобігти його похідним класам, але PHP 7 не допускає префікс [final abstract] для класу. Можна вибрати або одне, або інше, але не обидва;
  • рядок 16: замість [$this→doSomethingInTrait] тепер пишемо [Class01::doSomething], тобто викликаємо статичний метод [doSomething] класу [Class01];
  • рядок 28: повторюємо ту саму процедуру в [Class03];

Результати

1
2
3
4
Class01::doSomething ….
Class02->doSomething
Class01::doSomething ….
Class03->doSomethingElse

Ми отримали той самий результат, що й із рисою [Trait01], що свідчить про те, що її використання можна уникнути. Ми зазначали, що методи риси можуть працювати з атрибутом [$this→attribut1], навіть якщо сама риса не має цього атрибута. Тож саме класи, які використовують цей трейт, повинні надавати атрибут [$this→attribut1]. Це нетиповий випадок: можна «підняти» атрибут [$this→attribut1], який повинні мати класи, що використовують трейт, безпосередньо в сам трейт. Таким чином, він обов’язково стане частиною атрибутів класу, що використовує трейт.

9.5. Багаторазове успадкування за допомогою риси

У літературі часто можна прочитати, що trait дозволяє здійснювати множинне успадкування: можливість для класу успадковувати від декількох класів. Мова C++ має таку можливість, на відміну від мов Java чи C#, які підтримують лише просте успадкування. Ми покажемо, що хоча використання риси в похідному класі й дозволяє реалізувати щось, схоже на множинне успадкування, цей варіант застосування можна реалізувати й за допомогою простих класів.

Скрипт [trait-06.php] реалізує похідний клас і рис:


<?php

trait Trait01 {
  // атрибут
  private $i;

  // метод для спільного використання
  public function doSomethingInTrait01() {
    // зміна Trait01::$i
    $this->i++;
    // відображення
    print "Trait01::doSomethingInTrait01… i=$this->i\n";
  }

}

class Class02 {
  // атрибут
  protected $j = 0;

  // метод
  public function doSomethingInClass02(): void {
    // зміна Class02::j
    $this->j += 10;
    // відображення
    print "Class02->doSomethingInClass02… j=$this->j\n";
  }

}

// похідний клас
class Class03 extends Class02 {
  // успадковує від Class02:j та Trait01::i
  // включення Trait01
  use Trait01;

  // метод
  public function doSomethingInClass03(): void {
    // використання методу з Trait01
    $this->doSomethingInTrait01();
    // зміна Trait01::i
    $this->i += 100;
    // зміна Class03::j (==Class02::j)
    $this->j += 1000;
    // виведення
    print "Class03->doSomethingInClass03… i=$this->i, j=$this->j\n";
  }

}

// тест ----------------
(new Class02())->doSomethingInClass02();
(new Class03())->doSomethingInClass03();

Коментарі

  • рядки 3–15: повертаємося до трейсу [Trait01] з атрибутом та методом, що оперує ним;
  • рядки 17–29: клас [Class02], який не має нічого спільного з атрибутом [Trait01]. Він його не використовує;
  • рядок 19: оголошено єдиний атрибут класу [Class02] із видимістю [protected], щоб він був доступний у похідних класах;
  • рядок 32: клас [Class03] успадковує клас [Class02]. Крім того, він включає рис [Trait01] (рядок 35). Нарешті, він успадковує атрибути та методи класу [Class02] і включає атрибути та методи класу [Trait01]. Отже, ми маємо щось аналогічне до множинної спадковості;

Результати

1
2
3
Class02->doSomethingInClass02… j=10
Trait01::doSomethingInTrait01… i=1
Class03->doSomethingInClass03… i=101, j=1000

Так само, як це було зроблено в попередньому прикладі, ми покажемо, що:

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

Скрипт [trait-07.php] має такий вигляд:


<?php

class Class01 {
  // атрибут
  protected $i;

  // геттер і сеттер
  public function getI(): int {
    return $this->i;
  }

  public function setI(int $i): void {
    $this->i = $i;
  }

  // метод
  public function doSomethingInClass01(): void {
    // зміна Class01::$i
    $this->i++;
    // виведення
    print "Class01::doSomething in Class01… i=$this->i\n";
  }

}

class Class02 {
  // атрибут
  protected $j = 0;

  // метод класу
  public function doSomethingInClass02(): void {
    // зміна Class02::j
    $this->j += 10;
    // виведення
    print "Class02->doSomethingInClass02… j=$this->j\n";
  }

}

class Class03 extends Class02 {
  // включення Class01
  private $class01;

  // сеттер
  public function setClass01(Class01 $class01) {
    $this->class01 = $class01;
  }

  // локальний метод класу
  public function doSomethingInClass03(): void {
    // використання методу класу Class01
    $this->class01->doSomethingInClass01();
    // зміна Class01::i
    $i = $this->class01->getI();
    $i += 100;
    $this->class01->setI($i);
    // зміна Class03::j
    $this->j += 1000;
    // виведення
    print "Class03->doSomethingInClass03… i=$i, j=$this->j\n";
  }

}

// тест ----------------
$class01 = new Class01();
$class02 = new Class02();
$class03 = new Class03();
$class03->setClass01($class01);
$class02->doSomethingInClass02();
$class03->doSomethingInClass03();

Коментарі

  • рядки 3–24: клас [Class01] замінює рис [Trait01]. Оскільки клас [Class01] буде включено до класів за допомогою посилання, передбачено методи get / set для атрибута [$i];
  • рядки 26–38: клас [Classe02] не змінюється;
  • рядок 40: клас [Class03] успадковує клас [Classe02];
  • рядок 42: клас [Classe01] включено до класу [Classe03] за допомогою посилання;
  • рядки 45–47: передбачено клас setter для ініціалізації посилання на клас [Classe01];
  • рядки 50–61: метод [doSomethingInClass03] виконує те саме, що й раніше, проте з більш складним кодом;

Результати

1
2
3
Class02->doSomethingInClass02… j=10
Class01::doSomething in Class01… i=1
Class03->doSomethingInClass03… i=101, j=1000

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

9.6. Використання трейту замість абстрактного класу

Часто зустрічається такий випадок використання: створюється досить загальний інтерфейс I, який може породити кілька реалізацій. Вони мають спільний код, але відрізняються за іншими методами. Цей випадок використання можна реалізувати двома способами:

  1. створюється абстрактний клас C, який об’єднує код, спільний для похідних класів. Клас C реалізує інтерфейс I, але деякі методи, які мають бути оголошені в похідних класах, у класі C оголошені як абстрактні, а отже, сам клас C є абстрактним. Потім створюємо класи C1 та C2, похідні від C, які кожен по-своєму реалізують невизначені (абстрактні) методи свого батьківського класу C;
  2. створюється риса T, майже ідентична абстрактному класу C з попереднього рішення. Ця риса не реалізує інтерфейс I, оскільки синтаксично вона не може цього зробити. Далі створюються класи C1 та C2, які реалізують інтерфейс I та використовують рис T. Цим класам залишається лише реалізувати методи інтерфейсу I, які не реалізовані рисою T, яку вони імпортують;

Ось приклад, який ілюструє велику схожість цих двох рішень.

Додаток 1 реалізує описане вище рішення 1 — [trait-08.php]:


<?php

interface Interface1 {

  public function doSomething(): void;

  public function doSomethingElse(): void;
}

abstract class AbstractClass implements Interface1 {
  // атрибути
  protected $attr1 = 11;
  protected $attr2 = 12;

  // гетери та сеттери
  public function getAttr1() {
    return $this->attr1;
  }

  public function getAttr2() {
    return $this->attr2;
  }

  public function setAttr1($attr1) {
    $this->attr1 = $attr1;
    return $this;
  }

  public function setAttr2($attr2) {
    $this->attr2 = $attr2;
    return $this;
  }

  // реалізований метод
  public function doSomething(): void {
    print "AbstractClass::doSomething [$this->attr1,$this->attr2]\n";
  }

  // нереалізований метод
  abstract public function doSomethingElse(): void;
}

// похідний клас 1
class Class1 extends AbstractClass {
  // атрибут
  private $attr3 = 13;

  // геттер і сеттер
  public function getAttr3() {
    return $this->attr3;
  }

  public function setAttr3($attr3) {
    $this->attr3 = $attr3;
    return $this;
  }

  // реалізація doSomethingElse
  public function doSomethingElse(): void {
    print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
  }

}

// похідний клас 2
class Class2 extends AbstractClass {
  // атрибут
  private $attr4 = 14;

  public function getAttr4() {
    return $this->attr4;
  }

  public function setAttr4($attr4) {
    $this->attr4 = $attr4;
    return $this;
  }

  // реалізація doSomethingElse
  public function doSomethingElse(): void {
    print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
  }

}

// зовнішня функція
function useInterfaceWith(Interface1 $interface):void{
  $interface->doSomething();
  $interface->doSomethingElse();
}

// тести
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());

Коментарі

  • рядки 3–8: інтерфейс [Interface1] має два методи;
  • рядки 10–41: абстрактний клас [AbstractClass] реалізує інтерфейс [Interface1] (рядок 10). Він має два атрибути з відповідними методами getter та setter (рядки 12–32), реалізує метод [doSomething] інтерфейсу [Interface1] (рядки 35–37) але не може реалізувати метод [doSomethingElse]. Тому цей метод оголошено абстрактним (рядок 40). Абстрактний клас [AbstractClass] не може бути інстанційований, і щоб він мав якесь застосування, його обов’язково потрібно успадкувати;
  • рядки 44–63: клас Class1 успадковує абстрактний клас [AbstractClass] і, отже, реалізує інтерфейс [Interface1] (рядок 14). Він надає тіло методу [doSomethingElse], який його батьківський клас не визначив (рядки 59–61). Він також додає атрибут до атрибутів свого батьківського класу (рядки 46–56);
  • рядки 66–82: клас Class2 успадковує абстрактний клас [AbstractClass] і, отже, реалізує інтерфейс [Interface1] (рядок 66). Вона надає тіло методу [doSomethingElse], який не був визначений у її батьківському класі (рядки 80–82). Вона також додає атрибут до атрибутів свого батьківського класу (рядки 68–77);
  • рядки 87–90: функція [useInterfaceWith] отримує як параметр тип [Interface1] і викликає обидва методи цього інтерфейсу;
  • рядки 93–94: функцію [useInterfaceWith] викликають уперше з типом [Class1], а вдруге — з типом [Class2]. Це правильно, оскільки обидва ці типи реалізують інтерфейс [Interface1];

Результати


AbstractClass::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
AbstractClass::doSomething [11,12]
Class2::doSomethingElse [11,12,14]

Тепер реалізуємо рішення 2 за допомогою скрипта [trait-09.php]. Воно полягає в заміні абстрактного класу на рис:


<?php

interface Interface1 {

  public function doSomething(): void;

  public function doSomethingElse(): void;
}

trait Trait1 {
  // атрибути
  private $attr1 = 11;
  private $attr2 = 12;

  // гетери та сеттери
  public function getAttr1() {
    return $this->attr1;
  }

  public function getAttr2() {
    return $this->attr2;
  }

  public function setAttr1($attr1) {
    $this->attr1 = $attr1;
    return $this;
  }

  public function setAttr2($attr2) {
    $this->attr2 = $attr2;
    return $this;
  }

  // реалізований метод
  public function doSomething(): void {
    print "Trait::doSomething [$this->attr1,$this->attr2]\n";
  }
}

// похідний клас 1
class Class1 implements Interface1 {
  // використання риси
  use Trait1;
  // атрибут
  private $attr3 = 13;

  // геттери та сеттери
  public function getAttr3() {
    return $this->attr3;
  }

  public function setAttr3($attr3) {
    $this->attr3 = $attr3;
    return $this;
  }

  // реалізація doSomethingElse
  public function doSomethingElse(): void {
    print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
  }

}

// похідний клас 2
class Class2 implements Interface1 {
  // використання риси
  use Trait1;
  // атрибут
  private $attr4 = 14;

  public function getAttr4() {
    return $this->attr4;
  }

  public function setAttr4($attr4) {
    $this->attr4 = $attr4;
    return $this;
  }

  // реалізація doSomethingElse
  public function doSomethingElse(): void {
    print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
  }

}

// зовнішня функція, що використовує інтерфейс
function useInterfaceWith(Interface1 $interface): void {
  $interface->doSomething();
  $interface->doSomethingElse();
}

// тестування
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());

Коментарі

  • рядки 3–8: інтерфейс [Interface1] не змінився;
  • рядки 10–41: рис. [Trait1] замінює абстрактний клас [AbstractClass] із рішення 1. Код є однаковим, за винятком таких деталей:
    • рядок 10: риса [Trait1] не реалізує інтерфейс [Interface1]. Це синтаксично неможливо;
    • рядки 12–13: атрибут видимості [protected] атрибутів абстрактного класу [AbstractClass] тут стає [private]. Ці два атрибути призначені для надання похідним класам прямого доступу до атрибутів батьківського класу (protected) або риси (private) без необхідності використання геттерів та сеттерів;
    • трайт [Trait1] не оголошує абстрактний метод [doSomethingElse];
  • рядки 41–62: клас [Class1] з рішення 2 ідентичний класу [Class1] з рішення 1, за винятком таких деталей:
    • рядок 41: клас [Class1] реалізує інтерфейс [Interface1], тоді як у рішенні 1 він успадковував абстрактний клас [AbstractClass];
    • рядок 43: він використовує властивість [Trait1] для реалізації частини інтерфейсу;
  • рядки 65–85: тут можна зробити ті самі зауваження, що й щодо [Class1];
  • рядки 87–95: решта коду не змінюється;

Результати


Trait::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
Trait::doSomething [11,12]
Class2::doSomethingElse [11,12,14]

Ми дійсно отримуємо ті самі результати.

9.7. Conclusion

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