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






