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ą:

- 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:

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:

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

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]

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]

- 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ą:

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]

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]

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]

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

- 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]

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:
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.

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

- 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;

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

- 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:

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.













