7. Architettura a livelli e programmazione tramite interfacce
7.1. Introduction
Ci proponiamo di scrivere un’applicazione che consenta la visualizzazione dei voti degli studenti di una scuola media. Questa applicazione avrà un’architettura a più livelli:
![]() |
- il livello [présentation] è il livello a contatto con l'utente dell'applicazione.
- Il livello [metier] implementa le regole di gestione dell’applicazione, come il calcolo di uno stipendio o di una fattura. Questo livello utilizza i dati provenienti dall’utente tramite il livello [présentation] e quelli provenienti da SGBD tramite il livello [dao].
- Il livello [dao] (Data Access Objects) gestisce l'accesso ai dati del livello SGBD.
Illustreremo questa architettura con una semplice applicazione da console:
- non ci sarà alcun database,
- il livello [dao] gestirà le entità Eleve, Classe, Matiere, Note che consentono di gestire i voti degli studenti,
- il livello [métier] consentirà di calcolare indicatori relativi ai voti di uno studente specifico,
- il livello [présentation] sarà un'applicazione da console che visualizzerà i risultati calcolati dal livello [métier].
Il progetto Visual Studio dell’applicazione è il seguente:
![]() |
7.2. Le entità dell’applicazione
Le entità sono oggetti. In questo caso avremo quattro classi per ciascuna delle entità Eleve, Classe, Matiere, Note.
![]() |
Il file [entites.py] raggruppa quattro classi.
La classe [Classe] rappresenta una classe della scuola media:
class Classe:
# costruttore
def __init__(self,id,nom):
# si memorizzano i parametri
self.id=id
self.nom=nom
# toString
def __str__(self):
return "Classe[{0},{1}]".format(self.id,self.nom)
- righe 3-6: una classe è definita da un n. id (riga 5) e da un nom (riga 6);
- righe 9-10: il metodo di visualizzazione della classe.
La classe [Matiere] è la seguente:
class Matiere:
# produttore
def __init__(self,id,nom,coefficient):
# memorizzazione dei parametri
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)
- righe 3-7: una materia è definita dal suo numero (riga 5), dal suo nome (riga 6) e dal suo coefficiente (riga 7);
- righe 10-11: il metodo di visualizzazione della materia.
La classe [Eleve] è la seguente:
class Eleve:
# costruttore
def __init__(self,id,nom,prenom,classe):
# memorizzazione dei parametri
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)
- righe 3-8: uno studente è caratterizzato dal proprio numero (riga 5), dal proprio cognome (riga 6), dal proprio nome (riga 7) e dalla propria classe (riga 8). Quest’ultimo parametro è un riferimento a un oggetto [Classe];
- righe 11-12: il metodo di visualizzazione dello studente.
La classe [Note] è la seguente:
class Note:
# costruttore
def __init__(self,id,valeur,eleve,matiere):
# si memorizzano i parametri
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)
- righe 3-8: un oggetto [Note] è caratterizzato dal proprio numero (riga 5), dal valore del voto (riga 6), da un riferimento allo studente a cui è attribuito tale voto (riga 7) e da un riferimento alla materia oggetto del voto (riga 8);
- righe 11-12: il metodo di visualizzazione dell’oggetto [Note].
7.3. Il livello [dao]
![]() |
Il livello [dao] fornirà la seguente interfaccia al livello [métier]:
- getClasses restituisce l'elenco delle classi della scuola media;
- getMatieres restituisce l'elenco delle materie;
- getEleves restituisce l'elenco degli studenti;
- getNotes restituisce l'elenco dei voti.
Il livello [métier] utilizzerà solo questi metodi. Non deve sapere come sono implementati. Questi dati possono quindi provenire da diverse fonti (hardcoded, da un database, da file di testo, ...) senza che ciò abbia alcun impatto sul livello [métier]. Questo approccio è noto come programmazione tramite interfacce.
La classe [Dao] implementa questa interfaccia nel modo seguente:
# -*- coding=utf-8 -*-
# importazione del modulo delle entità
from entites import *
class Dao:
# costruttore
def __init__(self):
# si istanziano le classi
classe1=Classe(1,"classe1")
classe2=Classe(2,"classe2")
self.classes=[classe1,classe2]
# le materie
matiere1=Matiere(1,"matiere1",1)
matiere2=Matiere(2,"matiere2",2)
self.matieres=[matiere1,matiere2]
# gli studenti
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]
# i voti
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]
#-----------
# interfaccia
#-----------
# elenco delle classi
def getClasses(self):
return self.classes
# elenco delle materie
def getMatieres(self):
return self.matieres
# elenco degli studenti
def getEleves(self):
return self.eleves
# elenco dei voti
def getNotes(self):
return self.notes
- riga 4: si importa il modulo che contiene le entità gestite dal livello [dao];
- riga 8: il costruttore non ha parametri. Crea in modo statico quattro liste:
- righe 10-12: l'elenco delle classi;
- righe 14-16: l'elenco delle materie;
- righe 18-22: l'elenco degli studenti;
- righe 24-32: l'elenco dei voti.
- righe 39-52: i quattro metodi dell'interfaccia del livello [dao] si limitano a restituire un riferimento alle quattro liste create dal costruttore.
Un programma di test potrebbe essere il seguente:
# -*- coding=utf-8 -*-
# importazione del modulo delle entità e del livello [dao]
from entites import *
from dao import *
# istanziazione del livello [dao]
dao=Dao()
# elenco delle classi
for classe in dao.getClasses():
print classe
# elenco delle classi
for matiere in dao.getMatieres():
print matiere
# elenco delle classi
for eleve in dao.getEleves():
print eleve
# elenco delle classi
for note in dao.getNotes():
print note
I commenti sono autoesplicativi. Le righe 11-24 utilizzano l'interfaccia del livello [dao]. Qui non vi sono ipotesi sull'effettiva implementazione del livello. Alla riga 8 si istanzia il livello [dao]. Qui si formulano alcune ipotesi: nome della classe e tipo di costruttore. Esistono soluzioni che consentono di evitare questa dipendenza.
I risultati dell’esecuzione di questo script sono i seguenti:
7.4. Il livello [métier]
![]() |
Il livello [métier] implementa la seguente interfaccia:
- getClasses restituisce l'elenco delle classi della scuola media;
- getMatieres restituisce l'elenco delle materie;
- getEleves restituisce l'elenco degli studenti;
- getNotes restituisce l'elenco dei voti;
- getStatsForEleve restituisce i voti dello studente n. idEleve insieme alle relative informazioni: media ponderata, voto più basso, voto più alto.
Il livello [présentation] utilizzerà solo questi metodi. Non deve sapere come sono implementati.
Il metodo getStatsForEleve restituisce un oggetto di tipo [StatsForEleve] come segue:
class StatsForEleve:
# produttore
def __init__(self, eleve, notes):
# si memorizzano i parametri
self.eleve=eleve
self.notes=notes
# si interrompe se non ci sono note
if len(notes)==0:
return
# elaborazione dei voti
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
# calcolo della media dello studente
self.moyennePonderee=float(sommePonderee)/sommeCoeff
# toString
def __str__(self):
# caso dello studente senza voti
if len(self.notes)==0:
return "Eleve={0}, notes=[]".format(self.eleve)
# caso dello studente con voti
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)
- riga 3: il costruttore riceve due parametri:
- un riferimento allo studente di tipo [Eleve] per il quale si calcolano gli indicatori;
- un riferimento ai suoi voti, un elenco di oggetti [Note].
- righe 8-9: se l'elenco dei voti è vuoto, non si procede oltre.
- Altrimenti, righe 11-25, si calcolano i seguenti indicatori:
- self.moyennePonderee: la media dello studente ponderata in base ai coefficienti delle materie;
- self.min: il voto più basso dello studente;
- self.max: il suo voto più alto.
- riga 28: il metodo di visualizzazione della classe nella forma indicata alla riga 36.
Uno script di prova per questa classe potrebbe essere il seguente:
# -*- coding=utf-8 -*-
# importazione dei moduli delle entità, del livello [dao] e del livello [metier]
from entites import *
from dao import *
from metier import *
# una classe
classe1=Classe(1,"6e A")
# uno studente in questa classe
paul_durand=Eleve(1,"durand","paul",classe1)
# tre materie
maths=Matiere(1,"maths",1)
francais=Matiere(2,"francais",2)
anglais=Matiere(3,"anglais",3)
# i voti in queste materie per l'alunno
note_maths=Note(1,10,paul_durand,maths)
note_francais=Note(2,12,paul_durand,francais)
note_anglais=Note(3,14,paul_durand,anglais)
# vengono visualizzati gli indicatori
print StatsForEleve(paul_durand,[note_maths, note_francais,note_anglais])
I risultati visualizzati sullo schermo sono i seguenti:
Torniamo alla nostra architettura a livelli:
![]() |
La classe [Metier] implementa il livello [métier] nel modo seguente:
class Metier:
# costruttore
def __init__(self,dao):
# si memorizza il riferimento sul livello [dao]
self.dao=dao
#-----------
# interfaccia
#-----------
# gli indicatori sulle note
def getStatsForEleve(self,idEleve):
# Statistiche per lo studente n. idEleve
# ricerca dello studente
trouve=False
i=0
eleves=self.getEleves()
while not trouve and i<len(eleves):
trouve=eleves[i].id==idEleve
i+=1
# È stato trovato?
if not trouve:
raise RuntimeError("L'eleve [{0}] n'existe pas".format(idEleve))
else:
eleve=eleves[i-1]
# Elenco di tutti i voti
notes=[]
for note in self.getNotes():
# si aggiungono a "Voti" tutti i voti dello studente
if note.eleve.id==idEleve:
notes.append(note)
# si restituisce il risultato
return StatsForEleve(eleve,notes)
# l'elenco delle classi
def getClasses(self):
return self.dao.getClasses()
# l'elenco delle materie
def getMatieres(self):
return self.dao.getMatieres()
# l'elenco degli studenti
def getEleves(self):
return self.dao.getEleves()
# l'elenco dei voti
def getNotes(self):
return self.dao.getNotes()
- righe 3-5: il costruttore riceve un riferimento al livello [dao]. Il livello [métier] deve disporre di tale riferimento. In questo caso, gli viene fornito tramite il suo costruttore. Si potrebbero immaginare altre soluzioni. In un’architettura a livelli rappresentata orizzontalmente, ogni livello deve avere un riferimento al livello che si trova alla sua destra;
- righe 36-49: i metodi getClasses, getMatieres, getEleves, getNotes si limitano a delegare la chiamata ai metodi con lo stesso nome del livello [dao];
- riga 12: il metodo getStatsForEleve riceve come parametro il numero dello studente per il quale devono essere restituiti gli indicatori.
- riga 17: lo studente verrà cercato nell'elenco di tutti gli studenti;
- righe 18-20: il ciclo di ricerca;
- riga 23: se lo studente non è stato trovato, viene generata un'eccezione;
- altrimenti, alla riga 25, lo studente trovato viene memorizzato;
- righe 28-31: si cercano, tra tutte le note della scuola media, quelle che appartengono allo studente memorizzato;
- una volta individuati, è possibile costruire l’oggetto StatsForEleve richiesto.
7.5. Il livello [console]
![]() |
Il livello [console] è implementato dal seguente script:
# -*- coding=utf-8 -*-
# importazione del modulo delle entità, del modulo [dao], del modulo [metier]
from entites import *
from dao import *
from metier import *
# ----------- livello [console]
# istanziazione del livello [metier]
metier=Metier(Dao())
# richiesta/risposta
fini=False
while not fini:
# domanda
print "numero de l'eleve (>=1 et * pour arreter) : "
# risposta
reponse=raw_input()
# finito?
if reponse.strip()=="*":
break
# l'inserimento è corretto?
ok=False
try:
idEleve=int(reponse,10)
ok=idEleve>=1
except:
pass
# dato corretto?
if not ok:
print "Saisie incorrecte. Recommencez..."
continue
# calcolo
try:
print metier.getStatsForEleve(idEleve)
except RuntimeError,erreur:
print "L'erreur suivante s'est produite : {0}".format(erreur)
- riga 10: istanziazione sia del livello [dao] che del livello [métier]. Questa è l'unica dipendenza del nostro codice rispetto all'implementazione di questi livelli;
- riga 34: si utilizza l'interfaccia del livello [métier];
- riga 19: il metodo strip rimuove gli spazi all'inizio e alla fine della stringa;
- riga 20: break consente di uscire da un ciclo;
- riga 24: si tenta di convertire la stringa immessa in un numero intero decimale;
- riga 29: ok è vero solo se si è passati dalla riga 25;
- riga 31: continue consente di rientrare nel ciclo a metà del ciclo stesso;
- riga 34: calcolo degli indicatori;
- riga 35: si intercetta l'eccezione RuntimeError che può verificarsi nel livello [métier].
Ecco un esempio di esecuzione:






