Skip to content

10. Багатошарові додатки

10.1. Introduction

Image

Тепер ми дізнаємося, як структурувати додаток PHP за рівнями:

Image

На цій схемі лівий рівень ініціює використання правого рівня. Ролі рівнів такі:

  • [1]: шар, який називається [dao] (Data Access Objects), відповідає за взаємодію із зовнішніми сховищами даних [4] (файли, бази даних, веб-сервіси тощо). Цей рівень іноді називають [DAL] (Data Access Layer) — цей термін краще описує роль рівня. Цей рівень може зчитувати дані [3] або записувати їх [2]. Вона отримує запити від шару [métier] [6] і повертає йому результати [7];
  • [5]: рівень під назвою [métier], який об’єднує «бізнес-процедури», яким надаються всі необхідні дані. Зазвичай це найстабільніший рівень проекту, оскільки він не залежить від способу отримання даних. Дані мають два джерела:
    • [9]: дані, що надаються скриптом PHP;
    • [6,7]: дані, що запитуються у шарі [dao].
  • [8]: головний скрипт є «диригентом». У консольному додатку він:
    • створює шари [métier] та [dao];
    • виконує алгоритм програми. Цей алгоритм діє як диригент: у ньому немає жодного «бізнес-коду» чи коду доступу до даних. Головний скрипт лише викликає процедури шарів [métier] та [9]. Він повністю ігнорує рівень [dao] та зовнішні дані. Він може надавати рівню [métier] дані з рівня [9]. У консольному додатку ці дані можуть надходити з конфігураційних файлів або від користувача скрипта. Він отримує результати [10] від шару [métier]. Йому може знадобитися зберегти певні результати: для цього він знову використовує процедури шару [métier], які, у свою чергу, звертаються до шару [dao], який виконує цю роботу;
    • серед результатів головний скрипт може отримувати винятки. Його завданням є обробка винятків, що надходять з усіх шарів;

Такий поділ на шари призначений для полегшення розвитку додатка. Для цього кожен шар реалізує інтерфейс. Припустимо, що:

  • шар [dao] реалізовано класом [Dao], який реалізує інтерфейс [IDao];
  • шар [métier] реалізовано класом [Métier], який реалізує інтерфейс [IMétier];

Процедури рівня [métier] будуть використовувати інтерфейс [IDao] замість класу [Dao]. Це дозволяє модернізувати рівень [Dao], не зачіпаючи рівень [Métier]. Припустимо, що у версії 1 рівень [Dao1] використовує дані з бази даних. Після оновлення ці дані тепер надаються веб-сервісом у версії 2, [Dao2]. Необхідно переконатися, що обидва класи [Dao1] та [Dao2] реалізують один і той самий інтерфейс [IDao] і що вони генерують однакові винятки. Якщо це підтвердиться, рівень [Métier], який працює з інтерфейсом [IDao], залишиться без змін;

Те саме міркування стосується рівня [Métier].

Розглянемо реалізацію з використанням класів та інтерфейсів:

Image

Додаток реалізує таку багаторівневу структуру:

Image

10.2. Об’єкти, що обмінюються між шарами

Зазвичай шари обмінюються різними об’єктами. У цьому випадку вони обмінюватимуться таким класом: [Personne.php]:


<?php

// особа
class Personne {
  // ідентифікатор
  private $id;

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

  // toString
  public function __toString(): string {
    return "[Personne($this->id)]";
  }

}

Коментарі

  • рядок 6: особа має лише один атрибут — її ідентифікатор [$id]. Припустимо, що він позначає унікальну особу в сховищі даних;
  • рядки 9–11: конструктор, що дозволяє створити тип [Personne] із його ідентифікатором;
  • рядки 14–16: метод [__toString], який виводить ідентифікатор особи;

10.3. Рівень [dao]

Інтерфейс [IDao], реалізований рівнем [dao, 1], має такий вигляд: [IDao.php]:


<?php


// шар [dao] ------------------------
interface IDao {

  // вилучення особи зі зовнішнього складу
  // передача ідентифікатора особи
  public function get(int $id): Personne;

  // збереження даних особи у зовнішньому сховищі
  public function save(Personne $p): void;
}

Цей інтерфейс реалізовано наступним класом [Dao1] [Dao1.php]:


<?php

class Dao1 implements IDao {

  // збереження даних про особу у зовнішньому сховищі
  public function save(Personne $p): void {
    print "[Dao1] : Sauvegarde de la personne $p en base de données [locale]\n";
  }

  // витягнення особи з зовнішнього сховища
  public function get(int $id): Personne {
    print "[Dao1] : Récupération de la personne d'identité ($id) en base de données [locale]\n";
    return new Personne($id);
  }

}

Коментарі

  • клас не взаємодіє з сховищем даних. Ми обмежуємося відображенням повідомлень для відстеження виконання коду (рядки 7 та 12);
  • рядок 13: дійсно повертається особа з ідентифікатором, переданим як параметр методу (рядок 11);

Ми також реалізуємо інтерфейс [IDao] за допомогою наступного класу [Dao2] [Dao2.php]:


<?php

class Dao2 implements IDao {

  // збереження особи у зовнішньому сховищі
  public function save(Personne $p): void {
    print "[Dao2] : Sauvegarde de la personne $p en base de données [distante]\n";
  }

  // витягнення особи з зовнішнього сховища
  public function get(int $id): Personne {
    print "[Dao2] : Récupération de la personne d'identité ($id) en base de données [distante]\n";
    return new Personne($id);
  }

}

Клас [Dao2] аналогічний класу [Dao1], за винятком того, що ми змінили повідомлення, які відображаються.

10.4. Шар [métier]

Рівень [métier, 2] надає такий інтерфейс [IMétier]: [IMetier.php]:


<?php

// шар [métier] ------------------------
interface IMétier extends IDao {

  // обробка особи, ідентифікованої за її ідентифікатором
  public function doSomething(Personne $p): void;
}

Коментарі

  • Інтерфейс [IMétier] розширює інтерфейс [IDao]. Це зовсім не обов’язково. Ми робимо це тут, оскільки приклад є простим;
  • рядок 7: метод [doSomething] є власним для шару [Métier];

Інтерфейс [IMétier] буде реалізовано наступним класом [Métier] [Métier.php]:


<?php

class Métier implements IMétier {
  // шар [dao]
  private $dao;

  // геттер / сеттер
  public function setDao(IDao $dao): void {
    $this->dao = $dao;
  }

  public function getDao(): IDao {
    return $this->dao;
  }

  // використовується особа
  public function doSomething(Personne $p): void {
    // обробка
    print "Métier : Traitement métier de la personne $p\n";
  }

  // збереження даних про особу
  public function save(Personne $p): void {
    print "[Métier] : Sauvegarde de la personne $p\n";
    // запит до шару [dao] щодо збереження
    $this->dao->save($p);
  }

  public function get(int $id): Personne {
    print "[Métier] : récupération de la personne d'identifiant $id\n";
    // запит щодо особи до шару [dao]
    $personne = $this->dao->get($id);
    return $personne;
  }

}

Коментарі

  • рядок 5: шар [métier] повинен мати посилання на шар [dao], щоб мати змогу використовувати його методи;
  • рядки 8–10: метод [setDao] дозволяє надати шару [métier] посилання на шар [dao]. Зауважимо, що тип параметра — [IDao]. Таким чином, шар [métier] зможе працювати з будь-яким класом, що реалізує інтерфейс [IDao]. Якщо перейти від шару [Dao1] до шару [Dao2] і обидва вони реалізують інтерфейс [IDao], шар [métier] не потрібно переписувати;
  • рядки 17–20: реалізація [IMetier::doSomething];
  • рядки 23–27: реалізація [IMetier::save]. Цей метод повинен зберегти дані про особу в сховищі даних. Рівень [métier] не вміє цього робити. Він звертається до рівня [dao], щоб той здійснив це збереження;
  • рядки 29–34: реалізація методу [IMetier::get]. Цей метод повинен отримати зі сховища даних особу, ідентифікатор якої передано йому як параметр. Рівень [métier] не може цього зробити. Він звертається до рівня [dao], щоб той виконав цю роботу (рядок 32);

Висновок

Як тільки рівень [métier] потребує доступу до даних, що зберігаються у сховищі даних, він повинен звернутися до рівня [dao], який було створено саме для доступу до цих даних.

10.5. Головний скрипт

Ми напишемо два скрипти, які керуватимуть цією програмою. Перший, [main1.php], використовуватиме шар [Dao1], тоді як другий, [main2.php], використовуватиме шар [Dao2]. Ми хочемо показати, що це жодним чином не впливає на код шару [Métier].

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


<?php

// суворе дотримання параметрів функцій
declare (strict_types=1);

// включення класів та інтерфейсів
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao1.php";
require_once __DIR__."/Métier.php";

// тест ----------------
// створення шарів
$dao1 = new Dao1();
$métier = new Métier();
$métier->setDao($dao1);
// використання шару [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);

Коментарі

  • Нагадаємо кілька моментів: скрипт [main1.php] є «диригентом» додатка. Він створює шарову структуру додатка (рядки 15–17), а потім починає взаємодіяти з шаром [métier] (рядки 19–21). Структура, що складається з декількох рівнів, виглядає наступним чином:

Image

Згідно з цією схемою, скрипт [main1.php] повинен звертатися лише до шару [métier]. Він не повинен звертатися до шару [dao], навіть якщо це теоретично можливо.

Результати виконання такі:


[Métier] : récupération de la personne d'identifiant 4
[Dao1] : Récupération de la personne d'identité (4) en base de données [locale]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao1] : Sauvegarde de la personne [Personne(4)] en base de données [locale]

Коментарі

  • рядок 10 коду спричинив запис рядків 1 і 2 результатів;
  • рядок 20 коду спричинив запис рядка 3 результатів;
  • рядок 21 коду спричинив запис рядків 4 та 5 результатів;

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


<?php

// суворе дотримання параметрів функцій
declare (strict_types=1);

// включення класів та інтерфейсів
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao2.php";
require_once __DIR__."/Métier.php";

// тестування ----------------
// створення шарів
$dao2 = new Dao2();
$métier = new Métier();
$métier->setDao($dao2);
// використання шару [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);

Коментарі

  • рядки 36–38: шарова структура тепер використовує шар [Dao2];

Результати виконання такі:


[Métier] : récupération de la personne d'identifiant 4
[Dao2] : Récupération de la personne d'identité (4) en base de données [distante]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao2] : Sauvegarde de la personne [Personne(4)] en base de données [distante]

Шар [Dao1] імітує доступ до локальної бази даних, тоді як шар [Dao2] імітує доступ до віддаленої бази даних. Оскільки обидва ці шари відповідають інтерфейсу [IDao], бачимо, що код шару [Métier] не потрібно було змінювати.

Ми застосуємо те, що щойно вивчили, до вправи з розрахунку податків, яка слугує нам основною темою.