Skip to content

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:

Image

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

Image

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:

Image

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

Image

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]

Image

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]

Image

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

Image

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]

Image

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]

Image

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]

Image

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

Image

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

Image

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:

import config1
config1.configure()

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.

Image

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

Image

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

Image

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

Image

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

Image

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.