Skip to content

7. Laagarchitectuur en programmeren via interfaces

7.1. Introduction

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

  • de laag [présentation] is de laag die in contact staat met de gebruiker van de applicatie.
  • De laag [metier] implementeert de bedrijfsregels van de applicatie, zoals het berekenen van een salaris of een factuur. Deze laag gebruikt gegevens van de gebruiker via de laag [présentation] en van de laag SGBD via de laag [dao].
  • De laag [dao] (Data Access Objects) beheert de toegang tot de gegevens van de laag SGBD.

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

  • er is geen database,
  • de laag [dao] beheert entiteiten Eleve, Classe, Matiere en Note waarmee de cijfers van 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 [présentation] is een console-applicatie die de door de laag [métier] berekende resultaten weergeeft.

Het Visual Studio-project van de applicatie is als volgt:

  

7.2. De entiteiten van de applicatie

Entiteiten zijn objecten. We hebben hier vier klassen voor elk van de entiteiten Eleve, Classe, Matiere en Note.

Het bestand [entites.py] bevat vier klassen.

De klasse [Classe] vertegenwoordigt een klas van de middelbare school:


class Classe:
    # constructor
    def __init__(self,id,nom):
        # de parameters worden opgeslagen
        self.id=id
        self.nom=nom

    # toString
    def __str__(self):
        return "Classe[{0},{1}]".format(self.id,self.nom)
  • regels 3-6: een klas wordt gedefinieerd door een nummer id (regel 5) en een nom (regel 6);
  • regels 9-10: de weergavemethode van de klas.

De klasse [Matiere] is als volgt:


class Matiere:
    # fabrikant
    def __init__(self,id,nom,coefficient):
        # de parameters worden opgeslagen
        self.id=id
        self.nom=nom
        self.coefficient=coefficient

    # toString
    def __str__(self):
        return "Matiere[{0},{1},{2}]".format(self.id,self.nom,self.coefficient)

  • regels 3-7: een vak wordt gedefinieerd door het nummer (regel 5), de naam (regel 6) en de coëfficiënt (regel 7);
  • regels 10-11: de weergavemethode van het vak.

De klasse [Eleve] is als volgt:


class Eleve:
    # fabrikant
    def __init__(self,id,nom,prenom,classe):
        # de parameters worden opgeslagen
        self.id=id
        self.nom=nom
        self.prenom=prenom
        self.classe=classe

    # toString
    def __str__(self):
        return "Eleve[{0},{1},{2},{3}]".format(self.id,self.prenom,self.nom,self.classe)

  • regels 3-8: een leerling wordt gekenmerkt door zijn nummer (regel 5), zijn achternaam (regel 6), zijn voornaam (regel 7) en zijn klas (regel 8). Deze laatste parameter is een verwijzing naar een object [Classe];
  • regels 11-12: de weergavemethode van de leerling.

De klasse [Note] is als volgt:


class Note:
    # fabrikant
    def __init__(self,id,valeur,eleve,matiere):
        # de parameters worden opgeslagen
        self.id=id
        self.valeur=valeur
        self.eleve=eleve
        self.matiere=matiere

    # toString
    def __str__(self):
        return "Note[{0},{1},{2},{3}]".format(self.id,self.valeur,self.eleve,self.matiere)

  • regels 3-8: een object [Note] wordt gekenmerkt door zijn nummer (regel 5), de waarde van het cijfer (regel 6), een verwijzing naar de leerling die dit cijfer heeft (regel 7), een verwijzing naar het vak waarop het cijfer betrekking heeft (regel 8);
  • regels 11-12: de weergavemethode van het object [Note].

7.3. De laag [dao]

De laag [dao] biedt de volgende interface aan de laag [métier]:

  • getClasses geeft de lijst met klassen van de middelbare school weer;
  • getMatieres geeft de lijst met vakken weer;
  • getEleves geeft de lijst met leerlingen weer;
  • getNotes geeft de lijst met cijfers weer.

De laag [métier] zal alleen deze methoden gebruiken. Deze laag hoeft niet te weten hoe ze zijn geïmplementeerd. Deze gegevens kunnen dan afkomstig zijn uit verschillende bronnen (hardgecodeerd, uit een database, uit tekstbestanden, ...) zonder dat dit invloed heeft op de laag [métier]. Dit wordt programmeren via interfaces genoemd.

De klasse [Dao] implementeert deze interface als volgt:


# -*- coding=utf-8 -*-

# de entiteitenmodule importeren
from entites import *

class Dao:
    # constructor
    def __init__(self):
        # de klassen worden geïnstantieerd
        classe1=Classe(1,"classe1")
        classe2=Classe(2,"classe2")
        self.classes=[classe1,classe2]
        # de vakken
        matiere1=Matiere(1,"matiere1",1)
        matiere2=Matiere(2,"matiere2",2)
        self.matieres=[matiere1,matiere2]
        # de leerlingen
        eleve11=Eleve(11,"nom1","prenom1",classe1)
        eleve21=Eleve(21,"nom2","prenom2",classe1)
        eleve32=Eleve(32,"nom3","prenom3",classe2)
        eleve42=Eleve(42,"nom4","prenom4",classe2)
        self.eleves=[eleve11,eleve21,eleve32,eleve42]
        # de cijfers
        note1=Note(1,10,eleve11,matiere1)
        note2=Note(2,12,eleve21,matiere1)
        note3=Note(3,14,eleve32,matiere1)
        note4=Note(4,16,eleve42,matiere1)
        note5=Note(5,6,eleve11,matiere2)
        note6=Note(6,8,eleve21,matiere2)
        note7=Note(7,10,eleve32,matiere2)
        note8=Note(8,12,eleve42,matiere2)
        self.notes=[note1,note2,note3,note4,note5,note6,note7,note8]

    #-----------
    # interface
    #-----------
    
    # lijst met klassen
    def getClasses(self):
        return self.classes

    # lijst met vakken
    def getMatieres(self):
        return self.matieres
    
    # lijst met leerlingen
    def getEleves(self):
        return self.eleves

    # lijst met cijfers
    def getNotes(self):
        return self.notes

  • regel 4: we importeren de module die de entiteiten bevat die door de laag [dao] worden verwerkt;
  • regel 8: de constructor heeft geen parameters. Hij maakt vier lijsten vast:
    • regels 10-12: de lijst met klassen;
    • regels 14-16: de lijst met vakken;
    • regels 18-22: de lijst met leerlingen;
    • regels 24-32: de lijst met cijfers.
  • regels 39-52: de vier methoden van de interface van de laag [dao] geven alleen een verwijzing naar de vier lijsten die door de constructor zijn aangemaakt.

Een testprogramma zou er als volgt uit kunnen zien:


# -*- coding=utf-8 -*-

# import van de entiteitenmodule en de laag [dao]
from entites import *
from dao import *

# laag [dao] instantiëren
dao=Dao()

# lijst met klassen
for classe in dao.getClasses():
    print classe

# lijst met klassen
for matiere in dao.getMatieres():
    print matiere

# lijst met klassen
for eleve in dao.getEleves():
    print eleve

# lijst met klassen
for note in dao.getNotes():
    print note

De commentaren spreken voor zich. De regels 11-24 maken gebruik van de interface van de laag [dao]. Er worden hier geen aannames gedaan over de daadwerkelijke implementatie van de laag. In regel 8 wordt de laag [dao] geïnstantieerd. Hier worden aannames gedaan: de naam van de klasse en het type constructor. Er bestaan oplossingen waarmee deze afhankelijkheid kan worden vermeden.

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

Classe[1,classe1]
Classe[2,classe2]
Matiere[1,matiere1,1]
Matiere[2,matiere2,2]
Eleve[11,prenom1,nom1,Classe[1,classe1]]
Eleve[21,prenom2,nom2,Classe[1,classe1]]
Eleve[32,prenom3,nom3,Classe[2,classe2]]
Eleve[42,prenom4,nom4,Classe[2,classe2]]
Note[1,10,Eleve[11,prenom1,nom1,Classe[1,classe1]],Matiere[1,matiere1,1]]
Note[2,12,Eleve[21,prenom2,nom2,Classe[1,classe1]],Matiere[1,matiere1,1]]
Note[3,14,Eleve[32,prenom3,nom3,Classe[2,classe2]],Matiere[1,matiere1,1]]
Note[4,16,Eleve[42,prenom4,nom4,Classe[2,classe2]],Matiere[1,matiere1,1]]
Note[5,6,Eleve[11,prenom1,nom1,Classe[1,classe1]],Matiere[2,matiere2,2]]
Note[6,8,Eleve[21,prenom2,nom2,Classe[1,classe1]],Matiere[2,matiere2,2]]
Note[7,10,Eleve[32,prenom3,nom3,Classe[2,classe2]],Matiere[2,matiere2,2]]
Note[8,12,Eleve[42,prenom4,nom4,Classe[2,classe2]],Matiere[2,matiere2,2]]

7.4. De laag [métier]

De laag [métier] implementeert de volgende interface:

  • getClasses geeft de lijst met klassen van de middelbare school weer;
  • getMatieres geeft de lijst met vakken weer;
  • getEleves geeft de lijst met leerlingen weer;
  • getNotes geeft de lijst met cijfers weer;
  • getStatsForEleve geeft de cijfers weer van leerling nr. idEleve, evenals informatie hierover: gewogen gemiddelde, laagste cijfer, hoogste cijfer.

De laag [présentation] zal alleen deze methoden gebruiken. Deze laag hoeft niet te weten hoe ze zijn geïmplementeerd.

De methode getStatsForEleve retourneert een object van het type [StatsForEleve] met de volgende kenmerken:


class StatsForEleve:
    # fabrikant
    def __init__(self, eleve, notes):
        # de instellingen worden opgeslagen
        self.eleve=eleve
        self.notes=notes
        # er wordt gestopt als er geen notities zijn
        if len(notes)==0:
            return
        # verwerking van de cijfers
        sommePonderee=0
        sommeCoeff=0
        self.max=-1
        self.min=21
        for note in notes:
            valeur=note.valeur
            coeff=note.matiere.coefficient
            sommeCoeff+=coeff
            sommePonderee+=valeur*coeff
            if valeur<self.min:
                self.min=valeur
            if valeur>self.max:
                self.max=valeur
        # berekening van het gemiddelde van de leerling
        self.moyennePonderee=float(sommePonderee)/sommeCoeff
        
    # toString
    def __str__(self):
        # geval van een leerling zonder cijfers
        if len(self.notes)==0:
            return "Eleve={0}, notes=[]".format(self.eleve)
        # geval van een leerling met cijfers
        str=""
        for note in self.notes:
            str+="{0} ".format(note.valeur)
        return "Eleve={0}, notes=[{1}], max={2}, min={3}, moyenne={4}".format(self.eleve, str, self.max, self.min, self.moyennePonderee)

  • regel 3: de constructor ontvangt twee parameters:
    • een verwijzing naar de leerling van het type [Eleve] waarvoor indicatoren worden berekend;
    • een verwijzing naar zijn cijfers, een lijst met objecten van het type [Note].
  • regels 8-9: als de lijst met cijfers leeg is, wordt er niet verder gegaan.
  • anders, regels 11-25, worden de volgende indicatoren berekend:
    • self.moyennePonderee: het gemiddelde van de leerling, gewogen met de coëfficiënten van de vakken;
    • self.min: het laagste cijfer van de leerling;
    • self.max: het hoogste cijfer van de leerling.
  • regel 28: de methode om de klas weer te geven in de vorm die in regel 36 wordt aangegeven.

Een testscript voor deze klasse zou er als volgt uit kunnen zien:


# -*- coding=utf-8 -*-

# importeren van de entiteitsmodules, de laag [dao] en de laag [metier]
from entites import *
from dao import *
from metier import *

# een klas
classe1=Classe(1,"6e A")
# een leerling in deze klas
paul_durand=Eleve(1,"durand","paul",classe1)
# drie vakken
maths=Matiere(1,"maths",1)
francais=Matiere(2,"francais",2)
anglais=Matiere(3,"anglais",3)
# cijfers in deze vakken voor de leerling
note_maths=Note(1,10,paul_durand,maths)
note_francais=Note(2,12,paul_durand,francais)
note_anglais=Note(3,14,paul_durand,anglais)
# de indicatoren worden weergegeven
print StatsForEleve(paul_durand,[note_maths, note_francais,note_anglais])

De uitvoer op het scherm is als volgt:

Eleve=Eleve[1,paul,durand,Classe[1,6e A]], notes=[10 12 14 ], max=14, min=10, moyenne=12.6666666667

Laten we terugkeren naar onze gelaagde architectuur:

De klasse [Metier] implementeert de laag [métier] als volgt:


class Metier:
    # maker
    def __init__(self,dao):
        # de referentie wordt opgeslagen op de laag [dao]
        self.dao=dao

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

    # de indicatoren op de cijferlijsten
    def getStatsForEleve(self,idEleve):
        # Statistieken voor de leerling met nr. idEleve
        # zoeken naar de leerling
        trouve=False
        i=0
        eleves=self.getEleves()
        while not trouve and i<len(eleves):
            trouve=eleves[i].id==idEleve
            i+=1
        # Is de leerling gevonden?
        if not trouve:
            raise RuntimeError("L'eleve [{0}] n'existe pas".format(idEleve))
        else:
            eleve=eleves[i-1]
        # Lijst met alle cijfers
        notes=[]
        for note in self.getNotes():
            # alle cijfers van de leerling aan de cijfers toevoegen
            if note.eleve.id==idEleve:
                notes.append(note)
        # het resultaat wordt weergegeven
        return StatsForEleve(eleve,notes)

    # de lijst met klassen
    def getClasses(self):
        return self.dao.getClasses()
    
    # de lijst met vakken
    def getMatieres(self):
        return self.dao.getMatieres()
    
    # de lijst met leerlingen
    def getEleves(self):
        return self.dao.getEleves()
    
    # de lijst met cijfers
    def getNotes(self):
        return self.dao.getNotes()

  • regels 3-5: de constructor ontvangt een verwijzing naar de laag [dao]. De laag [métier] moet deze verwijzing hebben. Hier wordt deze via de constructor doorgegeven. Er zijn ook andere oplossingen denkbaar. In een horizontaal weergegeven gelaagde architectuur moet elke laag een verwijzing hebben naar de laag die zich rechts ervan bevindt;
  • regels 36-49: de methoden getClasses, getMatieres, getEleves, getNotes delegeren de aanroep gewoon naar de methoden met dezelfde naam in de laag [dao];
  • regel 12: de methode getStatsForEleve ontvangt als parameter het nummer van de leerling waarvoor indicatoren moeten worden weergegeven.
  • regel 17: de leerling wordt opgezocht in de lijst met alle leerlingen;
  • regels 18-20: de zoeklus;
  • regel 23: als de leerling niet is gevonden, wordt er een uitzondering gegenereerd;
  • anders regel 25: de gevonden leerling wordt opgeslagen;
  • regels 28-31: er wordt gezocht in alle cijfers van de school naar die welke bij de opgeslagen leerling horen;
  • zodra deze zijn gevonden, kan het gevraagde object StatsForEleve worden samengesteld.

7.5. De laag [console]

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


# -*- coding=utf-8 -*-

# import van de entiteitenmodule, de module [dao] en de module [metier]
from entites import *
from dao import *
from metier import *

# ----------- laag [console]
# instantie van laag [metier]
metier=Metier(Dao())
# verzoek/antwoord
fini=False
while not fini:
    # vraag
    print "numero de l'eleve (>=1 et * pour arreter) : "
    # antwoord
    reponse=raw_input()
    # klaar?
    if reponse.strip()=="*":
        break
    # is de invoer correct?
    ok=False
    try:
        idEleve=int(reponse,10)
        ok=idEleve>=1
    except:
        pass
    # zijn de gegevens correct?
    if not ok:
        print "Saisie incorrecte. Recommencez..."
        continue
    # berekening
    try:
        print metier.getStatsForEleve(idEleve)
    except RuntimeError,erreur:
        print "L'erreur suivante s'est produite : {0}".format(erreur)

  • regel 10: instantiatie van zowel de lagen [dao] als [métier]. Dit is de enige afhankelijkheid van onze code ten opzichte van de implementatie van deze lagen;
  • regel 34: we gebruiken de interface van de laag [métier];
  • regel 19: de methode strip verwijdert de spaties aan het begin en einde van de tekenreeks;
  • regel 20: met break kan men uit een lus stappen;
  • regel 24: er wordt geprobeerd de ingevoerde tekenreeks om te zetten in een decimaal geheel getal;
  • regel 29: ok is alleen waar als regel 25 is doorlopen;
  • regel 31: met continue kan men halverwege de lus opnieuw beginnen;
  • regel 34: berekening van de indicatoren;
  • regel 35: de uitzondering RuntimeError, die uit de laag [métier] kan komen, wordt opgevangen.

Hier volgt een uitvoervoorbeeld:

numero de l'eleve (>=1 et * pour arreter) :
xx
Saisie incorrecte. Recommencez...
numero de l'eleve (>=1 et * pour arreter) :
-4
Saisie incorrecte. Recommencez...
numero de l'eleve (>=1 et * pour arreter) :
11
Eleve=Eleve[11,prenom1,nom1,Classe[1,classe1]], notes=[10 6 ], max=10, min=6, mo
yenne=7.33333333333
numero de l'eleve (>=1 et * pour arreter) :
111
L'erreur suivante s'est produite : L'eleve [111] n'existe pas
numero de l'eleve (>=1 et * pour arreter) :
*