Skip to content

14. Schichtarchitektur und schnittstellenbasierte Programmierung

14.1. Einleitung

Wir schlagen vor, eine Anwendung zu schreiben, die die Noten von Mittelschülern anzeigt. Diese Anwendung kann eine mehrschichtige Architektur haben:

Image

  • Die [ui]-Schicht (User Interface) ist die Schicht, die mit dem Benutzer der Anwendung interagiert;
  • die [Business]-Schicht implementiert die Geschäftsregeln der Anwendung, wie beispielsweise die Berechnung eines Gehalts oder einer Rechnung. Diese Schicht nutzt Daten vom Benutzer über die [Präsentations]-Schicht und aus dem DBMS über die [DAO]-Schicht;
  • die [DAO]-Schicht (Data Access Objects) verwaltet den Zugriff auf Daten im DBMS (Datenbankmanagementsystem).

Dies ist die Architektur, die im |Python-2-Kurs| verwendet wurde. Es kann auch eine Variante eingeführt werden:

Image

Die Unterschiede zur vorherigen Schichtstruktur sind wie folgt:

  • Ein Hauptskript namens [main] organisiert die Instanziierung der Schichten;
  • Die Schichten [ui, business, dao] kommunizieren nicht mehr zwangsläufig miteinander. Falls dies erforderlich ist, stellt das [main]-Skript ihnen die Referenzen zu den benötigten Schichten zur Verfügung;

Der Code ist hier in Funktionsbereiche mit einem zentralen Koordinator gegliedert:

  • Der Orchestrator ist das Hauptskript [main];
  • die Schichten [ui], [dao] und [business] sind die Kompetenzzentren;

Wir könnten diese Struktur als orchestrale Organisation bezeichnen.

14.2. Beispiel 1

Wir werden die Schichtenarchitektur anhand einer einfachen Konsolenanwendung veranschaulichen:

  • Es wird keine Datenbank geben;
  • die [DAO]-Schicht verwaltet die Entitäten „Student“, „Class“, „Subject“ und „Grade“, um die Noten der Schüler zu verarbeiten;
  • Die [Business]-Schicht berechnet Kennzahlen auf der Grundlage der Noten eines bestimmten Schülers;
  • Die [UI]-Schicht ist eine Konsolenanwendung, die die Ergebnisse der Schüler anzeigt;

Das PyCharm-Projekt für die Anwendung sieht wie folgt aus:

Hinweis: Die blau markierten Ordner sind Teil des [Root Sources] des PyCharm-Projekts.

14.2.1. Die Entitäten der Anwendung

Wir bezeichnen Klassen, deren einzige Aufgabe darin besteht, Daten zu kapseln, als Entitäten. Zu diesem Zweck könnten auch Dictionaries verwendet werden. Der Vorteil einer Klasse besteht darin, dass sie es uns ermöglicht, die Gültigkeit der im Objekt gespeicherten Daten zu prüfen und eine Methode bereitzustellen, die die Identität des Objekts als Zeichenkette zurückgibt.

14.2.1.1. Die Entität [Class]

Die Entität [Class] (Class.py) repräsentiert eine Mittelschulklasse:

#  imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: class identifier
        #  name: class name
        return BaseEntity.get_allowed_keys() + ["nom"]

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

    #   setters
    @nom.setter
    def nom(self: object, nom: str):
        #  name must be a non-empty string
        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 [Class] leitet sich von der Entität [BaseEntity] ab, die im Abschnitt |Die Klasse BaseEntity| behandelt wurde;
  • Zeilen 11–16: Eine Klasse wird durch eine ID und einen Namen definiert (Zeile 16). Die Eigenschaft [id] wird von der Klasse [BaseEntity] bereitgestellt, der Name von der Klasse [Class];
  • Zeilen 18–30: Getter/Setter für das Attribut [name];

14.2.1.2. Die Entität [Subject]

Die Klasse [Subject] (subject.py) sieht wie folgt aus:

#  imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: material identifier
        #  name: material name
        #  coefficient: subject coefficient
        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

    #   setters
    @nom.setter
    def nom(self: object, nom: str):
        #  name must be a non-empty string
        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):
        #  the coefficient must be a real number >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        #  mistake?
        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 [Class] leitet sich von der Klasse [BaseEntity] ab;
  • Zeilen 11–17: Ein Subjekt wird durch seine ID [id], seinen Namen [name] und sein Gewicht [coefficient] definiert;
  • Zeilen 19–50: Getter/Setter für die Klassenattribute;

14.2.1.3. Die Entität [Student]

Die Klasse [Student] (student.py) sieht wie folgt aus:

#  imports
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: student identifier
        #  name: student's name
        #  first name: student's first name
        #  class: student's class
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    #  getters
    @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

    #   setters
    @nom.setter
    def nom(self: object, nom: str) -> str:
        #  name must be a non-empty string
        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:
        #  first name must be a non-empty string
        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:
            #  we expect a Class type
            if isinstance(value, Classe):
                self.__classe = value
            #  or a type dict
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            #  or a json type
            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 [Student] leitet sich von der Klasse [BaseEntity] ab;
  • Zeilen 13–20: Ein Student wird durch seine ID [id], seinen Nachnamen [lastName], seinen Vornamen [firstName] und seine Klasse [class] charakterisiert. Der letztgenannte Parameter ist eine Referenz auf ein [Class]-Objekt;
  • Zeilen 22–65: Getter/Setter für die Klassenattribute;

14.2.1.4. Die Entität [Note]

Die Klasse [Note] (note.py) sieht wie folgt aus:

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


class Note(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: note identifier
        #  value: the note itself
        #  student: student (of type Student) concerned by the note
        #  subject: subject (of type Subject) concerned by the grade
        #  the Note object is therefore a student's grade in a subject
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    #  getters
    @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

    #  getters
    @valeur.setter
    def valeur(self: object, valeur: float):
        #  the score must be a real number between 0 and 20
        if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
            self.__valeur = valeur
        else:
            raise MyException(31,
                              f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")

    @élève.setter
    def élève(self: object, value):
        try:
            #  we expect a Student type
            if isinstance(value, Elève):
                self.__élève = value
            #  or a type dict
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            #  or a json type
            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:
            #  we expect a Material type
            if isinstance(value, Matière):
                self.__matière = value
            #  or a type dict
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            #  or a json type
            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}")

Notizen

  • Zeile 8: Die Klasse [Note] leitet sich von der Klasse [BaseEntity] ab;
  • Zeilen 12–20: Ein [Note]-Objekt ist durch seine ID [id], den Notenwert [value], eine Referenz [student] auf den Schüler, der diese Note erhalten hat, und eine Referenz auf das mit der Note verbundene Fach [subject] gekennzeichnet;
  • Zeilen 22–75: Getter/Setter für die Klassenattribute;

14.2.2. Anwendungskonfiguration

Die Datei [config.py] konfiguriert die Umgebung sowohl für das Hauptskript [main] (1) als auch für die Tests (2). Alle diese Skripte enthalten am Anfang des Codes eine [import config]-Anweisung. Beachten Sie, dass das Verzeichnis, in dem sich das vom Befehl [python script] angegebene Skript befindet, automatisch Teil des Python-Pfads ist. Befindet sich [config] also im selben Verzeichnis wie die Skripte, die die [import config]-Anweisung enthalten, wird es gefunden. Die Dateien [1] und [2] sind hier identisch. Dies ist jedoch nicht immer der Fall.

Die Datei [config.sys] sieht wie folgt aus:

def configure():
    import os

    #  absolute path of this script's folder
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  root_dir
    root_dir="C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
    #  absolute dependencies
    absolute_dependencies=[
        #  local folders containing classes and interfaces
        f"{root_dir}/02/entities",
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
    ]

    #  update syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  return the config
    return {}
  • Zeilen 11–14: Die Verzeichnisse, die Teil des Python-Pfads (sys.path) sein müssen;
  • Das Verzeichnis [f"{root_dir}/02/entities"] bietet Zugriff auf die Klassen [BaseEntity] und [MyException];
  • Der Ordner [f"{script_dir}/../entities"] bietet Zugriff auf die Klassen [Student], [Class], [Subject] und [Grade];
  • 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. Entity-Tests

Hier schreiben wir Tests, die von einem Tool namens [unittest] ausgeführt werden. PyCharm enthält mehrere Test-Frameworks. Sie können eines davon in der PyCharm-Konfiguration auswählen:

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

#  configure the application
import config

config = config.configure()


class TestBaseEntity (unittest.TestCase):

    def test_note1(self):
        #  imports
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        #  construction of a note from a jSON string
        note = Note().fromjson(
            '{"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}}')
        #  checks
        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):
        #  imports
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        #  building a note from a dictionary
        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}})
        #  checks
        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()

Anmerkungen

  • Zeile 1: Wir importieren das Modul [unittest], das die verschiedenen Testmethoden bereitstellt;
  • Zeilen 3–6: Wir konfigurieren die Anwendung so, dass die für das Testen benötigten Klassen gefunden werden können;
  • Zeile 9: Eine [unittest]-Testklasse muss die Klasse [unittest.TestCase] erweitern;
  • Zeilen 11, 27: Testfunktionen müssen einen Namen haben, der mit [test] beginnt, andernfalls werden sie nicht erkannt;
  • Zeilen 13–16: Wir importieren die benötigten Klassen;
  • In dieser Testklasse wollen wir das Verhalten der Methoden [BaseEntity.fromdict] (Zeile 34) und [BaseEntity.fromjson] (Zeile 18) überprüfen. Die Klasse [Note] verfügt über Eigenschaften, die Verweise auf andere Klassen sind. Wir wollen sicherstellen, dass die beiden vorgenannten Methoden gültige [Note]-Objekte erstellen;
  • Zeile 18: Wir erstellen ein [Note]-Objekt aus einem JSON-Objekt;
  • Zeile 21: Wir überprüfen, ob das erstellte Objekt tatsächlich vom Typ [Note] ist. Die Methode [assertIsInstance] ist eine Methode der Klasse [unittest.TestCase], die die übergeordnete Klasse der Klasse [TestBaseEntity] ist;
  • Zeile 22: Wir überprüfen, ob [note.student] tatsächlich vom Typ [Student] ist;
  • Zeile 23: Wir überprüfen, ob [note.student.class] tatsächlich vom Typ [Class] ist;
  • Zeile 24: Wir überprüfen, ob [note.subject] tatsächlich vom Typ [Subject] ist;
  • Zeilen 33–42: Wir machen dasselbe mit der Methode [BaseEntity.fromdict];

Es gibt mehrere Möglichkeiten, die Tests auszuführen:

  • In [1-2] führen wir [TestBaseEntity] unter Verwendung des [UnitTest]-Frameworks aus;
  • In [3-5] schlagen die Tests fehl. [UnitTests] weist darauf hin, dass keine auszuführenden Tests gefunden wurden;

Die Tests schlagen aufgrund der Struktur des [TestBaseEntity]-Codes fehl:

1
2
3
4
5
6
7
8
9
import unittest

#  configure the application
import config

config = config.configure()


class TestBaseEntity(unittest.TestCase):

Was beim [UnitTest]-Framework zu Problemen führt, ist das Vorhandensein von ausführbarem Code (Zeilen 3–6) vor der Definition der Testklasse (Zeile 9).

Wir ordnen den Code daher wie folgt neu:

import unittest


class TestBaseEntity(unittest.TestCase):

    def setUp(self):
        #  configure the application
        import config

        config.configure()

    def test_note1(self):
        

    def test_note2(self):
        


if __name__ == '__main__':
    unittest.main()
  • Zeilen 6–10: Wir definieren eine [setUp]-Funktion. Diese Funktion hat eine bestimmte Aufgabe: Sie wird vor jeder Testfunktion (test_note1, test_note2) ausgeführt;

Sobald dies geschehen ist, liefert die Ausführung der Klasse [TestBaseEntity] die folgenden Ergebnisse:

Diesmal wurden beide Testmethoden ausgeführt und die Tests bestanden.

Schauen wir uns an, was passiert, wenn ein Test fehlschlägt. Ändern wir den Code in [test_note1] wie folgt:

1
2
3
4
5
6
    def test_note1(self):
        #  deliberate error - check that 1==2
        self.assertEqual(1,2)
        #  imports
        from Note import Note

  • Zeile 2: Wir prüfen, ob 1 == 2;

Die Ergebnisse der Ausführung lauten wie folgt:

Sie können die Ursache des Fehlers herausfinden, indem Sie auf den fehlgeschlagenen Test [2] klicken:

  • in [7-8] die Ursache des Fehlers;

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 bestanden wurden (wir haben den Fehler „1==2“ behoben);

Schließlich gibt es noch eine dritte Möglichkeit, die Testklasse [TestBaseEntity] auszuführen, ebenfalls im Terminal. Wir beenden die Testklasse mit den folgenden Zeilen 6–7;



        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 gerade ausgeführt wird. Wenn das Skript mit dem Befehl [python script.py] gestartet wird, ist die Variable [__name__] gleich [__main__] (zwei Unterstriche vor und nach dem Bezeichner). Daher wird Zeile 7 nur ausgeführt, wenn das Skript [TestBaseEntity] mit dem Befehl [python TestBaseEntity.py] gestartet wird. Die Anweisung [unittest.main()] startet die Ausführung des Skripts über das [UnitTest]-Framework. 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 [TestEntities]

Die Testklasse [TestEntities] sieht wie folgt aus:

import unittest


class TestEntités(unittest.TestCase):
    def setUp(self):
        #  configure the application
        import config

        config.configure()

    def test_code1a(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid 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
        #  check
        self.assertEqual(code, 1)

    def test_code41(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None

        try:
            #  invalid 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
        #  check
        self.assertEqual(code, 41)

    def test_code42(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid first name
            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
        #  check
        self.assertEqual(code, 42)

    def test_code43(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid class
            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
        #  check
        self.assertEqual(code, 43)

    def test_code1b(self):
        #  imports
        from Classe import Classe
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid identifier
            Classe().fromdict({"id": "x", "nom": "y"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 1)

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

        #  error code
        code = None
        try:
            #  invalid name
            Classe().fromdict({"id": 1, "nom": ""})
        except MyException as ex:
            code = ex.code
        #  check
        self.assertEqual(code, 11)

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

        #  error code
        code = None
        try:
            #  invalid identifier
            Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 1)

    def test_code21(self):
        #  imports
        from Matière import Matière
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid name
            Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 21)

    def test_code22(self):
        #  imports
        from Matière import Matière
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid coefficient
            Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 22)

    def test_code1d(self):
        #  imports
        from Note import Note
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid identifier
            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
        #  check
        self.assertEqual(code, 1)

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

        #  error code
        code = None
        try:
            #  invalid value
            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
        #  check
        self.assertEqual(code, 31)

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

        #  error code
        code = None
        try:
            #  disabled student
            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
        #  check
        self.assertEqual(code, 32)

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

        #  error code
        code = None
        try:
            #  invalid material
            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
        #  check
        self.assertEqual(code, 33)

    def test_exception(self):
        #  imports
        from Elève import Elève
        #  the test must launch type [MyException] to succeed
        from MyException import MyException
        with self.assertRaises(MyException):
            #  the test
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})


if __name__ == '__main__':
    unittest.main()
  • Der Zweck des Testskripts besteht darin, die Setter der Klasse zu testen: Es soll überprüft werden, ob den Attributen der verschiedenen Entitäten keine falschen Werte zugewiesen werden können;
  • Zeilen 11–24: Wir testen, ob einem Studenten keine ungültige ID zugewiesen werden kann. Da wir in Zeile 16 den Wert 'x' als ID des Studenten übergeben, erwarten wir, dass eine Ausnahme auftritt. Wir sollten daher zu den Zeilen 20–22 übergehen;
  • Zeile 21: Anzeige der Fehlermeldung;
  • Zeile 22: Abrufen des Fehlercodes (siehe Abschnitt |Die MyException-Entität|);
  • Zeile 24: Wir überprüfen (assert), ob der Fehlercode 1 ist. Hier überprüfen wir zwei Dinge:
    • 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: Wir testen, ob eine Aktion eine Ausnahme eines bestimmten Typs auslöst;
  • Zeile 220: Wir geben an, dass der Test erfolgreich ist, wenn er eine Ausnahme vom Typ [MyException] auslöst;

Ergebnisse

Wir führen das Testskript aus:

Die erhaltenen Ergebnisse lauten wie folgt:


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 bestanden

14.2.4. Die [dao]-Schicht

Image

Die [dao]-Schicht implementiert die Schnittstelle [InterfaceDao] [1]. Diese wird durch die Klasse [Dao] (2) implementiert. Das Skript [tests_dao] (3) testet die Methoden der [dao]-Schicht.

14.2.4.1. Schnittstelle [InterfaceDao]

Eine Schnittstelle ist eine Vereinbarung zwischen aufrufendem und aufgerufenem Code. Es ist der aufgerufene Code, der die Schnittstelle bereitstellt:

  • Der aufrufende Code [1] kennt die Implementierung des aufgerufenen Codes [3] nicht. Er weiß lediglich, wie er ihn aufrufen muss. Die Schnittstelle [2] sagt ihm, wie. Diese Schnittstelle definiert eine Reihe von Methoden/Funktionen, die zur Interaktion mit dem aufgerufenen Code verwendet werden. Diese Schnittstelle wird auch als API (Application Programming Interface) bezeichnet;

Die [dao]-Schicht wird die folgende Schnittstelle bereitstellen:

  • [get_classes] gibt die Liste der Mittelschulklassen zurück;
  • [get_subjects] gibt die Liste der an der Mittelschule unterrichteten Fächer zurück;
  • [get_students] gibt die Liste der Schüler der Mittelschule zurück;
  • [get_grades] gibt eine Liste aller Noten der Schüler zurück;
  • [get_grades_for_student_by_id] gibt die Noten eines bestimmten Schülers zurück;
  • [get_student_by_id] gibt einen Schüler zurück, der anhand seiner ID identifiziert wird;

Der aufrufende Code verwendet ausschließlich diese Methoden. Er muss nicht wissen, wie sie implementiert sind. Die Daten können dann aus verschiedenen Quellen stammen (fest codiert, aus einer Datenbank, aus Textdateien usw.), ohne dass dies Auswirkungen auf den aufrufenden Code hat. Dies wird als schnittstellenbasierte Programmierung bezeichnet.

Python 3 verfügt über ein Konzept, das dem einer Schnittstelle ähnelt: die abstrakte Klasse. Wir werden diese verwenden. Wir werden die Schnittstellen für dieses Beispiel im Ordner [interfaces] zusammenfassen.

Wir definieren eine abstrakte Klasse [InterfaceDao] (InterfaceDao.py) für die [dao]-Schicht:

#  imports
from abc import ABC, abstractmethod

#  dao interface
from Elève import Elève


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

    #  list of students
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    #  list of materials
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    #  lIST OF NOTES
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    #  list of student grades
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    #  search for a student by id
    @abstractmethod
    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

Anmerkungen:

  • Zeile 2: ABC = Abstract Base Class. Wir importieren die ABC-Klasse aus dem Modul [abc] sowie den Dekorator [abstractmethod], 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, wodurch die dekorierte Methode zu einer abstrakten Methode wird: Ihr Code ist nicht definiert. Wir fügen dort jedoch Code ein: die Anweisung [pass], die nichts tut;
  • Die abstrakte Klasse [InterfaceDao] kann nicht instanziiert werden. Nur von [InterfaceDao] abgeleitete Klassen, die alle Methoden von [InterfaceDao] implementiert haben, können instanziiert werden. Wenn wir also zwei Klassen [Dao1] und [Dao2] erstellen, die von der Klasse [InterfaceDao] abgeleitet sind, implementieren beide die abstrakten Methoden von [InterfaceDao]. Man könnte daher sagen, dass sie die Schnittstelle [InterfaceDao] implementieren;
  • Sprachen, die sowohl Schnittstellen als auch abstrakte Klassen unterstützen, 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 ihre Methoden definiert;

14.2.4.2. [Dao]-Implementierung

Die Klasse [Dao] (dao.py) implementiert die Schnittstelle [InterfaceDao] wie folgt:

#  import entities and interfaces
from Classe import Classe
from Elève import Elève
from InterfaceDao import InterfaceDao
from Matière import Matière
from MyException import MyException
from Note import Note


#  dao] layer implements InterfaceDao interface
class Dao(InterfaceDao):
    #  manufacturer
    #  we build hard lists
    def __init__(self):
        #  classes are instantiated
        classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
        classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
        self.classes = [classe1, classe2]
        #  materials
        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]
        #  students
        é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]
        #  student grades in various subjects
        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]

    # -----------
    #  interface IDao
    # -----------

Anmerkungen:

  • Zeilen 1–7: Wir importieren die Entitäten und die Schnittstelle [InterfaceDao];
  • 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 enthält vier fest codierte Listen:
    • Zeilen 15–18: die Liste der Klassen;
    • 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 [Dao-Schnittstelle]. Hier definieren wir sie nicht, um die von Python ausgegebene Fehlermeldung zu sehen;

Ein Testprogramm könnte wie folgt aussehen [tests-dao.py]:

#  configure the application
import config

config = config.configure()

#  layer instantiation [dao]
from Dao import Dao

daoImpl = Dao()

#  class list
for classe in daoImpl.get_classes():
    print(classe)

#  list of materials
for matière in daoImpl.get_matières():
    print(matière)

#  class list
for élève in daoImpl.get_élèves():
    print(élève)

#  lIST OF NOTES
for note in daoImpl.get_notes():
    print(note)

Hinweis: Das Skript [tests-dao.py] ist kein [unittest], da es keine Methoden enthält, deren Namen mit [test_] beginnen.

Die Kommentare sind selbsterklärend. In den Zeilen 11–25 wird die Schnittstelle der [dao]-Schicht verwendet. Hier werden keine Annahmen über die tatsächliche Implementierung der Schicht getroffen. In Zeile 9 instanziieren wir die [dao]-Schicht.

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

Wir sehen, dass ein Fehler auftritt, sobald die Klasse [Dao] instanziiert wird (Zeile 3 oben). Der Python-3-Interpreter teilt uns mit, dass er die Klasse nicht instanziieren kann, da wir die abstrakten Methoden [get_classes, get_subjects, get_grades, get_grades_for_student_by_id, get_student_by_id, get_students] nicht definiert haben.

PyCharm unterstützt ebenfalls abstrakte Klassen und bietet an, deren Methoden zu definieren:

  • Klicken Sie in [1] mit der rechten Maustaste auf den Code;
  • in [2-3] wählen Sie [Generate / Implement Methods], um die fehlenden Methoden der Klasse [Dao] zu implementieren;
  • Wählen Sie in [4] die zu implementierenden Methoden aus – in diesem Fall alle;

Sobald dies erledigt ist, vervollständigt PyCharm die Klasse [Dao] wie folgt:

    # -----------
    #  interface 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 vervollständigen die Klasse [Dao] wie folgt:

    # -----------
    #  interface IDao
    # -----------

    #  class list
    def get_classes(self) -> list:
        return self.classes

    #  list of materials
    def get_matières(self) -> list:
        return self.matières

    #  list of students
    def get_élèves(self) -> list:
        return self.élèves

    #  lIST OF NOTES
    def get_notes(self) -> list:
        return self.notes

    def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
        #  we're looking for the student
        élève = self.get_élève_by_id(élève_id)
        #  get your notes back
        notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
        #  we return the result
        return {"élève": élève, "notes": notes}

    def get_élève_by_id(self, élève_id: int) -> Elève:
        #  filtering students
        élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
        #  found?
        if not élèves:
            raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
        #  result
        return élèves[0]
  • Die Zeilen 5–19 sind unkompliziert;
  • Zeilen 29–36: Die Methode, die den Schüler mit der übergebenen ID zurückgibt. Wenn der Schüler nicht existiert, wird eine Ausnahme ausgelöst;
  • Zeile 31: Mit der Funktion [filter] können Sie eine Liste filtern:
    • Der erste Parameter ist das Filterkriterium;
    • der zweite Parameter ist die zu filternde Liste, in diesem Fall die Liste der Studenten;
  • Zeile 31: Das Filterkriterium für die Liste wird mithilfe einer Funktion [f(e:Student) -> bool] implementiert. Diese wird auf jedes Element der zu filternden Liste angewendet. Erfüllt das Element das Filterkriterium, bleibt es in der gefilterten Liste erhalten; andernfalls wird es ausgeschlossen. Hier haben wir zwei Möglichkeiten:
    • den Namen der Funktion f angeben und sie an anderer Stelle implementieren. Der Aufruf der Funktion [filter] lautet dann [filter(f, self.get_students)];
    • die Definition der Funktion f bereitstellen. Der Aufruf der Funktion [filter] lautet dann [filter(f(e :Student){…}, self.get_students())], wobei [e] ein Element der gefilterten Liste darstellt, d. h. einen Studenten. Dies ist hier der Fall. Die Definition der Funktion f wäre hier [f(e :Student){return e.id == student_id)]: Ein Schüler wird nur ausgewählt, wenn seine ID-Nummer [id] mit der gesuchten übereinstimmt. Eine solche Funktion kann durch eine sogenannte Lambda-Funktion ersetzt werden: [lambda e: e.id == student_id]:
      • e: steht für den Parameter der Funktion f, hier einen Studenten. Sie können einen beliebigen Namen verwenden;
      • e.id==student_id ist das Filterkriterium: Ein Student [e] wird nur ausgewählt, wenn seine ID [id] mit der gesuchten übereinstimmt;
  • Zeile 31: Die Funktion [filter] gibt die gefilterte Liste als einen Typ zurück, der nicht vom Typ [list] ist, aber in den Typ [list] konvertiert werden kann. Genau das tun wir hier mit dem Ausdruck [list(filtered_list)];
  • 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 diesen Punkt erreichen, bedeutet dies, dass keine Ausnahme ausgelöst wurde. Wir wissen dann, dass wir eine Liste mit einem Element abgerufen haben (es gibt keine zwei Schüler mit derselben [id]-Nummer). Wir geben daher das erste Element der Liste zurück;
  • Zeilen 21–27: Die Methode [get_notes_for_élève_by_id] muss die Noten für den Schüler zurückgeben, dessen [id] an sie übergeben wird;
  • Zeilen 22–23: Wir beginnen damit, den Schüler mit der ID [student_id] mithilfe der Methode [get_student_by_id] zu suchen, die wir gerade auskommentiert haben. Es kann zu einer Ausnahme kommen, wenn der gesuchte Schüler nicht existiert. Da es keinen try/catch-Block um die Anweisung in Zeile 23 gibt, wird die Ausnahme an den aufrufenden Code weitergeleitet. Dies ist das gewünschte Verhalten;
  • Zeilen 24–25: Sobald der Schüler abgerufen wurde, holen wir alle seine Noten ab. Dazu verwenden wir erneut einen Filter:
    • Der Filter lautet [filter(criterion, self_getnotes())]. Die zu filternde Liste ist somit die Liste aller Noten aller Schüler der Schule;
    • Das Filterkriterium wird mithilfe einer [lambda]-Funktion ausgedrückt: lambda n: n.student.id == student_id. Der Parameter n ist ein Element der zu filternden Liste, d. h. eine Note. Der Typ [Note] verfügt über eine Eigenschaft [student], die den Schüler repräsentiert, dem die Note gehört. Daher muss [n.student.id], das die ID dieses Schülers darstellt, mit der ID des gesuchten Schülers übereinstimmen;

Anschließend führen wir das Skript [tests-dao.py] aus.

#  configure the application
import config

config = config.configure()

#  layer instantiation [dao]
from Dao import Dao

daoImpl = Dao()

#  class list
for classe in daoImpl.get_classes():
    print(classe)

#  list of materials
for matière in daoImpl.get_matières():
    print(matière)

#  class list
for élève in daoImpl.get_élèves():
    print(élève)

#  lIST OF NOTES
for note in daoImpl.get_notes():
    print(note)

#  a special student
print(daoImpl.get_élève_by_id(11))

#  a list of his notes
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

Beachten Sie, dass bei der Anzeige einer Note (der Vorgang ist für andere Objekte ähnlich) zusätzlich Folgendes gilt:

  • den mit der Note verbundenen Schüler;
  • das Fach, auf das sich die Note bezieht;

Dieses Ergebnis wird von der Funktion [BaseEntity.asdict] erzeugt (siehe Abschnitt „Link“).

14.2.5. Die [business]-Ebene

  • [InterfaceMétier] ist die Schnittstelle der [business]-Schicht;
  • [Business] ist die Implementierungsklasse der [Business]-Schicht;
  • [TestBusiness] ist eine [UnitTest]-Klasse zum Testen der [Business]-Klasse;

14.2.5.1. Schnittstelle [BusinessInterface]

Die [Business]-Schicht implementiert die folgende [BusinessInterface]-Schnittstelle (BusinessInterface.py):

#  imports
from abc import ABC, abstractmethod

from StatsForElève import StatsForElève


#  business interface
class InterfaceMétier(ABC):
    #  calculating statistics for a student
    @abstractmethod
    def get_stats_for_élève(self, idElève: int) -> StatsForElève:
        pass
  • [get_stats_for_student] gibt die Noten für den Schüler idStudent zusammen mit Informationen zu diesen zurück: gewichteter Durchschnitt, niedrigste Note, höchste Note. Diese Informationen sind in einem Objekt vom Typ [StudentStats] gekapselt;

14.2.5.2. Die Entität [StatsForStudent]

Der Typ [StatsForStudent] (StatsForStudent.py), der die Statistiken eines Studenten (Noten, Min, Max, gewichteter Durchschnitt) kapselt, sieht wie folgt aus:

#  imports
from BaseEntity import BaseEntity


#  individual student statistics


class StatsForElève(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: note identifier
        #  pupil: the pupil concerned
        #  notes: his notes
        #  moyennePondérée: average weighted by subject coefficients
        #  min: its minimum score
        #  max: its maximum rating

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

    #  toString
    def __str__(self) -> str:
        #  students without grades
        if len(self.notes) == 0:
            return f"Elève={self.élève}, notes=[]"
        #  student with grades
        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 [StatsForStudent] leitet sich von der Klasse [BaseEntity] ab;
  • Zeilen 13–22: die Eigenschaften der Klasse;
    • eine Kennung [id] von [BaseEntity];
    • der Student [student], dessen Statistiken gekapselt sind;
    • seine Noten [grades];
    • seine gewichtete Durchschnittsnote [weighted_average];
    • seine niedrigste Note [min];
    • seine Höchstnote [max];
  • Wir definieren keine Getter/Setter für diese Attribute. Wir gehen davon aus, dass die [business]-Schicht Objekte dieses Typs erstellt und keine ungültigen Objekte erzeugt;
  • Zeilen 23–33: Die Funktion [__str__] gibt eine Zeichenkette zurück, die die Eigenschaften des Objekts enthält;

14.2.5.3. Die [Business]-Implementierung

Die [Business]-Implementierung (Metier.py) der [BusinessInterface]-Schnittstelle sieht wie folgt aus:

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


class Métier(InterfaceMétier):

    #  manufacturer
    def __init__(self, dao: InterfaceDao):
        #  the parameter
        self.__dao = dao

    # -----------
    #  interface
    # -----------

    #  indicators on a particular student's grades
    def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
        #  Stats for student no. idEleve
        #  id_élève : pupil number

        #  retrieve notes with the [dao] layer
        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"]

        #  we stop if there are no notes
        if len(notes) == 0:
            #  we return the result
            return StatsForElève().fromdict({"élève": élève, "notes": []})

        #  use of student notes
        somme_pondérée = 0
        somme_coeff = 0
        max = -1
        min = 21
        for note in notes:
            #  nOTE VALUE
            valeur = note.valeur
            #  material coefficient
            coeff = note.matière.coefficient
            #  sum of coefficients
            somme_coeff += coeff
            #  weighted sum
            somme_pondérée += valeur * coeff
            #  search for min
            if valeur < min:
                min = valeur
            #  search for the max
            if valeur > max:
                max = valeur
        #  calculation of missing indicators
        moyenne_pondérée = float(somme_pondérée) / somme_coeff

        #  the result is returned as type [StatsForElève]
        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 nimmt einen einzigen Parameter entgegen, einen Verweis auf die [dao]-Schicht. Beachten Sie in Zeile 10, dass wir dem Parameter [dao] den Typ [InterfaceDao] zugewiesen haben. Wir erwarten keine bestimmte Implementierung, sondern lediglich eine Implementierung, die die Schnittstelle [DaoInterface] einhält. Hier spielt das keine Rolle, da Python diesen Typ nicht berücksichtigt, aber es ist gute Praxis, mit Schnittstellen statt mit konkreten Implementierungen zu arbeiten. Der Code lässt sich dann leichter ändern;
  • Zeilen 19–60: Implementierung der Methode [get_stats_for_élève];
  • Zeile 19: Die Methode erhält einen einzigen Parameter, die [idElève] des Schülers, für den wir die Statistiken wünschen;
  • Zeile 24: Wir fordern die Noten des Schülers von der [dao]-Schicht an. Diese Anfrage führt zu einer Ausnahme, wenn der Schüler nicht existiert. Diese Ausnahme wird nicht abgefangen (kein try/catch) und daher an den aufrufenden Code zurückgegeben;
  • Zeile 25: Wir erreichen diesen Punkt, wenn keine Ausnahme aufgetreten ist. [student_grades] ist dann ein Wörterbuch mit zwei Schlüsseln [student, grade]:
    • Zeile 25: Wir rufen Informationen über den Schüler ab (Name, Klasse usw.);
    • Zeile 26: Wir rufen seine Noten ab;
  • Zeilen 28–31: Wir prüfen, ob der Schüler Noten hat. Ist dies nicht der Fall, gibt es keine Statistiken zu berechnen;
  • Zeile 31: Wir geben ein Objekt [StatsForStudent] zurück, das mithilfe der Methode [BaseEntity.fromdict] aus einem Wörterbuch erstellt wurde;
  • Zeilen 33–54: Wir verwenden die Noten des Schülers, um die angeforderten Statistiken zu berechnen. Die Code-Kommentare sollten zum Verständnis ausreichen;
  • Zeilen 56–60: Wir geben ein [StatsForStudent]-Objekt zurück, das mithilfe der Methode [BaseEntity.fromdict] aus einem Wörterbuch erstellt wurde;

14.2.5.4. Testen der [business]-Schicht

Ein [UnitTest]-Skript für die [business]-Schicht könnte wie folgt aussehen (TestMétier.py):

#  imports
import unittest


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

    def test_statsForEleve11(self):
        #  imports
        from Dao import Dao
        from Métier import Métier
        #  student indicators are tested 11
        dao = Dao()
        stats_for_élève = Métier(dao).get_stats_for_élève(11)
        #  display
        print(f"\nstats={stats_for_élève}")
        #  checks
        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 des Tests zu konfigurieren;
  • Zeile 16: Wir instanziieren die [dao]-Schicht;
  • Zeile 17: Wir instanziieren die [business]-Schicht und verwenden deren Methode [get_stats_for_student], um die Statistiken für den Studenten Nr. 11 zu berechnen;
  • Zeile 19: Das Ergebnis [StatsForStudent] wird angezeigt. Da [StatsForStudent] von [BaseEntity] abgeleitet ist, wird hier die JSON-Zeichenkette von [StatsForStudent] angezeigt;
  • Zeile 21: Wir prüfen die Mindestnote des Schülers;
  • Zeile 22: Wir prüfen die Höchstnote des Schülers;
  • Zeile 23: Wir prüfen, ob der gewichtete Durchschnitt 7,333 beträgt, mit einer Genauigkeit von 10⁻³. Im Allgemeinen ist es nicht möglich, reelle Zahlen exakt zu vergleichen, da sie intern meist nur als Näherungswerte 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 [ui]-Ebene

Image

  • in [1] die Schnittstelle der [ui]-Schicht;
  • in [2], die Implementierung dieser Schnittstelle;
  • in [3], das Hauptskript der Anwendung;

14.2.6.1. Schnittstelle [InterfaceUi]

Die Schnittstelle der [UI]-Schicht sieht wie folgt aus:

#  imports
from abc import ABC, abstractmethod


#  interface UI
class InterfaceUi(ABC):
    #  execute UI layer
    @abstractmethod
    def run(self: object):
        pass

Anmerkungen

  • Zeilen 9–10: Die [UI]-Ebene wird nur eine Methode haben, [run];

14.2.6.2. Die [Console]-Implementierung

Die [Console]-Schicht wird durch das folgende Skript [Console.py] implementiert:

#  layer imports

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

#  other dependencies
from MyException import MyException


class Console(InterfaceUi):
    #  manufacturer
    def __init__(self: object, métier: InterfaceMétier):
        #  business: the [business] layer

        #  attributes are memorized
        self.métier = métier


        # -----------
        #  interface
        # -----------

    def run(self):
        #  user dialog
        fini = False
        while not fini:
            #  question/answer
            réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
            #  finished?
            if réponse == "*":
                break
            #  is the input correct?
            ok = False
            try:
                id_élève = int(réponse, 10)
                ok = id_élève >= 1
            except ValueError as erreur:
                pass
            #  correct data?
            if not ok:
                print("Saisie incorrecte. Recommencez...")
                continue
            #  calculation of statistics for the selected student
            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 [business]-Schicht. Beachten Sie, dass wir diesem Parameter den Typ [BusinessInterface] zugewiesen haben, um zu betonen, dass wir mit Schnittstellen und nicht mit konkreten Implementierungen arbeiten;
  • Zeile 24: Implementierung der Methode [run] der Schnittstelle;
  • Zeile 27: eine Schleife, die endet, wenn die Bedingung in Zeile 31 erfüllt ist;
  • Zeile 29: Eingabe von Daten, die über die Tastatur eingegeben wurden. Die Funktion [input] erhält einen optionalen Parameter: die Meldung, die auf dem Bildschirm angezeigt werden soll, um zur Eingabe aufzufordern. Diese Eingabe wird immer als Zeichenkette abgerufen. Die Funktion [strip] entfernt alle führenden oder nachfolgenden Leerzeichen aus der Zeichenkette;
  • Zeilen 34–39: Wir überprüfen, ob die Eingabe, eine Studenten-ID, gültig ist. Sie muss eine ganze Zahl >= 1 sein. Beachten Sie, dass die Eingabe als Zeichenkette eingegeben wurde;
  • Zeile 36: Wir versuchen, die Eingabe in eine Ganzzahl zur Basis 10 umzuwandeln. Die Funktion [int] löst eine Ausnahme aus, wenn dies nicht möglich ist;
  • Zeile 37: Wir erreichen diesen Punkt nur, wenn keine Ausnahme aufgetreten ist. Wir überprüfen, ob die abgerufene Ganzzahl tatsächlich >= 1 ist;
  • Zeilen 38–39: Wir behandeln die Ausnahme. Wenn eine Ausnahme aufgetreten ist, bleibt die Variable [ok] aus Zeile 34 auf [False] gesetzt;
  • Zeilen 41–43: Wenn die Eingabe falsch war, wird eine Fehlermeldung angezeigt und die Schleife neu gestartet (Zeile 43);
  • Zeilen 45–48: Wir berechnen die Statistiken für den Studenten, dessen ID eingegeben wurde;
  • Zeile 46: Es wird die Methode [get_stats_for_student] aus der [business]-Schicht verwendet. Diese Methode löst eine Ausnahme aus, wenn der Student nicht existiert. Diese Ausnahme wird in den Zeilen 47–48 behandelt. Wir wissen, dass die [DAO]- und [business]-Schichten die Ausnahme [MyException] auslösen;

14.3. Das Hauptskript [main]

Das Hauptskript [main] sieht wie folgt aus (main.py):

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Console import Console
from Dao import Dao
from Métier import Métier

#  ----------- layer [console]
try:
    #  layer instantiation [dao]
    dao = Dao()
    #  instantiation layer [business]
    métier = Métier(dao)
    #  instantiation layer [ui]
    console = Console(métier)
    #  layer execution [console]
    console.run()
except BaseException as ex:
    #  error is displayed
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    pass
  • Zeilen 1–4: Konfigurieren des Python-Pfads der Anwendung;
  • Zeilen 6–9: Importieren der benötigten Klassen und Schnittstellen;
  • Zeile 14: Instanziieren der [DAO]-Schicht;
  • Zeile 16: Instanziieren der [Business]-Schicht;
  • Zeile 18: Instanziieren der [ui]-Schicht;
  • Zeile 20: Starten der Benutzeroberfläche;
  • Zeilen 13–20: Normalerweise werden in diesen Zeilen keine Ausnahmen ausgelöst. Alle Ausnahmen, die aus den [DAO]- und [Business]-Schichten weitergeleitet werden, werden von der [Console]-Schicht abgefangen. Die Ausnahmebehandlung ist eine schwierige Kunst, wenn man die verwendeten Schichten nicht vollständig versteht (was hier nicht der Fall ist). Im Zweifelsfall kann Code hinzugefügt werden, um jede Art von Ausnahme abzufangen, die vom ausführenden Code ausgelöst werden könnte. Genau das geschieht hier in den Zeilen 21–23. Wir fangen jede von [BaseException] abgeleitete Ausnahme ab, d. h. alle Ausnahmen;
  • Zeilen 24–25: Die [finally]-Klausel hat hier keine Funktion. Sie dient lediglich dazu, die Zeilen 21–23 auskommentieren zu können. Tatsächlich ist es im Debug-Modus nicht ratsam, Ausnahmen abzufangen. In diesem Fall fängt der Python-Interpreter sie ab und meldet anschließend die Zeilennummer, in der die Ausnahme aufgetreten ist. Dies ist eine wesentliche Information. Wenn die Zeilen 21–23 auskommentiert sind, gewährleistet das Vorhandensein der Zeilen 24–25 einen syntaktisch korrekten try/catch-Block. Ohne sie löst Python einen Fehler aus;

Hier ist ein Beispiel für die Ausführung:


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 mehrschichtige Architekturen soll die Vorteile der schnittstellenbasierten Programmierung veranschaulichen. Dieser Ansatz erleichtert die Wartung und das Testen von Anwendungen. Wir werden erneut eine dreischichtige Architektur verwenden:

Image

Jede Schicht wird auf zwei verschiedene Arten implementiert. Wir möchten zeigen, dass die Implementierung einer Schicht leicht geändert werden kann, ohne die anderen Schichten wesentlich zu beeinträchtigen.

14.4.1. Die [dao]-Schicht

Image

Die Schnittstelle [InterfaceDao] sieht wie folgt aus:

#  imports
from abc import ABC, abstractmethod


#  dao interface
class InterfaceDao(ABC):
    #  a single method
    @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:

1
2
3
4
5
6
7
from InterfaceDao import InterfaceDao


class DaoImpl1(InterfaceDao):
    #  implementation 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:

1
2
3
4
5
6
7
from InterfaceDao import InterfaceDao


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

14.4.2. Die [Business-]Schicht

Image

Die [BusinessInterface]-Schnittstelle sieht wie folgt aus:

#  imports
from abc import ABC, abstractmethod


#  business interface
class InterfaceMétier(ABC):
    #  a single method
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • Zeilen 8–10: Die Methode [do_something_in_business_layer] ist die einzige Methode in der Schnittstelle;

Die Klasse [AbstractBaseMétier] implementiert die Schnittstelle [InterfaceMétier] wie folgt:

#  imports
from abc import ABC, abstractmethod

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


class AbstractBaseMétier(InterfaceMétier, ABC):
    #  properties
    #  __dao is a reference to the [dao] layer
    @property
    def dao(self) -> InterfaceDao:
        return self.__dao

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

    #  interface implementation [InterfaceMétier]
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • Zeile 8: Die Klasse [AbstractBaseMétier] ist die Basis für zwei Klassen:
    • [BusinessInterface]: Die Klasse [AbstractBusinessBase] implementiert diese Schnittstelle in den Zeilen 19–22. Tatsächlich sehen wir, dass sie die Methode [do_something_in_business_layer] nicht implementiert hat, die sie als abstrakt deklariert hat (Zeile 20). Es ist Aufgabe der abgeleiteten Klassen, die Methode zu implementieren;
    • [ABC] für den Zugriff auf die [@abstractmethod]-Annotationen;
    • Die Reihenfolge ist entscheidend: Wenn wir sie hier umkehren, löst Python einen Laufzeitfehler aus;

Dies ist das erste Mal, dass wir Mehrfachvererbung (Vererbung von mehreren Klassen) verwenden. Die Klasse [AbstractBaseMétier] erbt Eigenschaften sowohl von der Klasse [InterfaceMétier] als auch von der Klasse [ABC].

  • Zeilen 9–17: Wir definieren die Eigenschaft [dao], die eine Referenz auf die [dao]-Schicht sein wird;

Eine Schnittstelle ist dazu gedacht, implementiert zu werden. Wenn verschiedene Implementierungen Eigenschaften gemeinsam nutzen, ist es sinnvoll, diese in einer übergeordneten Klasse unterzubringen, 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 implementiert.

Die Klasse [BusinessImpl1] implementiert die Schnittstelle [BusinessInterface] wie folgt:

1
2
3
4
5
6
7
8
9
from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl1(AbstractBaseMétier):
    #  interface implementation [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 [BusinessImpl1] leitet sich von der Klasse [AbstractBusinessBase] ab. Sie erbt daher die Eigenschaft [dao] von dieser Klasse;
  • Zeilen 6–9: Implementierung der Schnittstelle [BusinessInterface], die die übergeordnete Klasse [AbstractBusinessBase] nicht implementiert hat;
  • Zeile 9: Die [dao]-Schicht wird verwendet;

Die Klasse [BusinessImpl2] implementiert die Schnittstelle [BusinessInterface] auf ähnliche Weise:

1
2
3
4
5
6
7
8
9
from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl2(AbstractBaseMétier):
    #  interface implementation [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 [ui]-Schicht

Image

Die Schnittstelle [InterfaceUi] sieht wie folgt aus:

#  imports
from abc import ABC, abstractmethod


#  ui interface
class InterfaceUi(ABC):
    #  a single method
    @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:

#  imports
from abc import ABC, abstractmethod

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


class AbstractBaseUi(InterfaceUi, ABC):
    #  properties
    #  business is a reference to the [business] layer
    @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

    #  interface implementation [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] verfügt über eine Referenz auf die [business]-Schicht;

Die Implementierungsklasse [UiImpl1] sieht wie folgt aus:

1
2
3
4
5
6
7
8
9
from AbstractBaseUi import AbstractBaseUi


class UiImpl1(AbstractBaseUi):
    #  interface implementation [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 daher deren Eigenschaft [business]. Diese wird in Zeile 9 verwendet;

Die Implementierungsklasse [UiImpl2] ist ähnlich:

1
2
3
4
5
6
7
8
9
from AbstractBaseUi import AbstractBaseUi


class UiImpl2(AbstractBaseUi):
    #  interface implementation [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 daher deren Eigenschaft [business]. 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] sieht wie folgt aus:

def configure():
    #  step 1 ------
    #  absolute path of this script's folder
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  dependencies
    absolute_dependencies = [
        #  local Python Path folders
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    #  configure the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application layer configuration
    from DaoImpl1 import DaoImpl1
    from MétierImpl1 import MétierImpl1
    from UiImpl1 import UiImpl1
    #  layer instantiation
    #  dao
    dao = DaoImpl1()
    #  business
    métier = MétierImpl1()
    métier.dao = dao
    #  ui
    ui = UiImpl1()
    ui.métier = métier

    #  put the layer instances in the config
    #  only the ui layer is required here
    config = {"ui": ui}

    #  return the config
    return config
  • Zeilen 2–16: Konfiguration des Python-Pfads der Anwendung;
  • Zeilen 18–31: Instanziierung der [DAO-, Business-, UI-]Schichten. Zur Implementierung ihrer Schnittstellen wählen wir jedes Mal die erste erstellte Implementierung;
  • Zeilen 33–35: Wir fügen die Schichtenreferenzen zur Konfiguration hinzu. Hier benötigt das Hauptskript nur die [ui]-Schicht;

Die Datei [config2] ist ähnlich und implementiert jede Schnittstelle mit der zweiten verfügbaren Implementierung:

def configure():
    #  step 1 ---
    #  absolute path of this script's folder
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  dependencies
    absolute_dependencies = [
        #  local Python Path folders
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    #  configure the syspath
    from myutils import set_syspath

    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application layer configuration
    from DaoImpl2 import DaoImpl2
    from MétierImpl2 import MétierImpl2
    from UiImpl2 import UiImpl2
    #  layer instantiation
    #  dao
    dao = DaoImpl2()
    #  business
    métier = MétierImpl2()
    métier.dao = dao
    #  ui
    ui = UiImpl2()
    ui.métier = métier

    #  put the layer instances in the config
    #  only the ui layer is required here
    config = {"ui": ui}

    #  return the config
    return config

14.4.5. Das Hauptskript [main]

Image

Das Hauptskript lautet wie folgt:

#  imports
import importlib
import sys

#  hand ---------

#  you need two arguments
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()

#  application configuration
module = importlib.import_module(sys.argv[1])
config = module.configure()

#  execution of the [ui] layer
print(config["ui"].do_something_in_ui_layer(10, 20))

Dieses Skript benötigt 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] ist der zweite, …
  • Zeile 8: Wir ermitteln die Anzahl der Parameter;
  • Zeilen 9–11: Wir prüfen, ob tatsächlich ein Argument vorhanden ist und ob dessen Wert entweder [config1] oder [config2] ist. Ist dies nicht der Fall, wird eine Fehlermeldung angezeigt (Zeile 10) und wir beenden das Programm (Zeile 11);

Sobald die gewünschte Konfiguration bekannt ist, müssen wir diese Konfiguration ausführen. Wenn beispielsweise Konfiguration 1 gewä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 [sys.argv[1]. Um ein Modul zu importieren, dessen Name in einer Variablen gespeichert ist, müssen wir das [importlib]-Paket verwenden (Zeile 2).

  • Zeile 14: Wir importieren das Modul, dessen Name in [sys.argv[1]] steht
  • Zeile 15: Sobald dies erledigt ist, führen wir die Funktion [configure] dieses Moduls aus. Wir rufen ein Wörterbuch [config] ab, das die Konfiguration der Anwendung enthält;
  • Zeile 18: Wir wissen, dass sich ein Verweis auf die [ui]-Schicht in config['ui'] befindet. Wir verwenden ihn, um die Methode [do_something_in_ui_layer] aufzurufen. Wir wissen, dass diese Methode eine Methode in der [business]-Schicht aufruft, die wiederum eine Methode in der [dao]-Schicht aufruft;

Die Funktion [do_something_in_ui_layer] sieht beispielsweise wie folgt aus:

1
2
3
4
5
6
class UiImpl1(AbstractBaseUi):
    #  interface implementation [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)
  • In Zeile 6 oben wird die Eigenschaft [business] der Klasse [UiImpl1] aus Zeile 1 verwendet. In der Konfiguration [config1] wurde jedoch Folgendes geschrieben:

# métier
    métier = MétierImpl1()
    métier.dao = dao
    # ui
    ui = UiImpl1()
    ui.métier = métier
  • Zeile 6: Die Eigenschaft [business] von [UIImpl1] ist eine Referenz auf die Klasse [BusinessImpl1] (Zeile 2). Daher wird die Methode [do_something_in_ui_layer] der Klasse [BusinessImpl1] ausgeführt;

In der Klasse [MétierUiImpl1] steht geschrieben:

1
2
3
4
5
6
class MétierImpl1(AbstractBaseMétier):
    #  interface implementation [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 6: Die von der [ui]-Schicht aufgerufene Methode ruft wiederum eine Methode der [dao]-Eigenschaft der Klasse [BusinessImpl1] auf;

In der Konfiguration [config1] wurde jedoch Folgendes geschrieben:


# dao
    dao = DaoImpl1()
    # métier
    métier = MétierImpl1()
    métier.dao = dao
  • Zeile 5: Die Eigenschaft [BusinessImpl1.dao] ist vom Typ [DaoImpl1] (Zeile 2);

Was wir hier zeigen wollen, ist, dass sich das Skript [main] nicht um die Schichten [business] und [DAO] kümmern muss. Es muss sich nur um die Schicht [UI] kümmern, da die Verbindungen zwischen dieser Schicht und den anderen durch die Konfiguration hergestellt wurden.

Image

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

Image

  • Erstellen Sie in [1-2] eine sogenannte Laufzeitkonfiguration;
  • Geben Sie dieser Konfiguration in [3] einen Namen, damit Sie sie später wiederfinden können;
  • Wählen Sie in [4] das auszuführende Skript aus. Wenn Sie die Schritte in [1-2] befolgt haben, ist das richtige Skript bereits ausgewählt;
  • Geben Sie in [5] die Parameter ein, die an das Skript übergeben werden sollen. Hier übergeben wir die Zeichenfolge [config1], um das Skript anzuweisen, Konfiguration Nr. 1 zu verwenden;
  • Bestätigen Sie in [6] die Ausführungskonfiguration;

Image

  • Zeigen Sie in [1-2] die vorhandenen Ausführungskontexte an;
  • Wählen Sie in [3] den vorhandenen Ausführungskontext aus und duplizieren Sie ihn [4];

Image

  • in [5] den Namen für die neue Konfiguration. Dies ist die Konfiguration, die das Skript [main] [6] ausführt, indem sie ihm den Parameter [config2] [7] übergibt;

Die Ausführungskonfigurationen sind in der oberen rechten Ecke des PyCharm-Fensters verfügbar:

Image

Wählen Sie einfach [2] oder [3] aus und klicken Sie dann auf [4], um das Skript [main] mit dem Parameter [config1] oder [config2] auszuführen.

Mit [config1] liefert die Ausführung von [main] die folgenden 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

Mit [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 ist eingeladen, diese Ergebnisse zu überprüfen.