Skip to content

14. Laagarchitectuur en programmeren via interfaces

14.1. Inleiding

We willen een applicatie schrijven waarmee de cijfers van de leerlingen van een middelbare school kunnen worden weergegeven. Deze applicatie kan een meerlaagse architectuur hebben:

Image

  • de laag [ui] (gebruikersinterface) is de laag die in contact staat met de gebruiker van de applicatie;
  • De laag [métier] implementeert de bedrijfsregels van de applicatie, zoals de berekening van een salaris of een factuur. Deze laag maakt gebruik van gegevens die via de laag [présentation] van de gebruiker afkomstig zijn en via de laag [dao] van de lagen SGBD;
  • de laag [dao] (Data Access Objects) beheert de toegang tot de gegevens van de laag SGBD (databasebeheersysteem).

Dit is de architectuur die werd gebruikt in de |cursus over Python 2|. Er kan ook een variant worden geïntroduceerd:

Image

De verschillen ten opzichte van de vorige laagenstructuur zijn als volgt:

  • een hoofdscript met de naam [main] (hierboven genoemd) regelt het instantiëren van de lagen;
  • de lagen [ui, métier, dao] communiceren niet noodzakelijkerwijs meer met elkaar. Als dat wel nodig is, verstrekt het script [main] hen de referenties van de lagen die ze nodig hebben;

De code is hier georganiseerd in competentiegebieden met een ‘dirigent’:

  • de ‘dirigent’ is het hoofdscript [main];
  • de lagen [ui], [dao] en [métier] zijn de competentiegebieden;

Deze organisatie zou men een orkestrale organisatie kunnen noemen.

14.2. Voorbeeld 1

We zullen de gelaagde architectuur illustreren aan de hand van een eenvoudige console-applicatie:

  • er is geen database;
  • de laag [dao] beheert entiteiten Elève, Classe, Matière en Note waarmee de cijfers van de leerlingen kunnen worden beheerd;
  • de laag [métier] maakt het mogelijk om indicatoren te berekenen op basis van de cijfers van een specifieke leerling;
  • de laag [ui] is een console-applicatie die de resultaten van de leerlingen weergeeft;

Het project PyCharm van de applicatie is als volgt:

Opmerking: de blauw gemarkeerde mappen maken deel uit van [Sources Root] van het project PyCharm.

14.2.1. De entiteiten van de applicatie

We noemen klassen die uitsluitend dienen om gegevens in te kapselen ‘entiteiten’. Hiervoor zouden we ook woordenboeken kunnen gebruiken. Het voordeel van de klasse is dat hiermee de geldigheid van de in het object opgeslagen gegevens kan worden getest en dat er een methode beschikbaar is die de identiteit van het object in de vorm van een tekenreeks retourneert.

14.2.1.1. De entiteit [Classe]

De entiteit [Classe] (Classe.py) vertegenwoordigt een klas van de middelbare school:


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


class Classe(BaseEntity):
    # attributen die zijn uitgesloten van de klasse-status
    excluded_keys = []

    # klasse-eigenschappen
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificatiecode van de klasse
        # naam: naam van de klasse
        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):
        # naam moet een niet-lege tekenreeks zijn
        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")

Opmerkingen

  • regel 7: de entiteit [Classe] is afgeleid van de entiteit [BaseEntity] die is besproken in de paragraaf |De klas BaseEntity|;
  • regels 11-16: een klasse wordt gedefinieerd door een nummer id en een nom (regel 16). De eigenschap [id] wordt geleverd door de klasse [BaseEntity] en de naam door de klasse [Classe];
  • regels 18-30: getter/setter van het attribuut [nom];

14.2.1.2. De entiteit [Matière]

De klasse [Matière] (matière.py) is als volgt:


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


class Matière(BaseEntity):
    # attributen die zijn uitgesloten van de status van de klasse
    excluded_keys = []

    # eigenschappen van de klasse
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificatiecode van het vak
        # naam: naam van het vak
        # coëfficiënt: coëfficiënt van het vak
        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):
        # naam moet een niet-lege tekenreeks zijn
        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):
        # de coëfficiënt moet een reëel getal >=0 zijn
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # fout?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

Opmerkingen

  • regel 7: de klasse [Classe] is afgeleid van de klasse [BaseEntity];
  • regels 11-17: een vak wordt gedefinieerd door zijn nummer [id], zijn naam [nom] en zijn coëfficiënt [coefficient];
  • regels 19-50: getters/setters van de attributen van de klasse;

14.2.1.3. De entiteit [Elève]

De klasse [Elève] (élève.py) ziet er als volgt uit:


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

from Utils import Utils


class Elève(BaseEntity):
    # attributen die zijn uitgesloten van de klasse-status
    excluded_keys = []

    # eigenschappen van de klasse
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificatiecode van de leerling
        # achternaam: achternaam van de leerling
        # voornaam: voornaam van de leerling
        # klas: klas van de leerling
        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:
        # achternaam moet een niet-lege tekenreeks zijn
        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:
        # voornaam moet een niet-lege tekenreeks zijn
        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:
            # er wordt een klasse-type verwacht
            if isinstance(value, Classe):
                self.__classe = value
            # of een type 'dict'
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # of een 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}")

Opmerkingen

  • regel 9: de klasse [Elève] is afgeleid van de klasse [BaseEntity];
  • regels 13-20: een leerling wordt gekenmerkt door zijn nummer [id], zijn achternaam [nom], zijn voornaam [prénom] en zijn klas [classe]. Deze laatste parameter is een verwijzing naar een object [Classe];
  • regels 22-65: getters/setters van de attributen van de klasse;

14.2.1.4. De entiteit [Note]

De klasse [Note] (note.py) ziet er als volgt uit:


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


class Note(BaseEntity):
    # attributen die zijn uitgesloten van de klasse-status
    excluded_keys = []

    # eigenschappen van de klasse
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificatiecode van de notitie
        # waarde: het cijfer zelf
        # leerling: leerling (van het type Leerling) waarop het cijfer betrekking heeft
        # vak: vak (van het type Vak) waarop het cijfer betrekking heeft
        # het object ‘Cijfer’ is dus het cijfer van een leerling voor een bepaald vak
        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):
        # het cijfer moet een reëel getal tussen 0 en 20 zijn
        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:
            # er wordt een type ‘Leerling’ verwacht
            if isinstance(value, Elève):
                self.__élève = value
            # of een type dict
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # of een 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:
            # er wordt een type 'Vak' verwacht
            if isinstance(value, Matière):
                self.__matière = value
            # of een dict-type
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # of een 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}")

Opmerkingen

  • regel 8: de klasse [Note] is afgeleid van de klasse [BaseEntity];
  • regels 12-20: een object [Note] wordt gekenmerkt door zijn nummer [id], de waarde van het cijfer [valeur], een verwijzing [élève] naar de leerling die dit cijfer heeft, een verwijzing naar het vak [matière] waarop het cijfer betrekking heeft;
  • regels 22-75: getters / setters van de attributen van de klasse;

14.2.2. Configuratie van de applicatie

Het bestand [config.py] configureert de omgeving van het hoofdscript [main] (1) en die van de tests (2). Al deze scripts bevatten een instructie [import config] aan het begin van de code. Ter herinnering: de map die het script bevat waarop de opdracht [python script] betrekking heeft, maakt automatisch deel uit van de Python-map Path.Si. Aangezien [config] zich dus in dezelfde map bevindt als de scripts met de instructie [import config], zal het worden gevonden. De bestanden [1] en [2] zijn hier identiek. Dit hoeft echter niet altijd het geval te zijn.

Het bestand [config.sys] is als volgt:


def configure():
    import os

    # absoluut pad naar de map van dit script
    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 afhankelijkheden
    absolute_dependencies=[
        # lokale mappen met klassen en interfaces
        f"{root_dir}/02/entities",
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
    ]

    # syspath bijwerken
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # de configuratie wordt aangepast
    return {}
  • regels 11-14: de mappen die deel moeten uitmaken van het Python Path (sys.path);
  • de map [f"{root_dir}/02/entities"] geeft toegang tot de klassen [BaseEntity] en [MyException];
  • de map [f"{script_dir}/../entities"] biedt toegang tot de klassen [Elève], [Classe], [Matière] en [Note];
  • de map [f"{script_dir}/../interfaces",] biedt toegang tot de interfaces van de applicatie;
  • de map [f"{script_dir}/../services"] biedt toegang tot de klassen die de interfaces implementeren;

14.2.3. Testen van entiteiten

We gaan hier tests schrijven die worden uitgevoerd door een tool genaamd [unittest]. PyCharm wordt geleverd met verschillende testframeworks. De keuze voor een van deze frameworks wordt gemaakt in de configuratie van PyCharm:

Image

  • In [4] zijn verschillende testframeworks beschikbaar:

Image

14.2.3.1. De testklasse [TestBaseEntity]

Het testscript [TestBaseEntity] ziet er als volgt uit:


import unittest

# de applicatie wordt geconfigureerd
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
        # een notitie samenstellen op basis van een tekenreeks jSON
        note = Note().fromjson(
            '{"id": 8, "waarde": 12, "leerling": {"id": 42, "achternaam": "achternaam4", "voornaam": "voornaam4", "klas": {"id": 2, "naam": "klas2"}}, "vak": {"id": 2, "naam": "vak2", "coëfficiënt": 2}}')
        # controles
        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):
        # importen
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        # opstellen van een cijfer op basis van een woordenboek
        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}})
        # controles
        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()

Opmerkingen

  • regel 1: we importeren de module [unittest], die de verschillende testmethoden zal leveren;
  • regels 3-6: we configureren de applicatie zodat de klassen die nodig zijn voor de tests worden gevonden;
  • regel 9: een testklasse [unittest] moet de klasse [unittest.TestCase] uitbreiden;
  • regels 11, 27: de testfuncties moeten een naam hebben die begint met [test], anders worden ze niet herkend;
  • regels 13-16: we importeren de klassen die we nodig hebben;
  • in deze testklasse willen we het gedrag van de methoden [BaseEntity.fromdict] (regel 34) en [BaseEntity.fromjson] (regel 18) controleren. De klasse [Note] heeft eigenschappen die verwijzingen zijn naar andere klassen. We willen controleren of de twee voorgaande methoden geldige [Note]-objecten aanmaken;
  • regel 18: we maken een object van het type [Note] aan op basis van een object van het type jSON;
  • regel 21: we controleren of het aangemaakte object inderdaad van het type [Note] is. De methode [assertIsInstance] is een methode van de klasse [unittest.TestCase], de bovenliggende klasse van de klasse [TestBaseEntity];
  • regel 22: er wordt gecontroleerd of [note.élève] inderdaad van het type [Elève] is;
  • regel 23: er wordt gecontroleerd of [note.élève.classe] inderdaad van het type [Classe] is;
  • regel 24: we controleren of [note.matière] inderdaad van het type [Matière] is;
  • regels 33-42: we doen hetzelfde met de methode [BaseEntity.fromdict];

Er zijn verschillende manieren om de tests uit te voeren:

  • in [1-2] voeren we [TestBaseEntity] uit met het framework [UnitTest];
  • in [3-5] mislukken de tests. [UnitTests] geeft aan dat er geen tests zijn gevonden om uit te voeren;

Het mislukken van de tests is te wijten aan de opbouw van de code van [TestBaseEntity]:


import unittest

# de applicatie configureren
import config

config = config.configure()


class TestBaseEntity(unittest.TestCase):

Wat het framework [UnitTest] hindert, is de aanwezigheid van uitvoerbare code, regels 3-6, vóór de definitie van de testklasse, regel 9.

We herschikken de code daarom als volgt:


import unittest


class TestBaseEntity(unittest.TestCase):

    def setUp(self):
        # de applicatie configureren
        import config

        config.configure()

    def test_note1(self):
        

    def test_note2(self):
        


if __name__ == '__main__':
    unittest.main()
  • regels 6-10: er wordt een functie [setUp] gedefinieerd. Deze functie heeft een bijzondere rol: ze wordt vóór elke testfunctie (test_note1, test_note2) uitgevoerd;

Vervolgens levert de uitvoering van de klasse [TestBaseEntity] de volgende resultaten op:

Deze keer zijn beide testmethoden uitgevoerd en zijn de tests geslaagd.

Laten we eens kijken wat er gebeurt als een test mislukt. Laten we de code van [test_note1] als volgt aanpassen:


    def test_note1(self):
        # opzettelijke fout – we controleren of 1==2
        self.assertEqual(1,2)
        # imports
        from Note import Note

  • regel 2: we controleren of 1==2;

De resultaten van de uitvoering zijn dan als volgt:

We kunnen de oorzaak van de fout achterhalen door op de mislukte test [2] te klikken:

  • in [7-8], de oorzaak van de fout;

Een andere manier om een testklasse uit te voeren, is door deze in een terminal uit te voeren:


(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

Regel 6 geeft aan dat beide tests zijn geslaagd (de fout 1==2 is verwijderd);

Ten slotte is er nog een derde manier om de testklasse [TestBaseEntity] uit te voeren, eveneens in een terminal, en die is als volgt. We sluiten de testklasse af met de volgende regels 6-7;



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


if __name__ == '__main__':
    unittest.main()
  • regel 6: de variabele [__name__] is de naam die aan het uit te voeren script is gegeven. Wanneer het script wordt gestart met het commando [python script.py], is de variabele [__name__] gelijk aan [__main__] (twee onderstreepte tekens voor en na de identificatiecode). Regel 7 wordt dus alleen uitgevoerd wanneer het script [TestBaseEntity] wordt gestart door het commando [python TestBaseEntity.py]. De instructie [unittest.main()] start de uitvoering van het script via het framework [UnitTest]. Hier volgt een voorbeeld:

(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. De testklasse [TestEntités]

De testklasse [TestEntités] ziet er als volgt uit:


import unittest


class TestEntités(unittest.TestCase):
    def setUp(self):
        # de applicatie wordt geconfigureerd
        import config

        config.configure()

    def test_code1a(self):
        # importen
        from Elève import Elève
        from MyException import MyException
        # foutcode
        code = None
        try:
            # ongeldige 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
        # controle
        self.assertEqual(code, 1)

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

        try:
            # ongeldige naam
            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
        # controle
        self.assertEqual(code, 41)

    def test_code42(self):
        # import
        from Elève import Elève
        from MyException import MyException
        # foutcode
        code = None
        try:
            # ongeldige voornaam
            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
        # controle
        self.assertEqual(code, 42)

    def test_code43(self):
        # import
        from Elève import Elève
        from MyException import MyException
        # foutcode
        code = None
        try:
            # ongeldige 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
        # controle
        self.assertEqual(code, 43)

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

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

        # foutcode
        code = None
        try:
            # ongeldige naam
            Classe().fromdict({"id": 1, "nom": ""})
        except MyException as ex:
            code = ex.code
        # controle
        self.assertEqual(code, 11)

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

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

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

    def test_code22(self):
        # import
        from Matière import Matière
        from MyException import MyException
        # foutcode
        code = None
        try:
            # ongeldige coëfficiënt
            Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # controle
        self.assertEqual(code, 22)

    def test_code1d(self):
        # import
        from Note import Note
        from MyException import MyException
        # foutcode
        code = None
        try:
            # ongeldige ID
            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
        # controle
        self.assertEqual(code, 1)

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

        # foutcode
        code = None
        try:
            # ongeldige waarde
            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
        # controle
        self.assertEqual(code, 31)

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

        # foutcode
        code = None
        try:
            # ongeldige leerling
            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
        # controle
        self.assertEqual(code, 32)

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

        # foutcode
        code = None
        try:
            # ongeldig vak
            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
        # controle
        self.assertEqual(code, 33)

    def test_exception(self):
        # import
        from Elève import Elève
        # de test moet het type [MyException] starten om te slagen
        from MyException import MyException
        with self.assertRaises(MyException):
            # de test
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})


if __name__ == '__main__':
    unittest.main()
  • Het testscript is bedoeld om de setters van de klassen te testen: controleren of er geen onjuiste waarden kunnen worden toegewezen aan de attributen van de verschillende entiteiten;
  • regels 11-24: er wordt getest of er geen ongeldige identificatiecode aan een leerling kan worden toegewezen. Omdat in regel 16 de waarde 'x' als identificatiecode van de leerling wordt doorgegeven, verwachten we dat er een uitzondering optreedt. We zouden dus naar de regels 20-22 moeten gaan;
  • regel 21: weergave van het foutbericht;
  • regel 22: we halen de foutcode op (zie paragraaf |De entiteit MyException|);
  • regel 24: we controleren (assert) of de foutcode 1 is. Hier controleren we twee dingen:
    • dat er daadwerkelijk een fout is opgetreden;
    • dat de foutcode 1 is;
  • dit proces wordt herhaald met de functies in de regels 24-213;
  • regels 215-222: er wordt getest of een actie een uitzondering van een bepaald type genereert;
  • regel 220: er wordt aangegeven dat de test geslaagd is als er een uitzondering van het type [MyException] wordt gegenereerd;

Resultaten

Het testscript wordt uitgevoerd:

De verkregen resultaten zijn als volgt:


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 zijn alle tests geslaagd

14.2.4. De laag [dao]

Image

De laag [dao] implementeert de interface [InterfaceDao] [1]. Deze wordt geïmplementeerd door de klasse [Dao] (2). Het script [tests_dao] (3) test de methoden van de laag [dao].

14.2.4.1. Interface [InterfaceDao]

Een interface is een overeenkomst tussen de aanroepende code en de aangeroepen code. Het is de aangeroepen code die de interface aanbiedt:

  • De aanroepende code [1] kent de implementatie van de aangeroepen code [3] niet. Hij weet alleen hoe hij deze moet aanroepen. Dit wordt hem aangegeven door de interface [2]. Deze interface definieert een aantal methoden/functies die moeten worden gebruikt om met de aangeroepen code te communiceren. Deze interface wordt ook wel API (Application Programming Interface) genoemd;

De laag [dao] biedt de volgende interface:

  • [get_classes] geeft de lijst met klassen van de middelbare school weer;
  • [get_matières] geeft de lijst met vakken weer die op de middelbare school worden onderwezen;
  • [get_élèves] geeft de lijst met leerlingen van de middelbare school weer;
  • [get_notes] geeft de lijst met cijfers van alle leerlingen weer;
  • [get_notes_for_élève_by_id] geeft de cijfers van een bepaalde leerling weer;
  • [get_élève_by_id] geeft een leerling weer op basis van zijn nummer;

De aanroepende code gebruikt alleen deze methoden. De aanroepende code hoeft niet te weten hoe ze zijn geïmplementeerd. De gegevens kunnen dan uit verschillende bronnen afkomstig zijn (hardgecodeerd, uit een database, uit tekstbestanden…) zonder dat dit invloed heeft op de aanroepende code. Dit noemen we programmeren via interfaces.

Python 3 kent een concept dat dicht bij dat van een interface ligt: de abstracte klasse. Daar gaan we gebruik van maken. We gaan de interfaces van dit voorbeeld groeperen in de map [interfaces].

We definiëren een abstracte klasse [InterfaceDao] (InterfaceDao.py) voor de laag [dao]:


# imports
from abc import ABC, abstractmethod

# Dao-interface
from Elève import Elève


class InterfaceDao(ABC):
    # lijst met klassen
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # lijst met leerlingen
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # lijst met vakken
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    # lijst met cijfers
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    # lijst met cijfers van een leerling
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # een leerling zoeken op zijn of haar ID
    @abstractmethod
    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

Opmerkingen:

  • regel 2: ABC = Abstract Base Class. Uit de module [abc] worden de klasse ABC en de decorator [abstractmethod] geïmporteerd, die worden gebruikt in de regels 10, 15, 20, 25, 30 en 35;
  • regel 8: de abstracte klasse heet [InterfaceDao] en is afgeleid van de klasse [ABC];
  • de methoden van de abstracte klasse zijn voorzien van de decorator [@abstractmethod], waardoor de aldus gedecoreerde methode een abstracte methode wordt: de code ervan is niet gedefinieerd. Toch wordt er code in geplaatst: de instructie [pass], die niets doet;
  • de abstracte klasse [InterfaceDao] kan niet worden geïnstantieerd. Alleen klassen die zijn afgeleid van [InterfaceDao] en die alle methoden van [InterfaceDao] hebben geïmplementeerd, kunnen worden geïnstantieerd. Als we dus twee klassen [Dao1] en [Dao2] aanmaken die zijn afgeleid van de klasse [InterfaceDao], zullen ze beide de abstracte methoden van [InterfaceDao] implementeren. Men zou dus kunnen zeggen dat ze de interface [InterfaceDao] implementeren;
  • talen die zowel interfaces als abstracte klassen implementeren, geven de interface een andere rol dan die van de abstracte klasse. Een interface heeft geen attributen en kan niet worden geïnstantieerd. Een klasse kan een interface implementeren door alle methoden ervan te definiëren;

14.2.4.2. Implementatie [Dao]

De klasse [Dao] (dao.py) implementeert de interface [InterfaceDao] als volgt:


# entiteiten en interfaces importeren
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


# laag [dao] implementeert de interface InterfaceDao
class Dao(InterfaceDao):
    # constructor
    # er worden vaste lijsten aangemaakt
    def __init__(self):
        # de klassen worden geïnstantieerd
        classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
        classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
        self.classes = [classe1, classe2]
        # de vakken
        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]
        # de leerlingen
        é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]
        # de cijfers van de leerlingen voor de verschillende vakken
        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
    # -----------
    

Opmerkingen:

  • regels 1-7: de entiteiten en de interface [InterfaceDao] worden geïmporteerd;
  • regel 11: de klasse [Dao] is afgeleid van de abstracte klasse [InterfaceDao]. We zeggen dat deze de interface [InterfaceDao] implementeert;
  • regel 14: de constructor heeft geen parameters. Hij construeert vier lijsten hard:
    • regels 15-18: de lijst met klassen;
    • regels 19-22: de lijst met vakken;
    • regels 23-28: de lijst met leerlingen;
    • regels 29-38: de lijst met cijfers;
  • regels 40-44: implementatie van de methoden van de interface [Interface Dao]. Hier definiëren we deze niet om de foutmelding van Python te zien;

Een testprogramma zou er als volgt uit kunnen zien: [tests-dao.py]:


# de applicatie configureren
import config

config = config.configure()

# instantie van de laag [dao]
from Dao import Dao

daoImpl = Dao()

# lijst met klassen
for classe in daoImpl.get_classes():
    print(classe)

# lijst met vakken
for matière in daoImpl.get_matières():
    print(matière)

# lijst met klassen
for élève in daoImpl.get_élèves():
    print(élève)

# lijst met cijfers
for note in daoImpl.get_notes():
    print(note)

Opmerking: het script [tests-dao.py] is geen test voor [unittest], omdat het geen methoden bevat waarvan de naam begint met [test_].

De opmerkingen spreken voor zich. De regels 11-25 maken gebruik van de interface van de laag [dao]. Er worden hier geen aannames gedaan over de daadwerkelijke implementatie van de laag. In regel 9 wordt de laag [dao] geïnstantieerd.

De resultaten van de uitvoering van dit script zijn als volgt:


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

We zien dat er een fout optreedt zodra de klasse [Dao] wordt geïnstantieerd (regel 3 hierboven). De Python 3-interpreter meldt dat hij de klasse niet kan instantiëren, omdat we de abstracte methoden [get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves] niet hebben gedefinieerd.

PyCharm kent ook het begrip ‘abstracte klasse’ en stelt ons voor om de methoden daarvan te definiëren:

  • in [1], klik met de rechtermuisknop op de code;
  • in [2-3], kies [Generate / Implement Methods] om de ontbrekende methoden van de klasse [Dao] te implementeren;
  • in [4], kies de methoden die moeten worden geïmplementeerd, in dit geval alle;

Zodra dit is gebeurd, wordt de klasse [Dao] op de volgende manier aangevuld met PyCharm:


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

We vullen de klasse [Dao] als volgt aan:


    # -----------
    # interface IDao
    # -----------
    
    # lijst met klassen
    def get_classes(self) -> list:
        return self.classes

    # lijst met vakken
    def get_matières(self) -> list:
        return self.matières

    # lijst van leerlingen
    def get_élèves(self) -> list:
        return self.élèves

    # lijst met cijfers
    def get_notes(self) -> list:
        return self.notes

    def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
        # een leerling opzoeken
        élève = self.get_élève_by_id(élève_id)
        # zijn cijfers ophalen
        notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
        # het resultaat weergeven
        return {"élève": élève, "notes": notes}

    def get_élève_by_id(self, élève_id: int) -> Elève:
        # leerlingen filteren
        élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
        # gevonden?
        if not élèves:
            raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
        # resultaat
        return élèves[0]
  • de regels 5-19 leveren geen problemen op;
  • regels 29-36: de methode die de leerling retourneert waarvan het nummer wordt doorgegeven. Als de leerling niet bestaat, wordt er een uitzondering gegenereerd;
  • regel 31: de functie [filter] maakt het mogelijk een lijst te filteren:
    • de eerste parameter is het filtercriterium;
    • de tweede parameter is de te filteren lijst, in dit geval de lijst met leerlingen;
  • regel 31: het filtercriterium voor de lijst wordt geïmplementeerd met behulp van de functie [f(e :Elève)->bool]. Deze wordt toegepast op elk element van de te filteren lijst. Als het element aan het filtercriterium voldoet, wordt het in de gefilterde lijst opgenomen; anders wordt het uitgesloten. Hier kan men ofwel:
    • de naam van de functie f opgeven en deze elders implementeren. De aanroep van de functie [filter] wordt dan [filter(f,self.get_élèves()];
    • de definitie van de functie f opgeven. De aanroep van de functie [filter] wordt dan [filter(f(e :Elève){…},self.get_élèves()], waarbij [e] een element van de gefilterde lijst vertegenwoordigt, d.w.z. een leerling. Dit is hier gedaan. De definitie van de functie f zou hier [f(e :Elève){return e.id==élève_id)] zijn: een leerling wordt alleen geselecteerd als het nummer [id] niet gelijk is aan het gezochte nummer. Een dergelijke functie kan worden vervangen door een zogenaamde lambda-functie: [lambda e: e.id == élève_id]:
      • e: staat voor de parameter van de functie f, in dit geval een leerling. Je kunt elke gewenste naam gebruiken;
      • e.id==élève_id is het filtercriterium: een leerling met nummer [e] wordt alleen geselecteerd als zijn nummer [id] overeenkomt met het gezochte nummer;
  • regel 31: de functie [filter] retourneert de gefilterde lijst in een type dat niet het type [list] is, maar dat wel kan worden omgezet naar het type [list]. Dat is wat we hier doen met de uitdrukking [list(liste filtrée)];
  • regels 33-34: als de gefilterde lijst leeg is, betekent dit dat de gezochte leerling niet bestaat. Er wordt dan een uitzondering gegenereerd;
  • regel 36: als we hier terechtkomen, betekent dit dat er geen uitzondering is opgetreden. We weten dan dat we een lijst met één element hebben opgehaald (er zijn geen twee leerlingen met hetzelfde nummer [id]). We geven dus het eerste element van de lijst terug;
  • regels 21-27: de methode [get_notes_for_élève_by_id] moet de cijfers retourneren van de leerling wiens nummer [id] wordt doorgegeven;
  • regels 22-23: we beginnen met het zoeken naar de leerling met nr. [élève_id] met behulp van de methode [get_élève_by_id] die we zojuist hebben uitgelegd. Er kan een uitzondering optreden als de gezochte leerling niet bestaat. Aangezien er geen try/catch-blok rond de instructie op regel 23 staat, wordt de uitzondering doorgegeven aan de aanroepende code. Dit is de bedoeling;
  • regels 24-25: zodra de leerling is opgehaald, worden al zijn cijfers opgehaald. Dit gebeurt opnieuw met een filter:
    • het filter is [filter(critère, self_getnotes()]. De te filteren lijst is dus de lijst met alle cijfers van alle leerlingen van de middelbare school;
    • het filtercriterium wordt uitgedrukt met behulp van een functie [lambda]: lambda n: n.élève.id == élève_id. De parameter n is een element van de te filteren lijst, dus een cijfer. Het type [Note] heeft een eigenschap [élève] die de leerling vertegenwoordigt aan wie het cijfer toebehoort. [n.élève.id], dat het nummer van deze leerling vertegenwoordigt, moet dan gelijk zijn aan het nummer van de gezochte leerling;

Vervolgens voeren we het script [tests-dao.py] uit.


# de applicatie configureren
import config

config = config.configure()

# instantie van laag [dao]
from Dao import Dao

daoImpl = Dao()

# lijst met klassen
for classe in daoImpl.get_classes():
    print(classe)

# lijst met vakken
for matière in daoImpl.get_matières():
    print(matière)

# lijst met klassen
for élève in daoImpl.get_élèves():
    print(élève)

# lijst met cijfers
for note in daoImpl.get_notes():
    print(note)

# een bepaalde leerling
print(daoImpl.get_élève_by_id(11))

# de lijst met zijn cijfers
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}")

We krijgen dan de volgende resultaten:


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

We zien dat wanneer we een cijfer weergeven (voor de andere objecten geldt hetzelfde), we ook het volgende zien:

  • de leerling die eigenaar is van het cijfer;
  • het vak waarnaar het cijfer verwijst;

Dit resultaat wordt gegenereerd door de functie [BaseEntity.asdict] (zie paragraaf ‘link’).

14.2.5. De laag [métier]

  • [InterfaceMétier] is de interface van de laag [métier];
  • [Métier] is de implementatieklasse van de laag [métier];
  • [Testmétier] is een testklasse van de klasse [Métier];

14.2.5.1. Interface [InterfaceMétier]

De laag [métier] zal de volgende interface [InterfaceMétier] (InterfaceMétier.py) implementeren:


# importen
from abc import ABC, abstractmethod

from StatsForElève import StatsForElève


# vakinterface
class InterfaceMétier(ABC):
    # berekening van statistieken voor een leerling
    @abstractmethod
    def get_stats_for_élève(self, idElève: int) -> StatsForElève:
        pass
  • [get_stats_for_élève] geeft de cijfers van leerling nr. idElève weer, evenals informatie hierover: gewogen gemiddelde, laagste cijfer, hoogste cijfer. Deze informatie is ingekapseld in een object van het type [StatsForElève];

14.2.5.2. De entiteit [StatsForElève]

Het type [StatsForElève] (StatsForElève.py), dat de statistieken (cijfers, min, max, gewogen gemiddelde) van een leerling bevat, is als volgt:


# importen
from BaseEntity import BaseEntity


# statistieken van een bepaalde leerling


class StatsForElève(BaseEntity):
    # uitgesloten kenmerken van het klassenoverzicht
    excluded_keys = []

    # eigenschappen van de klas
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificatiecode van het cijfer
        # leerling: de betreffende leerling
        # cijfers: zijn/haar cijfers
        # moyennePondérée: zijn gemiddelde, gewogen volgens de coëfficiënten van de vakken
        # min: zijn laagste cijfer
        # max: zijn hoogste cijfer

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

    # toString
    def __str__(self) -> str:
        # geval van de leerling zonder cijfers
        if len(self.notes) == 0:
            return f"Elève={self.élève}, notes=[]"
        # geval van de leerling met cijfers
        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}"

Opmerkingen:

  • regel 8: de klasse [StatsForElève] is afgeleid van de klasse [BaseEntity];
  • regels 13-22: de eigenschappen van de klasse;
    • een identificatiecode [id] afkomstig van [BaseEntity];
    • de leerling [élève] waarvan de statistieken worden ingekapseld;
    • zijn cijfers [notes];
    • zijn gewogen gemiddelde [moyenne_pondérée];
    • zijn minimumscore [min];
    • de maximale waarde [max];
  • er worden geen getters/setters gedefinieerd voor deze attributen. Er wordt ervan uitgegaan dat de laag [métier] objecten van dit type aanmaakt en dat deze geen ongeldige objecten aanmaakt;
  • regels 23-33: de functie [__str__] retourneert een tekenreeks met de eigenschappen van het object;

14.2.5.3. De implementatie [Métier]

De implementatie [Métier] (Metier.py) van de interface [InterfaceMétier] ziet er als volgt uit:


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


class Métier(InterfaceMétier):

    # constructor
    def __init__(self, dao: InterfaceDao):
        # de parameter wordt opgeslagen
        self.__dao = dao

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

    # indicatoren voor de cijfers van een bepaalde leerling
    def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
        # Statistieken voor leerling nr. idEleve
        # id_élève: leerlingnummer

        # zijn cijfers worden opgehaald met de laag [dao]
        notes_élève = self.__dao.get_notes_for_élève_by_id(id_élève)
        élève = notes_élève["élève"]
        notes = notes_élève["notes"]

        # stop als er geen cijfers zijn
        if len(notes) == 0:
            # het resultaat wordt weergegeven
            return StatsForElève().fromdict({"élève": élève, "notes": []})

        # verwerking van de cijfers van de leerling
        somme_pondérée = 0
        somme_coeff = 0
        max = -1
        min = 21
        for note in notes:
            # cijferwaarde
            valeur = note.valeur
            # coëfficiënt van het vak
            coeff = note.matière.coefficient
            # som van de coëfficiënten
            somme_coeff += coeff
            # gewogen som
            somme_pondérée += valeur * coeff
            # minimum zoeken
            if valeur < min:
                min = valeur
            # het maximum zoeken
            if valeur > max:
                max = valeur
        # berekening van ontbrekende indicatoren
        moyenne_pondérée = float(somme_pondérée) / somme_coeff

        # het resultaat wordt weergegeven in de vorm van een 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})

Opmerkingen

  • regel 7: de klasse [Métier] is afgeleid van de klasse [InterfaceMétier]. Men zegt gewoonlijk dat deze de interface [InterfaceMétier] implementeert;
  • regels 9-12: de constructor ontvangt als enige parameter een verwijzing naar de laag [dao]. Op regel 10 valt op dat aan de parameter [dao] het type [InterfaceDao] is toegekend. Er wordt geen specifieke implementatie verwacht, maar alleen een implementatie die voldoet aan de interface [InterfaceDao]. In dit geval maakt het niet uit, aangezien Python geen rekening houdt met dit type, maar het is een goede gewoonte om met interfaces te werken in plaats van met specifieke implementaties. De code is dan gemakkelijker aan te passen;
  • regels 19-60: implementatie van de methode [get_stats_for_élève];
  • regel 19: de methode ontvangt één parameter, het nummer [idElève] van de leerling waarvan we de statistieken willen zien;
  • regel 24: de [dao]-laag wordt gevraagd om de cijfers van de leerling. Dit verzoek leidt tot een uitzondering als de leerling niet bestaat. Deze uitzondering wordt niet afgehandeld (geen try/catch) en wordt dus doorgegeven aan de aanroepende code;
  • regel 25: we komen hier terecht als er geen uitzondering is opgetreden. [notes_élève] is dan een woordenboek met twee sleutels [élève, note]:
    • regel 25: we halen de gegevens van de leerling op (zijn naam, zijn klas, …);
    • regel 26: we halen zijn cijfers op;
  • regels 28-31: we kijken of de leerling cijfers heeft. Als hij die niet heeft, hoeven er geen statistieken te worden berekend;
  • regel 31: er wordt een object [StatsForElève] geretourneerd, opgebouwd uit een woordenboek met behulp van de methode [BaseEntity.fromdict];
  • regels 33-54: de cijfers van de leerling worden gebruikt om de gevraagde statistieken te berekenen. De opmerkingen in de code zouden voldoende moeten zijn om deze te begrijpen;
  • regels 56-60: er wordt een object [StatsForElève] geretourneerd dat is opgebouwd uit een woordenboek met behulp van de methode [BaseEntity.fromdict];

14.2.5.4. Test van de laag [métier]

Een script [UnitTest] van de laag [métier] zou er als volgt uit kunnen zien (TestMétier.py):


# import
import unittest


class Testmétier(unittest.TestCase):
    def setUp(self):
        # de applicatie wordt geconfigureerd
        import config
        config.configure()

    def test_statsForEleve11(self):
        # imports
        from Dao import Dao
        from Métier import Métier
        # de indicatoren van leerling 11 worden getest
        dao = Dao()
        stats_for_élève = Métier(dao).get_stats_for_élève(11)
        # weergave
        print(f"\nstats={stats_for_élève}")
        # controles
        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()

Opmerkingen

  • regels 6-9: de functie [setUp] wordt hier gebruikt om het Python-pad van de test te configureren;
  • regel 16: de laag [dao] wordt geïnstantieerd;
  • regel 17: de laag [métier] wordt geïnstantieerd en de methode [get_stats_for_élève] ervan wordt gebruikt om de statistieken van leerling nr. 11 te berekenen;
  • regel 19: het verkregen resultaat [StatsForElève] wordt weergegeven. Aangezien [StatsForElève] is afgeleid van [BaseEntity], wordt hier de reeks jSON van [StatsForElève] weergegeven;
  • regel 21: het minimumcijfer van de leerling wordt gecontroleerd;
  • regel 22: het hoogste cijfer van de leerling wordt gecontroleerd;
  • regel 23: er wordt getest of het gewogen gemiddelde 7,333 is, tot op 10⁻³ nauwkeurig. Over het algemeen is het niet mogelijk om reële getallen exact te vergelijken, omdat ze intern meestal slechts bij benadering worden weergegeven;

De testresultaten zijn als volgt:


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

Image

  • in [1], de interface van de laag [ui];
  • in [2], de implementatie van deze interface;
  • in [3], het hoofdscript van de toepassing;

14.2.6.1. Interface [InterfaceUi]

De interface van de laag [UI] ziet er als volgt uit:


# import
from abc import ABC, abstractmethod


# interface UI
class InterfaceUi(ABC):
    # uitvoering van de laag UI
    @abstractmethod
    def run(self: object):
        pass

Opmerkingen

  • regels 9-10: de laag [UI] zal slechts één methode hebben, [run];

14.2.6.2. De implementatie [Console]

De laag [console] wordt geïmplementeerd door het volgende script [Console.py]:


# laagimporten

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

# andere afhankelijkheden
from MyException import MyException


class Console(InterfaceUi):
    # constructor
    def __init__(self: object, métier: InterfaceMétier):
        # bedrijfslogica: de laag [métier]

        # de attributen worden opgeslagen
        self.métier = métier


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

    def run(self):
        # gebruikersdialoog
        fini = False
        while not fini:
            # vraag / antwoord
            réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
            # klaar?
            if réponse == "*":
                break
            # is de invoer correct?
            ok = False
            try:
                id_élève = int(réponse, 10)
                ok = id_élève >= 1
            except ValueError as erreur:
                pass
            # gegevens correct?
            if not ok:
                print("Saisie incorrecte. Recommencez...")
                continue
            # statistieken berekenen voor de geselecteerde leerling
            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}")
  • regels 3-5: alle interfaces importeren;
  • regel 11: de klasse [Console] implementeert de interface [InterfaceUi];
  • regels 12-17: de constructor van de klasse [Console] ontvangt als parameter een verwijzing naar de laag [métier]. Merk op dat aan deze parameter het type [InterfaceMétier] is toegekend om te benadrukken dat we met interfaces werken in plaats van met specifieke implementaties;
  • regel 24: implementatie van de methode [run] van de interface;
  • regel 27: een lus die stopt wanneer aan de voorwaarde op regel 31 is voldaan;
  • regel 29: invoer van gegevens die via het toetsenbord worden ingevoerd. De functie [input] ontvangt een optionele parameter: het bericht dat op het scherm moet worden weergegeven om om invoer te vragen. Deze invoer wordt altijd opgehaald als een tekenreeks. De functie [strip] verwijdert de ‘spaties’ die voor of achter de tekenreeks staan;
  • regels 34-39: er wordt gecontroleerd of de invoer, een leerlingnummer, geldig is. Het moet een geheel getal >=1 zijn. Ter herinnering: de invoer is gedaan als tekenreeks;
  • regel 36: er wordt geprobeerd de invoer om te zetten in een geheel getal in basis 10. De functie [int] genereert een uitzondering als dit niet mogelijk is;
  • regel 37: we komen hier alleen terecht als er geen uitzondering is opgetreden. We controleren of het verkregen gehele getal inderdaad >=1 is;
  • regels 38-39: de uitzondering wordt afgehandeld. Als er een uitzondering is opgetreden, is de variabele [ok] uit regel 34 op de waarde [False] gebleven;
  • regels 41-43: als de invoer onjuist was, wordt er een foutmelding weergegeven en wordt de lus herhaald (regel 43);
  • regels 45-48: de statistieken van de leerling wiens nummer is ingevoerd, worden berekend;
  • regel 46: de methode [get_stats_for_élève] van de laag [métier] wordt gebruikt. Deze methode genereert een uitzondering als de leerling niet bestaat. Deze uitzondering wordt afgehandeld in de regels 47-48. We weten dat de lagen [dao] en [métier] de uitzondering [MyException] genereren;

14.3. Het hoofdscript [main]

Het hoofdscript [main] is als volgt (main.py):


# de applicatie configureren
import config

config = config.configure()

# het syspath is geconfigureerd – we kunnen de imports uitvoeren
from Console import Console
from Dao import Dao
from Métier import Métier

# ----------- laag [console]
try:
    # instantie van de laag [dao]
    dao = Dao()
    # instantie van de laag [métier]
    métier = Métier(dao)
    # instantie van laag [ui]
    console = Console(métier)
    # uitvoering van laag [console]
    console.run()
except BaseException as ex:
    # de fout wordt weergegeven
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    pass
  • regels 1-4: het Python-pad van de applicatie wordt geconfigureerd;
  • regels 6-9: de benodigde klassen en interfaces worden geïmporteerd;
  • regel 14: instantiëren van de laag [dao];
  • regel 16: instantiëren van de laag [métier];
  • regel 18: instantiëren van de laag [ui];
  • regel 20: we starten de dialoog met de gebruiker;
  • regels 13-20: normaal gesproken worden er geen uitzonderingen vanuit deze regels gegenereerd. Uitzonderingen die afkomstig zijn uit de lagen [dao] en [métier] worden opgevangen door de laag [Console]. Het afhandelen van uitzonderingen is een lastige kunst wanneer men de gebruikte lagen niet volledig kent (wat hier het geval is). Bij twijfel kan code worden toegevoegd om elk type uitzondering af te vangen dat door de uitgevoerde code kan worden gegenereerd. Dat is wat hier gebeurt, in de regels 21-23. We vangen elke uitzondering af die afstamt van [BaseException], d.w.z. alle uitzonderingen;
  • regels 24-25: de clausule [finally] doet hier niets. Deze is er alleen om de regels 21-23 te kunnen uitcommentariëren. In de debugmodus is het namelijk niet wenselijk om uitzonderingen op te vangen. In dit geval is het de Python-interpreter die ze opvangt en vervolgens het regelnummer geeft waar de uitzondering zich heeft voorgedaan. Dit is onmisbare informatie. Wanneer de regels 21-23 worden uitgecommentarieerd, zorgen de regels 24-25 ervoor dat de try/catch-constructie syntactisch correct blijft. Zonder deze regels geeft Python een foutmelding;

Hier volgt een uitvoervoorbeeld:


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. Voorbeeld 2

Dit nieuwe voorbeeld van gelaagde architecturen is bedoeld om het nut van programmeren via interfaces te illustreren. Dit vergemakkelijkt het onderhoud en het testen van applicaties. We gebruiken opnieuw een drielaagse architectuur:

Image

Elke laag wordt op twee verschillende manieren geïmplementeerd. We willen laten zien dat de implementatie van een laag eenvoudig kan worden gewijzigd met minimale gevolgen voor de andere lagen.

14.4.1. De laag [dao]

Image

De interface [InterfaceDao] is als volgt:


# imports
from abc import ABC, abstractmethod


# Dao-interface
class InterfaceDao(ABC):
    # één enkele methode
    @abstractmethod
    def do_something_in_dao_layer(self, x: int, y: int) -> int:
        pass
  • regels 8-10: de methode [do_something_in_dao_layer] is de enige methode van de interface;

De klasse [DaoImpl1] implementeert de interface [InterfaceDao] als volgt:


from InterfaceDao import InterfaceDao


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

De klasse [DaoImpl2] implementeert de interface [InterfaceDao] als volgt:


from InterfaceDao import InterfaceDao


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

14.4.2. De laag [métier]

Image

De interface [InterfaceMétier] is als volgt:


# importen
from abc import ABC, abstractmethod


# bedrijfsinterface
class InterfaceMétier(ABC):
    # één enkele methode
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • regels 8-10: de methode [do_something_in_métier_layer] is de enige methode van de interface;

De klasse [AbstractBaseMétier] implementeert de interface [InterfaceMétier] als volgt:


# imports
from abc import ABC, abstractmethod

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


class AbstractBaseMétier(InterfaceMétier, ABC):
    # eigenschappen
    # __dao is een referentie op de laag [dao]
    @property
    def dao(self) -> InterfaceDao:
        return self.__dao

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

    # implementatie van de interface [InterfaceMétier]
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • regel 8: de klasse [AbstractBaseMétier] is afgeleid van twee klassen:
    • [InterfaceMétier]: de klasse [AbstractBaseMétier] implementeert deze interface op de regels 19-22. In feite zien we dat de methode [do_something_in_métier_layer] niet is geïmplementeerd, maar als abstract is gedeclareerd (regel 20). Het is aan de afgeleide klassen om de methode te implementeren;
    • [ABC] om toegang te krijgen tot de annotaties [@abstractmethod];
    • de volgorde is van belang: als we deze hier omdraaien, geeft Python tijdens de uitvoering een foutmelding;

Dit is de eerste keer dat we meervoudige overerving gebruiken (van meerdere klassen erven). De klasse [AbstractBaseMétier] erft zowel de eigenschappen van de klassen [InterfaceMétier] als [ABC].

  • regels 9-17: we definiëren de eigenschap [dao], die een verwijzing zal zijn naar de laag [dao];

Een interface is bedoeld om te worden geïmplementeerd. Wanneer verschillende implementaties eigenschappen delen, is het zinvol om deze in een bovenliggende klasse onder te brengen om duplicatie te voorkomen. Dit is hier het geval voor de eigenschap [dao]. De bovenliggende klasse is doorgaans altijd abstract, omdat deze niet alle methoden van de interface kan implementeren.

De klasse [MétierImpl1] implementeert de interface [InterfaceMétier] als volgt:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl1(AbstractBaseMétier):
    # implementatie van de interface [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)
  • regel 4: de klasse [MétierImpl1] is afgeleid van de klasse [AbstractbaseMétier]. Ze erft dus de eigenschap [dao] van deze klasse;
  • regels 6-9: implementatie van de interface [InterfaceMétier], die niet is geïmplementeerd door de bovenliggende klasse [AbstractbaseMétier];
  • regel 9: er wordt gebruikgemaakt van de laag [dao];

De klasse [MétierImpl2] implementeert de interface [InterfaceMétier] op dezelfde manier:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl2(AbstractBaseMétier):
    # implementatie van de interface [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. De laag [ui]

Image

De interface [InterfaceUi] is als volgt:


# imports
from abc import ABC, abstractmethod


# interface Ui
class InterfaceUi(ABC):
    # één enkele methode
    @abstractmethod
    def do_something_in_ui_layer(self, x: int, y: int) -> int:
        pass
  • regels 8-10: de enige methode van de interface;

De klasse [AbstractBaseUi] implementeert de interface [InterfaceUi] als volgt:


# imports
from abc import ABC, abstractmethod

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


class AbstractBaseUi(InterfaceUi, ABC):
    # eigenschappen
    # de businesslogica is een referentie op de laag [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

    # implementatie van de interface [InterfaceUI]
    @abstractmethod
    def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
        pass
  • de klasse [AbstractBaseUi] is een abstracte klasse (regel 20). Deze moet worden afgeleid om de interface [InterfaceUi] te implementeren;
  • regels 9-17: de klasse [AbstractBaseUi] heeft een verwijzing naar de laag [métier];

De implementatieklasse [UiImpl1] is als volgt:


from AbstractBaseUi import AbstractBaseUi


class UiImpl1(AbstractBaseUi):
    # implementatie van de interface [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)
  • regel 4: de klasse [UiImpl1] is afgeleid van de klasse [AbstractBaseUi] en erft dus de eigenschap [métier]. Deze wordt gebruikt in regel 9;

De implementatieklasse [UiImpl2] is vergelijkbaar:


from AbstractBaseUi import AbstractBaseUi


class UiImpl2(AbstractBaseUi):
    # implementatie van de interface [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)
  • regel 4: de klasse [UiImpl2] is afgeleid van de klasse [AbstractBaseUi] en erft dus de eigenschap [métier]. Deze wordt gebruikt op regel 9;

14.4.4. De configuratiebestanden

Image

  • de bestanden [config1, config2] configureren de applicatie op twee verschillende manieren;
  • het bestand [main] is het hoofdscript van de applicatie;

Het bestand [config1] is als volgt:


def configure():
    # stap 1 ------
    # absoluut pad naar de map van dit script
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # afhankelijkheden
    absolute_dependencies = [
        # lokale mappen in het Python-pad
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # we configureren de syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # stap 2 ------
    # configuratie van de applicatielagen
    from DaoImpl1 import DaoImpl1
    from MétierImpl1 import MétierImpl1
    from UiImpl1 import UiImpl1
    # instantie van de lagen
    # DAO
    dao = DaoImpl1()
    # bedrijfslogica
    métier = MétierImpl1()
    métier.dao = dao
    # UI
    ui = UiImpl1()
    ui.métier = métier

    # de laaginstanties worden in de configuratie opgenomen
    # alleen de ui-laag is hier nodig
    config = {"ui": ui}

    # we genereren de configuratie
    return config
  • regels 2-16: configuratie van het Python-pad van de applicatie;
  • regels 18-31: instantiëren van de lagen [dao, métier, ui]. Om hun interfaces te implementeren, wordt telkens de eerste gebouwde implementatie gekozen;
  • regels 33-35: de verwijzingen naar de lagen worden in de configuratie opgenomen. Hier heeft het hoofdscript alleen de laag [ui] nodig;

Het bestand [config2] is vergelijkbaar en implementeert elke interface met de tweede beschikbare implementatie:


def configure():
    # stap 1 ---
    # absoluut pad naar de map van dit script
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # afhankelijkheden
    absolute_dependencies = [
        # lokale mappen in het Python-pad
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # we configureren de syspath
    from myutils import set_syspath

    set_syspath(absolute_dependencies)

    # stap 2 ------
    # configuratie van de applicatielagen
    from DaoImpl2 import DaoImpl2
    from MétierImpl2 import MétierImpl2
    from UiImpl2 import UiImpl2
    # instantie van de lagen
    # DAO
    dao = DaoImpl2()
    # businesslogica
    métier = MétierImpl2()
    métier.dao = dao
    # UI
    ui = UiImpl2()
    ui.métier = métier

    # de laaginstanties worden in de configuratie opgenomen
    # alleen de ui-laag is hier nodig
    config = {"ui": ui}

    # we genereren de configuratie
    return config

14.4.5. Het hoofdscript [main]

Image

Het hoofdscript is als volgt:


# imports
import importlib
import sys

# main ---------

# er zijn twee argumenten nodig
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()

# configuratie van de applicatie
module = importlib.import_module(sys.argv[1])
config = module.configure()

# uitvoering van de laag [ui]
print(config["ui"].do_something_in_ui_layer(10, 20))

Dit script ontvangt één parameter:

  • [config1] om configuratie nr. 1 te gebruiken;
  • [config2] om configuratie nr. 2 te gebruiken;

Python slaat de parameters op in een lijst [sys.argv]:

  • sys.argv[0] is de naam van het script, in dit geval [main]. Deze parameter is altijd aanwezig;
  • sys.argv[1] is de eerste parameter die aan het script wordt doorgegeven, sys.argv[2] de tweede, …

  • regel 8: we halen het aantal parameters op;

  • regels 9-11: we controleren of er inderdaad een parameter is en of de waarde daarvan [config1] of [config2] is. Als dat niet het geval is, wordt er een foutmelding weergegeven (regel 10) en wordt het programma afgesloten (regel 11);

Zodra de gewenste configuratie bekend is, moeten we deze configuratie uitvoeren. Als bijvoorbeeld configuratie 1 is gekozen, moeten we de volgende code uitvoeren:

import config1
config1.configure()

Het probleem hier is dat de te gebruiken configuratie in een variabele staat, namelijk de variabele [sys.argv[1]. Om een module te importeren waarvan de naam in een variabele staat, moeten we het pakket [importlib] gebruiken (regel 2).

  • regel 14: we importeren de module waarvan de naam in [sys.argv[1] staat;
  • regel 15: zodra dit is gebeurd, voeren we de functie [configure] van deze module uit. We ontvangen een woordenboek [config] dat de configuratie van de applicatie bevat;
  • regel 18: we weten dat er een verwijzing naar de laag [ui] in config[‘ui’] staat. Deze gebruiken we om de methode [do_something_in_ui_layer] aan te roepen. We weten dat deze methode een methode uit de laag [métier] zal aanroepen, die op haar beurt een methode uit de laag [dao] zal aanroepen;

De functie [do_something_in_ui_layer] ziet er bijvoorbeeld als volgt uit:


class UiImpl1(AbstractBaseUi):
    # implementatie van de interface [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)
  • regel 6 hierboven maakt gebruik van de eigenschap [métier] van de klasse [UiImpl1], regel 1. In de configuratie [config1] is echter het volgende geschreven:

# bedrijfslogica
    métier = MétierImpl1()
    métier.dao = dao
    # ui
    ui = UiImpl1()
    ui.métier = métier
  • regel 6: de eigenschap [métier] van [UIImpl1] is een verwijzing naar de klasse [MétierImpl1] (regel 2). Het is dus de methode [do_something_in_ui_layer] van de klasse [MétierImpl1] die zal worden uitgevoerd;

In de klasse [MétierUiImpl1] staat:


class MétierImpl1(AbstractBaseMétier):
    # implementatie van de interface [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)
  • regel 6: de methode die wordt aangeroepen door de laag [ui] roept op haar beurt een methode aan van de eigenschap [dao] van de klasse [MétierImpl1];

Maar in de configuratie [config1] staat het volgende geschreven:


# DAO
    dao = DaoImpl1()
    # bedrijfslogica
    métier = MétierImpl1()
    métier.dao = dao
  • regel 5: de eigenschap [MétierImpl1.dao] is van het type [DaoImpl1] (regel 2);

Wat we hier willen aantonen, is dat het script [main] zich niet hoeft bezig te houden met de lagen [métier] en [dao]. Het hoeft zich alleen bezig te houden met de laag [ui], aangezien de koppelingen tussen deze laag en de andere lagen via de configuratie zijn gemaakt.

Image

Om de parameter [config1] of [config2] door te geven aan het script [main], gaat men als volgt te werk:

Image

  • in [1-2] maakt u een zogenaamde uitvoeringsconfiguratie aan;
  • in [3] geeft men deze configuratie een naam om deze later terug te kunnen vinden;
  • in [4] selecteert men het uit te voeren script. Als men de procedure [1-2] heeft gevolgd, is het juiste script al geselecteerd;
  • in [5] worden hier de parameters opgegeven die naar het script moeten worden doorgegeven. We geven hier de tekenreeks [config1] door om het script te vragen configuratie nr. 1 te gebruiken;
  • in [6] bevestigen we de uitvoeringsconfiguratie;

Image

  • in [1-2] vragen we om de bestaande uitvoeringscontexten te bekijken;
  • in [3] selecteert men de bestaande uitvoeringscontext en dupliceert men deze naar [4];

Image

  • in [5] wordt de naam voor de nieuwe configuratie opgegeven. Dit is de configuratie die het script [main] [6] uitvoert en daarbij de parameter [config2] [7] doorgeeft;

De uitvoerconfiguraties zijn rechtsboven in het venster PyCharm beschikbaar:

Image

Selecteer gewoon [2] of [3] en druk vervolgens op [4] om het script [main] uit te voeren meteen van de parameters [config1] of [config2].

Met [config1] levert het uitvoeren van [main] de volgende resultaten op:


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

Met [config2] levert het uitvoeren van [main] de volgende resultaten op:


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

De lezer wordt verzocht deze resultaten te controleren.