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:

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

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:

- In [4] zijn verschillende testframeworks beschikbaar:

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]

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]

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

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]

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]

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]

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

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

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

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

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

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

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

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.













