Skip to content

7. Schichtarchitektur und schnittstellenbasierte Programmierung

7.1. Einführung

Wir schlagen vor, eine Anwendung zu schreiben, die die Noten von Schülern der Mittelstufe anzeigt. Diese Anwendung wird eine mehrschichtige Architektur aufweisen:

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

Wir werden diese Architektur anhand einer einfachen Konsolenanwendung veranschaulichen:

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

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

  

7.2. Die Entitäten der Anwendung

Entitäten sind Objekte. Hier werden wir für jede der Entitäten vier Klassen haben: Student, Klasse, Fach und Note.

Die Datei [entities.py] enthält vier Klassen.

Die Klasse [Class] repräsentiert eine Mittelschulklasse:


class Classe:
    # constructeur
    def __init__(self,id,nom):
        # on mémorise les paramètres
        self.id=id
        self.nom=nom
 
    # toString
    def __str__(self):
        return "Classe[{0},{1}]".format(self.id,self.nom)
  • Zeilen 3–6: Eine Klasse wird durch eine ID (Zeile 5) und einen Namen (Zeile 6) definiert;
  • Zeilen 9–10: die Methode zur Anzeige der Klasse.

Die Klasse [Subject] sieht wie folgt aus:


class Matiere:
    # constructeur
    def __init__(self,id,nom,coefficient):
        # on mémorise les paramètres
        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)
 
  • Zeilen 3–7: Ein Fach wird durch seine ID (Zeile 5), seinen Namen (Zeile 6) und seine Gewichtung (Zeile 7) definiert;
  • Zeilen 10–11: Die Methode zur Anzeige des Subjekts.

Die Klasse [Student] sieht wie folgt aus:


class Eleve:
    # constructeur
    def __init__(self,id,nom,prenom,classe):
        # on mémorise les paramètres
        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)

  • Zeilen 3–8: Ein Schüler wird durch seine ID (Zeile 5), seinen Nachnamen (Zeile 6), seinen Vornamen (Zeile 7) und seine Klasse (Zeile 8) charakterisiert. Dieser letzte Parameter ist eine Referenz auf ein [Class]-Objekt;
  • Zeilen 11–12: die Methode zur Anzeige des Schülers.

Die Klasse [Grade] sieht wie folgt aus:


class Note:
    # constructeur
    def __init__(self,id,valeur,eleve,matiere):
        # on mémorise les paramètres
        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)
 
  • Zeilen 3–8: Ein [Grade]-Objekt wird durch seine ID (Zeile 5), den Notenwert (Zeile 6), einen Verweis auf den Schüler, der diese Note erhalten hat (Zeile 7), und einen Verweis auf das Fach, für das die Note vergeben wurde (Zeile 8), charakterisiert;
  • Zeilen 11–12: Die Methode zur Anzeige des [Note]-Objekts.

7.3. Die [dao]-Ebene

Die [dao]-Schicht stellt der [business]-Schicht die folgende Schnittstelle zur Verfügung:

  • getClasses gibt die Liste der Mittelschulklassen zurück;
  • getMatieres gibt die Liste der Fächer zurück;
  • getStudents gibt die Liste der Schüler zurück;
  • getGrades gibt eine Liste der Noten zurück.

Die [Business]-Schicht verwendet ausschließlich diese Methoden. Sie muss nicht wissen, wie diese implementiert sind. Diese Daten können daher aus verschiedenen Quellen stammen (fest codierte Werte, eine Datenbank, Textdateien usw.), ohne dass dies Auswirkungen auf die [Business]-Schicht hat. Dies wird als schnittstellenbasierte Programmierung bezeichnet.

Die [Dao]-Klasse implementiert diese Schnittstelle wie folgt:

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

#    import entity module
from entites import *

class Dao:
    #    manufacturer
    def __init__(self):
        #  classes are instantiated
        classe1=Classe(1,"classe1")
        classe2=Classe(2,"classe2")
        self.classes=[classe1,classe2]
        #    materials
        matiere1=Matiere(1,"matiere1",1)
        matiere2=Matiere(2,"matiere2",2)
        self.matieres=[matiere1,matiere2]
        #    students
        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]
        #  the notes
        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
    #-----------

    #    class list
    def getClasses(self):
        return self.classes

    #    list of materials
    def getMatieres(self):
        return self.matieres

    #    list of students
    def getEleves(self):
        return self.eleves

    #    lIST OF NOTES
    def getNotes(self):
        return self.notes
  • Zeile 4: Wir importieren das Modul, das die von der [DAO]-Schicht verwalteten Entitäten enthält;
  • Zeile 8: Der Konstruktor hat keine Parameter. Er enthält vier fest codierte Listen:
    • Zeilen 10–12: die Liste der Klassen;
    • Zeilen 14–16: die Liste der Fächer;
    • Zeilen 18–22: die Liste der Schüler;
    • Zeilen 24–32: die Liste der Noten.
  • Zeilen 39–52: Die vier Methoden der [dao]-Layer-Schnittstelle geben lediglich einen Verweis auf die vier vom Konstruktor erstellten Listen zurück.

Ein Testprogramm könnte wie folgt aussehen:

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

#  importing the [dao] entity and layer module
from entites import *
from dao import *

#    layer instantiation [dao]
dao=Dao()

#    class list
for classe in dao.getClasses():
    print classe

#    class list
for matiere in dao.getMatieres():
    print matiere

#    class list
for eleve in dao.getEleves():
    print eleve

#    list of classes
for note in dao.getNotes():
    print note

Die Kommentare sprechen für sich. In den Zeilen 11–24 wird die [dao]-Layer-Schnittstelle verwendet. Hier werden keine Annahmen über die tatsächliche Implementierung des Layers getroffen. In Zeile 8 instanziieren wir den [dao]-Layer. Hier treffen wir Annahmen: Klassenname und Konstruktortyp. Es gibt Lösungen, mit denen wir diese Abhängigkeit vermeiden können.

Die Ergebnisse der Ausführung dieses Skripts lauten wie folgt:

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. Die [Geschäfts-]Ebene

Die [Business]-Schicht implementiert die folgende Schnittstelle:

  • getClasses gibt die Liste der Mittelschulklassen zurück;
  • getMatieres gibt die Liste der Fächer zurück;
  • getStudents gibt die Liste der Schüler zurück;
  • getGrades gibt eine Liste der Noten zurück;
  • getStatsForStudent gibt die Noten für den Schüler idStudent zusammen mit Informationen zu diesen zurück: gewichteter Durchschnitt, niedrigste Note, höchste Note.

Die [Präsentationsschicht] verwendet ausschließlich diese Methoden. Sie muss nicht wissen, wie diese implementiert sind.

Die Methode getStatsForStudent gibt ein Objekt vom Typ [StatsForStudent] wie folgt zurück:


class StatsForEleve:
    # constructeur
    def __init__(self, eleve, notes):
        # on mémorise les paramètres
        self.eleve=eleve
        self.notes=notes
        # on s'arrête s'il n'y a pas de notes
        if len(notes)==0:
            return
        # exploitation des notes
        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
        # calcul de la moyenne de l'élève
        self.moyennePonderee=float(sommePonderee)/sommeCoeff
 
    # toString
    def __str__(self):
        # cas de l'élève sans notes
        if len(self.notes)==0:
            return "Eleve={0}, notes=[]".format(self.eleve)
        # cas de l'élève avec notes
        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)

  • Zeile 3: Der Konstruktor nimmt zwei Parameter entgegen:
    • eine Referenz auf den Schüler vom Typ [Student], für den die Kennzahlen berechnet werden;
    • eine Referenz auf seine Noten, eine Liste von [Grade]-Objekten.
  • Zeilen 8–9: Wenn die Liste der Noten leer ist, brechen wir hier ab.
  • Andernfalls, Zeilen 11–25: Die folgenden Kennzahlen werden berechnet:
    • self.weightedAverage: der durch die Fachkoeffizienten gewichtete Durchschnitt des Schülers;
    • self.min: die niedrigste Note des Schülers;
    • self.max: die höchste Note des Schülers.
  • Zeile 28: Die Methode zur Anzeige der Klasse in dem in Zeile 36 angegebenen Format.

Ein Testskript für diese Klasse könnte wie folgt aussehen:

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

#  import of entity modules, [dao] layer and [metier] layer
from entites import *
from dao import *
from metier import *

#    a class
classe1=Classe(1,"6e A")
#  one student in this class
paul_durand=Eleve(1,"durand","paul",classe1)
#    three materials
maths=Matiere(1,"maths",1)
francais=Matiere(2,"francais",2)
anglais=Matiere(3,"anglais",3)
#  grades in these subjects for the student
note_maths=Note(1,10,paul_durand,maths)
note_francais=Note(2,12,paul_durand,francais)
note_anglais=Note(3,14,paul_durand,anglais)
#  indicators are displayed
print StatsForEleve(paul_durand,[note_maths, note_francais,note_anglais])

Die Bildschirmausgabe sieht wie folgt aus:

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

Kehren wir zu unserer mehrschichtigen Architektur zurück:

Die Klasse [Business] implementiert die [Business]-Schicht wie folgt:


class Metier:
    # constructeur
    def __init__(self,dao):
        # on mémorise la référence sur la couche [dao]
        self.dao=dao
 
    #-----------
    # interface
    #-----------
 
    # les indicateurs sur les notes
    def getStatsForEleve(self,idEleve):
        # Stats pour l'élève de n° idEleve
        # recherche de l'élève
        trouve=False
        i=0
        eleves=self.getEleves()
        while not trouve and i<len(eleves):
            trouve=eleves[i].id==idEleve
            i+=1
        # a-t-on trouvé ?
        if not trouve:
            raise RuntimeError("L'eleve [{0}] n'existe pas".format(idEleve))
        else:
            eleve=eleves[i-1]
        # liste de toutes les notes
        notes=[]
        for note in self.getNotes():
            # on ajoute à notes, toutes les notes de l'élève
            if note.eleve.id==idEleve:
                notes.append(note)
        # on rend le résultat
        return StatsForEleve(eleve,notes)
 
    # la liste des classes
    def getClasses(self):
        return self.dao.getClasses()
 
    # la liste des matières
    def getMatieres(self):
        return self.dao.getMatieres()
 
    # la liste des élèves
    def getEleves(self):
        return self.dao.getEleves()
 
    # la liste des notes
    def getNotes(self):
        return self.dao.getNotes()
 
  • Zeilen 3–5: Der Konstruktor erhält eine Referenz auf die [dao]-Schicht. Die [business]-Schicht muss über diese Referenz verfügen. Hier stellen wir sie über ihren Konstruktor bereit. Andere Lösungen sind möglich. In einer horizontal dargestellten Schichtenarchitektur muss jede Schicht eine Referenz auf die Schicht rechts von ihr haben;
  • Zeilen 36–49: Die Methoden getClasses, getSubjects, getStudents und getGrades delegieren den Aufruf einfach an die gleichnamigen Methoden in der [dao]-Schicht;
  • Zeile 12: Die Methode getStatsForStudent erhält als Parameter die Studenten-ID, für die Statistiken zurückgegeben werden sollen.
  • Zeile 17: Der Student wird in der Liste aller Studenten gesucht;
  • Zeilen 18–20: die Suchschleife;
  • Zeile 23: Wenn der Student nicht gefunden wird, wird eine Ausnahme ausgelöst;
  • ansonsten, Zeile 25: Der gefundene Schüler wird gespeichert;
  • Zeilen 28–31: Wir durchsuchen alle Mittelschulklassen nach denen, die zu dem gespeicherten Schüler gehören;
  • Sobald sie gefunden sind, kann das angeforderte StatsForEleve-Objekt erstellt werden.

7.5. Die [console]-Ebene

Die [console]-Ebene wird durch das folgende Skript implementiert:

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

#  import from entity module, [dao] module, [metier] module
from entites import *
from dao import *
from metier import *

#    ----------- layer [console]
#    instantiation layer [metier]
metier=Metier(Dao())
#  request/response
fini=False
while not fini:
    #    question
    print "numero de l'eleve (>=1 et * pour arreter) : "
    #    answer
    reponse=raw_input()
    #    finished?
    if reponse.strip()=="*":
        break
    #  is the input correct?
    ok=False
    try:
        idEleve=int(reponse,10)
        ok=idEleve>=1
    except:
        pass
    #    correct data?
    if not ok:
        print "Saisie incorrecte. Recommencez..."
        continue
    #  calculation
    try:
        print metier.getStatsForEleve(idEleve)
    except RuntimeError,erreur:
        print "L'erreur suivante s'est produite : {0}".format(erreur)
  • Zeile 10: Instanziierung sowohl der [dao]- als auch der [business]-Schicht. Dies ist die einzige Abhängigkeit unseres Codes von der Implementierung dieser Schichten;
  • Zeile 34: Wir verwenden die Schnittstelle der [business]-Schicht;
  • Zeile 19: Die Methode strip entfernt führende und nachstehende Leerzeichen aus der Zeichenkette;
  • Zeile 20: „break“ beendet eine Schleife;
  • Zeile 24: Versuch, die eingegebene Zeichenkette in eine dezimale Ganzzahl zu konvertieren;
  • Zeile 29: ok ist nur dann wahr, wenn Zeile 25 ausgeführt wurde;
  • Zeile 31: „continue“ ermöglicht es, die Schleife in der Mitte fortzusetzen;
  • Zeile 34: Berechnung der Kennzahlen;
  • Zeile 35: fängt die RuntimeError-Ausnahme ab, die möglicherweise aus der [Business]-Schicht stammt.

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

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