Skip to content

14. Багатошарова архітектура та програмування за допомогою інтерфейсів

14.1. Вступ

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

Image

  • шар [ui] (User Interface) — це шар, що взаємодіє з користувачем програми;
  • Рівень [métier] реалізує правила управління додатком, такі як розрахунок заробітної плати або рахунку-фактури. Цей рівень використовує дані, отримані від користувача через рівень [présentation], а також дані з SGBD через рівень [dao];
  • шар [dao] (об’єкти доступу до даних) керує доступом до даних з SGBD (система управління базами даних).

Саме цю архітектуру використовували в |курсі з Python 2|. Також можна запропонувати такий варіант:

Image

Відмінності від попередньої шарової структури такі:

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

Код тут організовано за напрямками компетенції з «диригентом»:

  • координатором є головний скрипт [main];
  • шари [ui], [dao] та [métier] є центрами компетенції;

Таку організацію можна назвати оркестровою.

14.2. Приклад 1

Ми проілюструємо багаторівневу архітектуру на прикладі простого консольного додатка:

  • бази даних не буде;
  • шар [dao] керуватиме сутностями Elève, Classe, Matière, Note, що дозволяють керувати оцінками учнів;
  • шар [métier] дозволить розраховувати показники за оцінками конкретного учня;
  • шар [ui] буде консольним додатком, що відображатиме результати учнів;

Проект PyCharm додатка виглядає наступним чином:

Примітка: папки, виділені синім кольором, входять до складу [Sources Root] проекту PyCharm.

14.2.1. Об’єкти програми

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

14.2.1.1. Ентітет [Classe]

Ентітет [Classe] (Classe.py) представляє клас коледжу:


# імпортує
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # атрибути, виключені зі стану класу
    excluded_keys = []

    # властивості класу
    @staticmethod
    def get_allowed_keys() -> list:
        # id: ідентифікатор класу
        # назва: назва класу
        return BaseEntity.get_allowed_keys() + ["nom"]

    # геттер
    @property
    def nom(self: object) -> str:
        return self.__nom

    # сетери
    @nom.setter
    def nom(self: object, nom: str):
        # ім'я має бути непорожнім рядком
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non vide")

Примітки

  • рядок 7: суть [Classe] походить від суті [BaseEntity], розглянутої в розділі |Клас BaseEntity|;
  • рядки 11–16: клас визначається номером id та nom (рядок 16). Властивість [id] надається класом [BaseEntity], а ім’я — класом [Classe];
  • рядки 18–30: методи getter/setter атрибута [nom];

14.2.1.2. Об’єкт [Matière]

Клас [Matière] (matière.py) має такий вигляд:


# імпорти
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # атрибути, виключені зі стану класу
    excluded_keys = []

    # властивості класу
    @staticmethod
    def get_allowed_keys() -> list:
        # id: ідентифікатор предмета
        # назва: назва предмета
        # коефіцієнт: коефіцієнт дисципліни
        return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]

    # геттер
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def coefficient(self: object) -> float:
        return self.__coefficient

    # сетери
    @nom.setter
    def nom(self: object, nom: str):
        # назва має бути непорожнім рядком
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non vide")

    @coefficient.setter
    def coefficient(self, coefficient: float):
        # коефіцієнт повинен бути дійсним числом >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # помилка?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

Примітки

  • рядок 7: клас [Classe] походить від класу [BaseEntity];
  • рядки 11–17: предмет визначається за його номером [id], назвою [nom] та коефіцієнтом [coefficient];
  • рядки 19–50: методи getter/setter атрибутів класу;

14.2.1.3. Об’єкт [Elève]

Клас [Elève] (élève.py) має такий вигляд:


# імпорти
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # атрибути, виключені зі стану класу
    excluded_keys = []

    # властивості класу
    @staticmethod
    def get_allowed_keys() -> list:
        # id: ідентифікатор учня
        # прізвище: прізвище учня
        # ім'я: ім'я учня
        # клас: клас учня
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    # гетери
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def prénom(self: object) -> str:
        return self.__prénom

    @property
    def classe(self: object) -> Classe:
        return self.__classe

    # сетери
    @nom.setter
    def nom(self: object, nom: str) -> str:
        # прізвище має бути непорожнім рядком
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @prénom.setter
    def prénom(self: object, prénom: str) -> str:
        # ім'я має бути непорожнім рядком
        if Utils.is_string_ok(prénom):
            self.__prénom = prénom
        else:
            raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @classe.setter
    def classe(self: object, value):
        try:
            # очікується тип «Клас»
            if isinstance(value, Classe):
                self.__classe = value
            # або тип dict
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # або типу json
            elif isinstance(value,str):
                self.__classe = Classe().fromjson(value)
        except BaseException as erreur:
            raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou dict ou json. Erreur : {erreur}")

Примітки

  • рядок 9: клас [Elève] походить від класу [BaseEntity];
  • рядки 13–20: учень характеризується своїм номером [id], прізвищем [nom], ім’ям [prénom] та класом [classe]. Останній параметр є посиланням на об’єкт [Classe];
  • рядки 22–65: методи getter/setter атрибутів класу;

14.2.1.4. Об’єкт [Note]

Клас [Note] (note.py) має такий вигляд:


# імпорти
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    # атрибути, виключені зі стану класу
    excluded_keys = []

    # властивості класу
    @staticmethod
    def get_allowed_keys() -> list:
        # id: ідентифікатор оцінки
        # значення: сама оцінка
        # учень: учень (типу «Учень»), якого стосується оцінка
        # предмет: предмет (типу «Предмет»), до якого відноситься оцінка
        # отже, об’єкт «Оцінка» — це оцінка учня з певного предмета
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    # гетери
    @property
    def valeur(self: object) -> float:
        return self.__valeur

    @property
    def élève(self: object) -> Elève:
        return self.__élève

    @property
    def matière(self: object) -> Matière:
        return self.__matière

    # геттери
    @valeur.setter
    def valeur(self: object, valeur: float):
        # оцінка має бути дійсним числом у діапазоні від 0 до 20
        if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
            self.__valeur = valeur
        else:
            raise MyException(31,
                              f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")

    @élève.setter
    def élève(self: object, value):
        try:
            # очікується тип «Учень»
            if isinstance(value, Elève):
                self.__élève = value
            # або тип dict
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # або типу json
            elif isinstance(value, str):
                self.__élève = Elève().fromjson(value)
        except BaseException as erreur:
            raise MyException(32,
                              f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict ou json. Erreur : {erreur}")

    @matière.setter
    def matière(self: object, value):
        try:
            # очікується тип «Предмет»
            if isinstance(value, Matière):
                self.__matière = value
            # або тип dict
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # або типу json
            elif isinstance(value, str):
                self.__matière = Matière().fromjson(value)
        except BaseException as erreur:
            raise MyException(33,
                              f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict ou json. Erreur : {erreur}")

Примітки

  • рядок 8: клас [Note] походить від класу [BaseEntity];
  • рядки 12–20: об’єкт [Note] характеризується своїм номером [id], значенням оцінки [valeur], посиланням [élève] на учня, який отримав цю оцінку, та посиланням на предмет [matière], що є предметом оцінки;
  • рядки 22–75: методи getter/setter атрибутів класу;

14.2.2. Налаштування додатка

Файл [config.py] налаштовує середовище основного скрипта [main] (1), а також середовище тестів (2). Усі ці скрипти містять інструкцію [import config] на початку коду. Нагадаємо, що папка, яка містить скрипт, що є об’єктом команди [python script], автоматично стає частиною Python-папки Path.Si, отже, оскільки [config] знаходиться в тій самій папці, що й скрипти з інструкцією [import config], його буде знайдено. Файли [1] та [2] тут є ідентичними. Однак це не завжди так.

Файл [config.sys] має такий вигляд:


def configure():
    import os

    # абсолютний шлях до папки, в якій знаходиться цей скрипт
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # root_dir
    root_dir="C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
    # абсолютні залежності
    absolute_dependencies=[
        # локальні папки, що містять класи та інтерфейси
        f"{root_dir}/02/entities",
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
    ]

    # оновлення syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # виконується конфігурація
    return {}
  • рядки 11–14: папки, які мають входити до Python Path (sys.path);
  • папка [f"{root_dir}/02/entities"] надає доступ до класів [BaseEntity] та [MyException];
  • папка [f"{script_dir}/../entities"] надає доступ до класів [Elève], [Classe], [Matière], [Note];
  • папка [f"{script_dir}/../interfaces",] надає доступ до інтерфейсів додатка;
  • папка [f"{script_dir}/../services"] надає доступ до класів, що реалізують інтерфейси;

14.2.3. Тестування сутностей

Тут ми будемо писати тести, які виконуються інструментом під назвою [unittest]. PyCharm постачається з кількома тестовими фреймворками. Вибір одного з них здійснюється у налаштуваннях PyCharm:

Image

  • У [4] доступні кілька фреймворків для тестування:

Image

14.2.3.1. Клас тестів [TestBaseEntity]

Скрипт тесту [TestBaseEntity] матиме такий вигляд:


import unittest

# налаштовуємо додаток
import config

config = config.configure()


class TestBaseEntity (unittest.TestCase):

    def test_note1(self):
        # імпорт
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        # створення оцінки на основі рядка jSON
        note = Note().fromjson(
            '{"id": 8, "значення": 12, "учень": {"id": 42, "прізвище": "прізвище4", "ім'я": "ім'я4", "клас": {"id": 2, "назва": "клас2"}}, "предмет": {"id": 2, "назва": "предмет2", "коефіцієнт": 2}}')
        # перевірки
        self.assertIsInstance(note, Note)
        self.assertIsInstance(note.élève, Elève)
        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)


    def test_note2(self):
        # імпорт
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        # формування оцінки на основі словника
        note = Note().fromdict(
            {"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4",
                                              "classe": {"id": 2, "nom": "classe2"}},
             "matière": {"id": 2, "nom": "matière2", "coefficient": 2}})
        # перевірки
        self.assertIsInstance(note, Note)
        self.assertIsInstance(note.élève, Elève)
        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)

if __name__ == '__main__':
    unittest.main()

Примітки

  • рядок 1: імпортується модуль [unittest], який надасть різні методи тестування;
  • рядки 3–6: налаштовуємо додаток так, щоб він міг знайти класи, необхідні для тестування;
  • рядок 9: тестовий клас [unittest] повинен успадковувати клас [unittest.TestCase];
  • рядки 11, 27: імена тестових функцій повинні починатися з [test], інакше вони не будуть розпізнані;
  • рядки 13–16: імпортуються необхідні класи;
  • у цьому тестовому класі ми хочемо перевірити поведінку методів [BaseEntity.fromdict] (рядок 34) та [BaseEntity.fromjson] (рядок 18). Клас [Note] має властивості, які є посиланнями на інші класи. Ми хочемо перевірити, чи обидва попередні методи створюють коректні об’єкти [Note];
  • рядок 18: створюємо об’єкт [Note] на основі об’єкта jSON;
  • рядок 21: перевіряємо, чи створений об’єкт дійсно має тип [Note]. Метод [assertIsInstance] є методом класу [unittest.TestCase], батьківського класу класу [TestBaseEntity];
  • рядок 22: перевіряється, чи [note.élève] дійсно має тип [Elève];
  • рядок 23: перевіряється, чи [note.élève.classe] дійсно належить до типу [Classe];
  • рядок 24: перевіряємо, чи [note.matière] дійсно належить до типу [Matière];
  • рядки 33–42: те саме робимо з методом [BaseEntity.fromdict];

Існує кілька способів виконання тестів:

  • у [1-2] виконується [TestBaseEntity] із використанням фреймворку [UnitTest];
  • у [3-5] тести завершуються з помилкою. [UnitTests] повідомляє, що не знайдено жодного тесту для виконання;

Невдача тестів пов’язана з організацією коду [TestBaseEntity]:


import unittest

# налаштування програми
import config

config = config.configure()


class TestBaseEntity(unittest.TestCase):

Що заважає фреймворку [UnitTest], так це наявність виконуваного коду (рядки 3–6) перед визначенням тестового класу (рядок 9).

Тож переорганізуємо код наступним чином:


import unittest


class TestBaseEntity(unittest.TestCase):

    def setUp(self):
        # налаштування програми
        import config

        config.configure()

    def test_note1(self):
        

    def test_note2(self):
        


if __name__ == '__main__':
    unittest.main()
  • рядки 6–10: визначається функція [setUp]. Ця функція виконує особливу роль: вона виконується перед кожною тестовою функцією (test_note1, test_note2);

Після цього виконання класу [TestBaseEntity] дає такі результати:

Цього разу обидва методи тестування були виконані, і тести пройшли успішно.

Давайте подивимося, що відбувається, коли тест завершується невдало. Змінимо код класу [test_note1] наступним чином:


    def test_note1(self):
        # навмисна помилка — перевіряємо, чи 1==2
        self.assertEqual(1,2)
        # імпорт
        from Note import Note

  • рядок 2: перевіряється, чи 1==2;

Результати виконання будуть такими:

Дізнатися причину помилки можна, натиснувши на тест, що завершився невдало, [2]:

  • у [7-8] вказано причину помилки;

Інший спосіб запустити набір тестів — виконати його в терміналі:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python -m unittest TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.026s

OK

У рядку 6 вказано, що обидва тести пройшли успішно (помилка 1==2 була усунена);

Нарешті, третій спосіб виконання класу тестів [TestBaseEntity], також у терміналі, є таким. Клас тестів завершується наступними рядками 6–7;



        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)


if __name__ == '__main__':
    unittest.main()
  • рядок 6: змінна [__name__] — це ім’я скрипта, що виконується. Коли скрипт запускається командою [python script.py], значення змінної [__name__] дорівнює [__main__] (2 символи з підкресленням перед та після ідентифікатора). Отже, рядок 7 виконується лише тоді, коли скрипт [TestBaseEntity] запускається командою [python TestBaseEntity.py]. Інструкція [unittest.main()] запускає виконання скрипта за допомогою фреймворку [UnitTest]. Ось приклад:

(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.013s

OK

14.2.3.2. Клас тестів [TestEntités]

Клас тестів [TestEntités] має такий вигляд:


import unittest


class TestEntités(unittest.TestCase):
    def setUp(self):
        # налагодження програми
        import config

        config.configure()

    def test_code1a(self):
        # імпорт
        from Elève import Elève
        from MyException import MyException
        # код помилки
        code = None
        try:
            # недійсний ідентифікатор
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 1)

    def test_code41(self):
        # імпорт
        from Elève import Elève
        from MyException import MyException
        # код помилки
        code = None

        try:
            # неправильне ім'я
            Elève().fromdict({"id": 1, "nom": "", "prénom": "z", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 41)

    def test_code42(self):
        # імпорт
        from Elève import Elève
        from MyException import MyException
        # код помилки
        code = None
        try:
            # неправильне ім'я
            Elève().fromdict({"id": 1, "nom": "y", "prénom": "", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 42)

    def test_code43(self):
        # імпорт
        from Elève import Elève
        from MyException import MyException
        # код помилки
        code = None
        try:
            # неправильний клас
            Elève().fromdict({"id": 1, "nom": "y", "prénom": "z", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 43)

    def test_code1b(self):
        # імпорт
        from Classe import Classe
        from MyException import MyException
        # код помилки
        code = None
        try:
            # неправильний ідентифікатор
            Classe().fromdict({"id": "x", "nom": "y"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 1)

    def test_code11(self):
        # імпорт
        from Classe import Classe
        from MyException import MyException

        # код помилки
        code = None
        try:
            # неправильне ім'я
            Classe().fromdict({"id": 1, "nom": ""})
        except MyException as ex:
            code = ex.code
        # перевірка
        self.assertEqual(code, 11)

    def test_code1c(self):
        # імпорт
        from Matière import Matière
        from MyException import MyException

        # код помилки
        code = None
        try:
            # неправильний ідентифікатор
            Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 1)

    def test_code21(self):
        # імпорт
        from Matière import Matière
        from MyException import MyException
        # код помилки
        code = None
        try:
            # неправильне ім'я
            Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 21)

    def test_code22(self):
        # імпорт
        from Matière import Matière
        from MyException import MyException
        # код помилки
        code = None
        try:
            # неправильний коефіцієнт
            Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 22)

    def test_code1d(self):
        # імпорт
        from Note import Note
        from MyException import MyException
        # код помилки
        code = None
        try:
            # неправильний ідентифікатор
            Note().fromdict({"id": "x", "valeur": "x", "élève": "y", "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 1)

    def test_code31(self):
        # імпорт
        from Note import Note
        from MyException import MyException

        # код помилки
        code = None
        try:
            # неправильне значення
            Note().fromdict({"id": 1, "valeur": "x", "élève": "y", "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 31)

    def test_code32(self):
        # імпорт
        from Note import Note
        from MyException import MyException

        # код помилки
        code = None
        try:
            # неправильний учень
            Note().fromdict({"id": 1, "valeur": 10, "élève": "y", "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 32)

    def test_code33(self):
        # імпорт
        from Elève import Elève
        from Note import Note
        from Classe import Classe
        from MyException import MyException

        # код помилки
        code = None
        try:
            # неправильний предмет
            classe = Classe().fromdict({"id": 1, "nom": "x"})
            élève = Elève().fromdict({"id": 1, "nom": "a", "prénom": "b", "classe": classe})
            Note().fromdict({"id": 1, "valeur": 10, "élève": élève, "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # перевірка
        self.assertEqual(code, 33)

    def test_exception(self):
        # імпорт
        from Elève import Elève
        # для успішного виконання тесту потрібно запустити тип [MyException]
        from MyException import MyException
        with self.assertRaises(MyException):
            # тест
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})


if __name__ == '__main__':
    unittest.main()
  • мета тестового скрипта — перевірити методи-сеттери класів: переконатися, що атрибутам різних об’єктів неможливо присвоїти некоректні значення;
  • рядки 11–24: перевіряється, чи неможливо передати недійсний ідентифікатор учня. Оскільки в рядку 16 як ідентифікатор учня передається значення «x», очікується виникнення винятку. Отже, виконання має перейти до рядків 20–22;
  • рядок 21: виведення повідомлення про помилку;
  • рядок 22: отримуємо код помилки (див. параграф |Об’єкт MyException|);
  • рядок 24: перевіряємо (assert), що код помилки дорівнює 1. Тут перевіряємо дві речі:
    • що помилка дійсно сталася;
    • що код помилки дорівнює 1;
  • цей процес повторюється з функціями у рядках 24–213;
  • рядки 215–222: перевіряється, чи дія викликає виняток певного типу;
  • рядок 220: вказується, що тест пройшов успішно, якщо він викликає виняток типу [MyException];

Результати

Виконуємо тестовий скрипт:

Отримано такі результати:


Testing started at 09:39 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests


code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Elève.Elève'> doit être un entier >=0]

code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Classe.Classe'> doit être un entier >=0]

code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Matière.Matière'> doit être un entier >=0]

code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Note.Note'> doit être un entier >=0]

code erreur=21, message=MyException[21, Le nom de la matière 1 doit être une chaîne de caractères non vide]

code erreur=22, message=MyException[22, Le coefficient de la matière y doit être un réel >=0]

code erreur=31, message=MyException[31, L'attribut x de la note 1 doit être un nombre dans l'intervalle [0,20]]

code erreur=32, message=MyException[32, L'attribut [y] de la note 1 doit être de type Elève ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]

code erreur=33, message=MyException[33, L'attribut [z] de la note 1 doit être de type Matière ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]

code erreur=41, message=MyException[41, Le nom de l'élève 1 doit être une chaîne de caractères non vide]

code erreur=42, message=MyException[42, Le prénom de l'élève 1 doit être une chaîne de caractères non vide]

code erreur=43, message=MyException[43, L'attribut [t] de l'élève 1 doit être de type Classe ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]


Ran 14 tests in 0.040s

OK

Process finished with exit code 0

У цьому випадку всі тести пройшли успішно

14.2.4. Рівень [dao]

Image

Рівень [dao] реалізує інтерфейс [InterfaceDao] [1]. Цей інтерфейс реалізовано класом [Dao] (2). Скрипт [tests_dao] (3) перевіряє методи шару [dao].

14.2.4.1. Інтерфейс [InterfaceDao]

Інтерфейс — це угода між кодом, що викликає, та кодом, що викликається. Саме код, що викликається, надає інтерфейс:

  • Викликаючий код [1] не знає реалізації викликаного коду [3]. Він знає лише, як його викликати. Про це йому повідомляє інтерфейс [2]. Цей інтерфейс визначає певну кількість методів/функцій, які слід використовувати для взаємодії з викликаним кодом. Цей інтерфейс також називають API (Application Programming Interface);

Рівень [dao] надаватиме такий інтерфейс:

  • [get_classes] повертає список класів середньої школи;
  • [get_matières] повертає список предметів, що викладаються в гімназії;
  • [get_élèves] видає список учнів середньої школи;
  • [get_notes] видає список оцінок усіх учнів;
  • [get_notes_for_élève_by_id] виводить оцінки конкретного учня;
  • [get_élève_by_id] повертає учня за його номером;

Код, що викликає ці методи, використовуватиме лише їх. Йому не потрібно знати, як вони реалізовані. Дані можуть надходити з різних джерел (вбудовані, з бази даних, з текстових файлів тощо) без впливу на код, що їх викликає. Це називається програмуванням за допомогою інтерфейсів.

У Python 3 є поняття, близьке до поняття інтерфейсу: абстрактний клас. Ми будемо ним користуватися. Ми об’єднаємо інтерфейси цього прикладу в папці [interfaces].

Ми визначаємо абстрактний клас [InterfaceDao] (InterfaceDao.py) для рівня [dao]:


# імпорт
from abc import ABC, abstractmethod

# інтерфейс Dao
from Elève import Elève


class InterfaceDao(ABC):
    # список класів
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # список учнів
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # список предметів
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    # список оцінок
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    # список оцінок учня
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # пошук учня за його ідентифікатором
    @abstractmethod
    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

Примітки:

  • рядок 2: ABC = абстрактний базовий клас. З модуля [abc] імпортується клас ABC, а також декоратор [abstractmethod], який використовується у рядках 10, 15, 20, 25, 30 та 35;
  • рядок 8: абстрактний клас називається [InterfaceDao] і походить від класу [ABC];
  • методи абстрактного класу декоровані декоратором [@abstractmethod], який перетворює декорований таким чином метод на абстрактний: його код не визначено. Проте туди вставляється код: інструкція [pass], яка нічого не робить;
  • абстрактний клас [InterfaceDao] не може бути інстанційований. Інстанціювати можна лише класи, похідні від [InterfaceDao], які реалізували всі методи класу [InterfaceDao]. Отже, якщо створити два класи [Dao1] та [Dao2], що походять від класу [InterfaceDao], то обидва вони реалізують абстрактні методи класу [InterfaceDao]. Таким чином, можна сказати, що вони реалізують інтерфейс [InterfaceDao];
  • мови програмування, що реалізують як інтерфейси, так і абстрактні класи, надають інтерфейсу іншу роль, ніж абстрактному класу. Інтерфейс не має атрибутів і не може бути інстанційований. Клас може реалізувати інтерфейс, визначивши всі його методи;

14.2.4.2. Реалізація [Dao]

Клас [Dao] (dao.py) реалізує інтерфейс [InterfaceDao] наступним чином:


# імпорт сутностей та інтерфейсів
from Classe import Classe
from Elève import Elève
from InterfaceDao import InterfaceDao
from Matière import Matière
from MyException import MyException
from Note import Note


# шар [dao] реалізує інтерфейс InterfaceDao
class Dao(InterfaceDao):
    # конструктор
    # створюються жорстко задані списки
    def __init__(self):
        # створюються екземпляри класів
        classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
        classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
        self.classes = [classe1, classe2]
        # предмети
        matière1 = Matière().fromdict({"id": 1, "nom": "matière1", "coefficient": 1})
        matière2 = Matière().fromdict({"id": 2, "nom": "matière2", "coefficient": 2})
        self.matières = [matière1, matière2]
        # учні
        élève11 = Elève().fromdict({"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": classe1})
        élève21 = Elève().fromdict({"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": classe1})
        élève32 = Elève().fromdict({"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": classe2})
        élève42 = Elève().fromdict({"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": classe2})
        self.élèves = [élève11, élève21, élève32, élève42]
        # оцінки учнів з різних предметів
        note1 = Note().fromdict({"id": 1, "valeur": 10, "élève": élève11, "matière": matière1})
        note2 = Note().fromdict({"id": 2, "valeur": 12, "élève": élève21, "matière": matière1})
        note3 = Note().fromdict({"id": 3, "valeur": 14, "élève": élève32, "matière": matière1})
        note4 = Note().fromdict({"id": 4, "valeur": 16, "élève": élève42, "matière": matière1})
        note5 = Note().fromdict({"id": 5, "valeur": 6, "élève": élève11, "matière": matière2})
        note6 = Note().fromdict({"id": 6, "valeur": 8, "élève": élève21, "matière": matière2})
        note7 = Note().fromdict({"id": 7, "valeur": 10, "élève": élève32, "matière": matière2})
        note8 = Note().fromdict({"id": 8, "valeur": 12, "élève": élève42, "matière": matière2})
        self.notes = [note1, note2, note3, note4, note5, note6, note7, note8]

    # -----------
    # інтерфейс IDao
    # -----------
    

Примітки:

  • рядки 1–7: імпортуються сутності та інтерфейс [InterfaceDao];
  • рядок 11: клас [Dao] походить від абстрактного класу [InterfaceDao]. Скажемо, що він реалізує інтерфейс [InterfaceDao];
  • рядок 14: конструктор не має параметрів. Він жорстко створює чотири списки:
    • рядки 15–18: список класів;
    • рядки 19–22: список предметів;
    • рядки 23–28: список учнів;
    • рядки 29–38: список оцінок;
  • рядки 40–44: реалізація методів інтерфейсу [Interface Dao]. Тут ми їх не визначаємо, щоб побачити повідомлення про помилку, яке видає Python;

Приклад тестової програми може виглядати так: [tests-dao.py]:


# налаштування програми
import config

config = config.configure()

# створення екземпляра шару [dao]
from Dao import Dao

daoImpl = Dao()

# список класів
for classe in daoImpl.get_classes():
    print(classe)

# список предметів
for matière in daoImpl.get_matières():
    print(matière)

# список класів
for élève in daoImpl.get_élèves():
    print(élève)

# список оцінок
for note in daoImpl.get_notes():
    print(note)

Примітка: скрипт [tests-dao.py] не є тестом [unittest], оскільки він не містить методів, імена яких починаються з [test_].

Коментарі не потребують додаткових пояснень. У рядках 11–25 використовується інтерфейс шару [dao]. Тут немає жодних припущень щодо фактичної реалізації шару. У рядку 9 створюється екземпляр шару [dao].

Результати виконання цього скрипта такі:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py", line 9, in <module>
    daoImpl = Dao()
TypeError: Can't instantiate abstract class Dao with abstract methods get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves

Process finished with exit code 1

Бачимо, що помилка виникає одразу після створення екземпляра класу [Dao] (рядок 3 вище). Інтерпретатор Python 3 повідомляє, що не може створити екземпляр класу, оскільки ми не визначили абстрактні методи [get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves].

Pycharm також підтримує поняття абстрактного класу і пропонує нам визначити його методи:

  • у [1] — клацніть правою кнопкою миші на коді;
  • у [2-3] виберіть [Generate / Implement Methods], щоб реалізувати відсутні методи класу [Dao];
  • у [4] виберіть методи, які потрібно реалізувати, у даному випадку всі;

Після цього клас [Dao] доповнюється класом PyCharm наступним чином:


    # -----------
    # інтерфейс IDao
    # -----------

    def get_classes(self: object) -> list:
        pass

    def get_élèves(self: object) -> list:
        pass

    def get_matières(self: object) -> list:
        pass

    def get_notes(self: object) -> list:
        pass

    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

Ми доповнюємо клас [Dao] наступним чином:


    # -----------
    # інтерфейс IDao
    # -----------
    
    # список класів
    def get_classes(self) -> list:
        return self.classes

    # список предметів
    def get_matières(self) -> list:
        return self.matières

    # список учнів
    def get_élèves(self) -> list:
        return self.élèves

    # список оцінок
    def get_notes(self) -> list:
        return self.notes

    def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
        # пошук учня
        élève = self.get_élève_by_id(élève_id)
        # отримання його оцінок
        notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
        # повернення результату
        return {"élève": élève, "notes": notes}

    def get_élève_by_id(self, élève_id: int) -> Elève:
        # фільтрування учнів
        élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
        # знайдено?
        if not élèves:
            raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
        # результат
        return élèves[0]
  • рядки 5–19 не викликають труднощів;
  • рядки 29–36: метод, який повертає учня, номер якого передається. Якщо такого учня не існує, генерується виняток;
  • рядок 31: функція [filter] дозволяє відфільтрувати список:
    • перший параметр — критерій фільтрації;
    • другий параметр — це список, який потрібно відфільтрувати, у даному випадку список учнів;
  • рядок 31: критерій фільтрації списку реалізовано за допомогою функції [f(e :Elève)->bool]. Ця функція застосовується до кожного елемента списку, що фільтрується. Якщо елемент відповідає критерію фільтрації, він залишається у відфільтрованому списку, інакше — виключається з нього. Тут можна або:
    • задати ім’я функції f і реалізувати її в іншому місці. Тоді виклик функції [filter] перетворюється на [filter(f,self.get_élèves()];
    • надати визначення функції f. Виклик функції [filter] тоді перетворюється на [filter(f(e :Elève){…},self.get_élèves()], де [e] представляє елемент відфільтрованого списку, тобто учня. Саме це й було зроблено в даному випадку. Визначення функції f у цьому випадку буде таким: [f(e :Elève){return e.id==élève_id)] — учень відбирається лише тоді, коли номер [id] не дорівнює шуканому. Таку функцію можна замінити так званою лямбда-функцією: [lambda e: e.id == élève_id]:
      • e: позначає параметр функції f, у даному випадку — учня. Можна використовувати будь-яке ім’я на власний вибір;
      • e.id==élève_id — це критерій фільтрації: учень [e] відбирається лише в тому випадку, якщо його номер [id] відповідає шуканому;
  • рядок 31: функція [filter] повертає відфільтрований список у типі, який не є типом [list], але який можна перетворити на тип [list]. Саме це ми робимо тут за допомогою виразу [list(liste filtrée)];
  • рядки 33–34: якщо відфільтрований список порожній, це означає, що шуканого учня не існує. У такому разі генерується виняток;
  • рядок 36: якщо ми дійшли до цього місця, значить, винятку не сталося. Тоді ми знаємо, що отримали список з 1 елементом (немає двох учнів з однаковим номером [id]). Тому повертаємо перший елемент списку;
  • рядки 21–27: метод [get_notes_for_élève_by_id] повинен повернути оцінки учня, номер якого передано йому як [id];
  • рядки 22–23: спочатку виконується пошук учня з номером [élève_id] за допомогою методу [get_élève_by_id], який щойно було закомментований. Якщо шуканий учень не існує, може виникнути виняток. Оскільки навколо оператора в рядку 23 немає блоку try/catch, виняток буде передано до коду, що викликає. Це саме те, що потрібно;
  • рядки 24–25: після того, як учень знайдений, отримуємо всі його оцінки. Це робимо знову за допомогою фільтра:
    • фільтр має ім’я [filter(critère, self_getnotes()]. Отже, список, що підлягає фільтруванню, — це список усіх оцінок усіх учнів коледжу;
    • критерій фільтрації виражається за допомогою функції [lambda]: lambda n: n.élève.id == élève_id. Параметр n є елементом списку, що фільтрується, тобто оцінкою. Тип [Note] має властивість [élève], яка позначає учня, якому належить оцінка. Отже, [n.élève.id], що позначає номер цього учня, має дорівнювати номеру шуканого учня;

Потім ми виконуємо скрипт [tests-dao.py].


# налаштування програми
import config

config = config.configure()

# створення екземпляра шару [dao]
from Dao import Dao

daoImpl = Dao()

# список класів
for classe in daoImpl.get_classes():
    print(classe)

# перелік предметів
for matière in daoImpl.get_matières():
    print(matière)

# список класів
for élève in daoImpl.get_élèves():
    print(élève)

# список оцінок
for note in daoImpl.get_notes():
    print(note)

# конкретний учень
print(daoImpl.get_élève_by_id(11))

# список його оцінок
dict1 = daoImpl.get_notes_for_élève_by_id(11)
print(f"élève n° 11 = {dict1['élève']}")
for note in dict1["notes"]:
    print(f"note de l'élève n° 11 = {note}")

У результаті отримуємо такі результати:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
{"id": 1, "nom": "classe1"}
{"id": 2, "nom": "classe2"}
{"id": 1, "nom": "matière1", "coefficient": 1}
{"id": 2, "nom": "matière2", "coefficient": 2}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
{"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}
{"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}
{"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}
{"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 2, "valeur": 12, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 3, "valeur": 14, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 4, "valeur": 16, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 6, "valeur": 8, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 7, "valeur": 10, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
élève n° 11 = {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
note de l'élève n° 11 = {"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
note de l'élève n° 11 = {"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}

Process finished with exit code 0

Можна помітити, що під час відображення оцінки (для інших об’єктів ситуація аналогічна) також відображається:

  • ім’я учня, якому належить запис;
  • предмет, до якого відноситься оцінка;

Цей результат генерує функція [BaseEntity.asdict] (див. параграф «посилання»).

14.2.5. Шар [métier]

  • [InterfaceMétier] є інтерфейсом шару [métier];
  • [Métier] — це клас реалізації шару [métier];
  • [Testmétier] — це тестовий клас [UnitTest] для класу [Métier];

14.2.5.1. Інтерфейс [InterfaceMétier]

Рівень [métier] реалізує наступний інтерфейс [InterfaceMétier] (InterfaceMétier.py):


# імпортні дані
from abc import ABC, abstractmethod

from StatsForElève import StatsForElève


# бізнес-інтерфейс
class InterfaceMétier(ABC):
    # розрахунок статистичних даних для учня
    @abstractmethod
    def get_stats_for_élève(self, idElève: int) -> StatsForElève:
        pass
  • [get_stats_for_élève] повертає оцінки учня № idElève, а також інформацію про них: зважений середній бал, найнижчу оцінку, найвищу оцінку. Ця інформація інкапсульована в об’єкт типу [StatsForElève];

14.2.5.2. Суть [StatsForElève]

Тип [StatsForElève] (StatsForElève.py), що інкапсулює статистичні дані (оцінки, мінімальне, максимальне значення, зважений середній бал) про учня, має такий вигляд:


# імпорт
from BaseEntity import BaseEntity


# статистика конкретного учня


class StatsForElève(BaseEntity):
    # атрибути, виключені зі звіту про клас
    excluded_keys = []

    # властивості класу
    @staticmethod
    def get_allowed_keys() -> list:
        # id: ідентифікатор оцінки
        # учень: відповідний учень
        # оцінки: його оцінки
        # moyennePondérée: його середній бал, зважений за коефіцієнтами предметів
        # min: його найнижча оцінка
        # max: його максимальна оцінка

        return BaseEntity.get_allowed_keys() + ["élève", "notes", "moyenne_pondérée", "min", "max"]

    # toString
    def __str__(self) -> str:
        # випадок, коли учень не має оцінок
        if len(self.notes) == 0:
            return f"Elève={self.élève}, notes=[]"
        # випадок учня з оцінками
        str = ""
        for note in self.notes:
            str += f"{note.valeur} "
        return f"Elève={self.élève}, notes=[{str.strip()}], max={self.max}, min={self.min}, " \
               f"moyenne pondérée={self.moyenne_pondérée:4.2f}"

Примітки:

  • рядок 8: клас [StatsForElève] походить від класу [BaseEntity];
  • рядки 13–22: властивості класу;
    • ідентифікатор [id], що походить від [BaseEntity];
    • учень [élève], статистичні дані якого інкапсульовано;
    • його оцінки [notes];
    • його зважений середній бал [moyenne_pondérée];
    • його мінімальна оцінка [min];
    • максимальне значення — [max];
  • для цих атрибутів не визначаються методи getter/setter. Виходимо з того, що саме рівень [métier] створює об’єкти цього типу і що він не створює недійсних об’єктів;
  • рядки 23–33: функція [__str__] повертає рядок, що містить властивості об’єкта;

14.2.5.3. Реалізація [Métier]

Реалізація [Métier] (Metier.py) інтерфейсу [InterfaceMétier] буде такою:


# імпортні дані
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from StatsForElève import StatsForElève


class Métier(InterfaceMétier):

    # конструктор
    def __init__(self, dao: InterfaceDao):
        # зберігаємо параметр
        self.__dao = dao

    # -----------
    # інтерфейс
    # -----------

    # показники щодо оцінок конкретного учня
    def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
        # Статистика для учня № idEleve
        # id_élève: номер учня

        # оцінки цього учня можна отримати за допомогою шару [dao]
        notes_élève = self.__dao.get_notes_for_élève_by_id(id_élève)
        élève = notes_élève["élève"]
        notes = notes_élève["notes"]

        # процес зупиняється, якщо оцінок немає
        if len(notes) == 0:
            # повертаємо результат
            return StatsForElève().fromdict({"élève": élève, "notes": []})

        # обробка оцінок учня
        somme_pondérée = 0
        somme_coeff = 0
        max = -1
        min = 21
        for note in notes:
            # значення оцінки
            valeur = note.valeur
            # коефіцієнт предмета
            coeff = note.matière.coefficient
            # сума коефіцієнтів
            somme_coeff += coeff
            # зважена сума
            somme_pondérée += valeur * coeff
            # пошук мінімального значення
            if valeur < min:
                min = valeur
            # пошук максимального значення
            if valeur > max:
                max = valeur
        # обчислення відсутніх показників
        moyenne_pondérée = float(somme_pondérée) / somme_coeff

        # результат видається у вигляді типу [StatsForElève]
        return StatsForElève(). \
            fromdict({"élève": élève, "notes": notes,
                      "moyenne_pondérée": moyenne_pondérée,
                      "min": min, "max": max})

Примітки

  • рядок 7: клас [Métier] походить від класу [InterfaceMétier]. Зазвичай кажуть, що він реалізує інтерфейс [InterfaceMétier];
  • рядки 9–12: конструктор отримує як єдиний параметр посилання на шар [dao]. У рядку 10 слід зауважити, що параметру [dao] присвоєно тип [InterfaceDao]. Ми не очікуємо конкретної реалізації, а лише реалізації, що відповідає інтерфейсу [InterfaceDao]. У даному випадку це не має значення, оскільки Python не враховуватиме цей тип, але це хороша практика — працювати з інтерфейсами, а не з конкретними реалізаціями. Тоді код легше модифікувати;
  • рядки 19–60: реалізація методу [get_stats_for_élève];
  • рядок 19: метод отримує єдиний параметр — номер [idElève] учня, статистику якого потрібно отримати;
  • рядок 24: до шару [dao] надсилається запит на отримання оцінок учня. Цей запит призводить до винятку, якщо учень не існує. Цей виняток не обробляється (відсутність try / catch) і тому передається до коду, що викликає;
  • рядок 25: сюди потрапляємо, якщо винятку не сталося. [notes_élève] є тоді словником із двома ключами [élève, note]:
    • рядок 25: отримуємо інформацію про учня (його ім’я, клас тощо);
    • рядок 26: отримуємо його оцінки;
  • рядки 28–31: перевіряємо, чи є в учня оцінки. Якщо їх немає, то обчислювати статистику немає чого;
  • рядок 31: повертається об’єкт [StatsForElève], побудований на основі словника за допомогою методу [BaseEntity.fromdict];
  • рядки 33–54: використовуються оцінки учня для обчислення необхідних статистичних даних. Коментарів у коді має вистачити для його розуміння;
  • рядки 56–60: повертається об’єкт [StatsForElève], побудований на основі словника за допомогою методу [BaseEntity.fromdict];

14.2.5.4. Тестування шару [métier]

Скрипт [UnitTest] шару [métier] може виглядати наступним чином (TestMétier.py):


# імпорт
import unittest


class Testmétier(unittest.TestCase):
    def setUp(self):
        # конфігурація додатка
        import config
        config.configure()

    def test_statsForEleve11(self):
        # імпортуємо
        from Dao import Dao
        from Métier import Métier
        # тестування показників учня 11
        dao = Dao()
        stats_for_élève = Métier(dao).get_stats_for_élève(11)
        # відображення
        print(f"\nstats={stats_for_élève}")
        # перевірки
        self.assertEqual(stats_for_élève.min, 6)
        self.assertEqual(stats_for_élève.max, 10)
        self.assertAlmostEqual(stats_for_élève.moyenne_pondérée, 7.333, delta=1e-3)


if __name__ == '__main__':
    unittest.main()

Примітки

  • рядки 6–9: функція [setUp] використовується тут для налаштування Python Path тесту;
  • рядок 16: створюється екземпляр шару [dao];
  • рядок 17: створюється екземпляр шару [métier] і використовується його метод [get_stats_for_élève] для обчислення статистичних даних учня № 11;
  • рядок 19: виводиться отриманий результат [StatsForElève]. Оскільки [StatsForElève] походить від [BaseEntity], тут виводиться ланцюжок jSON від [StatsForElève];
  • рядок 21: перевіряється мінімальна оцінка учня;
  • рядок 22: перевіряється його максимальна оцінка;
  • рядок 23: перевіряється, чи зважний середній бал дорівнює 7,333 з точністю до 10⁻³. Як правило, неможливо точно порівнювати дійсні числа, оскільки внутрішньо вони найчастіше мають лише наближене представлення;

Результати перевірки такі:


Testing started at 18:17 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests



Ran 1 test in 0.015s

OK

stats=Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33

Process finished with exit code 0

14.2.6. Шар [ui]

Image

  • у [1] — інтерфейс шару [ui];
  • у [2] — реалізація цього інтерфейсу;
  • у [3] — головний скрипт додатка;

14.2.6.1. Інтерфейс [InterfaceUi]

Інтерфейс рівня [UI] матиме такий вигляд:


# імпорт
from abc import ABC, abstractmethod


# інтерфейс UI
class InterfaceUi(ABC):
    # виконання шару UI
    @abstractmethod
    def run(self: object):
        pass

Примітки

  • рядки 9–10: рівень [UI] матиме лише один метод, [run];

14.2.6.2. Реалізація [Console]

Рівень [console] реалізовано за допомогою наступного скрипта [Console.py]:


# імпорт рівнів

from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi

# інші залежності
from MyException import MyException


class Console(InterfaceUi):
    # конструктор
    def __init__(self: object, métier: InterfaceMétier):
        # бізнес-логіка: шар [métier]

        # зберігання атрибутів
        self.métier = métier


        # -----------
        # інтерфейс
        # -----------

    def run(self):
        # діалог з користувачем
        fini = False
        while not fini:
            # питання / відповідь
            réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
            # закінчено?
            if réponse == "*":
                break
            # чи введено дані правильно?
            ok = False
            try:
                id_élève = int(réponse, 10)
                ok = id_élève >= 1
            except ValueError as erreur:
                pass
            # дані правильні?
            if not ok:
                print("Saisie incorrecte. Recommencez...")
                continue
            # розрахунок статистичних даних для обраного учня
            try:
                print(self.métier.get_stats_for_élève(id_élève))
            except MyException as erreur:
                print(f"L'erreur suivante s'est produite : {erreur}")
  • рядки 3–5: імпорт усіх інтерфейсів;
  • рядок 11: клас [Console] реалізує інтерфейс [InterfaceUi];
  • рядки 12–17: конструктор класу [Console] отримує як параметр посилання на шар [métier]. Зауважимо, що цьому параметру присвоєно тип [InterfaceMétier], щоб нагадати, що ми працюємо з інтерфейсами, а не з конкретними реалізаціями;
  • рядок 24: реалізація методу [run] інтерфейсу;
  • рядок 27: цикл, який зупиняється, коли виконується умова з рядка 31;
  • рядок 29: введення даних, набраних з клавіатури. Функція [input] приймає необов’язковий параметр: повідомлення, яке потрібно вивести на екран для запиту введення даних. Ці дані завжди отримуються у вигляді символьного рядка. Функція [strip] видаляє з нього «пробіли», що йдуть перед або після нього;
  • рядки 34–39: перевіряється, чи введене значення — номер учня — є правильним. Це має бути ціле число >=1. Нагадаємо, що введене значення було отримано у вигляді символьного рядка;
  • рядок 36: робиться спроба перетворити введене значення на ціле число в десятковій системі числення. Функція [int] генерує виняток, якщо це неможливо;
  • рядок 37: до цього рядка доходимо лише в тому випадку, якщо виняток не виник. Перевіряємо, чи отримане ціле число дійсно >=1;
  • рядки 38–39: обробляється виняток. Якщо стався виняток, змінна [ok] з рядка 34 залишилася на рівні [False];
  • рядки 41–43: якщо введення було неправильним, виводиться повідомлення про помилку і виконується повторний цикл (рядок 43);
  • рядки 45–48: обчислюються статистичні дані про учня, номер якого було введено;
  • рядок 46: використовується метод [get_stats_for_élève] з шару [métier]. Цей метод генерує виняток, якщо учень не існує. Цей виняток обробляється в рядках 47–48. Відомо, що шари [dao] та [métier] генерують виняток [MyException];

14.3. Головний скрипт [main]

Головний скрипт [main] має такий вигляд (main.py):


# налаштування програми
import config

config = config.configure()

# syspath налаштовано — можна виконувати імпорт
from Console import Console
from Dao import Dao
from Métier import Métier

# ----------- рівень [console]
try:
    # інстанціювання шару [dao]
    dao = Dao()
    # інстанціювання шару [métier]
    métier = Métier(dao)
    # інстанціювання шару [ui]
    console = Console(métier)
    # виконання шару [console]
    console.run()
except BaseException as ex:
    # виводиться повідомлення про помилку
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    pass
  • рядки 1–4: налаштовується Python Path додатка;
  • рядки 6–9: імпортуються необхідні класи та інтерфейси;
  • рядок 14: створення екземпляра шару [dao];
  • рядок 16: створення екземпляра шару [métier];
  • рядок 18: створення екземпляра шару [ui];
  • рядок 20: запускається діалог з користувачем;
  • рядки 13–20: зазвичай з цих рядків не генеруються винятки. Ті винятки, що надходять із шарів [dao] та [métier], зупиняються шаром [Console]. Обробка винятків — це складне мистецтво, коли ви не досконало знаєте використовувані шари (у даному випадку це не так). У разі сумнівів можна додати код, щоб перехопити будь-який тип винятку, який може бути згенерований виконуваним кодом. Саме це й зроблено тут, у рядках 21–23. Перехоплюються всі винятки, що походять від [BaseException], тобто всі винятки;
  • рядки 24–25: оператор [finally] тут нічого не робить. Він присутній лише для того, щоб можна було закомментувати рядки 21–23. Адже в режимі налагодження немає сенсу перехоплювати винятки. У цьому випадку їх зупиняє інтерпретатор Python, який потім видає номер рядка, де сталося виключення. Це необхідна інформація. Коли рядки 21–23 закомментовані, наявність рядків 24–25 дозволяє отримати синтаксично правильний блок try/catch. За їх відсутності Python видає помилку;

Ось приклад виконання:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/main/main.py
Numéro de l'élève (>=1 et * pour arrêter) : 11
Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33
Numéro de l'élève (>=1 et * pour arrêter) : 1
L'erreur suivante s'est produite : MyException[10, L'élève d'identifiant 1 n'existe pas]
Numéro de l'élève (>=1 et * pour arrêter) : *

Process finished with exit code 0

14.4. Приклад 2

Цей новий приклад багаторівневої архітектури має на меті продемонструвати переваги програмування за допомогою інтерфейсів. Воно полегшує обслуговування та тестування додатків. Ми знову використаємо трирівневу архітектуру:

Image

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

14.4.1. Рівень [dao]

Image

Інтерфейс [InterfaceDao] виглядає так:


# імпорт
from abc import ABC, abstractmethod


# інтерфейс DAO
class InterfaceDao(ABC):
    # єдиний метод
    @abstractmethod
    def do_something_in_dao_layer(self, x: int, y: int) -> int:
        pass
  • рядки 8–10: метод [do_something_in_dao_layer] є єдиним методом інтерфейсу;

Клас [DaoImpl1] реалізує інтерфейс [InterfaceDao] наступним чином:


from InterfaceDao import InterfaceDao


class DaoImpl1(InterfaceDao):
    # реалізація InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x + y

Клас [DaoImpl2] реалізує інтерфейс [InterfaceDao] наступним чином:


from InterfaceDao import InterfaceDao


class DaoImpl2(InterfaceDao):
    # реалізація InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x - y

14.4.2. Рівень [métier]

Image

Інтерфейс [InterfaceMétier] має такий вигляд:


# імпорт
from abc import ABC, abstractmethod


# бізнес-інтерфейс
class InterfaceMétier(ABC):
    # єдиний метод
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • рядки 8–10: метод [do_something_in_métier_layer] є єдиним методом інтерфейсу;

Клас [AbstractBaseMétier] реалізує інтерфейс [InterfaceMétier] наступним чином:


# імпорт
from abc import ABC, abstractmethod

from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier


class AbstractBaseMétier(InterfaceMétier, ABC):
    # властивості
    # __dao є посиланням на рівень [dao]
    @property
    def dao(self) -> InterfaceDao:
        return self.__dao

    @dao.setter
    def dao(self, dao: InterfaceDao):
        self.__dao = dao

    # реалізація інтерфейсу [InterfaceMétier]
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • рядок 8: клас [AbstractBaseMétier] є похідним від двох класів:
    • [InterfaceMétier]: клас [AbstractBaseMétier] реалізує цей інтерфейс у рядках 19–22. Насправді бачимо, що він не реалізував метод [do_something_in_métier_layer], який оголосив абстрактним (рядок 20). Реалізувати цей метод повинні похідні класи;
    • [ABC], щоб отримати доступ до анотацій [@abstractmethod];
    • порядок має значення: якщо його тут змінити на протилежний, Python видасть помилку під час виконання;

Це перший випадок використання множинної спадковості (спадкування від декількох класів). Клас [AbstractBaseMétier] одночасно успадковує властивості класів [InterfaceMétier] та [ABC].

  • рядки 9–17: ми визначаємо властивість [dao], яка буде посиланням на шар [dao];

Інтерфейс призначений для реалізації. Коли різні реалізації мають спільні властивості, доцільно розмістити їх у батьківському класі, щоб уникнути їх дублювання. Саме так відбувається у випадку з властивістю [dao]. Батьківський клас, як правило, завжди є абстрактним, оскільки він не реалізує всіх методів інтерфейсу.

Клас [MétierImpl1] реалізує інтерфейс [InterfaceMétier] наступним чином:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl1(AbstractBaseMétier):
    # реалізація інтерфейсу [InterfaceMétier]
    def do_something_in_métier_layer(self:AbstractBaseMétier, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.dao.do_something_in_dao_layer(x, y)
  • рядок 4: клас [MétierImpl1] походить від класу [AbstractbaseMétier]. Отже, він успадковує властивість [dao] цього класу;
  • рядки 6–9: реалізація інтерфейсу [InterfaceMétier], який не реалізував батьківський клас [AbstractbaseMétier];
  • рядок 9: використовується шар [dao];

Клас [MétierImpl2] реалізує інтерфейс [InterfaceMétier] аналогічним чином:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl2(AbstractBaseMétier):
    # реалізація інтерфейсу [InterfaceMétier]
    def do_something_in_métier_layer(self:AbstractBaseMétier, x: int, y: int) -> int:
        x -= 1
        y -= 1
        return self.dao.do_something_in_dao_layer(x, y)

14.4.3. Рівень [ui]

Image

Інтерфейс [InterfaceUi] має такий вигляд:


# імпорт
from abc import ABC, abstractmethod


# інтерфейс Ui
class InterfaceUi(ABC):
    # єдиний метод
    @abstractmethod
    def do_something_in_ui_layer(self, x: int, y: int) -> int:
        pass
  • рядки 8–10: єдиний метод інтерфейсу;

Клас [AbstractBaseUi] реалізує інтерфейс [InterfaceUi] наступним чином:


# імпорт
from abc import ABC, abstractmethod

from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi


class AbstractBaseUi(InterfaceUi, ABC):
    # властивості
    # бізнес-логіка є посиланням на рівні [métier]
    @property
    def métier(self) -> InterfaceMétier:
        return self.__métier

    @métier.setter
    def métier(self, métier: InterfaceMétier):
        self.__métier = métier

    # реалізація інтерфейсу [InterfaceUI]
    @abstractmethod
    def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
        pass
  • клас [AbstractBaseUi] є абстрактним класом (рядок 20). Він повинен бути похідним, щоб реалізувати інтерфейс [InterfaceUi];
  • рядки 9–17: клас [AbstractBaseUi] має посилання на шар [métier];

Клас реалізації [UiImpl1] має такий вигляд:


from AbstractBaseUi import AbstractBaseUi


class UiImpl1(AbstractBaseUi):
    # реалізація інтерфейсу [InterfaceUi]
    def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.métier.do_something_in_métier_layer(x, y)
  • рядок 4: клас [UiImpl1] походить від класу [AbstractBaseUi] і, отже, успадковує його властивість [métier]. Ця властивість використовується в рядку 9;

Клас реалізації [UiImpl2] є аналогічним:


from AbstractBaseUi import AbstractBaseUi


class UiImpl2(AbstractBaseUi):
    # реалізація інтерфейсу [InterfaceUi]
    def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
        x -= 1
        y -= 1
        return self.métier.do_something_in_métier_layer(x, y)
  • рядок 4: клас [UiImpl2] походить від класу [AbstractBaseUi] і, отже, успадковує його властивість [métier]. Ця властивість використовується в рядку 9;

14.4.4. Конфігураційні файли

Image

  • файли [config1, config2] налаштовують додаток двома різними способами;
  • файл [main] є головним скриптом додатка;

Файл [config1] має такий вигляд:


def configure():
    # крок 1 ------
    # абсолютний шлях до папки цього скрипта
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # залежності
    absolute_dependencies = [
        # локальні папки Python Path
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # налаштування syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # крок 2 ------
    # налаштування шарів додатка
    from DaoImpl1 import DaoImpl1
    from MétierImpl1 import MétierImpl1
    from UiImpl1 import UiImpl1
    # інстанціювання шарів
    # DAO
    dao = DaoImpl1()
    # бізнес-логіка
    métier = MétierImpl1()
    métier.dao = dao
    # UI
    ui = UiImpl1()
    ui.métier = métier

    # інстанції шарів додаємо до конфігурації
    # тут потрібен лише шар ui
    config = {"ui": ui}

    # зберігаємо конфігурацію
    return config
  • рядки 2–16: налаштування Python Path додатка;
  • рядки 18–31: створення екземплярів шарів [dao, métier, ui]. Для реалізації їхніх інтерфейсів щоразу обирається перша створена реалізація;
  • рядки 33–35: додаємо посилання на шари до конфігурації. У цьому випадку головному скрипту потрібен лише шар [ui];

Файл [config2] є аналогічним і реалізує кожен інтерфейс за допомогою другої доступної реалізації:


def configure():
    # крок 1 ---
    # абсолютний шлях до папки цього скрипта
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # залежності
    absolute_dependencies = [
        # локальні папки Python Path
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # налаштування syspath
    from myutils import set_syspath

    set_syspath(absolute_dependencies)

    # крок 2 ------
    # налаштування шарів додатка
    from DaoImpl2 import DaoImpl2
    from MétierImpl2 import MétierImpl2
    from UiImpl2 import UiImpl2
    # інстанціювання шарів
    # DAO
    dao = DaoImpl2()
    # бізнес-логіка
    métier = MétierImpl2()
    métier.dao = dao
    # UI
    ui = UiImpl2()
    ui.métier = métier

    # інстанції шарів додаємо до конфігурації
    # тут потрібен лише шар ui
    config = {"ui": ui}

    # створюємо конфігурацію
    return config

14.4.5. Головний скрипт [main]

Image

Головний скрипт має такий вигляд:


# імпортуємо
import importlib
import sys

# main ---------

# потрібні два аргументи
nb_args = len(sys.argv)
if nb_args != 2 or (sys.argv[1] != "config1" and sys.argv[1] != "config2"):
    print(f"Syntaxe : {sys.argv[0]} config1 ou config2")
    sys.exit()

# конфігурація додатка
module = importlib.import_module(sys.argv[1])
config = module.configure()

# виконання шару [ui]
print(config["ui"].do_something_in_ui_layer(10, 20))

Цей скрипт приймає один параметр:

  • [config1] — для використання конфігурації № 1;
  • [config2] — для використання конфігурації № 2;

Python записує параметри у список [sys.argv]:

  • sys.argv[0] — це ім’я скрипта, у даному випадку [main]. Цей параметр завжди присутній;
  • sys.argv[1] — це перший параметр, що передається скрипту, sys.argv[2] — другий, …

  • рядок 8: отримуємо кількість параметрів;

  • рядки 9–11: перевіряється, чи дійсно є параметр і чи його значення дорівнює [config1] або [config2]. Якщо це не так, виводиться повідомлення про помилку (рядок 10) і програма завершується (рядок 11);

Як тільки бажана конфігурація відома, нам потрібно виконати цю конфігурацію. Наприклад, якщо було обрано конфігурацію 1, нам потрібно виконати код:

import config1
config1.configure()

Проблема тут полягає в тому, що конфігурація, яку потрібно використовувати, знаходиться у змінній [len[liste1]]. Щоб імпортувати модуль, ім’я якого знаходиться у змінній, нам потрібно використати пакет [importlib] (рядок 2).

  • рядок 14: імпортуємо модуль, ім’я якого міститься у [sys.argv[1];
  • рядок 15: після цього виконується функція [configure] цього модуля. Отримується словник [config], який є конфігурацією додатка;
  • рядок 18: відомо, що посилання на рівень [ui] міститься в config[‘ui’]. Його використовують для виклику методу [do_something_in_ui_layer]. Відомо, що цей метод викличе метод шару [métier], який, у свою чергу, викличе метод шару [dao];

Наприклад, функція [do_something_in_ui_layer] має такий вигляд:


class UiImpl1(AbstractBaseUi):
    # реалізація інтерфейсу [InterfaceUi]
    def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.métier.do_something_in_métier_layer(x, y)
  • у рядку 6 вище використовується властивість [métier] класу [UiImpl1], рядок 1. Однак у конфігурації [config1] було написано:

# бізнес-логіка
    métier = MétierImpl1()
    métier.dao = dao
    # інтерфейс користувача
    ui = UiImpl1()
    ui.métier = métier
  • рядок 6: властивість [métier] класу [UIImpl1] є посиланням на клас [MétierImpl1] (рядок 2). Отже, буде виконано метод [do_something_in_ui_layer] класу [MétierImpl1];

У класі [MétierUiImpl1] зазначено:


class MétierImpl1(AbstractBaseMétier):
    # реалізація інтерфейсу [InterfaceMétier]
    def do_something_in_métier_layer(self: AbstractBaseMétier, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.dao.do_something_in_dao_layer(x, y)
  • у рядку 6 метод, викликаний шаром [ui], у свою чергу викликає метод властивості [dao] класу [MétierImpl1];

Однак у конфігурації [config1] записано:


# DAO
    dao = DaoImpl1()
    # бізнес-логіка
    métier = MétierImpl1()
    métier.dao = dao
  • рядок 5: властивість [MétierImpl1.dao] має тип [DaoImpl1] (рядок 2);

Тут ми хочемо показати, що скрипт [main] не повинен враховувати шари [métier] та [dao]. Йому потрібно опікуватися лише шаром [ui], оскільки зв’язки між цим шаром та іншими вже встановлено під час налаштування.

Image

Щоб передати параметр [config1] або [config2] скрипту [main], , слід виконати такі дії:

Image

  • у [1-2] створюється так звана конфігурація виконання;
  • у [3] надаємо цій конфігурації ім’я, щоб можна було її знайти;
  • у [4] вибираємо скрипт для виконання. Якщо ми дотримувалися процедури [1-2], потрібний скрипт уже вибрано;
  • у полі «[5]» вказуються параметри, які потрібно передати скрипту. Тут вводиться рядок «[config1]», щоб скрипт використовував конфігурацію № 1;
  • у [6] підтверджується конфігурація виконання;

Image

  • у [1-2] запитуємо перелік існуючих контекстів виконання;
  • у [3] вибираємо існуючий контекст виконання та дублюємо його у [4];

Image

  • у [5] вказується ім’я нової конфігурації. Саме вона буде виконувати скрипт [main] [6], передаючи йому параметр [config2] [7];

Конфігурації виконання доступні у верхньому правому куті вікна PyCharm:

Image

Достатньо вибрати [2] або [3], а потім натиснути [4], щоб виконати скрипт [main] зодним із параметрів [config1] або [config2].

З параметром [config1] виконання [main] дає такі результати:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config1
34

Process finished with exit code 0

З параметром [config2] виконання [main] дає такі результати:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config2
-10

Process finished with exit code 0

Просимо читача перевірити ці результати.