Skip to content

14. Architektura warstwowa i programowanie oparte na interfejsach

14.1. Wprowadzenie

Zamierzamy napisać aplikację umożliwiającą wyświetlanie ocen uczniów gimnazjum. Aplikacja ta może mieć architekturę wielowarstwową:

Image

  • warstwa [ui] (interfejs użytkownika) jest warstwą mającą kontakt z użytkownikiem aplikacji;
  • warstwa [métier] realizuje reguły zarządzania aplikacją, takie jak obliczanie wynagrodzenia lub faktury. Warstwa ta wykorzystuje dane pochodzące od użytkownika za pośrednictwem warstwy [présentation] oraz dane z warstw SGBD za pośrednictwem warstwy [dao];
  • warstwa [dao] (obiekty dostępu do danych) zarządza dostępem do danych z warstwy SGBD (system zarządzania bazami danych).

Jest to architektura, która została zastosowana w |cours sur Python 2|. Można również wprowadzić wariant:

Image

Różnice w stosunku do poprzedniej struktury warstwowej są następujące:

  • główny skrypt o nazwie [main], o którym mowa powyżej, organizuje instancjonowanie warstw;
  • warstwy [ui, métier, dao] niekoniecznie komunikują się już między sobą. Jeśli jest to konieczne, skrypt [main] dostarcza im odniesienia do warstw, których potrzebują;

Kod jest tutaj zorganizowany w centra kompetencji z jednym koordynatorem:

  • koordynatorem jest główny skrypt [main];
  • warstwy [ui], [dao] i [métier] stanowią centra kompetencji;

Tę strukturę można by nazwać organizacją orkiestrową.

14.2. Przykład 1

Zilustrujemy architekturę warstwową na przykładzie prostej aplikacji konsolowej:

  • nie będzie bazy danych;
  • warstwa [dao] będzie obsługiwać encje Elève, Classe, Matière, Note, umożliwiające zarządzanie ocenami uczniów;
  • warstwa [métier] umożliwi obliczanie wskaźników na podstawie ocen konkretnego ucznia;
  • warstwa [ui] będzie aplikacją konsolową wyświetlającą wyniki uczniów;

Projekt aplikacji PyCharm przedstawia się następująco:

Uwaga: foldery zaznaczone na niebiesko należą do projektu [Sources Root] w ramach projektu PyCharm.

14.2.1. Elementy aplikacji

Jako encje nazywamy klasy, których jedyną rolą jest hermetyzacja danych. W tym celu można by wykorzystać słowniki. Zaletą klasy jest umożliwienie sprawdzenia poprawności danych przechowywanych w obiekcie oraz udostępnienie metody zwracającej tożsamość obiektu w postaci ciągu znaków.

14.2.1.1. Entyteta [Classe]

Entyteta [Classe] (Classe.py) reprezentuje klasę w szkole średniej:


# importy
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # atrybuty wykluczone ze stanu klasy
    excluded_keys = []

    # właściwości klasy
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identyfikator klasy
        # nazwa: nazwa klasy
        return BaseEntity.get_allowed_keys() + ["nom"]

    # metoda pobierająca
    @property
    def nom(self: object) -> str:
        return self.__nom

    # metody ustawiające
    @nom.setter
    def nom(self: object, nom: str):
        # nazwa musi być niepustym ciągiem znaków
        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")

Uwagi

  • wiersz 7: jednostka [Classe] wywodzi się z jednostki [BaseEntity] omówionej w akapicie dotyczącym jednostki |La classe BaseEntity|;
  • wiersze 11–16: klasa jest definiowana przez numer id oraz nom (wiersz 16). Właściwość [id] jest dostarczana przez klasę [BaseEntity], a nazwa przez klasę [Classe];
  • wiersze 18–30: metody getter/setter atrybutu [nom];

14.2.1.2. Entyteta [Matière]

Klasa [Matière] (matière.py) ma następującą postać:


# importy
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # atrybuty wykluczone ze stanu klasy
    excluded_keys = []

    # właściwości klasy
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identyfikator przedmiotu
        # nazwa: nazwa przedmiotu
        # współczynnik: współczynnik przedmiotu
        return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]

    # getter
    @property
    def nom(self: object) -> str:
        return self.__nom

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

    # ustawki
    @nom.setter
    def nom(self: object, nom: str):
        # nazwa musi być niepustym ciągiem znaków
        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):
        # współczynnik musi być liczbą rzeczywistą >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # błąd?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

Uwagi

  • wiersz 7: klasa [Classe] wywodzi się z klasy [BaseEntity];
  • wiersze 11–17: przedmiot definiowany jest przez swój numer [id], nazwę [nom] oraz współczynnik [coefficient];
  • wiersze 19–50: metody getter i setter atrybutów klasy;

14.2.1.3. Entyteta [Elève]

Klasa [Elève] (élève.py) ma następującą postać:


# importy
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # atrybuty wykluczone ze stanu klasy
    excluded_keys = []

    # właściwości klasy
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identyfikator ucznia
        # nazwisko: nazwisko ucznia
        # imię: imię ucznia
        # klasa: klasa ucznia
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    # metody pobierające
    @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

    # metody ustawiające
    @nom.setter
    def nom(self: object, nom: str) -> str:
        # nazwisko musi być niepustym ciągiem znaków
        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:
        # imię musi być niepustym ciągiem znaków
        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:
            # oczekuje się typu Klasa
            if isinstance(value, Classe):
                self.__classe = value
            # lub typu „dict”
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # lub typu 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}")

Uwagi

  • wiersz 9: klasa [Elève] wywodzi się z klasy [BaseEntity];
  • wiersze 13–20: uczeń jest identyfikowany przez swój numer [id], nazwisko [nom], imię [prénom] oraz klasę [classe]. Ten ostatni parametr jest odwołaniem do obiektu [Classe];
  • wiersze 22–65: metody pobierające i ustawiające atrybuty klasy;

14.2.1.4. Entyteta [Note]

Klasa [Note] (note.py) ma następującą postać:


# importy
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    # atrybuty wykluczone ze stanu klasy
    excluded_keys = []

    # właściwości klasy
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identyfikator notatki
        # wartość: sama ocena
        # uczeń: uczeń (typu Uczeń), którego dotyczy ocena
        # przedmiot: przedmiot (typu Przedmiot), którego dotyczy ocena
        # Obiekt „Ocena” to zatem ocena ucznia z danego przedmiotu
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    # metody pobierające
    @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

    # metody pobierające
    @valeur.setter
    def valeur(self: object, valeur: float):
        # ocena musi być liczbą rzeczywistą z przedziału od 0 do 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:
            # oczekuje się typu „Uczeń”
            if isinstance(value, Elève):
                self.__élève = value
            # lub typu dict
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # lub typu 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:
            # oczekuje się typu „Przedmiot”
            if isinstance(value, Matière):
                self.__matière = value
            # lub typu „dict”
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # lub typu 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}")

Uwagi

  • wiersz 8: klasa [Note] wywodzi się z klasy [BaseEntity];
  • wiersze 12–20: obiekt [Note] jest charakteryzowany przez swój numer [id], wartością oceny [valeur], odniesieniem [élève] do ucznia, który uzyskał tę ocenę, oraz odniesieniem do przedmiotu [matière], którego dotyczy ocena;
  • wiersze 22–75: metody getter i setter atrybutów klasy;

14.2.2. Konfiguracja aplikacji

Plik [config.py] konfiguruje środowisko głównego skryptu [main] (1), a także środowisko testów (2). Wszystkie te skrypty zawierają instrukcję [import config] na początku kodu. Przypominamy, że folder zawierający skrypt, którego dotyczy polecenie [python script], automatycznie staje się częścią środowiska Python Path.Si, więc plik [config] znajduje się w tym samym folderze co skrypty zawierające instrukcję [import config] i zostanie znaleziony. Pliki [1] i [2] są tutaj identyczne. Nie zawsze jednak tak jest.

Plik [config.sys] ma następującą treść:


def configure():
    import os

    # absolutna ścieżka do folderu zawierającego ten skrypt
    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"
    # absolutne zależności
    absolute_dependencies=[
        # lokalne foldery zawierające klasy i interfejsy
        f"{root_dir}/02/entities",
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
    ]

    # aktualizacja ścieżki systemowej
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # konfiguracja
    return {}
  • wiersze 11–14: foldery, które powinny znajdować się w ścieżce Python Path (sys.path);
  • folder [f"{root_dir}/02/entities"] zapewnia dostęp do klas [BaseEntity] i [MyException];
  • folder [f"{script_dir}/../entities"] zapewnia dostęp do klas [Elève], [Classe], [Matière], [Note];
  • folder [f"{script_dir}/../interfaces",] zapewnia dostęp do interfejsów aplikacji;
  • folder [f"{script_dir}/../services"] zapewnia dostęp do klas implementujących interfejsy;

14.2.3. Testy encji

W tym miejscu napiszemy testy wykonywane przez narzędzie o nazwie [unittest]. PyCharm zawiera kilka frameworków testowych. Wybór jednego z nich dokonuje się w konfiguracji PyCharm:

Image

  • W [4] dostępnych jest kilka frameworków testowych:

Image

14.2.3.1. Klasa testowa [TestBaseEntity]

Skrypt testowy [TestBaseEntity] będzie wyglądał następująco:


import unittest

# konfigurujemy aplikację
import config

config = config.configure()


class TestBaseEntity (unittest.TestCase):

    def test_note1(self):
        # importy
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        # tworzenie oceny na podstawie ciągu znaków jSON
        note = Note().fromjson(
            '{"id": 8, "wartość": 12, "uczeń": {"id": 42, "nazwisko": "nazwisko4", "imię": "imię4", "klasa": {"id": 2, "nazwa": "klasa2"}}, "przedmiot": {"id": 2, "nazwa": "przedmiot2", "współczynnik": 2}}')
        # sprawdzenia
        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):
        # importy
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        # tworzenie oceny na podstawie słownika
        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}})
        # sprawdzanie
        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()

Uwagi

  • wiersz 1: importujemy moduł [unittest], który udostępni różne metody testowe;
  • wiersze 3–6: konfigurujemy aplikację tak, aby mogła odnaleźć klasy niezbędne do przeprowadzenia testów;
  • wiersz 9: klasa testowa [unittest] musi dziedziczyć po klasie [unittest.TestCase];
  • wiersze 11, 27: nazwy funkcji testowych muszą zaczynać się od [test], w przeciwnym razie nie zostaną rozpoznane;
  • wiersze 13–16: importujemy potrzebne klasy;
  • w tej klasie testowej chcemy sprawdzić zachowanie metod [BaseEntity.fromdict] (wiersz 34) i [BaseEntity.fromjson] (wiersz 18). Klasa [Note] posiada właściwości, które są odwołaniami do innych klas. Chcemy sprawdzić, czy obie powyższe metody tworzą prawidłowe obiekty klasy [Note];
  • wiersz 18: tworzymy obiekt [Note] na podstawie obiektu jSON;
  • wiersz 21: sprawdzamy, czy utworzony obiekt jest rzeczywiście typu [Note]. Metoda [assertIsInstance] jest metodą klasy [unittest.TestCase], która jest klasą nadrzędną klasy [TestBaseEntity];
  • wiersz 22: sprawdzamy, czy [note.élève] jest rzeczywiście typu [Elève];
  • wiersz 23: sprawdzamy, czy [note.élève.classe] jest rzeczywiście typu [Classe];
  • wiersz 24: sprawdzamy, czy [note.matière] jest rzeczywiście typu [Matière];
  • wiersze 33–42: postępujemy analogicznie w przypadku metody [BaseEntity.fromdict];

Istnieje kilka sposobów przeprowadzenia testów:

  • w przypadku [1-2] uruchamia się [TestBaseEntity] z wykorzystaniem frameworka [UnitTest];
  • w [3-5] testy kończą się niepowodzeniem. [UnitTests] wskazuje, że nie znaleziono żadnych testów do wykonania;

Niepowodzenie testów wynika z organizacji kodu w [TestBaseEntity]:


import unittest

# konfiguracja aplikacji
import config

config = config.configure()


class TestBaseEntity(unittest.TestCase):

Problem dla frameworka [UnitTest] stanowi obecność kodu wykonywalnego (wiersze 3–6) przed definicją klasy testowej (wiersz 9).

W związku z tym reorganizujemy kod w następujący sposób:


import unittest


class TestBaseEntity(unittest.TestCase):

    def setUp(self):
        # konfiguracja aplikacji
        import config

        config.configure()

    def test_note1(self):
        

    def test_note2(self):
        


if __name__ == '__main__':
    unittest.main()
  • wiersze 6–10: definiujemy funkcję [setUp]. Funkcja ta pełni szczególną rolę: jest wykonywana przed każdą funkcją testową (test_note1, test_note2);

Po wykonaniu tej czynności uruchomienie klasy [TestBaseEntity] daje następujące wyniki:

Tym razem obie metody testowe zostały wykonane, a testy zakończyły się powodzeniem.

Zobaczmy, co się dzieje, gdy test zakończy się niepowodzeniem. Zmodyfikujmy kod klasy [test_note1] w następujący sposób:


    def test_note1(self):
        # celowy błąd – sprawdzamy, czy 1 == 2
        self.assertEqual(1,2)
        # importy
        from Note import Note

  • wiersz 2: sprawdzamy, czy 1==2;

Wyniki wykonania są wówczas następujące:

Można poznać przyczynę błędu, klikając nieudany test [2]:

  • w [7-8], przyczyna błędu;

Innym sposobem uruchomienia zestawu testów jest wykonanie go w terminalu:


(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

Wiersz 6 wskazuje, że oba testy zakończyły się powodzeniem (usunięto błąd 1==2);

Wreszcie trzeci sposób uruchomienia klasy testów [TestBaseEntity], również w terminalu, jest następujący. Klasę testów kończymy następującymi wierszami 6–7:



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


if __name__ == '__main__':
    unittest.main()
  • wiersz 6: zmienna [__name__] to nazwa nadana uruchamianemu skryptowi. Gdy skrypt jest uruchamiany poleceniem [python script.py], zmienna [__name__] przyjmuje wartość [__main__] (2 znaki podkreślone przed i po identyfikatorze). W ten sposób wiersz 7 jest wykonywany tylko wtedy, gdy skrypt [TestBaseEntity] jest uruchamiany przez polecenie [python TestBaseEntity.py]. Instrukcja [unittest.main()] uruchamia skrypt za pomocą frameworka [UnitTest]. Oto przykład:

(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. Klasa testowa [TestEntités]

Klasa testowa [TestEntités] wygląda następująco:


import unittest


class TestEntités(unittest.TestCase):
    def setUp(self):
        # konfiguracja aplikacji
        import config

        config.configure()

    def test_code1a(self):
        # importy
        from Elève import Elève
        from MyException import MyException
        # kod błędu
        code = None
        try:
            # nieprawidłowy identyfikator
            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
        # weryfikacja
        self.assertEqual(code, 1)

    def test_code41(self):
        # importy
        from Elève import Elève
        from MyException import MyException
        # kod błędu
        code = None

        try:
            # nieprawidłowa nazwa
            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
        # weryfikacja
        self.assertEqual(code, 41)

    def test_code42(self):
        # importy
        from Elève import Elève
        from MyException import MyException
        # kod błędu
        code = None
        try:
            # nieprawidłowe imię
            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
        # weryfikacja
        self.assertEqual(code, 42)

    def test_code43(self):
        # importy
        from Elève import Elève
        from MyException import MyException
        # kod błędu
        code = None
        try:
            # nieprawidłowa klasa
            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
        # weryfikacja
        self.assertEqual(code, 43)

    def test_code1b(self):
        # importy
        from Classe import Classe
        from MyException import MyException
        # kod błędu
        code = None
        try:
            # nieprawidłowy identyfikator
            Classe().fromdict({"id": "x", "nom": "y"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # weryfikacja
        self.assertEqual(code, 1)

    def test_code11(self):
        # importy
        from Classe import Classe
        from MyException import MyException

        # kod błędu
        code = None
        try:
            # nieważna nazwa
            Classe().fromdict({"id": 1, "nom": ""})
        except MyException as ex:
            code = ex.code
        # weryfikacja
        self.assertEqual(code, 11)

    def test_code1c(self):
        # importy
        from Matière import Matière
        from MyException import MyException

        # kod błędu
        code = None
        try:
            # nieprawidłowy identyfikator
            Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # weryfikacja
        self.assertEqual(code, 1)

    def test_code21(self):
        # importy
        from Matière import Matière
        from MyException import MyException
        # kod błędu
        code = None
        try:
            # nieważna nazwa
            Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # weryfikacja
        self.assertEqual(code, 21)

    def test_code22(self):
        # importy
        from Matière import Matière
        from MyException import MyException
        # kod błędu
        code = None
        try:
            # nieważny współczynnik
            Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # weryfikacja
        self.assertEqual(code, 22)

    def test_code1d(self):
        # importy
        from Note import Note
        from MyException import MyException
        # kod błędu
        code = None
        try:
            # nieprawidłowy identyfikator
            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
        # weryfikacja
        self.assertEqual(code, 1)

    def test_code31(self):
        # importy
        from Note import Note
        from MyException import MyException

        # kod błędu
        code = None
        try:
            # nieprawidłowa wartość
            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
        # weryfikacja
        self.assertEqual(code, 31)

    def test_code32(self):
        # importy
        from Note import Note
        from MyException import MyException

        # kod błędu
        code = None
        try:
            # nieprawidłowy uczeń
            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
        # weryfikacja
        self.assertEqual(code, 32)

    def test_code33(self):
        # importy
        from Elève import Elève
        from Note import Note
        from Classe import Classe
        from MyException import MyException

        # kod błędu
        code = None
        try:
            # przedmiot nieprawidłowy
            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
        # weryfikacja
        self.assertEqual(code, 33)

    def test_exception(self):
        # importy
        from Elève import Elève
        # aby test zakończył się powodzeniem, należy uruchomić typ [MyException]
        from MyException import MyException
        with self.assertRaises(MyException):
            # test
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})


if __name__ == '__main__':
    unittest.main()
  • Skrypt testowy ma na celu przetestowanie metod setterów klas: sprawdzenie, czy nie można przypisać nieprawidłowych wartości do atrybutów poszczególnych encji;
  • wiersze 11–24: sprawdzamy, czy nie można przypisać uczniowi nieprawidłowego identyfikatora. Ponieważ w wierszu 16 przekazujemy wartość „x” jako identyfikator ucznia, spodziewamy się wystąpienia wyjątku. Powinno więc nastąpić przejście do wierszy 20–22;
  • wiersz 21: wyświetlenie komunikatu o błędzie;
  • wiersz 22: pobieramy kod błędu (patrz akapit |L'entité MyException|);
  • wiersz 24: sprawdzamy (assert), czy kod błędu wynosi 1. W tym miejscu sprawdzamy dwie rzeczy:
    • czy rzeczywiście wystąpił błąd;
    • że kod błędu wynosi 1;
  • proces ten jest powtarzany z funkcjami z wierszy 24–213;
  • wiersze 215–222: sprawdza się, czy dana akcja generuje wyjątek określonego typu;
  • wiersz 220: wskazujemy, że test zakończył się powodzeniem, jeśli wygenerowano wyjątek typu [MyException];

Wyniki

Uruchamiamy skrypt testowy:

Uzyskane wyniki są następujące:


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

W tym przypadku wszystkie testy zakończyły się powodzeniem

14.2.4. Warstwa [dao]

Image

Warstwa [dao] implementuje interfejs [InterfaceDao] [1]. Jest on implementowany przez klasę [Dao] (2). Skrypt [tests_dao] (3) testuje metody warstwy [dao].

14.2.4.1. Interfejs [InterfaceDao]

Interfejs to umowa zawarta między kodem wywołującym a kodem wywoływanym. To kod wywoływany udostępnia interfejs:

  • Kod wywołujący [1] nie zna implementacji kodu wywoływanego [3]. Zna jedynie sposób jego wywołania. Wskazuje mu to interfejs [2]. Interfejs ten definiuje pewną liczbę metod/funkcji, których należy używać w celu interakcji z kodem wywoływanym. Interfejs ten nazywany jest również API (Application Programming Interface);

Warstwa [dao] udostępni następujący interfejs:

  • [get_classes] zwraca listę klas w gimnazjum;
  • [get_matières] zwraca listę przedmiotów nauczanych w gimnazjum;
  • [get_élèves] zwraca listę uczniów gimnazjum;
  • [get_notes] zwraca listę ocen wszystkich uczniów;
  • [get_notes_for_élève_by_id] zwraca oceny konkretnego ucznia;
  • [get_élève_by_id] zwraca ucznia zidentyfikowanego na podstawie jego numeru;

Kod wywołujący będzie korzystał wyłącznie z tych metod. Nie musi on wiedzieć, w jaki sposób są one zaimplementowane. Dane mogą pochodzić z różnych źródeł (z pamięci stałej, z bazy danych, z plików tekstowych…) bez wpływu na kod wywołujący. Nazywa się to programowaniem za pomocą interfejsów.

Python 3 posiada pojęcie zbliżone do interfejsu: klasę abstrakcyjną. Wykorzystamy ją. Zgrupujemy interfejsy z tego przykładu w folderze [interfaces].

Definiujemy klasę abstrakcyjną [InterfaceDao] (InterfaceDao.py) dla warstwy [dao]:


# importy
from abc import ABC, abstractmethod

# interfejs DAO
from Elève import Elève


class InterfaceDao(ABC):
    # lista klas
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # lista uczniów
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # lista przedmiotów
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    # lista ocen
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    # lista ocen ucznia
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # wyszukiwanie ucznia według identyfikatora
    @abstractmethod
    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

Uwagi:

  • wiersz 2: ABC = Abstract Base Class. Z modułu [abc] importujemy klasę ABC oraz dekorator [abstractmethod] używany w wierszach 10, 15, 20, 25, 30 i 35;
  • wiersz 8: klasa abstrakcyjna nosi nazwę [InterfaceDao] i wywodzi się z klasy [ABC];
  • metody klasy abstrakcyjnej są ozdobione dekoratorem [@abstractmethod], który sprawia, że tak ozdobiona metoda staje się metodą abstrakcyjną: jej kod nie jest zdefiniowany. Niemniej jednak umieszczamy w niej kod: instrukcję [pass], która nic nie robi;
  • Klasy abstrakcyjnej [InterfaceDao] nie można instancjonować. Instancjonować można jedynie klasy pochodne od [InterfaceDao], które zaimplementowały wszystkie metody klasy [InterfaceDao]. Jeśli zatem utworzymy dwie klasy [Dao1] i [Dao2], będące klasami pochodnymi klasy [InterfaceDao], obie będą implementowały abstrakcyjne metody klasy [InterfaceDao]. Można by zatem powiedzieć, że implementują one interfejs [InterfaceDao];
  • języki programowania, w których implementuje się zarówno interfejsy, jak i klasy abstrakcyjne, nadają interfejsowi inną rolę niż klasie abstrakcyjnej. Interfejs nie posiada atrybutów i nie można go instancjonować. Klasa może implementować interfejs poprzez zdefiniowanie wszystkich jego metod;

14.2.4.2. Implementacja [Dao]

Klasa [Dao] (dao.py) implementuje interfejs [InterfaceDao] w następujący sposób:


# importowanie encji i interfejsów
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


# warstwa [dao] implementuje interfejs InterfaceDao
class Dao(InterfaceDao):
    # konstruktor
    # tworzy się listy stałe
    def __init__(self):
        # instancjonuje się klasy
        classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
        classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
        self.classes = [classe1, classe2]
        # przedmioty
        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]
        # uczniowie
        é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]
        # oceny uczniów z poszczególnych przedmiotów
        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]

    # -----------
    # interfejs IDao
    # -----------
    

Uwagi:

  • wiersze 1–7: importuje się jednostki i interfejs [InterfaceDao];
  • wiersz 11: klasa [Dao] wywodzi się z klasy abstrakcyjnej [InterfaceDao]. Można powiedzieć, że implementuje ona interfejs [InterfaceDao];
  • wiersz 14: konstruktor nie ma parametrów. Tworzy on na stałe cztery listy:
    • wiersze 15–18: lista klas;
    • wiersze 19–22: lista przedmiotów;
    • wiersze 23–28: lista uczniów;
    • wiersze 29–38: lista ocen;
  • wiersze 40–44: implementacja metod interfejsu [Interface Dao]. W tym przypadku nie definiujemy ich, aby zobaczyć komunikat o błędzie generowany przez Python;

Program testowy mógłby wyglądać następująco: [tests-dao.py]:


# konfiguracja aplikacji
import config

config = config.configure()

# utworzenie instancji warstwy [dao]
from Dao import Dao

daoImpl = Dao()

# lista klas
for classe in daoImpl.get_classes():
    print(classe)

# lista przedmiotów
for matière in daoImpl.get_matières():
    print(matière)

# lista klas
for élève in daoImpl.get_élèves():
    print(élève)

# lista ocen
for note in daoImpl.get_notes():
    print(note)

Uwaga: skrypt [tests-dao.py] nie jest testem [unittest], ponieważ nie zawiera metod, których nazwy zaczynają się od [test_].

Komentarze są wystarczająco jasne. Wiersze 11–25 wykorzystują interfejs warstwy [dao]. Nie ma tu żadnych założeń dotyczących rzeczywistej implementacji tej warstwy. W wierszu 9 tworzona jest instancja warstwy [dao].

Wyniki wykonania tego skryptu są następujące:


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

Widać, że błąd pojawia się już podczas instancjonowania klasy [Dao] (wiersz 3 powyżej). Interpreter Python 3 informuje nas, że nie może instancjonować tej klasy, ponieważ nie zdefiniowaliśmy metod abstrakcyjnych [get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves].

Pycharm również obsługuje pojęcie klasy abstrakcyjnej i proponuje nam zdefiniowanie jej metod:

  • w [1], kliknij prawym przyciskiem myszy na kod;
  • w [2-3], wybierz [Generate / Implement Methods], aby zaimplementować brakujące metody klasy [Dao];
  • w pliku [4] należy wybrać metody do zaimplementowania, w tym przypadku wszystkie;

Po wykonaniu tych czynności klasa [Dao] zostaje uzupełniona o klasę PyCharm w następujący sposób:


    # -----------
    # interfejs 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

Uzupełniamy klasę [Dao] w następujący sposób:


    # -----------
    # interfejs IDao
    # -----------
    
    # lista klas
    def get_classes(self) -> list:
        return self.classes

    # lista przedmiotów
    def get_matières(self) -> list:
        return self.matières

    # lista uczniów
    def get_élèves(self) -> list:
        return self.élèves

    # lista ocen
    def get_notes(self) -> list:
        return self.notes

    def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
        # wyszukiwanie ucznia
        élève = self.get_élève_by_id(élève_id)
        # pobieranie ocen
        notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
        # zwracanie wyniku
        return {"élève": élève, "notes": notes}

    def get_élève_by_id(self, élève_id: int) -> Elève:
        # filtrowanie uczniów
        élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
        # znaleziono?
        if not élèves:
            raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
        # wynik
        return élèves[0]
  • wiersze 5–19 nie sprawiają trudności;
  • wiersze 29–36: metoda zwracająca ucznia o podanym numerze. Jeśli uczeń nie istnieje, zgłaszany jest wyjątek;
  • wiersz 31: funkcja [filter] umożliwia filtrowanie listy:
    • pierwszy parametr to kryterium filtrowania;
    • drugi parametr to lista do przefiltrowania, w tym przypadku lista uczniów;
  • wiersz 31: kryterium filtrowania listy jest realizowane za pomocą funkcji [f(e :Elève)->bool]. Funkcja ta jest stosowana do każdego elementu listy, która ma zostać przefiltrowana. Jeśli element spełnia kryterium filtrowania, zostaje zachowany na przefiltrowanej liście, w przeciwnym razie jest z niej wykluczany. Można tutaj albo:
    • podać nazwę funkcji f i zaimplementować ją w innym miejscu. Wywołanie funkcji [filter] zmienia się wówczas na [filter(f,self.get_élèves()];
    • podać definicję funkcji f. Wywołanie funkcji [filter] staje się wówczas [filter(f(e :Elève){…},self.get_élèves()], gdzie [e] reprezentuje element listy przefiltrowanej, tj. ucznia. Tak właśnie postąpiono w tym przypadku. Definicja funkcji f brzmiałaby w tym przypadku: [f(e :Elève){return e.id==élève_id)] – uczeń jest wybierany tylko wtedy, gdy numer [id] nie jest równy szukanemu. Taką funkcję można zastąpić tzw. funkcją lambda: [lambda e: e.id == élève_id]:
      • e: oznacza parametr funkcji f, w tym przypadku ucznia. Można użyć dowolnej nazwy;
      • e.id==élève_id to kryterium filtrowania: uczeń o numerze [e] zostanie wybrany tylko wtedy, gdy jego numer [id] odpowiada szukanemu numerowi;
  • wiersz 31: funkcja [filter] zwraca przefiltrowaną listę w typie, który nie jest typem [list], ale który można przekształcić w typ [list]. Właśnie to robimy tutaj za pomocą wyrażenia [list(liste filtrée)];
  • wiersze 33–34: jeśli przefiltrowana lista jest pusta, oznacza to, że poszukiwany uczeń nie istnieje. W takim przypadku zgłaszamy wyjątek;
  • wiersz 36: jeśli dotarliśmy do tego miejsca, oznacza to, że nie wystąpił wyjątek. Wiemy zatem, że uzyskaliśmy listę zawierającą jeden element (nie ma dwóch uczniów o tym samym numerze [id]). Zwracamy więc pierwszy element listy;
  • wiersze 21–27: metoda [get_notes_for_élève_by_id] musi zwrócić oceny ucznia, którego numer [id] został jej przekazany;
  • wiersze 22–23: najpierw wyszukujemy ucznia o numerze [élève_id] za pomocą metody [get_élève_by_id], którą właśnie skomentowaliśmy. Jeśli szukany uczeń nie istnieje, może wystąpić wyjątek. Ponieważ wokół instrukcji w wierszu 23 nie ma bloku try/catch, wyjątek zostanie przekazany do kodu wywołującego. Taki jest zamierzony efekt;
  • wiersze 24–25: po pobraniu danych ucznia pobieramy wszystkie jego oceny. Robimy to ponownie za pomocą filtra:
    • filtrem jest [filter(critère, self_getnotes()]. Listą do filtrowania jest zatem lista wszystkich ocen wszystkich uczniów gimnazjum;
    • kryterium filtrowania jest wyrażone za pomocą funkcji [lambda]: lambda n: n.élève.id == élève_id. Parametr n jest elementem listy do przefiltrowania, a więc oceną. Typ [Note] posiada właściwość [élève], która reprezentuje ucznia, do którego należy ocena. Musi zatem [n.élève.id], reprezentujący numer tego ucznia, być równy numerowi szukanego ucznia;

Następnie uruchamiamy skrypt [tests-dao.py].


# konfiguracja aplikacji
import config

config = config.configure()

# tworzenie instancji warstwy [dao]
from Dao import Dao

daoImpl = Dao()

# lista klas
for classe in daoImpl.get_classes():
    print(classe)

# lista przedmiotów
for matière in daoImpl.get_matières():
    print(matière)

# lista klas
for élève in daoImpl.get_élèves():
    print(élève)

# lista ocen
for note in daoImpl.get_notes():
    print(note)

# konkretny uczeń
print(daoImpl.get_élève_by_id(11))

# lista jego ocen
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}")

Otrzymujemy wówczas następujące wyniki:


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

Można zauważyć, że podczas wyświetlania oceny (w przypadku innych obiektów jest podobnie) widoczne są również:

  • uczeń, który jest właścicielem notatki;
  • przedmiot, do którego odnosi się notatka;

To funkcja [BaseEntity.asdict] generuje ten wynik (patrz akapit „link”).

14.2.5. Warstwa [métier]

  • [InterfaceMétier] stanowi interfejs warstwy [métier];
  • [Métier] jest klasą implementacyjną warstwy [métier];
  • [Testmétier] to wersja testowa klasy [UnitTest], która z kolei jest wersją testową klasy [Métier];

14.2.5.1. Interfejs [InterfaceMétier]

Warstwa [métier] będzie implementować następujący interfejs [InterfaceMétier] (InterfaceMétier.py):


# importy
from abc import ABC, abstractmethod

from StatsForElève import StatsForElève


# interfejs biznesowy
class InterfaceMétier(ABC):
    # obliczanie statystyk dla ucznia
    @abstractmethod
    def get_stats_for_élève(self, idElève: int) -> StatsForElève:
        pass
  • [get_stats_for_élève] zwraca oceny ucznia nr idElève wraz z informacjami na ich temat: średnią ważoną, najniższą ocenę, najwyższą ocenę. Informacje te są zawarte w obiekcie typu [StatsForElève];

14.2.5.2. Entyteta [StatsForElève]

Typ [StatsForElève] (StatsForElève.py), który zawiera statystyki (oceny, min, max, średnia ważona) ucznia, wygląda następująco:


# importy
from BaseEntity import BaseEntity


# statystyki konkretnego ucznia


class StatsForElève(BaseEntity):
    # atrybuty wykluczone ze stanu klasy
    excluded_keys = []

    # właściwości klasy
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identyfikator oceny
        # uczeń: dany uczeń
        # oceny: jego oceny
        # moyennePondérée: średnia ważona według współczynników przedmiotów
        # min: jego najniższa ocena
        # max: jego ocena maksymalna

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

    # toString
    def __str__(self) -> str:
        # przypadek ucznia bez ocen
        if len(self.notes) == 0:
            return f"Elève={self.élève}, notes=[]"
        # przypadek ucznia z ocenami
        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}"

Uwagi:

  • wiersz 8: klasa [StatsForElève] wywodzi się z klasy [BaseEntity];
  • wiersze 13–22: właściwości klasy;
    • identyfikator [id] pochodzący z [BaseEntity];
    • uczeń [élève], którego statystyki są enkapsulowane;
    • jego oceny [notes];
    • jego średnia ważona [moyenne_pondérée];
    • jego najniższa ocena [min];
    • jego maksymalna ocena [max];
  • nie definiuje się metod getterów/setterów dla tych atrybutów. Zakłada się, że to warstwa [métier] tworzy obiekty tego typu i że nie tworzy ona obiektów nieprawidłowych;
  • wiersze 23–33: funkcja [__str__] zwraca ciąg znaków zawierający właściwości obiektu;

14.2.5.3. Implementacja [Métier]

Implementacja [Métier] (Metier.py) interfejsu [InterfaceMétier] będzie wyglądać następująco:


# importy
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from StatsForElève import StatsForElève


class Métier(InterfaceMétier):

    # konstruktor
    def __init__(self, dao: InterfaceDao):
        # zapisujemy parametr
        self.__dao = dao

    # -----------
    # interfejs
    # -----------

    # wskaźniki dotyczące ocen konkretnego ucznia
    def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
        # Statystyki dla ucznia o nr idEleve
        # id_élève: numer ucznia

        # oceny tego ucznia można pobrać za pomocą warstwy [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"]

        # zatrzymujemy się, jeśli nie ma ocen
        if len(notes) == 0:
            # zwracamy wynik
            return StatsForElève().fromdict({"élève": élève, "notes": []})

        # przetwarzanie ocen ucznia
        somme_pondérée = 0
        somme_coeff = 0
        max = -1
        min = 21
        for note in notes:
            # wartość oceny
            valeur = note.valeur
            # współczynnik przedmiotu
            coeff = note.matière.coefficient
            # suma współczynników
            somme_coeff += coeff
            # suma ważona
            somme_pondérée += valeur * coeff
            # wyszukiwanie wartości minimalnej
            if valeur < min:
                min = valeur
            # wyszukiwanie wartości maksymalnej
            if valeur > max:
                max = valeur
        # obliczanie brakujących wskaźników
        moyenne_pondérée = float(somme_pondérée) / somme_coeff

        # wynik jest przedstawiany w postaci typu [StatsForElève]
        return StatsForElève(). \
            fromdict({"élève": élève, "notes": notes,
                      "moyenne_pondérée": moyenne_pondérée,
                      "min": min, "max": max})

Uwagi

  • wiersz 7: klasa [Métier] wywodzi się z klasy [InterfaceMétier]. Zwyczajowo mówi się, że implementuje ona interfejs [InterfaceMétier];
  • wiersze 9–12: konstruktor przyjmuje jako jedyny parametr odwołanie do warstwy [dao]. W wierszu 10 należy zauważyć, że parametrowi [dao] nadano typ [InterfaceDao]. Nie oczekuje się konkretnej implementacji, a jedynie implementacji zgodnej z interfejsem [InterfaceDao]. W tym przypadku nie ma to znaczenia, ponieważ Python nie będzie brał pod uwagę tego typu, ale dobrą praktyką jest praca z interfejsami, a nie z konkretnymi implementacjami. Dzięki temu kod można łatwiej modyfikować;
  • wiersze 19–60: implementacja metody [get_stats_for_élève];
  • wiersz 19: metoda otrzymuje jeden parametr – numer [idElève] ucznia, którego statystyki chcemy uzyskać;
  • wiersz 24: wysyłane jest zapytanie do warstwy [dao] o oceny ucznia. Zapytanie to powoduje wyjątek, jeśli uczeń nie istnieje. Wyjątek ten nie jest obsługiwany (brak try / catch) i w związku z tym jest przekazywany do kodu wywołującego;
  • wiersz 25: dochodzimy do tego miejsca, jeśli nie wystąpił wyjątek. [notes_élève] jest wówczas słownikiem z dwoma kluczami [élève, note]:
    • wiersz 25: pobieramy informacje o uczniu (jego imię i nazwisko, klasę itp.);
    • wiersz 26: pobieramy oceny ucznia;
  • wiersze 28–31: sprawdzamy, czy uczeń ma oceny. Jeśli nie ma, nie ma statystyk do obliczenia;
  • wiersz 31: zwracamy obiekt [StatsForElève] utworzony na podstawie słownika za pomocą metody [BaseEntity.fromdict];
  • wiersze 33–54: wykorzystuje się oceny ucznia do obliczenia wymaganych statystyk. Komentarze w kodzie powinny wystarczyć do jego zrozumienia;
  • wiersze 56–60: zwracany jest obiekt [StatsForElève] utworzony na podstawie słownika za pomocą metody [BaseEntity.fromdict];

14.2.5.4. Test warstwy [métier]

Skrypt [UnitTest] z warstwy [métier] mógłby wyglądać następująco (TestMétier.py):


# importy
import unittest


class Testmétier(unittest.TestCase):
    def setUp(self):
        # konfiguracja aplikacji
        import config
        config.configure()

    def test_statsForEleve11(self):
        # importy
        from Dao import Dao
        from Métier import Métier
        # testujemy wskaźniki ucznia nr 11
        dao = Dao()
        stats_for_élève = Métier(dao).get_stats_for_élève(11)
        # wyświetlanie
        print(f"\nstats={stats_for_élève}")
        # sprawdzanie
        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()

Uwagi

  • wiersze 6–9: funkcja [setUp] służy tutaj do skonfigurowania ścieżki Python Path dla testu;
  • wiersz 16: tworzona jest instancja warstwy [dao];
  • wiersz 17: tworzona jest instancja warstwy [métier] i wykorzystywana jest jej metoda [get_stats_for_élève] do obliczenia statystyk ucznia nr 11;
  • wiersz 19: wyświetlany jest uzyskany wynik [StatsForElève]. Ponieważ [StatsForElève] wywodzi się z [BaseEntity], wyświetlany jest tutaj ciąg jSON z [StatsForElève];
  • wiersz 21: sprawdzana jest minimalna ocena ucznia;
  • wiersz 22: sprawdzana jest maksymalna ocena ucznia;
  • wiersz 23: sprawdzamy, czy średnia ważona wynosi 7,333 z dokładnością do 10⁻³. Zasadniczo nie jest możliwe dokładne porównywanie liczb rzeczywistych, ponieważ wewnętrznie są one najczęściej reprezentowane jedynie w przybliżeniu;

Wyniki testu są następujące:


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. Warstwa [ui]

Image

  • w [1] – interfejs warstwy [ui];
  • w [2] – implementacja tego interfejsu;
  • w [3] – główny skrypt aplikacji;

14.2.6.1. Interfejs [InterfaceUi]

Interfejs warstwy [UI] będzie wyglądał następująco:


# importy
from abc import ABC, abstractmethod


# interfejs UI
class InterfaceUi(ABC):
    # wykonanie warstwy UI
    @abstractmethod
    def run(self: object):
        pass

Uwagi

  • wiersze 9–10: warstwa [UI] będzie miała tylko jedną metodę, [run];

14.2.6.2. Implementacja [Console]

Warstwa [console] jest zaimplementowana przez następujący skrypt [Console.py]:


# importy warstw

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

# inne zależności
from MyException import MyException


class Console(InterfaceUi):
    # konstruktor
    def __init__(self: object, métier: InterfaceMétier):
        # domeny biznesowej: warstwa [métier]

        # zapisywanie atrybutów
        self.métier = métier


        # -----------
        # interfejs
        # -----------

    def run(self):
        # dialog z użytkownikiem
        fini = False
        while not fini:
            # pytanie / odpowiedź
            réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
            # czy to już wszystko?
            if réponse == "*":
                break
            # czy dane zostały wprowadzone poprawnie?
            ok = False
            try:
                id_élève = int(réponse, 10)
                ok = id_élève >= 1
            except ValueError as erreur:
                pass
            # dane poprawne?
            if not ok:
                print("Saisie incorrecte. Recommencez...")
                continue
            # obliczanie statystyk dla wybranego ucznia
            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}")
  • wiersze 3–5: import wszystkich interfejsów;
  • wiersz 11: klasa [Console] implementuje interfejs [InterfaceUi];
  • wiersze 12–17: konstruktor klasy [Console] przyjmuje jako parametr odwołanie do warstwy [métier]. Należy zauważyć, że temu parametrowi nadano typ [InterfaceMétier], aby podkreślić, że mamy do czynienia z interfejsami, a nie z konkretnymi implementacjami;
  • wiersz 24: implementacja metody [run] z interfejsu;
  • wiersz 27: pętla, która kończy się, gdy spełniony zostanie warunek z wiersza 31;
  • wiersz 29: wprowadzenie danych za pomocą klawiatury. Funkcja [input] przyjmuje opcjonalny parametr: komunikat, który ma zostać wyświetlony na ekranie z prośbą o wprowadzenie danych. Dane te są zawsze pobierane jako ciąg znaków. Funkcja [strip] usuwa z niego „spacje”, które znajdują się przed lub po nim;
  • wiersze 34–39: sprawdzamy, czy wprowadzona wartość, czyli numer ucznia, jest prawidłowa. Musi to być liczba całkowita >=1. Przypominamy, że wartość została wprowadzona jako ciąg znaków;
  • wiersz 36: próbuje się przekształcić wprowadzone dane w liczbę całkowitą w systemie dziesiętnym. Funkcja [int] zgłasza wyjątek, jeśli nie jest to możliwe;
  • wiersz 37: do tego miejsca dochodzi się tylko wtedy, gdy nie wystąpił wyjątek. Sprawdzamy, czy uzyskana liczba całkowita jest rzeczywiście >=1;
  • wiersze 38–39: obsługujemy wyjątek. Jeśli wystąpił wyjątek, zmienna [ok] z wiersza 34 pozostała na wartości [False];
  • wiersze 41–43: jeśli dane zostały wprowadzone nieprawidłowo, wyświetlany jest komunikat o błędzie i następuje powrót do początku pętli (wiersz 43);
  • wiersze 45–48: obliczane są statystyki ucznia, którego numer został wprowadzony;
  • wiersz 46: wykorzystuje się metodę [get_stats_for_élève] z warstwy [métier]. Metoda ta zgłasza wyjątek, jeśli uczeń nie istnieje. Wyjątek ten jest obsługiwany w wierszach 47–48. Wiadomo, że warstwy [dao] i [métier] generują wyjątek [MyException];

14.3. Główny skrypt [main]

Główny skrypt [main] wygląda następująco (main.py):


# konfiguracja aplikacji
import config

config = config.configure()

# ścieżka systemowa jest skonfigurowana – można przeprowadzić importy
from Console import Console
from Dao import Dao
from Métier import Métier

# ----------- warstwa [console]
try:
    # utworzono instancję warstwy [dao]
    dao = Dao()
    # instancja warstwy [métier]
    métier = Métier(dao)
    # instancja warstwy [ui]
    console = Console(métier)
    # wykonanie warstwy [console]
    console.run()
except BaseException as ex:
    # wyświetlany jest błąd
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    pass
  • wiersze 1–4: konfiguracja ścieżki Python Path aplikacji;
  • wiersze 6–9: importuje się potrzebne klasy i interfejsy;
  • wiersz 14: instancjonowanie warstwy [dao];
  • wiersz 16: instancja warstwy [métier];
  • wiersz 18: tworzenie instancji warstwy [ui];
  • wiersz 20: uruchamiamy okno dialogowe z użytkownikiem;
  • wiersze 13–20: zazwyczaj z tych wierszy nie wynika żaden wyjątek. Wyjątki pochodzące z warstw [dao] i [métier] są zatrzymywane przez warstwę [Console]. Obsługa wyjątków to trudna sztuka, gdy nie znamy dokładnie używanych warstw (w tym przypadku tak właśnie jest). W razie wątpliwości można dodać kod, który przechwytuje każdy rodzaj wyjątku, jaki może zostać wygenerowany przez wykonywany kod. Tak właśnie zrobiono tutaj, w wierszach 21–23. Przechwytujemy wszystkie wyjątki pochodzące od [BaseException], czyli wszystkie wyjątki;
  • wiersze 24–25: klauzula [finally] nie ma tutaj żadnego znaczenia. Znajduje się tam wyłącznie po to, aby umożliwić skomentowanie wierszy 21–23. W trybie debugowania nie ma bowiem sensu zatrzymywać wyjątków. W tym przypadku to interpreter języka Python je zatrzymuje i podaje numer linii, w której wystąpił wyjątek. Jest to niezbędna informacja. Gdy linie 21–23 są skomentowane, obecność linii 24–25 pozwala na uzyskanie składniowo poprawnego bloku try/catch. W przypadku ich braku Python zgłasza błąd;

Oto przykład wykonania:


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. Przykład 2

Ten nowy przykład architektury warstwowej ma na celu pokazanie zalet programowania opartego na interfejsach. Ułatwia ono konserwację i testowanie aplikacji. Ponownie wykorzystamy architekturę trójwarstwową:

Image

Każda warstwa zostanie zaimplementowana na dwa różne sposoby. Chcemy pokazać, że można łatwo zmienić implementację jednej warstwy, wywierając minimalny wpływ na pozostałe.

14.4.1. Warstwa [dao]

Image

Interfejs [InterfaceDao] wygląda następująco:


# importy
from abc import ABC, abstractmethod


# interfejs DAO
class InterfaceDao(ABC):
    # jedna metoda
    @abstractmethod
    def do_something_in_dao_layer(self, x: int, y: int) -> int:
        pass
  • wiersze 8–10: metoda [do_something_in_dao_layer] jest jedyną metodą tego interfejsu;

Klasa [DaoImpl1] implementuje interfejs [InterfaceDao] w następujący sposób:


from InterfaceDao import InterfaceDao


class DaoImpl1(InterfaceDao):
    # implementacja InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x + y

Klasa [DaoImpl2] implementuje interfejs [InterfaceDao] w następujący sposób:


from InterfaceDao import InterfaceDao


class DaoImpl2(InterfaceDao):
    # implementacja InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x - y

14.4.2. Warstwa [métier]

Image

Interfejs [InterfaceMétier] wygląda następująco:


# importy
from abc import ABC, abstractmethod


# interfejs biznesowy
class InterfaceMétier(ABC):
    # pojedyncza metoda
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • wiersze 8–10: metoda [do_something_in_métier_layer] jest jedyną metodą tego interfejsu;

Klasa [AbstractBaseMétier] implementuje interfejs [InterfaceMétier] w następujący sposób:


# importy
from abc import ABC, abstractmethod

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


class AbstractBaseMétier(InterfaceMétier, ABC):
    # właściwości
    # __dao jest odwołaniem w warstwie [dao]
    @property
    def dao(self) -> InterfaceDao:
        return self.__dao

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

    # implementacja interfejsu [InterfaceMétier]
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • wiersz 8: klasa [AbstractBaseMétier] wywodzi się z dwóch klas:
    • [InterfaceMétier]: klasa [AbstractBaseMétier] implementuje ten interfejs w wierszach 19–22. W rzeczywistości widać, że nie zaimplementowała ona metody [do_something_in_métier_layer], którą zadeklarowała jako abstrakcyjną (wiersz 20). Zadaniem klas pochodnych będzie zaimplementowanie tej metody;
    • [ABC], aby uzyskać dostęp do adnotacji [@abstractmethod];
    • kolejność ma znaczenie: jeśli ją tutaj odwrócimy, Python zgłosi błąd podczas wykonywania;

Po raz pierwszy stosujemy dziedziczenie wielokrotne (dziedziczenie po wielu klasach). Klasa [AbstractBaseMétier] dziedziczy jednocześnie właściwości klas [InterfaceMétier] i [ABC].

  • wiersze 9–17: definiujemy właściwość [dao], która będzie odwoływaniem się do warstwy [dao];

Interfejs jest przeznaczony do implementacji. Gdy różne implementacje mają wspólne właściwości, warto umieścić je w klasie nadrzędnej, aby uniknąć ich powielania. Tak jest w tym przypadku z właściwością [dao]. Klasa nadrzędna jest zazwyczaj zawsze abstrakcyjna, ponieważ nie jest w stanie zaimplementować wszystkich metod interfejsu.

Klasa [MétierImpl1] implementuje interfejs [InterfaceMétier] w następujący sposób:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl1(AbstractBaseMétier):
    # implementacja interfejsu [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)
  • wiersz 4: klasa [MétierImpl1] wywodzi się z klasy [AbstractbaseMétier]. Dziedziczy zatem właściwość [dao] tej klasy;
  • wiersze 6–9: implementacja interfejsu [InterfaceMétier], którego nie zaimplementowała klasa nadrzędna [AbstractbaseMétier];
  • wiersz 9: wykorzystuje się warstwę [dao];

Klasa [MétierImpl2] implementuje interfejs [InterfaceMétier] w podobny sposób:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl2(AbstractBaseMétier):
    # implementacja interfejsu [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. Warstwa [ui]

Image

Interfejs [InterfaceUi] ma następujący wygląd:


# importy
from abc import ABC, abstractmethod


# interfejs Ui
class InterfaceUi(ABC):
    # jedna metoda
    @abstractmethod
    def do_something_in_ui_layer(self, x: int, y: int) -> int:
        pass
  • wiersze 8–10: jedyna metoda interfejsu;

Klasa [AbstractBaseUi] implementuje interfejs [InterfaceUi] w następujący sposób:


# importy
from abc import ABC, abstractmethod

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


class AbstractBaseUi(InterfaceUi, ABC):
    # właściwości
    # biznesowy jest odniesieniem w warstwie [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

    # implementacja interfejsu [InterfaceUI]
    @abstractmethod
    def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
        pass
  • klasa [AbstractBaseUi] jest klasą abstrakcyjną (wiersz 20). Musi zostać odziedziczona, aby zaimplementować interfejs [InterfaceUi];
  • wiersze 9–17: klasa [AbstractBaseUi] posiada odwołanie do warstwy [métier];

Klasa implementacyjna [UiImpl1] ma następującą postać:


from AbstractBaseUi import AbstractBaseUi


class UiImpl1(AbstractBaseUi):
    # implementacja interfejsu [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)
  • wiersz 4: klasa [UiImpl1] wywodzi się z klasy [AbstractBaseUi] i w związku z tym dziedziczy jej właściwość [métier]. Jest ona używana w wierszu 9;

Klasa implementacyjna [UiImpl2] jest analogiczna:


from AbstractBaseUi import AbstractBaseUi


class UiImpl2(AbstractBaseUi):
    # implementacja interfejsu [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)
  • wiersz 4: klasa [UiImpl2] wywodzi się z klasy [AbstractBaseUi] i w związku z tym dziedziczy jej właściwość [métier]. Właściwość ta jest wykorzystywana w wierszu 9;

14.4.4. Pliki konfiguracyjne

Image

  • Pliki [config1, config2] konfigurują aplikację na dwa różne sposoby;
  • plik [main] jest głównym skryptem aplikacji;

Plik [config1] ma następującą treść:


def configure():
    # krok 1 ------
    # absolutna ścieżka do folderu tego skryptu
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # zależności
    absolute_dependencies = [
        # lokalne katalogi w ścieżce Python Path
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # konfigurujemy ścieżkę syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # krok 2 ------
    # konfiguracja warstw aplikacji
    from DaoImpl1 import DaoImpl1
    from MétierImpl1 import MétierImpl1
    from UiImpl1 import UiImpl1
    # instancjonowanie warstw
    # DAO
    dao = DaoImpl1()
    # logika biznesowa
    métier = MétierImpl1()
    métier.dao = dao
    # interfejs użytkownika
    ui = UiImpl1()
    ui.métier = métier

    # instancje warstw umieszczamy w konfiguracji
    # tutaj potrzebna jest tylko warstwa ui
    config = {"ui": ui}

    # przekazujemy konfigurację
    return config
  • wiersze 2–16: konfiguracja ścieżki Python Path aplikacji;
  • wiersze 18–31: instancjonowanie warstw [dao, métier, ui]. Aby zaimplementować ich interfejsy, za każdym razem wybiera się pierwszą skonstruowaną implementację;
  • wiersze 33–35: umieszczamy odniesienia do warstw w konfiguracji. W tym przypadku główny skrypt potrzebuje jedynie warstwy [ui];

Plik [config2] działa analogicznie i implementuje każdy interfejs przy użyciu drugiej dostępnej implementacji:


def configure():
    # krok 1 ---
    # absolutna ścieżka do folderu tego skryptu
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # zależności
    absolute_dependencies = [
        # lokalne foldery w ścieżce Python Path
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # konfiguracja ścieżki syspath
    from myutils import set_syspath

    set_syspath(absolute_dependencies)

    # krok 2 ------
    # konfiguracja warstw aplikacji
    from DaoImpl2 import DaoImpl2
    from MétierImpl2 import MétierImpl2
    from UiImpl2 import UiImpl2
    # instancjonowanie warstw
    # DAO
    dao = DaoImpl2()
    # logika biznesowa
    métier = MétierImpl2()
    métier.dao = dao
    # interfejs użytkownika
    ui = UiImpl2()
    ui.métier = métier

    # instancje warstw umieszczamy w konfiguracji
    # tutaj potrzebna jest tylko warstwa ui
    config = {"ui": ui}

    # tworzymy konfigurację
    return config

14.4.5. Główny skrypt [main]

Image

Główny skrypt ma następującą postać:


# importy
import importlib
import sys

# main ---------

# potrzebne są dwa argumenty
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()

# konfiguracja aplikacji
module = importlib.import_module(sys.argv[1])
config = module.configure()

# uruchomienie warstwy [ui]
print(config["ui"].do_something_in_ui_layer(10, 20))

Skrypt ten przyjmuje jeden parametr:

  • [config1], aby użyć konfiguracji nr 1;
  • [config2], aby użyć konfiguracji nr 2;

Python zapisuje parametry na liście [sys.argv]:

  • sys.argv[0] to nazwa skryptu, w tym przypadku [main]. Ten parametr jest zawsze obecny;
  • sys.argv[1] to pierwszy parametr przekazany do skryptu, sys.argv[2] to drugi, …

  • wiersz 8: pobieramy liczbę parametrów;

  • wiersze 9–11: sprawdzamy, czy rzeczywiście istnieje parametr i czy jego wartość to albo [config1], albo [config2]. Jeśli tak nie jest, wyświetlany jest komunikat o błędzie (wiersz 10) i program zostaje zakończony (wiersz 11);

Gdy znana jest już żądana konfiguracja, należy ją wykonać. Na przykład, jeśli wybrano konfigurację 1, należy wykonać kod:

import config1
config1.configure()

Problem polega na tym, że konfiguracja, której należy użyć, znajduje się w zmiennej o nazwie [len[liste1]]. Aby zaimportować moduł, którego nazwa znajduje się w zmiennej, musimy użyć pakietu [importlib] (wiersz 2).

  • wiersz 14: importujemy moduł, którego nazwa znajduje się w [sys.argv[1];
  • wiersz 15: po wykonaniu tej czynności wywołujemy funkcję [configure] z tego modułu. Otrzymujemy słownik [config], który stanowi konfigurację aplikacji;
  • wiersz 18: wiadomo, że w config[‘ui’] znajduje się odwołanie do warstwy [ui]. Wykorzystuje się je do wywołania metody [do_something_in_ui_layer]. Wiadomo, że ta metoda wywoła metodę z warstwy [métier], która z kolei wywoła metodę z warstwy [dao];

Na przykład funkcja [do_something_in_ui_layer] ma następującą postać:


class UiImpl1(AbstractBaseUi):
    # implementacja interfejsu [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)
  • Wiersz 6 powyżej wykorzystuje właściwość [métier] klasy [UiImpl1], wiersz 1. Jednak w konfiguracji [config1] zapisano:

# funkcjonalność biznesowa
    métier = MétierImpl1()
    métier.dao = dao
    # interfejs użytkownika
    ui = UiImpl1()
    ui.métier = métier
  • wiersz 6: właściwość [métier] klasy [UIImpl1] stanowi odwołanie do klasy [MétierImpl1] (wiersz 2). W związku z tym zostanie wykonana metoda [do_something_in_ui_layer] klasy [MétierImpl1];

W klasie [MétierUiImpl1] zapisano:


class MétierImpl1(AbstractBaseMétier):
    # implementacja interfejsu [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)
  • w wierszu 6 metoda wywołana przez warstwę [ui] z kolei wywołuje metodę właściwości [dao] klasy [MétierImpl1];

Natomiast w konfiguracji [config1] zapisano:


# DAO
    dao = DaoImpl1()
    # biznes
    métier = MétierImpl1()
    métier.dao = dao
  • wiersz 5: właściwość [MétierImpl1.dao] jest typu [DaoImpl1] (wiersz 2);

Chcemy tutaj pokazać, że skrypt [main] nie musi zajmować się warstwami [métier] i [dao]. Skrypt ten musi zajmować się wyłącznie warstwą [ui], ponieważ powiązania między tą warstwą a pozostałymi zostały ustanowione w ramach konfiguracji.

Image

Aby przekazać parametr [config1] lub [config2] do skryptu [main], należy postępować w następujący sposób:

Image

  • w [1-2] należy utworzyć tzw. konfigurację wykonania;
  • w [3] nadajemy nazwę tej konfiguracji, aby móc ją później odnaleźć;
  • w [4] wybieramy skrypt do wykonania. Jeśli postępowaliśmy zgodnie z procedurą [1-2], właściwy skrypt został już wybrany;
  • w [5] należy wprowadzić parametry, które mają zostać przekazane do skryptu. Przekazujemy tutaj ciąg znaków [config1], aby skrypt użył konfiguracji nr 1;
  • w [6] zatwierdza się konfigurację wykonania;

Image

  • w [1-2] wyświetla się listę istniejących kontekstów wykonania;
  • w [3] wybieramy istniejący kontekst wykonania i duplikujemy go do [4];

Image

  • w [5] podaje się nazwę nowej konfiguracji. To właśnie ona uruchomi skrypt [main] [6], przekazując mu parametr [config2] [7];

Konfiguracje uruchamiania są dostępne w prawym górnym rogu okna PyCharm:

Image

Wystarczy wybrać [2] lub [3], a następnie nacisnąć [4], aby uruchomić skrypt [main] zjednym z parametrów: [config1] lub [config2].

W przypadku [config1] uruchomienie skryptu [main] daje następujące wyniki:


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

W przypadku [config2] uruchomienie [main] daje następujące wyniki:


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

Zachęcamy czytelnika do sprawdzenia tych wyników.