7. Architettura a livelli e programmazione basata su interfacce
7.1. Introduzione
Proponiamo di scrivere un'applicazione che visualizzi i voti degli studenti delle scuole medie. Questa applicazione avrà un'architettura a più livelli:
![]() |
- Il livello [presentazione] è il livello che interagisce con l'utente dell'applicazione.
- Il livello [logica di business] implementa le regole di business dell'applicazione, come il calcolo di uno stipendio o di una fattura. Questo livello utilizza i dati provenienti dall'utente tramite il livello [presentazione] e dal DBMS tramite il livello [DAO].
- Il livello [DAO] (Data Access Objects) gestisce l'accesso ai dati nel DBMS.
Illustreremo questa architettura con una semplice applicazione console:
- non ci sarà alcun database,
- il livello [DAO] gestirà le entità Studente, Classe, Materia e Voto per gestire i voti degli studenti,
- il livello [logica di business] calcolerà le metriche in base ai voti di uno studente specifico,
- il livello [presentazione] sarà un'applicazione console che visualizza i risultati calcolati dal livello [business].
Il progetto Visual Studio per l'applicazione è il seguente:
![]() |
7.2. Le entità dell'applicazione
Le entità sono oggetti. In questo caso, avremo quattro classi per ciascuna delle entità: Studente, Classe, Materia e Voto.
![]() |
Il file [entities.py] contiene quattro classi.
La classe [Class] rappresenta una classe della scuola media:
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)
- righe 3–6: una classe è definita da un ID (riga 5) e da un nome (riga 6);
- righe 9-10: il metodo per visualizzare la classe.
La classe [Subject] è la seguente:
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)
- righe 3-7: un soggetto è definito dal suo ID (riga 5), dal suo nome (riga 6) e dal suo peso (riga 7);
- righe 10-11: il metodo per visualizzare il soggetto.
La classe [Student] è la seguente:
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)
- righe 3–8: uno studente è caratterizzato dal proprio ID (riga 5), cognome (riga 6), nome (riga 7) e classe (riga 8). Quest'ultimo parametro è un riferimento a un oggetto [Class];
- righe 11-12: il metodo per visualizzare lo studente.
La classe [Grade] è la seguente:
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)
- righe 3-8: un oggetto [Grade] è caratterizzato dal proprio ID (riga 5), dal valore del voto (riga 6), da un riferimento allo studente che ha ricevuto questo voto (riga 7) e da un riferimento alla materia per la quale è stato assegnato il voto (riga 8);
- righe 11-12: il metodo per visualizzare l'oggetto [Note].
7.3. Il livello [dao]
![]() |
Il livello [dao] fornirà la seguente interfaccia al livello [business]:
- getClasses restituisce l'elenco delle classi della scuola media;
- getMatieres restituisce l'elenco delle materie;
- getStudents restituisce l'elenco degli studenti;
- getGrades restituisce un elenco dei voti.
Il livello [business] utilizzerà solo questi metodi. Non ha bisogno di sapere come sono implementati. Questi dati possono quindi provenire da varie fonti (valori hard-coded, un database, file di testo, ecc.) senza influire sul livello [business]. Questo è noto come programmazione basata su interfaccia.
La classe [Dao] implementa questa interfaccia come segue:
- riga 4: importiamo il modulo contenente le entità gestite dal livello [DAO];
- riga 8: il costruttore non ha parametri. Codifica in modo fisso quattro elenchi:
- 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] restituiscono semplicemente un riferimento alle quattro liste costruite dal costruttore.
Un programma di prova potrebbe apparire così:
I commenti parlano da soli. Le righe 11–24 utilizzano l'interfaccia del livello [dao]. Qui non vi sono presupposti riguardo all'effettiva implementazione del livello. Alla riga 8, istanziamo il livello [dao]. Qui facciamo delle ipotesi: nome della classe e tipo di costruttore. Esistono soluzioni che ci consentono di evitare questa dipendenza.
I risultati dell'esecuzione di questo script sono i seguenti:
7.4. Il livello [aziendale]
![]() |
Il livello [business] implementa la seguente interfaccia:
- getClasses restituisce l'elenco delle classi della scuola media;
- getMatieres restituisce l'elenco delle materie;
- getStudents restituisce l'elenco degli studenti;
- getGrades restituisce un elenco di voti;
- getStatsForStudent restituisce i voti dello studente idStudent insieme alle relative informazioni: media ponderata, voto più basso, voto più alto.
Il livello [di presentazione] utilizzerà solo questi metodi. Non ha bisogno di sapere come sono implementati.
Il metodo getStatsForStudent restituisce un oggetto di tipo [StatsForStudent] come segue:
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)
- Riga 3: Il costruttore accetta due parametri:
- un riferimento allo studente di tipo [Student] per il quale vengono calcolate le metriche;
- un riferimento ai suoi voti, una lista di oggetti [Grade].
- righe 8-9: se l'elenco dei voti è vuoto, ci fermiamo qui.
- Altrimenti, righe 11–25: vengono calcolate le seguenti metriche:
- self.weightedAverage: la media dello studente ponderata dai coefficienti delle materie;
- self.min: il voto più basso dello studente;
- self.max: il voto più alto dello studente.
- riga 28: il metodo per visualizzare la classe nel formato specificato nella riga 36.
Uno script di test per questa classe potrebbe essere il seguente:
Il risultato sullo schermo è il seguente:
Torniamo alla nostra architettura a livelli:
![]() |
La classe [Business] implementa il livello [business] come segue:
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()
- righe 3–5: il costruttore riceve un riferimento al livello [dao]. Il livello [business] deve disporre di questo riferimento. Qui lo forniamo tramite il suo costruttore. Sono possibili altre soluzioni. In un'architettura a livelli rappresentata orizzontalmente, ogni livello deve avere un riferimento al livello alla sua destra;
- righe 36-49: i metodi getClasses, getSubjects, getStudents e getGrades delegano semplicemente la chiamata ai metodi con lo stesso nome nel livello [dao];
- riga 12: il metodo getStatsForStudent riceve come parametro l'ID dello studente per il quale devono essere restituite le statistiche.
- 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 viene trovato, viene generata un'eccezione;
- altrimenti, riga 25: lo studente trovato viene memorizzato;
- righe 28–31: si effettuano ricerche in tutte le classi della scuola media per individuare quelle a cui appartiene lo studente memorizzato;
- una volta trovati, è possibile costruire l'oggetto StatsForEleve richiesto.
7.5. Il livello [console]
![]() |
Il livello [console] è implementato dal seguente script:
- riga 10: istanziamento dei livelli [dao] e [business]. Questa è l'unica dipendenza che il nostro codice ha dall'implementazione di questi livelli;
- riga 34: utilizziamo l'interfaccia del livello [business];
- Riga 19: il metodo strip rimuove gli spazi iniziali e finali dalla stringa;
- riga 20: break esce da un ciclo;
- riga 24: tenta di convertire la stringa inserita in un numero intero decimale;
- riga 29:
okè vero solo se la riga 25 è stata eseguita; - riga 31: continue permette al ciclo di ricominciare da metà;
- riga 34: calcolo degli indicatori;
- riga 35: intercetta l'eccezione RuntimeError che potrebbe provenire dal livello [business].
Ecco un esempio di esecuzione:






