Skip to content

7. Architektura warstwowa i programowanie za pomocą interfejsów

7.1. Introduction

Zamierzamy napisać aplikację umożliwiającą wyświetlanie ocen uczniów gimnazjum. Aplikacja ta będzie miała architekturę wielowarstwową:

  • warstwa [présentation] stanowi interfejs użytkownika aplikacji.
  • warstwa [metier] realizuje reguły zarządzania aplikacją, takie jak obliczanie wynagrodzenia lub faktury. Warstwa ta wykorzystuje dane pochodzące od użytkownika za pośrednictwem warstwy [présentation] oraz dane z warstw SGBD i [dao].
  • Warstwa [dao] (Data Access Objects) zarządza dostępem do danych z warstwy SGBD.

Zilustrujemy tę architekturę za pomocą prostej aplikacji konsolowej:

  • nie będzie bazy danych,
  • warstwa [dao] będzie zarządzać encjami Eleve, Classe, Matiere, Note, umożliwiającymi zarządzanie ocenami uczniów,
  • warstwa [métier] umożliwi obliczanie wskaźników na podstawie ocen konkretnego ucznia,
  • warstwa [présentation] będzie aplikacją konsolową, która wyświetli wyniki obliczone przez warstwę [métier].

Projekt aplikacji w Visual Studio ma następującą nazwę:

  

7.2. Elementy składowe aplikacji

Entities to obiekty. W tym przypadku będziemy mieli cztery klasy dla każdej z entytet: Eleve, Classe, Matiere, Note.

Plik [entites.py] zawiera cztery klasy.

Klasa [Classe] reprezentuje klasę gimnazjalną:


class Classe:
    # konstruktor
    def __init__(self,id,nom):
        # zapisujemy parametry
        self.id=id
        self.nom=nom

    # toString
    def __str__(self):
        return "Classe[{0},{1}]".format(self.id,self.nom)
  • wiersze 3–6: klasa jest definiowana przez numer id (wiersz 5) oraz nom (wiersz 6);
  • wiersze 9–10: sposób wyświetlania klasy.

Klasa [Matiere] ma następującą postać:


class Matiere:
    # producent
    def __init__(self,id,nom,coefficient):
        # zapisywanie parametrów
        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)

  • wiersze 3–7: przedmiot jest definiowany przez jego numer (wiersz 5), nazwę (wiersz 6) oraz współczynnik (wiersz 7);
  • wiersze 10–11: sposób wyświetlania przedmiotu.

Klasa [Eleve] ma następujący wygląd:


class Eleve:
    # producent
    def __init__(self,id,nom,prenom,classe):
        # zapisywanie parametrów
        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)

  • wiersze 3–8: uczeń jest charakteryzowany przez swój numer (wiersz 5), nazwisko (wiersz 6), imię (wiersz 7) oraz klasę (wiersz 8). Ten ostatni parametr stanowi odwołanie do obiektu [Classe];
  • wiersze 11–12: sposób wyświetlania ucznia.

Klasa [Note] ma następującą postać:


class Note:
    # konstruktor
    def __init__(self,id,valeur,eleve,matiere):
        # zapisywanie parametrów
        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)

  • wiersze 3–8: obiekt [Note] jest charakteryzowany przez swój numer (wiersz 5), wartość oceny (wiersz 6), odwołanie do ucznia, który otrzymał tę ocenę (wiersz 7), oraz odwołanie do przedmiotu, którego dotyczy ocena (wiersz 8);
  • wiersze 11–12: sposób wyświetlania obiektu [Note].

7.3. Warstwa [dao]

Warstwa [dao] udostępni warstwie [métier] następujący interfejs:

  • getClasses zwraca listę klas gimnazjum;
  • getMatieres zwraca listę przedmiotów;
  • getEleves zwraca listę uczniów;
  • getNotes zwraca listę ocen.

Warstwa [métier] będzie korzystać wyłącznie z tych metod. Nie musi ona wiedzieć, w jaki sposób są one zaimplementowane. Dane te mogą pochodzić z różnych źródeł (z pamięci stałej, z bazy danych, z plików tekstowych itp.), co nie ma wpływu na warstwę [métier]. Nazywa się to programowaniem opartym na interfejsach.

Klasa [Dao] implementuje ten interfejs w następujący sposób:


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

# import modułu obiektów
from entites import *

class Dao:
    # konstruktor
    def __init__(self):
        # tworzenie instancji klas
        classe1=Classe(1,"classe1")
        classe2=Classe(2,"classe2")
        self.classes=[classe1,classe2]
        # przedmioty
        matiere1=Matiere(1,"matiere1",1)
        matiere2=Matiere(2,"matiere2",2)
        self.matieres=[matiere1,matiere2]
        # uczniowie
        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]
        # oceny
        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]

    #-----------
    # interfejs
    #-----------
    
    # lista klas
    def getClasses(self):
        return self.classes

    # lista przedmiotów
    def getMatieres(self):
        return self.matieres
    
    # lista uczniów
    def getEleves(self):
        return self.eleves

    # lista ocen
    def getNotes(self):
        return self.notes

  • wiersz 4: importujemy moduł zawierający encje obsługiwane przez warstwę [dao];
  • wiersz 8: konstruktor nie ma parametrów. Tworzy na stałe cztery listy:
    • wiersze 10–12: lista klas;
    • wiersze 14–16: lista przedmiotów;
    • wiersze 18–22: lista uczniów;
    • wiersze 24–32: lista ocen.
  • wiersze 39–52: cztery metody interfejsu warstwy [dao] ograniczają się do zwracania odwołania do czterech list utworzonych przez konstruktor.

Program testowy mógłby wyglądać następująco:


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

# import modułu jednostek i warstwy [dao]
from entites import *
from dao import *

# instancjonowanie warstwy [dao]
dao=Dao()

# lista klas
for classe in dao.getClasses():
    print classe

# lista klas
for matiere in dao.getMatieres():
    print matiere

# lista klas
for eleve in dao.getEleves():
    print eleve

# lista klas
for note in dao.getNotes():
    print note

Komentarze są wystarczająco jasne. Wiersze 11–24 wykorzystują interfejs warstwy [dao]. Nie ma tu żadnych założeń dotyczących rzeczywistej implementacji warstwy. W wierszu 8 tworzy się instancję warstwy [dao]. W tym miejscu przyjmujemy pewne założenia: nazwę klasy i typ konstruktora. Istnieją rozwiązania pozwalające uniknąć tej zależności.

Wyniki wykonania tego skryptu są następujące:

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. Warstwa [métier]

Warstwa [métier] implementuje następujący interfejs:

  • getClasses wyświetla listę klas gimnazjum;
  • getMatieres zwraca listę przedmiotów;
  • getEleves zwraca listę uczniów;
  • getNotes zwraca listę ocen;
  • getStatsForEleve zwraca oceny ucznia o numerze idEleve wraz z informacjami na ich temat: średnią ważoną, najniższą ocenę, najwyższą ocenę.

Warstwa [présentation] będzie korzystać wyłącznie z tych metod. Nie musi wiedzieć, w jaki sposób są one zaimplementowane.

Metoda getStatsForEleve zwraca obiekt typu [StatsForEleve] o następującej strukturze:


class StatsForEleve:
    # konstruktor
    def __init__(self, eleve, notes):
        # zapisywanie parametrów
        self.eleve=eleve
        self.notes=notes
        # zatrzymujemy się, jeśli nie ma ocen
        if len(notes)==0:
            return
        # przetwarzanie ocen
        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
        # obliczanie średniej ucznia
        self.moyennePonderee=float(sommePonderee)/sommeCoeff
        
    # toString
    def __str__(self):
        # przypadek ucznia bez ocen
        if len(self.notes)==0:
            return "Eleve={0}, notes=[]".format(self.eleve)
        # przypadek ucznia z ocenami
        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)

  • wiersz 3: konstruktor otrzymuje dwa parametry:
    • odwołanie do ucznia typu [Eleve], dla którego obliczane są wskaźniki;
    • odwołanie do jego ocen, czyli listę obiektów typu [Note].
  • wiersze 8–9: jeśli lista ocen jest pusta, nie kontynuuje się dalszych obliczeń.
  • w przeciwnym razie w wierszach 11–25 obliczane są następujące wskaźniki:
    • self.moyennePonderee: średnia ucznia ważona współczynnikami przedmiotów;
    • self.min: najniższa ocena ucznia;
    • self.max: najwyższa ocena ucznia.
  • wiersz 28: metoda wyświetlania klasy w formie wskazanej w wierszu 36.

Skrypt testowy tej klasy mógłby wyglądać następująco:


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

# import modułów obiektów, warstwy [dao] oraz warstwy [metier]
from entites import *
from dao import *
from metier import *

# klasa
classe1=Classe(1,"6e A")
# uczeń w tej klasie
paul_durand=Eleve(1,"durand","paul",classe1)
# trzy przedmioty
maths=Matiere(1,"maths",1)
francais=Matiere(2,"francais",2)
anglais=Matiere(3,"anglais",3)
# oceny z tych przedmiotów dla ucznia
note_maths=Note(1,10,paul_durand,maths)
note_francais=Note(2,12,paul_durand,francais)
note_anglais=Note(3,14,paul_durand,anglais)
# wyświetlane są wskaźniki
print StatsForEleve(paul_durand,[note_maths, note_francais,note_anglais])

Wyniki wyświetlane na ekranie są następujące:

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

Wróćmy do naszej architektury warstwowej:

Klasa [Metier] implementuje warstwę [métier] w następujący sposób:


class Metier:
    # twórca
    def __init__(self,dao):
        # zapisuje się odniesienie na warstwie [dao]
        self.dao=dao

    #-----------
    # interfejs
    #-----------

    # wskaźniki na notatkach
    def getStatsForEleve(self,idEleve):
        # Statystyki dla ucznia o nr idEleve
        # wyszukiwanie ucznia
        trouve=False
        i=0
        eleves=self.getEleves()
        while not trouve and i<len(eleves):
            trouve=eleves[i].id==idEleve
            i+=1
        # czy znaleziono?
        if not trouve:
            raise RuntimeError("L'eleve [{0}] n'existe pas".format(idEleve))
        else:
            eleve=eleves[i-1]
        # Lista wszystkich ocen
        notes=[]
        for note in self.getNotes():
            # dodajemy do listy ocen wszystkie oceny ucznia
            if note.eleve.id==idEleve:
                notes.append(note)
        # zwracamy wynik
        return StatsForEleve(eleve,notes)

    # lista klas
    def getClasses(self):
        return self.dao.getClasses()
    
    # lista przedmiotów
    def getMatieres(self):
        return self.dao.getMatieres()
    
    # lista uczniów
    def getEleves(self):
        return self.dao.getEleves()
    
    # lista ocen
    def getNotes(self):
        return self.dao.getNotes()

  • wiersze 3–5: konstruktor otrzymuje odwołanie do warstwy [dao]. Warstwa [métier] musi posiadać to odwołanie. W tym przypadku przekazujemy je jej za pośrednictwem jej konstruktora. Można by sobie wyobrazić inne rozwiązania. W architekturze warstwowej przedstawionej w układzie poziomym każda warstwa musi posiadać odwołanie do warstwy znajdującej się po jej prawej stronie;
  • wiersze 36–49: metody getClasses, getMatieres, getEleves, getNotes ograniczają się do przekazania wywołania do metod o tych samych nazwach w warstwie [dao];
  • wiersz 12: metoda getStatsForEleve otrzymuje jako parametr numer ucznia, dla którego należy wygenerować wskaźniki.
  • wiersz 17: uczeń zostanie wyszukany na liście wszystkich uczniów;
  • wiersze 18–20: pętla wyszukiwania;
  • wiersz 23: jeśli nie znaleziono ucznia, zgłaszany jest wyjątek;
  • w przeciwnym razie w wierszu 25 odnotowuje się znalezionego ucznia;
  • wiersze 28–31: przeszukuje się zbiór ocen w szkole, szukając tych, które należą do zapamiętanego ucznia;
  • gdy zostaną znalezione, można utworzyć żądany obiekt StatsForEleve.

7.5. Warstwa [console]

Warstwa [console] jest zaimplementowana przez następujący skrypt:


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

# import modułu podmiotów, modułu [dao] oraz modułu [metier]
from entites import *
from dao import *
from metier import *

# ----------- warstwa [console]
# instancjonowanie warstwy [metier]
metier=Metier(Dao())
# żądanie / odpowiedź
fini=False
while not fini:
    # pytanie
    print "numero de l'eleve (>=1 et * pour arreter) : "
    # odpowiedź
    reponse=raw_input()
    # czy to już koniec?
    if reponse.strip()=="*":
        break
    # czy dane zostały wprowadzone poprawnie?
    ok=False
    try:
        idEleve=int(reponse,10)
        ok=idEleve>=1
    except:
        pass
    # dane są poprawne?
    if not ok:
        print "Saisie incorrecte. Recommencez..."
        continue
    # obliczenie
    try:
        print metier.getStatsForEleve(idEleve)
    except RuntimeError,erreur:
        print "L'erreur suivante s'est produite : {0}".format(erreur)

  • wiersz 10: instancjonowanie zarówno warstw [dao], jak i [métier]. Jest to jedyna zależność naszego kodu od implementacji tych warstw;
  • wiersz 34: wykorzystujemy interfejs warstwy [métier];
  • wiersz 19: metoda strip usuwa spacje na początku i na końcu ciągu znaków;
  • wiersz 20: break pozwala wyjść z pętli;
  • wiersz 24: podejmowana jest próba konwersji wprowadzonego ciągu znaków na liczbę całkowitą w systemie dziesiętnym;
  • wiersz 29: ok jest prawdziwe tylko wtedy, gdy wykonano wcześniej wiersz 25;
  • wiersz 31: continue umożliwia powrót do środka pętli;
  • wiersz 34: obliczanie wskaźników;
  • wiersz 35: przechwytuje się wyjątek RuntimeError, który może wystąpić w warstwie [métier].

Oto przykład wykonania:

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