14. Schichtenarchitektur und Programmierung über Schnittstellen
14.1. Einleitung
Wir wollen eine Anwendung schreiben, mit der die Noten der Schüler einer Mittelschule angezeigt werden können. Diese Anwendung kann eine mehrschichtige Architektur haben:

- Die Schicht [ui] (Benutzeroberfläche) ist die Schicht, die mit dem Benutzer der Anwendung in Kontakt steht;
- Die Schicht [métier] implementiert die Geschäftsregeln der Anwendung, wie beispielsweise die Berechnung eines Gehalts oder einer Rechnung. Diese Schicht nutzt Daten, die vom Benutzer über die Schicht [présentation] und von den Schichten SGBD über die Schicht [dao] bereitgestellt werden;
- die Schicht [dao] (Data Access Objects) verwaltet den Zugriff auf die Daten aus der Schicht SGBD (Datenbankmanagementsystem).
Dies ist die Architektur, die im |Kurs zu Python 2| verwendet wurde. Man kann auch eine Variante einführen:

Die Unterschiede zur vorherigen Schichtenstruktur sind folgende:
- Ein oben als [main] bezeichnetes Hauptskript organisiert die Instanziierung der Schichten;
- die Schichten [ui, métier, dao] kommunizieren nicht mehr zwangsläufig untereinander. Falls dies erforderlich ist, stellt das Skript [main] ihnen die Referenzen der benötigten Schichten zur Verfügung;
Der Code ist hier in Kompetenzzentren mit einem „Dirigenten“ organisiert:
- Der „Dirigent“ ist das Hauptskript [main];
- Die Ebenen [ui], [dao] und [métier] sind die Kompetenzzentren;
Man könnte diese Organisation als orchestrale Organisation bezeichnen.
14.2. Beispiel 1
Wir werden die Schichtenarchitektur anhand einer einfachen Konsolenanwendung veranschaulichen:
- Es wird keine Datenbank geben;
- Die Schicht [dao] verwaltet die Entitäten Elève, Classe, Matière und Note, mit denen die Noten der Schüler verwaltet werden können;
- Die Schicht [métier] ermöglicht die Berechnung von Kennzahlen zu den Noten eines bestimmten Schülers;
- Die Ebene [ui] ist eine Konsolenanwendung, die die Ergebnisse der Schüler anzeigt;
Das Projekt PyCharm der Anwendung sieht wie folgt aus:
![]() |
Hinweis: Die blau markierten Ordner gehören zu [Sources Root] des Projekts PyCharm.
14.2.1. Die Entitäten der Anwendung
Als Entitäten bezeichnen wir Klassen, deren einzige Aufgabe darin besteht, Daten zu kapseln. Dazu könnten auch Dictionaries verwendet werden. Der Vorteil der Klasse besteht darin, dass sie es ermöglicht, die Gültigkeit der im Objekt gespeicherten Daten zu prüfen und eine Methode bereitzustellen, die die Identität des Objekts in Form einer Zeichenkette liefert.
![]() |
14.2.1.1. Die Entität [Classe]
Die Entität [Classe] (Classe.py) repräsentiert eine Klasse der Schule:
# Importe
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Classe(BaseEntity):
# aus dem Klassenstatus ausgeschlossene Attribute
excluded_keys = []
# Eigenschaften der Klasse
@staticmethod
def get_allowed_keys() -> list:
# ID: Kennung der Klasse
# Name: Name der Klasse
return BaseEntity.get_allowed_keys() + ["nom"]
# Getter
@property
def nom(self: object) -> str:
return self.__nom
# Setter
@nom.setter
def nom(self: object, nom: str):
# name muss eine nicht leere Zeichenkette sein
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")
Anmerkungen
- Zeile 7: Die Entität [Classe] leitet sich von der Entität [BaseEntity] ab, die im Abschnitt |Die Klasse BaseEntity| behandelt wurde;
- Zeilen 11–16: Eine Klasse wird durch eine Nummer id und eine nom (Zeile 16) definiert. Die Eigenschaft [id] wird von der Klasse [BaseEntity] bereitgestellt und der Name von der Klasse [Classe];
- Zeilen 18–30: Getter/Setter für das Attribut [nom];
14.2.1.2. Die Entität [Matière]
Die Klasse [Matière] (matière.py) lautet wie folgt:
# Importe
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Matière(BaseEntity):
# aus dem Klassenstatus ausgeschlossene Attribute
excluded_keys = []
# Eigenschaften der Klasse
@staticmethod
def get_allowed_keys() -> list:
# ID: Kennung des Fachs
# Name: Name des Fachs
# Koeffizient: Koeffizient des Fachs
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
# Setter
@nom.setter
def nom(self: object, nom: str):
# Der Name muss eine nicht leere Zeichenkette sein
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):
# Der Koeffizient muss eine reelle Zahl >= 0 sein
erreur = False
if isinstance(coefficient, (int, float)):
if coefficient >= 0:
self.__coefficient = coefficient
else:
erreur = True
else:
erreur = True
# Fehler?
if erreur:
raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")
Anmerkungen
- Zeile 7: Die Klasse [Classe] leitet sich von der Klasse [BaseEntity] ab;
- Zeilen 11–17: Ein Fach wird durch seine Nummer [id], seinen Namen [nom] und seinen Koeffizienten [coefficient] definiert;
- Zeilen 19–50: Getter/Setter für die Attribute der Klasse;
14.2.1.3. Die Entität [Elève]
Die Klasse [Elève] (élève.py) sieht wie folgt aus:
# Importe
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException
from Utils import Utils
class Elève(BaseEntity):
# Aus dem Klassenstatus ausgeschlossene Attribute
excluded_keys = []
# Eigenschaften der Klasse
@staticmethod
def get_allowed_keys() -> list:
# ID: Kennung des Schülers
# Nachname: Nachname des Schülers
# Vorname: Vorname des Schülers
# Klasse: Klasse des Schülers
return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]
# Getter
@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
# Setter
@nom.setter
def nom(self: object, nom: str) -> str:
# Nachname muss eine nicht leere Zeichenkette sein
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:
# Vorname muss eine nicht leere Zeichenkette sein
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:
# Es wird ein Klassentyp erwartet
if isinstance(value, Classe):
self.__classe = value
# oder ein Typ „dict“
elif isinstance(value,dict):
self.__classe=Classe().fromdict(value)
# oder ein Typ „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}")
Anmerkungen
- Zeile 9: Die Klasse [Elève] leitet sich von der Klasse [BaseEntity] ab;
- Zeilen 13–20: Ein Schüler wird durch seine Nummer [id], seinen Nachnamen [nom], seinen Vornamen [prénom] und seine Klasse [classe] charakterisiert. Dieser letzte Parameter ist ein Verweis auf ein Objekt [Classe];
- Zeilen 22–65: Getter/Setter für die Attribute der Klasse;
14.2.1.4. Die Entität [Note]
Die Klasse [Note] (note.py) sieht wie folgt aus:
# Importe
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException
class Note(BaseEntity):
# Attribute, die aus dem Zustand der Klasse ausgeschlossen sind
excluded_keys = []
# Eigenschaften der Klasse
@staticmethod
def get_allowed_keys() -> list:
# ID: Kennung der Note
# Wert: die Note selbst
# Schüler: Schüler (vom Typ „Schüler“), auf den sich die Note bezieht
# Fach: Fach (vom Typ „Fach“), auf das sich die Note bezieht
# Das Objekt „Note“ ist also die Note eines Schülers in einem Fach
return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]
# Getter
@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
# Getter
@valeur.setter
def valeur(self: object, valeur: float):
# Die Note muss eine reelle Zahl zwischen 0 und 20 sein
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:
# Es wird der Typ „Schüler“ erwartet
if isinstance(value, Elève):
self.__élève = value
# oder vom Typ „dict“
elif isinstance(value, dict):
self.__élève = Elève().fromdict(value)
# oder vom Typ „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:
# Es wird ein Typ „Fach“ erwartet
if isinstance(value, Matière):
self.__matière = value
# oder einen Typ „dict“
elif isinstance(value, dict):
self.__matière = Matière().fromdict(value)
# oder einen Typ „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}")
Anmerkungen
- Zeile 8: Die Klasse [Note] leitet sich von der Klasse [BaseEntity] ab;
- Zeilen 12–20: Ein Objekt [Note] wird durch seine Nummer [id], den Wert der Note [valeur], eine Referenz [élève] auf den Schüler, der diese Note hat, sowie eine Referenz auf das Fach [matière], das Gegenstand der Note ist;
- Zeilen 22–75: Getter/Setter der Attribute der Klasse;
14.2.2. Konfiguration der Anwendung
![]() |
Die Datei [config.py] konfiguriert die Umgebung des Hauptskripts [main] (1) sowie die der Tests (2). Alle diese Skripte enthalten am Anfang des Codes eine Anweisung [import config]. Zur Erinnerung: Der Ordner, der das Skript enthält, auf das sich der Befehl [python script] bezieht, ist automatisch Teil von Python Path.Si. Da sich [config] also im selben Ordner befindet wie die Skripte mit der Anweisung [import config], wird es gefunden. Die Dateien [1] und [2] sind hier identisch. Dies muss jedoch nicht unbedingt der Fall sein.
Die Datei [config.sys] lautet wie folgt:
def configure():
import os
# absoluter Pfad zum Ordner dieses Skripts
script_dir = os.path.dirname(os.path.abspath(__file__))
# root_dir
root_dir="C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
# absolute Abhängigkeiten
absolute_dependencies=[
# lokale Ordner, die Klassen und Schnittstellen enthalten
f"{root_dir}/02/entities",
f"{script_dir}/../entities",
f"{script_dir}/../interfaces",
f"{script_dir}/../services",
]
# Aktualisierung des Syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# die Konfiguration wird angepasst
return {}
- Zeilen 11–14: Die Ordner, die Teil des Python-Pfads sein müssen (sys.path);
- der Ordner [f"{root_dir}/02/entities"] ermöglicht den Zugriff auf die Klassen [BaseEntity] und [MyException];
- Der Ordner „[f"{script_dir}/../entities"]“ bietet Zugriff auf die Klassen „[Elève]“, „[Classe]“, „[Matière]“ und „[Note]“;
- der Ordner „[f"{script_dir}/../interfaces",]“ bietet Zugriff auf die Schnittstellen der Anwendung;
- Der Ordner „[f"{script_dir}/../services"]“ bietet Zugriff auf die Klassen, die die Schnittstellen implementieren;
14.2.3. Tests der Entitäten
![]() |
Hier werden wir Tests schreiben, die von einem Tool namens [unittest] ausgeführt werden. PyCharm wird mit mehreren Test-Frameworks ausgeliefert. Die Auswahl eines dieser Frameworks erfolgt in der Konfiguration von PyCharm:

- In [4] stehen mehrere Test-Frameworks zur Verfügung:

14.2.3.1. Die Testklasse [TestBaseEntity]
Das Testskript [TestBaseEntity] sieht wie folgt aus:
import unittest
# die Anwendung wird konfiguriert
import config
config = config.configure()
class TestBaseEntity (unittest.TestCase):
def test_note1(self):
# Importe
from Note import Note
from Elève import Elève
from Classe import Classe
from Matière import Matière
# Erstellung einer Note aus einer Zeichenkette jSON
note = Note().fromjson(
'{"id": 8, "wert": 12, "Schüler": {"id": 42, "Nachname": "Nachname4", "Vorname": "Vorname4", "Klasse": {"id": 2, "name": "Klasse2"}}, "Fach": {"id": 2, "name": "Fach2", "Gewichtung": 2}}')
# Überprüfungen
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):
# Importe
from Note import Note
from Elève import Elève
from Classe import Classe
from Matière import Matière
# Erstellung einer Note anhand eines Wörterbuchs
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}})
# Prüfungen
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()
Hinweise
- Zeile 1: Das Modul [unittest] wird importiert, das die verschiedenen Testmethoden bereitstellt;
- Zeilen 3–6: Die Anwendung wird so konfiguriert, dass die für die Tests erforderlichen Klassen gefunden werden;
- Zeile 9: Eine Testklasse [unittest] muss die Klasse [unittest.TestCase] erweitern;
- Zeilen 11, 27: Die Testfunktionen müssen einen Namen haben, der mit „[test]“ beginnt, andernfalls werden sie nicht erkannt;
- Zeilen 13–16: Die benötigten Klassen werden importiert;
- In dieser Testklasse soll das Verhalten der Methoden [BaseEntity.fromdict] (Zeile 34) und [BaseEntity.fromjson] (Zeile 18) überprüft werden. Die Klasse [Note] verfügt über Eigenschaften, die Verweise auf andere Klassen sind. Wir wollen überprüfen, ob die beiden vorgenannten Methoden gültige Objekte vom Typ [Note] erstellen;
- Zeile 18: Es wird ein Objekt vom Typ [Note] ausgehend von einem Objekt vom Typ jSON erstellt;
- Zeile 21: Es wird überprüft, ob das erstellte Objekt tatsächlich vom Typ [Note] ist. Die Methode [assertIsInstance] ist eine Methode der Klasse [unittest.TestCase], der übergeordneten Klasse der Klasse [TestBaseEntity];
- Zeile 22: Es wird überprüft, ob [note.élève] tatsächlich vom Typ [Elève] ist;
- Zeile 23: Es wird überprüft, ob [note.élève.classe] tatsächlich vom Typ [Classe] ist;
- Zeile 24: Es wird überprüft, ob [note.matière] tatsächlich vom Typ [Matière] ist;
- Zeilen 33–42: Das Gleiche wird mit der Methode [BaseEntity.fromdict] durchgeführt;
Es gibt mehrere Möglichkeiten, die Tests auszuführen:
![]() |
- Bei [1-2] wird [TestBaseEntity] mit dem Framework [UnitTest] ausgeführt;
- In [3-5] schlagen die Tests fehl. [UnitTests] meldet, dass keine auszuführenden Tests gefunden wurden;
Das Scheitern der Tests ist auf die Struktur des Codes von [TestBaseEntity] zurückzuführen:
import unittest
# Konfiguration der Anwendung
import config
config = config.configure()
class TestBaseEntity(unittest.TestCase):
Was das Framework [UnitTest] stört, ist das Vorhandensein von ausführbarem Code in den Zeilen 3–6 vor der Definition der Testklasse in Zeile 9.
Wir strukturieren den Code daher wie folgt um:
import unittest
class TestBaseEntity(unittest.TestCase):
def setUp(self):
# Konfiguration der Anwendung
import config
config.configure()
def test_note1(self):
…
def test_note2(self):
…
if __name__ == '__main__':
unittest.main()
- Zeilen 6–10: Es wird eine Funktion [setUp] definiert. Diese Funktion hat eine besondere Rolle: Sie wird vor jeder Testfunktion (test_note1, test_note2) ausgeführt;
Danach liefert die Ausführung der Klasse [TestBaseEntity] folgende Ergebnisse:
![]() |
Diesmal wurden beide Testmethoden ausgeführt und die Tests waren erfolgreich.
Schauen wir uns an, was passiert, wenn ein Test fehlschlägt. Ändern wir den Code von [test_note1] wie folgt:
def test_note1(self):
# Absichtlicher Fehler – Überprüfung, ob 1 == 2
self.assertEqual(1,2)
# Importe
from Note import Note
…
- Zeile 2: Es wird geprüft, ob 1 == 2 ist;
Die Ergebnisse der Ausführung lauten dann wie folgt:
![]() |
Die Fehlerursache lässt sich durch Klicken auf den fehlgeschlagenen Test [2] ermitteln:
![]() |
- in [7-8], die Fehlerursache;
Eine weitere Möglichkeit, eine Testklasse auszuführen, besteht darin, sie in einem Terminal auszuführen:
![]() |
(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
Zeile 6 zeigt an, dass beide Tests erfolgreich waren (der Fehler „1==2“ wurde behoben);
Schließlich gibt es noch eine dritte Möglichkeit, die Testklasse [TestBaseEntity] ebenfalls in einem Terminal auszuführen. Die Testklasse wird mit den folgenden Zeilen 6–7 abgeschlossen;
…
self.assertIsInstance(note.élève.classe, Classe)
self.assertIsInstance(note.matière, Matière)
if __name__ == '__main__':
unittest.main()
- Zeile 6: Die Variable [__name__] ist der Name des Skripts, das ausgeführt wird. Wenn es sich bei dem Skript um das durch den Befehl [python script.py] gestartete Skript handelt, hat die Variable [__name__] den Wert [__main__] (zwei Unterstriche vor und nach der Kennung). Somit wird Zeile 7 nur ausgeführt, wenn das Skript [TestBaseEntity] durch den Befehl [python TestBaseEntity.py] gestartet wird. Die Anweisung [unittest.main()] startet die Ausführung des Skripts über das Framework [UnitTest]. Hier ein Beispiel:
(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. Die Testklasse [TestEntités]
Die Testklasse [TestEntités] lautet wie folgt:
import unittest
class TestEntités(unittest.TestCase):
def setUp(self):
# Die Anwendung wird konfiguriert
import config
config.configure()
def test_code1a(self):
# Importe
from Elève import Elève
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültige ID
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
# Überprüfung
self.assertEqual(code, 1)
def test_code41(self):
# Importe
from Elève import Elève
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültiger Name
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
# Überprüfung
self.assertEqual(code, 41)
def test_code42(self):
# Importe
from Elève import Elève
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültiger Vorname
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
# Überprüfung
self.assertEqual(code, 42)
def test_code43(self):
# Importe
from Elève import Elève
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültige Klasse
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
# Überprüfung
self.assertEqual(code, 43)
def test_code1b(self):
# Importe
from Classe import Classe
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültige Kennung
Classe().fromdict({"id": "x", "nom": "y"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# Überprüfung
self.assertEqual(code, 1)
def test_code11(self):
# Importe
from Classe import Classe
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültiger Name
Classe().fromdict({"id": 1, "nom": ""})
except MyException as ex:
code = ex.code
# Überprüfung
self.assertEqual(code, 11)
def test_code1c(self):
# Importe
from Matière import Matière
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültige Kennung
Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# Überprüfung
self.assertEqual(code, 1)
def test_code21(self):
# Importe
from Matière import Matière
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültiger Name
Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# Überprüfung
self.assertEqual(code, 21)
def test_code22(self):
# Importe
from Matière import Matière
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültiger Koeffizient
Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# Überprüfung
self.assertEqual(code, 22)
def test_code1d(self):
# Importe
from Note import Note
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültige Kennung
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
# Überprüfung
self.assertEqual(code, 1)
def test_code31(self):
# Importe
from Note import Note
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültiger Wert
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
# Überprüfung
self.assertEqual(code, 31)
def test_code32(self):
# Importe
from Note import Note
from MyException import MyException
# Fehlercode
code = None
try:
# ungültiger Schüler
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
# Überprüfung
self.assertEqual(code, 32)
def test_code33(self):
# Importe
from Elève import Elève
from Note import Note
from Classe import Classe
from MyException import MyException
# Fehlercode
code = None
try:
# Ungültiges Fach
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
# Überprüfung
self.assertEqual(code, 33)
def test_exception(self):
# Importe
from Elève import Elève
# Der Test muss den Typ [MyException] ausführen, um erfolgreich zu sein
from MyException import MyException
with self.assertRaises(MyException):
# Der Test
Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})
if __name__ == '__main__':
unittest.main()
- Das Testskript dient dazu, die Setter der Klassen zu testen: Es soll überprüft werden, ob den Attributen der verschiedenen Entitäten keine falschen Werte zugewiesen werden können;
- Zeilen 11–24: Es wird getestet, ob einem Schüler keine ungültige Kennung zugewiesen werden kann. Da in Zeile 16 der Wert „x“ als Kennung des Schülers übergeben wird, ist eine Ausnahme zu erwarten. Daher sollte das Skript die Zeilen 20–22 durchlaufen;
- Zeile 21: Anzeige der Fehlermeldung;
- Zeile 22: Der Fehlercode wird abgerufen (siehe Abschnitt |Die Entität MyException|);
- Zeile 24: Es wird überprüft (Assert), ob der Fehlercode 1 ist. Hier werden zwei Dinge überprüft:
- dass tatsächlich ein Fehler aufgetreten ist;
- dass der Fehlercode 1 ist;
- dieser Vorgang wird mit den Funktionen in den Zeilen 24–213 wiederholt;
- Zeilen 215–222: Es wird geprüft, ob eine Aktion eine Ausnahme eines bestimmten Typs auslöst;
- Zeile 220: Es wird angegeben, dass der Test erfolgreich ist, wenn eine Ausnahme vom Typ [MyException] ausgelöst wird;
Ergebnisse
Das Testskript wird ausgeführt:
![]() |
Die folgenden Ergebnisse werden erzielt:
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
Hier wurden alle Tests erfolgreich bestanden
14.2.4. Die Schicht [dao]

Die Schicht [dao] implementiert die Schnittstelle [InterfaceDao] [1]. Diese wird durch die Klasse [Dao] (2) implementiert. Das Skript [tests_dao] (3) testet die Methoden der Schicht [dao].
14.2.4.1. Schnittstelle [InterfaceDao]
Eine Schnittstelle ist eine Vereinbarung zwischen aufrufendem und aufgerufenem Code. Der aufgerufene Code stellt die Schnittstelle bereit:
![]() |
- Der aufrufende Code [1] kennt die Implementierung des aufgerufenen Codes [3] nicht. Er kennt lediglich die Art und Weise, wie er ihn aufrufen kann. Dies wird ihm durch die Schnittstelle [2] vorgegeben. Diese definiert eine Reihe von Methoden bzw. Funktionen, die zur Interaktion mit dem aufgerufenen Code verwendet werden müssen. Diese Schnittstelle wird auch als API (Application Programming Interface) bezeichnet;
Die Schicht [dao] wird die folgende Schnittstelle bereitstellen:
- [get_classes] gibt die Liste der Klassen der Mittelschule zurück;
- [get_matières] gibt die Liste der an der Mittelschule unterrichteten Fächer zurück;
- [get_élèves] gibt die Liste der Schüler der Mittelschule zurück;
- [get_notes] gibt die Liste der Noten aller Schüler aus;
- [get_notes_for_élève_by_id] gibt die Noten eines bestimmten Schülers aus;
- [get_élève_by_id] gibt einen Schüler anhand seiner Nummer zurück;
Der aufrufende Code verwendet ausschließlich diese Methoden. Er muss nicht wissen, wie sie implementiert sind. Die Daten können somit aus verschiedenen Quellen stammen (fest codiert, aus einer Datenbank, aus Textdateien …), ohne dass dies Auswirkungen auf den aufrufenden Code hat. Dies wird als Schnittstellenprogrammierung bezeichnet.
Python 3 verfügt über ein Konzept, das dem einer Schnittstelle nahekommt: die abstrakte Klasse. Diese werden wir verwenden. Wir werden die Schnittstellen dieses Beispiels im Ordner [interfaces] zusammenfassen.
Wir definieren eine abstrakte Klasse [InterfaceDao] (InterfaceDao.py) für die Schicht [dao]:
# Importe
from abc import ABC, abstractmethod
# Dao-Schnittstelle
from Elève import Elève
class InterfaceDao(ABC):
# Klassenliste
@abstractmethod
def get_classes(self: object) -> list:
pass
# Liste der Schüler
@abstractmethod
def get_élèves(self: object) -> list:
pass
# Fächerliste
@abstractmethod
def get_matières(self: object) -> list:
pass
# Notenliste
@abstractmethod
def get_notes(self: object) -> list:
pass
# Notenliste eines Schülers
@abstractmethod
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
pass
# Schüler anhand seiner ID suchen
@abstractmethod
def get_élève_by_id(self, élève_id: int) -> Elève:
pass
Anmerkungen:
- Zeile 2: ABC = abstrakte Basisklasse. Aus dem Modul [abc] werden die Klasse ABC sowie der Dekorator [abstractmethod] importiert, der in den Zeilen 10, 15, 20, 25, 30 und 35 verwendet wird;
- Zeile 8: Die abstrakte Klasse heißt [InterfaceDao] und leitet sich von der Klasse [ABC] ab;
- Die Methoden der abstrakten Klasse sind mit dem Dekorator [@abstractmethod] versehen, der die so dekorierte Methode zu einer abstrakten Methode macht: Ihr Code ist nicht definiert. Dennoch wird dort Code eingefügt: die Anweisung [pass], die nichts bewirkt;
- Die abstrakte Klasse [InterfaceDao] kann nicht instanziiert werden. Instanziiert werden können lediglich die von [InterfaceDao] abgeleiteten Klassen, die alle Methoden von [InterfaceDao] implementiert haben. Wenn man also zwei Klassen [Dao1] und [Dao2] erstellt, die von der Klasse [InterfaceDao] abgeleitet sind, implementieren beide die abstrakten Methoden von [InterfaceDao]. Man könnte also sagen, dass sie die Schnittstelle [InterfaceDao] implementieren;
- Sprachen, die sowohl Schnittstellen als auch abstrakte Klassen implementieren, weisen der Schnittstelle eine andere Rolle zu als der abstrakten Klasse. Eine Schnittstelle hat keine Attribute und kann nicht instanziiert werden. Eine Klasse kann eine Schnittstelle implementieren, indem sie alle Methoden dieser Schnittstelle definiert;
14.2.4.2. Implementierung von [Dao]
Die Klasse [Dao] (dao.py) implementiert die Schnittstelle [InterfaceDao] wie folgt:
# Import von Entitäten und Schnittstellen
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
# Die Klasse [dao] implementiert die Schnittstelle InterfaceDao
class Dao(InterfaceDao):
# Konstruktor
# Es werden feste Listen erstellt
def __init__(self):
# die Klassen werden instanziiert
classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
self.classes = [classe1, classe2]
# die Fächer
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]
# die Schüler
é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]
# die Noten der Schüler in den verschiedenen Fächern
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]
# -----------
# Schnittstelle IDao
# -----------
Anmerkungen:
- Zeilen 1–7: Die Entitäten und die Schnittstelle [InterfaceDao] werden importiert;
- Zeile 11: Die Klasse [Dao] leitet sich von der abstrakten Klasse [InterfaceDao] ab. Wir sagen, dass sie die Schnittstelle [InterfaceDao] implementiert;
- Zeile 14: Der Konstruktor hat keine Parameter. Er erstellt vier Listen fest:
- Zeilen 15–18: die Klassenliste;
- Zeilen 19–22: die Liste der Fächer;
- Zeilen 23–28: die Liste der Schüler;
- Zeilen 29–38: die Liste der Noten;
- Zeilen 40–44: Implementierung der Methoden der Schnittstelle [Interface Dao]. Hier definieren wir sie nicht, um die von Python ausgegebene Fehlermeldung zu sehen;
Ein Testprogramm könnte wie folgt aussehen: [tests-dao.py]:
# Konfiguration der Anwendung
import config
config = config.configure()
# Instanziierung der Schicht [dao]
from Dao import Dao
daoImpl = Dao()
# Liste der Klassen
for classe in daoImpl.get_classes():
print(classe)
# Liste der Fächer
for matière in daoImpl.get_matières():
print(matière)
# Liste der Klassen
for élève in daoImpl.get_élèves():
print(élève)
# Notenliste
for note in daoImpl.get_notes():
print(note)
Hinweis: Das Skript [tests-dao.py] ist kein Test für [unittest], da es keine Methoden enthält, deren Name mit [test_] beginnt.
Die Kommentare sprechen für sich. In den Zeilen 11–25 wird die Schnittstelle der Schicht [dao] verwendet. Dabei werden keine Annahmen über die tatsächliche Implementierung der Schicht getroffen. In Zeile 9 wird die Schicht [dao] instanziiert.
Die Ergebnisse der Ausführung dieses Skripts lauten wie folgt:
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
Man sieht, dass bereits bei der Instanziierung der Klasse [Dao] (Zeile 3 oben) ein Fehler auftritt. Der Python-3-Interpreter teilt uns mit, dass er die Klasse nicht instanziieren kann, da wir die abstrakten Methoden [get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves] nicht definiert haben.
PyCharm kennt ebenfalls das Konzept der abstrakten Klasse und schlägt uns vor, deren Methoden zu definieren:
![]() |
- in [1], Rechtsklick auf den Code;
- in [2-3], wählen Sie [Generate / Implement Methods], um die fehlenden Methoden der Klasse [Dao] zu implementieren;
- in [4] die zu implementierenden Methoden auswählen, hier alle;
Anschließend wird die Klasse [Dao] durch PyCharm wie folgt ergänzt:
# -----------
# Schnittstelle 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
Wir ergänzen die Klasse [Dao] wie folgt:
# -----------
# Schnittstelle IDao
# -----------
# Liste der Klassen
def get_classes(self) -> list:
return self.classes
# Fächerliste
def get_matières(self) -> list:
return self.matières
# Schülerliste
def get_élèves(self) -> list:
return self.élèves
# Notenliste
def get_notes(self) -> list:
return self.notes
def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
# Schüler suchen
élève = self.get_élève_by_id(élève_id)
# Noten abrufen
notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
# Ergebnis ausgeben
return {"élève": élève, "notes": notes}
def get_élève_by_id(self, élève_id: int) -> Elève:
# Schüler filtern
élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
# Gefunden?
if not élèves:
raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
# Ergebnis
return élèves[0]
- Die Zeilen 5–19 bereiten keine Schwierigkeiten;
- Zeilen 29–36: Die Methode, die den Schüler zurückgibt, dessen Nummer übergeben wird. Wenn der Schüler nicht existiert, wird eine Ausnahme ausgelöst;
- Zeile 31: Die Funktion [filter] ermöglicht das Filtern einer Liste:
- Der erste Parameter ist das Filterkriterium;
- der zweite Parameter ist die zu filternde Liste, hier die Liste der Schüler;
- Zeile 31: Das Filterkriterium für die Liste wird mithilfe der Funktion [f(e :Elève)->bool] implementiert. Diese wird auf jedes Element der zu filternden Liste angewendet. Erfüllt das Element das Filterkriterium, wird es in die gefilterte Liste aufgenommen, andernfalls wird es ausgeschlossen. Hier hat man folgende Möglichkeiten:
- den Namen der Funktion f angeben und diese an anderer Stelle implementieren. Der Aufruf der Funktion [filter] wird dann zu [filter(f,self.get_élèves()];
- die Definition der Funktion f angeben. Der Aufruf der Funktion [filter] wird dann zu [filter(f(e :Elève){…},self.get_élèves()], wobei [e] ein Element der gefilterten Liste darstellt, d. h. einen Schüler. Dies wurde hier so umgesetzt. Die Definition der Funktion f wäre hier [f(e :Elève){return e.id==élève_id)]: Ein Schüler wird nur dann ausgewählt, wenn die Nummer [id] nicht mit der gesuchten übereinstimmt. Eine solche Funktion kann durch eine sogenannte Lambda-Funktion ersetzt werden: [lambda e: e.id == élève_id]:
- e: steht für den Parameter der Funktion f, hier einen Schüler. Man kann einen beliebigen Namen verwenden;
- e.id==élève_id ist das Filterkriterium: Ein Schüler mit der Nummer [e] wird nur berücksichtigt, wenn seine Nummer [id] mit der gesuchten übereinstimmt;
- Zeile 31: Die Funktion [filter] gibt die gefilterte Liste in einem Typ zurück, der nicht dem Typ [list] entspricht, aber in den Typ [list] umgewandelt werden kann. Genau das tun wir hier mit dem Ausdruck [list(liste filtrée)];
- Zeilen 33–34: Ist die gefilterte Liste leer, bedeutet dies, dass der gesuchte Schüler nicht existiert. In diesem Fall wird eine Ausnahme ausgelöst;
- Zeile 36: Wenn wir hier angelangt sind, bedeutet das, dass keine Ausnahme aufgetreten ist. Wir wissen dann, dass wir eine Liste mit einem Element erhalten haben (es gibt keine zwei Schüler mit derselben Nummer [id]). Wir geben also das erste Element der Liste zurück;
- Zeilen 21–27: Die Methode [get_notes_for_élève_by_id] muss die Noten des Schülers zurückgeben, dessen Nummer [id] übergeben wurde;
- Zeilen 22–23: Zunächst wird der Schüler mit der Nummer [élève_id] mithilfe der Methode [get_élève_by_id] gesucht, die wir gerade auskommentiert haben. Es kann zu einer Ausnahme kommen, wenn der gesuchte Schüler nicht existiert. Da die Anweisung in Zeile 23 nicht von einem try/catch-Block umgeben ist, wird die Ausnahme an den aufrufenden Code weitergeleitet. Dies ist beabsichtigt;
- Zeilen 24–25: Sobald der Schüler abgerufen wurde, werden alle seine Noten abgerufen. Dies geschieht erneut mithilfe eines Filters:
- Der Filter lautet [filter(critère, self_getnotes()]. Die zu filternde Liste ist also die Liste aller Noten aller Schüler der Schule;
- Das Filterkriterium wird mithilfe einer Funktion [lambda] ausgedrückt: lambda n: n.élève.id == élève_id. Der Parameter n ist ein Element der zu filternden Liste, also eine Note. Der Typ [Note] verfügt über eine Eigenschaft [élève], die den Schüler angibt, dem die Note gehört. Daher muss [n.élève.id], das die Nummer dieses Schülers angibt, mit der Nummer des gesuchten Schülers übereinstimmen;
Anschließend führen wir das Skript [tests-dao.py] aus.
# die Anwendung wird konfiguriert
import config
config = config.configure()
# Instanziierung der Schicht [dao]
from Dao import Dao
daoImpl = Dao()
# Klassenliste
for classe in daoImpl.get_classes():
print(classe)
# Liste der Fächer
for matière in daoImpl.get_matières():
print(matière)
# Liste der Klassen
for élève in daoImpl.get_élèves():
print(élève)
# Notenliste
for note in daoImpl.get_notes():
print(note)
# ein bestimmter Schüler
print(daoImpl.get_élève_by_id(11))
# die Liste seiner Noten
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}")
Wir erhalten dann folgende Ergebnisse:
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
Man kann feststellen, dass bei der Anzeige einer Note (bei anderen Objekten verhält es sich ähnlich) ebenfalls Folgendes angezeigt wird:
- den Schüler, dem die Notiz gehört;
- das Fach, auf das sich die Notiz bezieht;
Dieses Ergebnis wird von der Funktion [BaseEntity.asdict] erzeugt (siehe Abschnitt „Link“).
14.2.5. Die Ebene [métier]
![]() | ![]() |
- [InterfaceMétier] ist die Schnittstelle der Schicht [métier];
- [Métier] ist die Implementierungsklasse der Schicht [métier];
- [Testmétier] ist eine Testklasse [UnitTest] der Klasse [Métier];
14.2.5.1. Schnittstelle [InterfaceMétier]
Die Schicht [métier] wird die folgende Schnittstelle [InterfaceMétier] (InterfaceMétier.py) implementieren:
# Importe
from abc import ABC, abstractmethod
from StatsForElève import StatsForElève
# Geschäftsschnittstelle
class InterfaceMétier(ABC):
# Berechnung von Statistiken für einen Schüler
@abstractmethod
def get_stats_for_élève(self, idElève: int) -> StatsForElève:
pass
- [get_stats_for_élève] gibt die Noten des Schülers Nr. idElève sowie Informationen zu diesen Noten zurück: gewichteter Durchschnitt, niedrigste Note, höchste Note. Diese Informationen sind in einem Objekt vom Typ [StatsForElève] gekapselt;
14.2.5.2. Die Entität [StatsForElève]
Der Typ [StatsForElève] (StatsForElève.py), der die Statistiken (Noten, Min, Max, gewichteter Durchschnitt) eines Schülers kapselt, sieht wie folgt aus:
# Importe
from BaseEntity import BaseEntity
# Statistiken zu einem bestimmten Schüler
class StatsForElève(BaseEntity):
# aus dem Klassenbericht ausgeschlossene Attribute
excluded_keys = []
# Eigenschaften der Klasse
@staticmethod
def get_allowed_keys() -> list:
# ID: Kennung der Note
# Schüler: der betreffende Schüler
# Noten: seine Noten
# moyennePondérée: sein nach den Fachgewichten gewichteter Notendurchschnitt
# min: seine Mindestnote
# max: seine Höchstnote
return BaseEntity.get_allowed_keys() + ["élève", "notes", "moyenne_pondérée", "min", "max"]
# toString
def __str__(self) -> str:
# Fall eines Schülers ohne Noten
if len(self.notes) == 0:
return f"Elève={self.élève}, notes=[]"
# Fall eines Schülers mit Noten
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}"
Anmerkungen:
- Zeile 8: Die Klasse [StatsForElève] leitet sich von der Klasse [BaseEntity] ab;
- Zeilen 13–22: die Eigenschaften der Klasse;
- eine Kennung [id], abgeleitet von [BaseEntity];
- der Schüler [élève], dessen Statistiken gekapselt werden;
- seine Noten [notes];
- sein gewichteter Durchschnitt [moyenne_pondérée];
- seine Mindestnote [min];
- seine Höchstnote [max];
- Für diese Attribute werden keine Getter/Setter definiert. Es wird davon ausgegangen, dass die Schicht [métier] Objekte dieses Typs erstellt und dass diese keine ungültigen Objekte erstellt;
- Zeilen 23–33: Die Funktion [__str__] gibt eine Zeichenkette zurück, die die Eigenschaften des Objekts enthält;
14.2.5.3. Die Implementierung [Métier]
Die Implementierung [Métier] (Metier.py) der Schnittstelle [InterfaceMétier] sieht wie folgt aus:
# Importe
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):
# Der Parameter wird gespeichert
self.__dao = dao
# -----------
# Schnittstelle
# -----------
# Kennzeichen zu den Noten eines bestimmten Schülers
def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
# Statistiken für den Schüler mit der Nr. idEleve
# id_élève: Schüler-Nr.
# Die Noten werden über die Ebene [dao] abgerufen
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"]
# Man bricht ab, wenn keine Noten vorhanden sind
if len(notes) == 0:
# das Ergebnis wird ausgegeben
return StatsForElève().fromdict({"élève": élève, "notes": []})
# Auswertung der Noten des Schülers
somme_pondérée = 0
somme_coeff = 0
max = -1
min = 21
for note in notes:
# Notenwert
valeur = note.valeur
# Gewichtungsfaktor des Fachs
coeff = note.matière.coefficient
# Summe der Gewichtungskoeffizienten
somme_coeff += coeff
# gewichtete Summe
somme_pondérée += valeur * coeff
# Suche nach dem Minimum
if valeur < min:
min = valeur
# Suche nach dem Maximalwert
if valeur > max:
max = valeur
# Berechnung fehlender Indikatoren
moyenne_pondérée = float(somme_pondérée) / somme_coeff
# Das Ergebnis wird in der Form eines Typs [StatsForElève] ausgegeben
return StatsForElève(). \
fromdict({"élève": élève, "notes": notes,
"moyenne_pondérée": moyenne_pondérée,
"min": min, "max": max})
Anmerkungen
- Zeile 7: Die Klasse [Métier] leitet sich von der Klasse [InterfaceMétier] ab. Üblicherweise sagt man, dass sie die Schnittstelle [InterfaceMétier] implementiert;
- Zeilen 9–12: Der Konstruktor erhält als einzigen Parameter eine Referenz auf die Schicht [dao]. In Zeile 10 ist zu beachten, dass dem Parameter [dao] der Typ [InterfaceDao] zugewiesen wurde. Es wird keine konkrete Implementierung erwartet, sondern lediglich eine Implementierung, die die Schnittstelle [InterfaceDao] einhält. In diesem Fall spielt das keine Rolle, da Python diesen Typ nicht berücksichtigt, aber es ist eine gute Praxis, mit Schnittstellen statt mit konkreten Implementierungen zu arbeiten. Der Code lässt sich so leichter ändern;
- Zeilen 19–60: Implementierung der Methode [get_stats_for_élève];
- Zeile 19: Die Methode erhält einen einzigen Parameter, die Nummer [idElève] des Schülers, dessen Statistiken abgefragt werden sollen;
- Zeile 24: Die [dao]-Schicht wird um die Noten des Schülers angefragt. Diese Anfrage führt zu einer Ausnahme, wenn der Schüler nicht existiert. Diese wird nicht abgefangen (kein try/catch) und wird daher an den aufrufenden Code weitergeleitet;
- Zeile 25: Hier gelangt man hin, wenn keine Ausnahme aufgetreten ist. [notes_élève] ist dann ein Wörterbuch mit zwei Schlüsseln [élève, note]:
- Zeile 25: Hier werden die Informationen zum Schüler abgerufen (sein Name, seine Klasse usw.);
- Zeile 26: Man ruft seine Noten ab;
- Zeilen 28–31: Es wird geprüft, ob der Schüler Noten hat. Hat er keine, gibt es keine Statistiken zu berechnen;
- Zeile 31: Es wird ein Objekt [StatsForElève] zurückgegeben, das mithilfe der Methode [BaseEntity.fromdict] aus einem Dictionary erstellt wurde;
- Zeilen 33–54: Die Noten des Schülers werden ausgewertet, um die angeforderten Statistiken zu berechnen. Die Kommentare im Code sollten zum Verständnis ausreichen;
- Zeilen 56–60: Es wird ein Objekt vom Typ [StatsForElève] zurückgegeben, das mithilfe der Methode [BaseEntity.fromdict] aus einem Wörterbuch erstellt wurde;
14.2.5.4. Test der Schicht [métier]
Ein Skript [UnitTest] der Schicht [métier] könnte wie folgt aussehen (TestMétier.py):
# Importe
import unittest
class Testmétier(unittest.TestCase):
def setUp(self):
# Konfiguration der Anwendung
import config
config.configure()
def test_statsForEleve11(self):
# Importe
from Dao import Dao
from Métier import Métier
# die Indikatoren von Schüler 11 werden getestet
dao = Dao()
stats_for_élève = Métier(dao).get_stats_for_élève(11)
# Anzeige
print(f"\nstats={stats_for_élève}")
# Überprüfungen
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()
Anmerkungen
- Zeilen 6–9: Die Funktion [setUp] wird hier verwendet, um den Python-Pfad für den Test zu konfigurieren;
- Zeile 16: Die Schicht [dao] wird instanziiert;
- Zeile 17: Die Schicht [métier] wird instanziiert und ihre Methode [get_stats_for_élève] wird verwendet, um die Statistiken für Schüler Nr. 11 zu berechnen;
- Zeile 19: Das erhaltene Ergebnis [StatsForElève] wird ausgegeben. Da [StatsForElève] von [BaseEntity] abgeleitet ist, wird hier die Zeichenkette jSON von [StatsForElève] ausgegeben;
- Zeile 21: Die Mindestnote des Schülers wird überprüft;
- Zeile 22: Die Höchstnote des Schülers wird überprüft;
- Zeile 23: Es wird geprüft, ob der gewichtete Durchschnitt auf 7,333 genau (auf 10⁻³ genau) ist. Im Allgemeinen ist es nicht möglich, reelle Zahlen exakt zu vergleichen, da sie intern meist nur annähernd dargestellt werden;
Die Testergebnisse lauten wie folgt:
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. Die Schicht [ui]

- in [1] die Schnittstelle der Schicht [ui];
- in [2] die Implementierung dieser Schnittstelle;
- in [3] das Hauptskript der Anwendung;
14.2.6.1. Schnittstelle [InterfaceUi]
Die Schnittstelle der Schicht [UI] sieht wie folgt aus:
# Importe
from abc import ABC, abstractmethod
# Schnittstelle UI
class InterfaceUi(ABC):
# Ausführung der Ebene UI
@abstractmethod
def run(self: object):
pass
Anmerkungen
- Zeilen 9–10: Die Schicht [UI] wird nur eine Methode haben, nämlich [run];
14.2.6.2. Die Implementierung [Console]
Die Schicht [console] wird durch das folgende Skript [Console.py] implementiert:
# Einfuhren der Ebenen
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi
# weitere Abhängigkeiten
from MyException import MyException
class Console(InterfaceUi):
# Konstruktor
def __init__(self: object, métier: InterfaceMétier):
# Geschäftsbereich: die Schicht [métier]
# Die Attribute werden gespeichert
self.métier = métier
# -----------
# Schnittstelle
# -----------
def run(self):
# Benutzerdialog
fini = False
while not fini:
# Frage / Antwort
réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
# Fertig?
if réponse == "*":
break
# Ist die Eingabe korrekt?
ok = False
try:
id_élève = int(réponse, 10)
ok = id_élève >= 1
except ValueError as erreur:
pass
# Daten korrekt?
if not ok:
print("Saisie incorrecte. Recommencez...")
continue
# Berechnung der Statistiken für den ausgewählten Schüler
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}")
- Zeilen 3–5: Import aller Schnittstellen;
- Zeile 11: Die Klasse [Console] implementiert die Schnittstelle [InterfaceUi];
- Zeilen 12–17: Der Konstruktor der Klasse [Console] erhält als Parameter eine Referenz auf die Schicht [métier]. Es sei angemerkt, dass diesem Parameter der Typ [InterfaceMétier] zugewiesen wurde, um deutlich zu machen, dass wir mit Schnittstellen und nicht mit konkreten Implementierungen arbeiten;
- Zeile 24: Implementierung der Methode [run] der Schnittstelle;
- Zeile 27: eine Schleife, die endet, sobald die Bedingung in Zeile 31 erfüllt ist;
- Zeile 29: Eingabe eines über die Tastatur eingegebenen Werts. Die Funktion [input] erhält einen optionalen Parameter: die Meldung, die auf dem Bildschirm angezeigt werden soll, um zur Eingabe aufzufordern. Diese wird immer als Zeichenkette abgerufen. Die Funktion [strip] entfernt die vorangehenden und nachfolgenden „Leerzeichen“ aus dieser Zeichenkette;
- Zeilen 34–39: Es wird überprüft, ob die Eingabe, eine Schüler-ID, gültig ist. Es muss sich um eine ganze Zahl >= 1 handeln. Zur Erinnerung: Die Eingabe erfolgte als Zeichenkette;
- Zeile 36: Es wird versucht, die Eingabe in eine ganze Zahl zur Basis 10 umzuwandeln. Die Funktion [int] löst eine Ausnahme aus, wenn dies nicht möglich ist;
- Zeile 37: Diese Zeile wird nur ausgeführt, wenn keine Ausnahme aufgetreten ist. Es wird überprüft, ob die erhaltene ganze Zahl tatsächlich >= 1 ist;
- Zeilen 38–39: Die Ausnahme wird behandelt. Wenn eine Ausnahme aufgetreten ist, ist die Variable [ok] aus Zeile 34 auf dem Wert [False] geblieben;
- Zeilen 41–43: Wenn die Eingabe fehlerhaft war, wird eine Fehlermeldung angezeigt und der Vorgang wiederholt (Zeile 43);
- Zeilen 45–48: Es werden die Statistiken des Schülers berechnet, dessen Nummer eingegeben wurde;
- Zeile 46: Es wird die Methode [get_stats_for_élève] der Ebene [métier] verwendet. Diese löst eine Ausnahme aus, wenn der Schüler nicht existiert. Diese wird in den Zeilen 47–48 behandelt. Es ist bekannt, dass die Schichten [dao] und [métier] die Ausnahme [MyException] auslösen;
14.3. Das Hauptskript [main]
Das Hauptskript [main] lautet wie folgt (main.py):
# Konfiguration der Anwendung
import config
config = config.configure()
# Der Syspath ist konfiguriert – die Importe können durchgeführt werden
from Console import Console
from Dao import Dao
from Métier import Métier
# ----------- Schicht [console]
try:
# Instanziierung der Schicht [dao]
dao = Dao()
# Instanziierung der Schicht [métier]
métier = Métier(dao)
# Instanzierung der Schicht [ui]
console = Console(métier)
# Ausführung der Schicht [console]
console.run()
except BaseException as ex:
# Der Fehler wird angezeigt
print(f"L'erreur suivante s'est produite : {ex}")
finally:
pass
- Zeilen 1–4: Der Python-Pfad der Anwendung wird konfiguriert;
- Zeilen 6–9: Die benötigten Klassen und Schnittstellen werden importiert;
- Zeile 14: Instanziierung der Schicht [dao];
- Zeile 16: Instanziierung der Schicht [métier];
- Zeile 18: Instanziierung der Schicht [ui];
- Zeile 20: Der Dialog mit dem Benutzer wird gestartet;
- Zeilen 13–20: Normalerweise werden in diesen Zeilen keine Ausnahmen ausgelöst. Die Ausnahmen, die aus den Schichten [dao] und [métier] weitergeleitet werden, werden von der Schicht [Console] abgefangen. Die Ausnahmebehandlung ist eine schwierige Kunst, wenn man die verwendeten Schichten nicht genau kennt (was hier der Fall ist). Im Zweifelsfall kann man Code hinzufügen, um jede Art von Ausnahme abzufangen, die vom ausgeführten Code ausgelöst werden könnte. Genau das geschieht hier in den Zeilen 21–23. Es werden alle von [BaseException] abgeleiteten Ausnahmen abgefangen, d. h. alle Ausnahmen;
- Zeilen 24–25: Die Klausel [finally] hat hier keine Funktion. Sie dient lediglich dazu, die Zeilen 21–23 auskommentieren zu können. Im Debug-Modus ist es nämlich nicht sinnvoll, Ausnahmen abzufangen. In diesem Fall fängt der Python-Interpreter die Ausnahmen ab und gibt die Zeilennummer an, in der die Ausnahme aufgetreten ist. Eine unverzichtbare Information. Wenn die Zeilen 21–23 auskommentiert sind, sorgen die Zeilen 24–25 dafür, dass die try/catch-Anweisung syntaktisch korrekt ist. Fehlen sie, meldet Python einen Fehler;
Hier ein Ausführungsbeispiel:
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. Beispiel 2
Dieses neue Beispiel für eine mehrschichtige Architektur soll den Nutzen der Schnittstellenprogrammierung verdeutlichen. Diese erleichtert die Wartung und das Testen von Anwendungen. Wir verwenden erneut eine dreischichtige Architektur:

Jede Schicht wird auf zwei verschiedene Arten implementiert. Wir möchten zeigen, dass sich die Implementierung einer Schicht leicht ändern lässt, ohne dass dies nennenswerte Auswirkungen auf die anderen Schichten hat.
14.4.1. Die Schicht [dao]

Die Schnittstelle [InterfaceDao] sieht wie folgt aus:
# Importe
from abc import ABC, abstractmethod
# Dao-Schnittstelle
class InterfaceDao(ABC):
# eine einzige Methode
@abstractmethod
def do_something_in_dao_layer(self, x: int, y: int) -> int:
pass
- Zeilen 8–10: Die Methode [do_something_in_dao_layer] ist die einzige Methode der Schnittstelle;
Die Klasse [DaoImpl1] implementiert die Schnittstelle [InterfaceDao] wie folgt:
from InterfaceDao import InterfaceDao
class DaoImpl1(InterfaceDao):
# Implementierung InterfaceDao
def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
return x + y
Die Klasse [DaoImpl2] implementiert die Schnittstelle [InterfaceDao] wie folgt:
from InterfaceDao import InterfaceDao
class DaoImpl2(InterfaceDao):
# Implementierung InterfaceDao
def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
return x - y
14.4.2. Die Schicht [métier]

Die Schnittstelle [InterfaceMétier] lautet wie folgt:
# Importe
from abc import ABC, abstractmethod
# Geschäftsschnittstelle
class InterfaceMétier(ABC):
# eine einzige Methode
@abstractmethod
def do_something_in_métier_layer(self, x: int, y: int) -> int:
pass
- Zeilen 8–10: Die Methode [do_something_in_métier_layer] ist die einzige Methode der Schnittstelle;
Die Klasse [AbstractBaseMétier] implementiert die Schnittstelle [InterfaceMétier] wie folgt:
# Importe
from abc import ABC, abstractmethod
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
class AbstractBaseMétier(InterfaceMétier, ABC):
# Eigenschaften
# __dao ist eine Referenz auf der Schicht [dao]
@property
def dao(self) -> InterfaceDao:
return self.__dao
@dao.setter
def dao(self, dao: InterfaceDao):
self.__dao = dao
# Implementierung der Schnittstelle [InterfaceMétier]
@abstractmethod
def do_something_in_métier_layer(self, x: int, y: int) -> int:
pass
- Zeile 8: Die Klasse „[AbstractBaseMétier]“ leitet zwei Klassen ab:
- [InterfaceMétier]: Die Klasse [AbstractBaseMétier] implementiert diese Schnittstelle in den Zeilen 19–22. Tatsächlich ist zu erkennen, dass sie die Methode [do_something_in_métier_layer] nicht implementiert hat, sondern als abstrakt deklariert hat (Zeile 20). Es obliegt den abgeleiteten Klassen, die Methode zu implementieren;
- [ABC], um Zugriff auf die Annotationen [@abstractmethod] zu erhalten;
- Die Reihenfolge ist entscheidend: Wenn man sie hier umkehrt, meldet Python zur Laufzeit einen Fehler;
Dies ist das erste Mal, dass wir Mehrfachvererbung (Vererbung von mehreren Klassen) verwenden. Die Klasse [AbstractBaseMétier] erbt gleichzeitig die Eigenschaften der Klassen [InterfaceMétier] und [ABC].
- Zeilen 9–17: Hier wird die Eigenschaft [dao] definiert, die eine Referenz auf die Schicht [dao] darstellt;
Eine Schnittstelle ist dazu bestimmt, implementiert zu werden. Wenn verschiedene Implementierungen gemeinsame Eigenschaften haben, ist es sinnvoll, diese in einer übergeordneten Klasse zusammenzufassen, um Duplikate zu vermeiden. Dies ist hier bei der Eigenschaft [dao] der Fall. Die übergeordnete Klasse ist in der Regel immer abstrakt, da sie nicht alle Methoden der Schnittstelle implementieren kann.
Die Klasse [MétierImpl1] implementiert die Schnittstelle [InterfaceMétier] wie folgt:
from AbstractBaseMétier import AbstractBaseMétier
class MétierImpl1(AbstractBaseMétier):
# Implementierung der Schnittstelle [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)
- Zeile 4: Die Klasse [MétierImpl1] leitet sich von der Klasse [AbstractbaseMétier] ab. Sie erbt somit die Eigenschaft [dao] dieser Klasse;
- Zeilen 6–9: Implementierung der Schnittstelle [InterfaceMétier], die von der übergeordneten Klasse [AbstractbaseMétier] nicht implementiert wurde;
- Zeile 9: Es wird die Schicht [dao] verwendet;
Die Klasse [MétierImpl2] implementiert die Schnittstelle [InterfaceMétier] auf ähnliche Weise:
from AbstractBaseMétier import AbstractBaseMétier
class MétierImpl2(AbstractBaseMétier):
# Implementierung der Schnittstelle [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. Die Schicht [ui]

Die Schnittstelle [InterfaceUi] lautet wie folgt:
# Importe
from abc import ABC, abstractmethod
# Schnittstelle Ui
class InterfaceUi(ABC):
# eine einzige Methode
@abstractmethod
def do_something_in_ui_layer(self, x: int, y: int) -> int:
pass
- Zeilen 8–10: die einzige Methode der Schnittstelle;
Die Klasse [AbstractBaseUi] implementiert die Schnittstelle [InterfaceUi] wie folgt:
# Importe
from abc import ABC, abstractmethod
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi
class AbstractBaseUi(InterfaceUi, ABC):
# Eigenschaften
# Geschäftsprozess ist eine Referenz auf der Ebene [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
# Implementierung der Schnittstelle [InterfaceUI]
@abstractmethod
def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
pass
- Die Klasse [AbstractBaseUi] ist eine abstrakte Klasse (Zeile 20). Sie muss abgeleitet werden, um die Schnittstelle [InterfaceUi] zu implementieren;
- Zeilen 9–17: Die Klasse [AbstractBaseUi] enthält eine Referenz auf die Schicht [métier];
Die Implementierungsklasse [UiImpl1] lautet wie folgt:
from AbstractBaseUi import AbstractBaseUi
class UiImpl1(AbstractBaseUi):
# Implementierung der Schnittstelle [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)
- Zeile 4: Die Klasse [UiImpl1] leitet sich von der Klasse [AbstractBaseUi] ab und erbt somit deren Eigenschaft [métier]. Diese wird in Zeile 9 verwendet;
Die Implementierungsklasse [UiImpl2] ist analog:
from AbstractBaseUi import AbstractBaseUi
class UiImpl2(AbstractBaseUi):
# Implementierung der Schnittstelle [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)
- Zeile 4: Die Klasse [UiImpl2] leitet sich von der Klasse [AbstractBaseUi] ab und erbt somit deren Eigenschaft [métier]. Diese wird in Zeile 9 verwendet;
14.4.4. Die Konfigurationsdateien

- Die Dateien [config1, config2] konfigurieren die Anwendung auf zwei verschiedene Arten;
- Die Datei [main] ist das Hauptskript der Anwendung;
Die Datei [config1] lautet wie folgt:
def configure():
# Schritt 1 ------
# Absoluter Pfad zum Ordner dieses Skripts
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
# Abhängigkeiten
absolute_dependencies = [
# lokale Verzeichnisse im Python-Pfad
f"{script_dir}/../dao",
f"{script_dir}/../ui",
f"{script_dir}/../métier",
]
# Konfiguration des Syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# Schritt 2 ------
# Konfiguration der Anwendungsschichten
from DaoImpl1 import DaoImpl1
from MétierImpl1 import MétierImpl1
from UiImpl1 import UiImpl1
# Instanziierung der Schichten
# DAO
dao = DaoImpl1()
# Geschäftslogik
métier = MétierImpl1()
métier.dao = dao
# Benutzeroberfläche
ui = UiImpl1()
ui.métier = métier
# Die Instanzen der Schichten werden in die Konfiguration aufgenommen
# Hier ist nur die UI-Schicht erforderlich
config = {"ui": ui}
# Die Konfiguration wird zurückgesetzt
return config
- Zeilen 2–16: Konfiguration des Python-Pfads der Anwendung;
- Zeilen 18–31: Instanziierung der Schichten [dao, métier, ui]. Zur Implementierung ihrer Schnittstellen wird jeweils die erste erstellte Implementierung ausgewählt;
- Zeilen 33–35: Die Layer-Referenzen werden in die Konfiguration aufgenommen. Hier benötigt das Hauptskript nur den Layer [ui];
Die Datei [config2] ist analog aufgebaut und implementiert jede Schnittstelle mit der zweiten verfügbaren Implementierung:
def configure():
# Schritt 1 ---
# Absoluter Pfad zum Ordner dieses Skripts
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
# Abhängigkeiten
absolute_dependencies = [
# Lokale Verzeichnisse im Python-Pfad
f"{script_dir}/../dao",
f"{script_dir}/../ui",
f"{script_dir}/../métier",
]
# Konfiguration des Syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# Schritt 2 ------
# Konfiguration der Anwendungsschichten
from DaoImpl2 import DaoImpl2
from MétierImpl2 import MétierImpl2
from UiImpl2 import UiImpl2
# Instanziierung der Schichten
# DAO
dao = DaoImpl2()
# Geschäftslogik
métier = MétierImpl2()
métier.dao = dao
# Benutzeroberfläche
ui = UiImpl2()
ui.métier = métier
# Die Instanzen der Schichten werden in die Konfiguration aufgenommen
# Hier ist nur die UI-Schicht erforderlich
config = {"ui": ui}
# Die Konfiguration wird zurückgegeben
return config
14.4.5. Das Hauptskript [main]

Das Hauptskript lautet wie folgt:
# Importe
import importlib
import sys
# main ---------
# Es sind zwei Argumente erforderlich
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()
# Konfiguration der Anwendung
module = importlib.import_module(sys.argv[1])
config = module.configure()
# Ausführung der Schicht [ui]
print(config["ui"].do_something_in_ui_layer(10, 20))
Dieses Skript erhält einen Parameter:
- [config1], um Konfiguration Nr. 1 zu verwenden;
- [config2], um Konfiguration Nr. 2 zu verwenden;
Python speichert die Parameter in einer Liste [sys.argv]:
- sys.argv[0] ist der Name des Skripts, hier [main]. Dieser Parameter ist immer vorhanden;
-
sys.argv[1] ist der erste an das Skript übergebene Parameter, sys.argv[2] der zweite, …
-
Zeile 8: Die Anzahl der Parameter wird ermittelt;
- Zeilen 9–11: Es wird überprüft, ob tatsächlich ein Parameter vorhanden ist und ob dessen Wert entweder [config1] oder [config2] lautet. Ist dies nicht der Fall, wird eine Fehlermeldung angezeigt (Zeile 10) und das Programm wird beendet (Zeile 11);
Sobald die gewünschte Konfiguration bekannt ist, müssen wir diese Konfiguration ausführen. Wenn beispielsweise Konfiguration 1 ausgewählt wurde, müssen wir den folgenden Code ausführen:
Das Problem hierbei ist, dass die zu verwendende Konfiguration in einer Variablen gespeichert ist, nämlich in der Variablen [len[liste1]]. Um ein Modul zu importieren, dessen Name in einer Variablen gespeichert ist, müssen wir das Paket [importlib] verwenden (Zeile 2).
- Zeile 14: Wir importieren das Modul, dessen Name in [email.message_from_bytes(data2[0][1])] gespeichert ist;
- Zeile 15: Anschließend wird die Funktion [configure] dieses Moduls ausgeführt. Dabei wird ein Wörterbuch [config] abgerufen, das die Konfiguration der Anwendung enthält;
- Zeile 18: Es ist bekannt, dass sich eine Referenz der Schicht [ui] in config[‘ui’] befindet. Diese wird verwendet, um die Methode [do_something_in_ui_layer] aufzurufen. Wir wissen, dass diese Methode eine Methode der Schicht [métier] aufrufen wird, die ihrerseits eine Methode der Schicht [dao] aufrufen wird;
Beispielsweise lautet die Funktion [do_something_in_ui_layer] wie folgt:
class UiImpl1(AbstractBaseUi):
# Implementierung der Schnittstelle [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)
- Zeile 6 oben verwendet die Eigenschaft [métier] der Klasse [UiImpl1], Zeile 1. In der Konfiguration [config1] wurde jedoch Folgendes geschrieben:
# Geschäftsprozess
métier = MétierImpl1()
métier.dao = dao
# Benutzeroberfläche
ui = UiImpl1()
ui.métier = métier
- Zeile 6: Die Eigenschaft [métier] von [UIImpl1] ist ein Verweis auf die Klasse [MétierImpl1] (Zeile 2). Somit wird die Methode [do_something_in_ui_layer] der Klasse [MétierImpl1] ausgeführt;
In der Klasse [MétierUiImpl1] steht:
class MétierImpl1(AbstractBaseMétier):
# Implementierung der Schnittstelle [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)
- In Zeile 6 ruft die von der Schicht [ui] aufgerufene Methode wiederum eine Methode der Eigenschaft [dao] der Klasse [MétierImpl1] auf;
In der Konfiguration [config1] steht jedoch:
# DAO
dao = DaoImpl1()
# Geschäftsprozess
métier = MétierImpl1()
métier.dao = dao
- Zeile 5: Die Eigenschaft [MétierImpl1.dao] ist vom Typ [DaoImpl1] (Zeile 2);
Damit soll hier gezeigt werden, dass sich das Skript [main] nicht um die Ebenen [métier] und [dao] kümmern muss. Es muss sich lediglich um die Ebene [ui] kümmern, da die Verknüpfungen zwischen dieser Ebene und den anderen bereits über die Konfiguration hergestellt wurden.

Um den Parameter [config1] oder [config2] an das Skript [main], zu übergeben, geht man wie folgt vor:

- In [1-2] erstellen Sie eine sogenannte Ausführungskonfiguration;
- In [3] wird dieser Konfiguration ein Name zugewiesen, um sie wiederfinden zu können;
- in [4] wählen wir das auszuführende Skript aus. Wenn wir die Vorgehensweise in [1-2] befolgt haben, wurde das richtige Skript bereits ausgewählt;
- In „[5]“ werden hier die an das Skript zu übergebenden Parameter eingegeben. Hier wird die Zeichenfolge „[config1]“ übergeben, um das Skript anzuweisen, die Konfiguration Nr. 1 zu verwenden;
- Bei [6] wird die Ausführungskonfiguration bestätigt;

- In [1-2] werden die vorhandenen Ausführungskontexte angezeigt;
- In [3] wählen Sie den vorhandenen Ausführungskontext aus und duplizieren ihn in [4];

- In [5] wird der Name für die neue Konfiguration vergeben. Diese wird das Skript [main] [6] ausführen und dabei den Parameter [config2] [7] übergeben;
Die Ausführungskonfigurationen sind oben rechts im Fenster „PyCharm“ verfügbar:

Wählen Sie einfach [2] oder [3] aus und klicken Sie dann auf [4], um das Skript [main] miteinem der beiden Parameter [config1] oder [config2] auszuführen.
Mit [config1] liefert die Ausführung von [main] folgende Ergebnisse:
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
Bei Verwendung von [config2] liefert die Ausführung von [main] folgende Ergebnisse:
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
Der Leser wird gebeten, diese Ergebnisse zu überprüfen.













