14. Architettura a livelli e programmazione basata su interfacce
14.1. Introduzione
Proponiamo di scrivere un'applicazione che visualizzi i voti degli studenti delle scuole medie. Questa applicazione può avere un'architettura a più livelli:

- il livello [ui] (User Interface) è il livello che interagisce con l'utente dell'applicazione;
- il livello [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 [presentation] e dal DBMS tramite il livello [DAO];
- il livello [DAO] (Data Access Objects) gestisce l'accesso ai dati nel DBMS (Database Management System).
Questa è l'architettura utilizzata nel |corso Python 2|. È possibile introdurre anche una variante:

Le differenze rispetto alla precedente struttura a livelli sono le seguenti:
- uno script principale chiamato [main] sopra organizza l'istanziazione dei livelli;
- i livelli [ui, business, dao] non comunicano più necessariamente tra loro. Se necessario, lo script [main] fornisce loro i riferimenti ai livelli di cui hanno bisogno;
Il codice qui è organizzato in aree funzionali con un coordinatore centrale:
- l'orchestratore è lo script principale [main];
- i livelli [ui], [dao] e [business] sono i centri di competenza;
Potremmo definire questa struttura un'organizzazione orchestrale.
14.2. Esempio 1
Illustreremo l'architettura a livelli utilizzando 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 [business] calcolerà le metriche in base ai voti di uno studente specifico;
- il livello [ui] sarà un'applicazione console che visualizza i risultati degli studenti;
Il progetto PyCharm per l'applicazione è il seguente:
![]() |
Nota: le cartelle in blu fanno parte della [Root Sources] del progetto PyCharm.
14.2.1. Le entità dell'applicazione
Ci riferiremo alle classi il cui unico ruolo è incapsulare i dati come entità. A questo scopo si potrebbero utilizzare i dizionari. Il vantaggio di una classe è che ci permette di verificare la validità dei dati memorizzati nell’oggetto e di fornire un metodo che restituisce l’identità dell’oggetto come stringa.
![]() |
14.2.1.1. L'entità [Class]
L'entità [Class] (Class.py) rappresenta una classe della scuola media:
Note
- riga 7: l'entità [Class] deriva dall'entità [BaseEntity] descritta nella sezione |La classe BaseEntity|;
- righe 11–16: una classe è definita da un ID e da un nome (riga 16). La proprietà [id] è fornita dalla classe [BaseEntity] e il nome dalla classe [Class];
- righe 18–30: getter/setter per l'attributo [name];
14.2.1.2. L'entità [Subject]
La classe [Subject] (subject.py) è la seguente:
Note
- riga 7: la classe [Class] deriva dalla classe [BaseEntity];
- righe 11–17: un soggetto è definito dal suo ID [id], dal suo nome [name] e dal suo peso [coefficient];
- righe 19–50: getter/setter per gli attributi della classe;
14.2.1.3. L'entità [Student]
La classe [Student] (student.py) è la seguente:
Note
- riga 9: la classe [Student] deriva dalla classe [BaseEntity];
- righe 13–20: uno studente è caratterizzato dal proprio ID [id], cognome [lastName], nome [firstName] e classe [class]. Quest'ultimo parametro è un riferimento a un oggetto [Class];
- righe 22–65: getter/setter per gli attributi della classe;
14.2.1.4. L'entità [Note]
La classe [Note] (note.py) è la seguente:
Note
- riga 8: la classe [Note] deriva dalla classe [BaseEntity];
- righe 12–20: un oggetto [Note] è caratterizzato dal proprio ID [id], dal valore del voto [value], da un riferimento [student] allo studente che ha ricevuto tale voto e da un riferimento alla materia [subject] associata al voto;
- righe 22–75: getter/setter per gli attributi della classe;
14.2.2. Configurazione dell'applicazione
![]() |
Il file [config.py] configura l'ambiente sia per lo script principale [main] (1) che per i test (2). Tutti questi script hanno un'istruzione [import config] all'inizio del codice. Si noti che la directory contenente lo script a cui punta il comando [python script] fa automaticamente parte del Python Path. Pertanto, se [config] si trova nella stessa directory degli script contenenti l'istruzione [import config], verrà individuato. I file [1] e [2] sono identici in questo caso. Ciò potrebbe non verificarsi sempre.
Il file [config.sys] è il seguente:
- Righe 11–14: le directory che devono far parte del Python Path (sys.path);
- La directory [f"{root_dir}/02/entities"] fornisce l'accesso alle classi [BaseEntity] e [MyException];
- la cartella [f"{script_dir}/../entities"] fornisce l'accesso alle classi [Student], [Class], [Subject], [Grade];
- la cartella [f"{script_dir}/../interfaces"] fornisce l'accesso alle interfacce dell'applicazione;
- la cartella [f"{script_dir}/../services"] fornisce l'accesso alle classi che implementano le interfacce;
14.2.3. Test delle entità
![]() |
Qui scriveremo dei test eseguiti da uno strumento chiamato [unittest]. PyCharm include diversi framework di test. È possibile sceglierne uno nella configurazione di PyCharm:

- in [4] sono disponibili diversi framework di test:

14.2.3.1. La classe di test [TestBaseEntity]
Lo script di test [TestBaseEntity] sarà il seguente:
Note
- riga 1: importiamo il modulo [unittest], che fornisce i vari metodi di test;
- righe 3–6: configuriamo l'applicazione in modo che le classi necessarie per il test possano essere individuate;
- riga 9: una classe di test [unittest] deve estendere la classe [unittest.TestCase];
- righe 11, 27: le funzioni di test devono avere un nome che inizi con [test], altrimenti non verranno riconosciute;
- righe 13–16: importiamo le classi di cui abbiamo bisogno;
- In questa classe di test, vogliamo verificare il comportamento dei metodi [BaseEntity.fromdict] (riga 34) e [BaseEntity.fromjson] (riga 18). La classe [Note] ha proprietà che sono riferimenti ad altre classi. Vogliamo verificare che i due metodi precedenti creino oggetti [Note] validi;
- riga 18: creiamo un oggetto [Note] da un oggetto JSON;
- Riga 21: verifichiamo che l'oggetto creato sia effettivamente di tipo [Note]. Il metodo [assertIsInstance] è un metodo della classe [unittest.TestCase], che è la classe padre della classe [TestBaseEntity];
- riga 22: verifichiamo che [note.student] sia effettivamente di tipo [Student];
- riga 23: verifichiamo che [note.student.class] sia effettivamente di tipo [Class];
- riga 24: verifichiamo che [note.subject] sia effettivamente di tipo [Subject];
- righe 33–42: facciamo lo stesso con il metodo [BaseEntity.fromdict];
Esistono diversi modi per eseguire i test:
![]() |
- in [1-2], eseguiamo [TestBaseEntity] utilizzando il framework [UnitTest];
- nei paragrafi [3-5], i test falliscono. [UnitTests] indica che non sono stati trovati test da eseguire;
I test falliscono a causa della struttura del codice [TestBaseEntity]:
Ciò che causa problemi al framework [UnitTest] è la presenza di codice eseguibile (righe 3–6) prima della definizione della classe di test (riga 9).
Riorganizziamo quindi il codice come segue:
- Righe 6–10: Definiamo una funzione [setUp]. Questa funzione ha un ruolo specifico: viene eseguita prima di ogni funzione di test (test_note1, test_note2);
Una volta fatto ciò, l'esecuzione della classe [TestBaseEntity] produce i seguenti risultati:
![]() |
Questa volta, entrambi i metodi di test sono stati eseguiti e i test sono stati superati.
Vediamo cosa succede quando un test fallisce. Modifichiamo il codice in [test_note1] come segue:
- riga 2: verifichiamo che 1==2;
I risultati dell'esecuzione sono i seguenti:
![]() |
È possibile scoprire la causa dell'errore cliccando sul test fallito [2]:
![]() |
- in [7-8], la causa dell'errore;
Un altro modo per eseguire una classe di test è eseguirla in un terminale:
![]() |
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python -m unittest TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.026s
OK
La riga 6 indica che entrambi i test sono stati superati (abbiamo eliminato l'errore 1==2);
Infine, un terzo modo per eseguire la classe di test [TestBaseEntity], sempre in un terminale, è il seguente. Concludiamo la classe di test con le seguenti righe 6–7;
…
self.assertIsInstance(note.élève.classe, Classe)
self.assertIsInstance(note.matière, Matière)
if __name__ == '__main__':
unittest.main()
- riga 6: la variabile [__name__] è il nome assegnato allo script in esecuzione. Quando lo script è quello avviato dal comando [python script.py], la variabile [__name__] è [__main__] (2 trattini bassi prima e dopo l'identificatore). Pertanto, la riga 7 viene eseguita solo quando lo script [TestBaseEntity] viene avviato dal comando [python TestBaseEntity.py]. L'istruzione [unittest.main()] avvia l'esecuzione dello script tramite il framework [UnitTest]. Ecco un esempio:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.013s
OK
14.2.3.2. La classe di test [TestEntities]
La classe di test [TestEntities] è la seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
- Lo scopo dello script di test è quello di testare i setter della classe: verificare che non sia possibile assegnare valori errati agli attributi delle varie entità;
- righe 11–24: verifichiamo che non sia possibile assegnare un ID non valido a uno studente. Poiché alla riga 16 passiamo il valore 'x' come ID dello studente, ci aspettiamo che si verifichi un'eccezione. Dovremmo quindi passare alle righe 20–22;
- riga 21: visualizza il messaggio di errore;
- riga 22: recuperiamo il codice di errore (vedere la sezione |L'entità MyException|);
- riga 24: verifichiamo (assert) che il codice di errore sia 1. Qui verifichiamo due cose:
- che si sia effettivamente verificato un errore;
- che il codice di errore sia 1;
- questo processo viene ripetuto con le funzioni nelle righe 24–213;
- righe 215–222: verifichiamo se un'azione genera un'eccezione di un determinato tipo;
- riga 220: indichiamo che il test ha esito positivo se genera un'eccezione di tipo [MyException];
Risultati
Eseguiamo lo script di test:
![]() |
I risultati ottenuti sono i seguenti:
Testing started at 09:39 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Elève.Elève'> doit être un entier >=0]
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Classe.Classe'> doit être un entier >=0]
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Matière.Matière'> doit être un entier >=0]
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Note.Note'> doit être un entier >=0]
code erreur=21, message=MyException[21, Le nom de la matière 1 doit être une chaîne de caractères non vide]
code erreur=22, message=MyException[22, Le coefficient de la matière y doit être un réel >=0]
code erreur=31, message=MyException[31, L'attribut x de la note 1 doit être un nombre dans l'intervalle [0,20]]
code erreur=32, message=MyException[32, L'attribut [y] de la note 1 doit être de type Elève ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
code erreur=33, message=MyException[33, L'attribut [z] de la note 1 doit être de type Matière ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
code erreur=41, message=MyException[41, Le nom de l'élève 1 doit être une chaîne de caractères non vide]
code erreur=42, message=MyException[42, Le prénom de l'élève 1 doit être une chaîne de caractères non vide]
code erreur=43, message=MyException[43, L'attribut [t] de l'élève 1 doit être de type Classe ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
Ran 14 tests in 0.040s
OK
Process finished with exit code 0
Qui, tutti i test sono stati superati
14.2.4. Il livello [dao]

Il livello [dao] implementa l'interfaccia [InterfaceDao] [1]. Questa è implementata dalla classe [Dao] (2). Lo script [tests_dao] (3) verifica i metodi del livello [dao].
14.2.4.1. Interfaccia [InterfaceDao]
Un'interfaccia è un contratto tra il codice chiamante e il codice chiamato. È il codice chiamato a fornire l'interfaccia:
![]() |
- il codice chiamante [1] non conosce l'implementazione del codice chiamato [3]. Sa solo come chiamarlo. L'interfaccia [2] gli dice come farlo. Questa interfaccia definisce un insieme di metodi/funzioni da utilizzare per interagire con il codice chiamato. Questa interfaccia è nota anche come API (Application Programming Interface);
Il livello [dao] fornirà la seguente interfaccia:
- [get_classes] restituisce l'elenco delle classi della scuola media;
- [get_subjects] restituisce l'elenco delle materie insegnate nella scuola media;
- [get_students] restituisce l'elenco degli studenti della scuola media;
- [get_grades] restituisce un elenco di tutti i voti degli studenti;
- [get_grades_for_student_by_id] restituisce i voti di uno studente specifico;
- [get_student_by_id] restituisce uno studente identificato dal proprio ID;
Il codice chiamante utilizzerà solo questi metodi. Non ha bisogno di sapere come sono implementati. I dati possono quindi provenire da diverse fonti (hard-coded, da un database, da file di testo, ecc.) senza influire sul codice chiamante. Questo si chiama programmazione basata su interfaccia.
Python 3 ha un concetto simile a quello di un'interfaccia: la classe astratta. La useremo. Raggrupperemo le interfacce per questo esempio nella cartella [interfaces].
Definiamo una classe astratta [InterfaceDao] (InterfaceDao.py) per il livello [dao]:
Note:
- riga 2: ABC = Abstract Base Class. Importiamo la classe ABC dal modulo [abc], così come il decoratore [abstractmethod] utilizzato alle righe 10, 15, 20, 25, 30 e 35;
- riga 8: la classe astratta si chiama [InterfaceDao] e deriva dalla classe [ABC];
- i metodi della classe astratta sono decorati con il decoratore [@abstractmethod], che rende il metodo decorato un metodo astratto: il suo codice non è definito. Tuttavia, includiamo del codice lì: l'istruzione [pass], che non fa nulla;
- La classe astratta [InterfaceDao] non può essere istanziata. Possono essere istanziate solo le classi derivate da [InterfaceDao] che hanno implementato tutti i metodi di [InterfaceDao]. Pertanto, se creiamo due classi [Dao1] e [Dao2] derivate dalla classe [InterfaceDao], entrambe implementeranno i metodi astratti di [InterfaceDao]. Potremmo quindi dire che implementano l'interfaccia [InterfaceDao];
- i linguaggi che supportano sia le interfacce che le classi astratte assegnano all'interfaccia un ruolo diverso rispetto alla classe astratta. Un'interfaccia non ha attributi e non può essere istanziata. Una classe può implementare un'interfaccia definendo tutti i suoi metodi;
14.2.4.2. Implementazione di [Dao]
La classe [Dao] (dao.py) implementa l'interfaccia [InterfaceDao] come segue:
Note:
- righe 1-7: importiamo le entità e l'interfaccia [InterfaceDao];
- riga 11: la classe [Dao] deriva dalla classe astratta [InterfaceDao]. Si dice che implementa l'interfaccia [InterfaceDao];
- riga 14: il costruttore non ha parametri. Codifica in modo fisso quattro elenchi:
- righe 15–18: l'elenco delle classi;
- righe 19–22: l'elenco delle materie;
- righe 23–28: l'elenco degli studenti;
- righe 29–38: l'elenco dei voti;
- righe 40–44: implementazione dei metodi dell'[interfaccia Dao]. In questo caso, non li definiamo per vedere il messaggio di errore generato da Python;
Un programma di test potrebbe essere simile a questo [tests-dao.py]:
Nota: lo script [tests-dao.py] non è un [unittest] perché non contiene alcun metodo il cui nome inizi con [test_].
I commenti sono autoesplicativi. Le righe 11–25 utilizzano l'interfaccia del livello [dao]. Qui non vi sono presupposti riguardo all'effettiva implementazione del livello. Alla riga 9, istanziamo il livello [dao].
I risultati dell'esecuzione di questo script sono i seguenti:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
Traceback (most recent call last):
File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py", line 9, in <module>
daoImpl = Dao()
TypeError: Can't instantiate abstract class Dao with abstract methods get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves
Process finished with exit code 1
Notiamo che si verifica un errore non appena viene istanziata la classe [Dao] (riga 3 sopra). L'interprete Python 3 ci comunica che non è in grado di istanziare la classe poiché non abbiamo definito i metodi astratti [get_classes, get_subjects, get_grades, get_grades_for_student_by_id, get_student_by_id, get_students].
PyCharm supporta anche le classi astratte e offre la possibilità di definirne i metodi:
![]() |
- in [1], fare clic con il tasto destro del mouse sul codice;
- in [2-3], selezionare [Genera / Implementa metodi] per implementare i metodi mancanti della classe [Dao];
- in [4], selezionare i metodi da implementare — in questo caso, tutti;
Una volta fatto ciò, PyCharm completa la classe [Dao] come segue:
Completiamo la classe [Dao] come segue:
- Le righe 5–19 sono semplici;
- righe 29–36: il metodo che restituisce lo studente il cui ID viene passato. Se lo studente non esiste, viene generata un'eccezione;
- riga 31: la funzione [filter] consente di filtrare una lista:
- il primo parametro è il criterio di filtraggio;
- il secondo parametro è l'elenco da filtrare, in questo caso l'elenco degli studenti;
- riga 31: il criterio di filtraggio per l'elenco è implementato utilizzando una funzione [f(e:Student) -> bool]. Questa viene applicata a ciascun elemento dell'elenco da filtrare. Se l'elemento soddisfa il criterio di filtraggio, viene mantenuto nell'elenco filtrato; altrimenti, viene escluso. Qui, possiamo:
- specificare il nome della funzione f e implementarla altrove. La chiamata alla funzione [filter] diventa quindi [filter(f, self.get_students)];
- fornire la definizione della funzione f. La chiamata alla funzione [filter] diventa quindi [filter(f(e :Student){…}, self.get_students())], dove [e] rappresenta un elemento della lista filtrata, ovvero uno studente. Questo è ciò che è stato fatto qui. La definizione della funzione f in questo caso sarebbe [f(e :Student){return e.id == student_id)]: uno studente viene selezionato solo se il suo numero ID [id] corrisponde a quello cercato. Una funzione di questo tipo può essere sostituita da una cosiddetta funzione lambda: [lambda e: e.id == student_id]:
- e: rappresenta il parametro della funzione f, in questo caso uno studente. È possibile utilizzare qualsiasi nome si desideri;
- e.id==student_id è il criterio di filtraggio: uno studente [e] viene selezionato solo se il suo ID [id] corrisponde a quello che si sta cercando;
- riga 31: la funzione [filter] restituisce l'elenco filtrato come un tipo che non è di tipo [list], ma che può essere convertito in tipo [list]. È ciò che facciamo qui con l'espressione [list(filtered_list)];
- righe 33–34: se la lista filtrata è vuota, significa che lo studente cercato non esiste. Viene quindi generata un'eccezione;
- riga 36: se arriviamo a questo punto, significa che non è stata generata alcuna eccezione. Sappiamo quindi di aver recuperato un elenco con 1 elemento (non ci sono due studenti con lo stesso numero [id]). Restituiamo quindi il primo elemento dell'elenco;
- righe 21–27: il metodo [get_notes_for_élève_by_id] deve restituire i voti dello studente il cui [id] gli viene passato;
- Righe 22–23: Iniziamo cercando lo studente con ID [student_id] utilizzando il metodo [get_student_by_id], che abbiamo appena commentato. Potrebbe verificarsi un'eccezione se lo studente che stiamo cercando non esiste. Poiché non c'è un blocco try/catch attorno all'istruzione alla riga 23, l'eccezione verrà propagata al codice chiamante. Questo è il comportamento desiderato;
- Righe 24–25: Una volta recuperato lo studente, recuperiamo tutti i suoi voti. Lo facciamo di nuovo utilizzando un filtro:
- il filtro è [filter(criterion, self_getnotes()]. L'elenco da filtrare è quindi l'elenco di tutti i voti di tutti gli studenti della scuola;
- il criterio di filtraggio è espresso utilizzando una funzione [lambda]: lambda n: n.student.id == student_id. Il parametro n è un elemento dell'elenco da filtrare, ovvero un voto. Il tipo [Note] ha una proprietà [student] che rappresenta lo studente a cui appartiene il voto. Pertanto, [n.student.id], che rappresenta l'ID di quello studente, deve essere uguale all'ID dello studente che stiamo cercando;
Quindi eseguiamo lo script [tests-dao.py].
Otteniamo quindi i seguenti risultati:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
{"id": 1, "nom": "classe1"}
{"id": 2, "nom": "classe2"}
{"id": 1, "nom": "matière1", "coefficient": 1}
{"id": 2, "nom": "matière2", "coefficient": 2}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
{"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}
{"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}
{"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}
{"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 2, "valeur": 12, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 3, "valeur": 14, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 4, "valeur": 16, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 6, "valeur": 8, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 7, "valeur": 10, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
élève n° 11 = {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
note de l'élève n° 11 = {"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
note de l'élève n° 11 = {"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
Process finished with exit code 0
Si noti che quando si visualizza un voto (il processo è simile per altri oggetti), abbiamo anche:
- lo studente associato al voto;
- la materia a cui fa riferimento il voto;
Questo risultato è prodotto dalla funzione [BaseEntity.asdict] (vedere la sezione "link").
14.2.5. Il livello [business]
![]() | ![]() |
- [InterfaceMétier] è l'interfaccia del livello [business];
- [Business] è la classe di implementazione del livello [business];
- [TestBusiness] è una classe [UnitTest] per testare la classe [Business];
14.2.5.1. Interfaccia [BusinessInterface]
Il livello [business] implementerà la seguente interfaccia [BusinessInterface] (BusinessInterface.py):
- [get_stats_for_student] restituisce i voti dello studente idStudent insieme alle informazioni relative: media ponderata, voto più basso, voto più alto. Queste informazioni sono incapsulate in un oggetto di tipo [StudentStats];
14.2.5.2. L'entità [StatsForStudent]
Il tipo [StatsForStudent] (StatsForStudent.py), che incapsula le statistiche di uno studente (voti, min, max, media ponderata), è il seguente:
Note:
- riga 8: la classe [StatsForStudent] deriva dalla classe [BaseEntity];
- righe 13–22: le proprietà della classe;
- un identificatore [id] da [BaseEntity];
- lo studente [student] le cui statistiche sono incapsulate;
- i suoi voti [grades];
- la media ponderata [weighted_average];
- il suo voto più basso [min];
- il suo voto massimo [max];
- Non definiamo getter/setter per questi attributi. Supponiamo che il livello [business] crei oggetti di questo tipo e che non crei oggetti non validi;
- righe 23–33: la funzione [__str__] restituisce una stringa contenente le proprietà dell'oggetto;
14.2.5.3. L'implementazione [Business]
L'implementazione [Business] (Metier.py) dell'interfaccia [BusinessInterface] sarà la seguente:
Note
- riga 7: la classe [Métier] deriva dalla classe [InterfaceMétier]. È consuetudine dire che implementa l'interfaccia [InterfaceMétier];
- righe 9–12: il costruttore accetta un unico parametro, un riferimento al livello [dao]. Alla riga 10, si noti che abbiamo assegnato il tipo [InterfaceDao] al parametro [dao]. Non ci aspettiamo un'implementazione specifica, ma semplicemente un'implementazione che rispetti l'interfaccia [DaoInterface]. In questo caso non ha importanza, poiché Python non terrà conto di questo tipo, ma è buona pratica lavorare con le interfacce piuttosto che con implementazioni specifiche. Il codice è quindi più facile da modificare;
- righe 19–60: implementazione del metodo [get_stats_for_élève];
- riga 19: il metodo riceve un unico parametro, l'[idElève] dello studente per il quale vogliamo le statistiche;
- riga 24: richiediamo i voti dello studente dal livello [dao]. Questa richiesta genera un'eccezione se lo studente non esiste. Questa eccezione non viene gestita (nessun try/catch) e viene quindi propagata al codice chiamante;
- riga 25: si arriva a questo punto se non si è verificata alcuna eccezione. [student_grades] è quindi un dizionario con due chiavi [student, grade]:
- riga 25: recuperiamo le informazioni relative allo studente (nome, classe, ecc.);
- riga 26: recuperiamo i suoi voti;
- righe 28–31: verifichiamo se lo studente ha dei voti. In caso contrario, non ci sono statistiche da calcolare;
- riga 31: restituiamo un oggetto [StatsForStudent] costruito da un dizionario utilizzando il metodo [BaseEntity.fromdict];
- righe 33–54: usiamo i voti dello studente per calcolare le statistiche richieste. I commenti nel codice dovrebbero essere sufficienti per la comprensione;
- righe 56–60: restituiamo un oggetto [StatsForStudent] costruito da un dizionario utilizzando il metodo [BaseEntity.fromdict];
14.2.5.4. Test del livello [business]
Uno script [UnitTest] per il livello [business] potrebbe apparire così (TestMétier.py):
Note
- righe 6–9: la funzione [setUp] viene utilizzata qui per configurare il percorso Python del test;
- riga 16: istanziamo il livello [dao];
- riga 17: istanziamo il livello [business] e utilizziamo il suo metodo [get_stats_for_student] per calcolare le statistiche relative allo studente n. 11;
- riga 19: viene visualizzato il risultato [StatsForStudent]. Poiché [StatsForStudent] deriva da [BaseEntity], qui viene visualizzata la stringa JSON di [StatsForStudent];
- riga 21: controlliamo il voto minimo dello studente;
- riga 22: controlliamo il suo voto massimo;
- riga 23: verifichiamo che la media ponderata sia 7,333, con una precisione di 10⁻³. In generale, non è possibile confrontare i numeri reali in modo esatto perché, internamente, sono solitamente rappresentati solo come approssimazioni;
I risultati del test sono i seguenti:
Testing started at 18:17 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests
Ran 1 test in 0.015s
OK
stats=Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33
Process finished with exit code 0
14.2.6. Il livello [ui]

- in [1], l'interfaccia del livello [ui];
- in [2], l'implementazione di questa interfaccia;
- in [3], lo script principale dell'applicazione;
14.2.6.1. Interfaccia [InterfaceUi]
L'interfaccia del livello [UI] sarà la seguente:
Note
- righe 9-10: il livello [UI] avrà un solo metodo, [run];
14.2.6.2. L'implementazione della [Console]
Il livello [console] è implementato dal seguente script [Console.py]:
- righe 3-5: importazione di tutte le interfacce;
- riga 11: la classe [Console] implementa l'interfaccia [InterfaceUi];
- righe 12-17: il costruttore della classe [Console] riceve come parametro un riferimento al livello [business]. Si noti che abbiamo assegnato a questo parametro il tipo [BusinessInterface] per sottolineare che stiamo lavorando con interfacce piuttosto che con implementazioni specifiche;
- riga 24: implementazione del metodo [run] dell'interfaccia;
- riga 27: un ciclo che si interrompe quando viene soddisfatta la condizione alla riga 31;
- riga 29: immissione dei dati digitati sulla tastiera. La funzione [input] riceve un parametro opzionale: il messaggio da visualizzare sullo schermo per richiedere l'immissione. Questo input viene sempre recuperato come stringa. La funzione [strip] rimuove eventuali spazi bianchi iniziali o finali dalla stringa;
- righe 34–39: verifichiamo che l'input, un ID studente, sia valido. Deve essere un numero intero >= 1. Ricordiamo che l'input è stato inserito come stringa;
- riga 36: tentiamo di convertire l'input in un numero intero in base 10. La funzione [int] genera un'eccezione se ciò non è possibile;
- riga 37: raggiungiamo questo punto solo se non si è verificata alcuna eccezione. Verifichiamo che il numero intero recuperato sia effettivamente >=1;
- righe 38–39: gestiamo l'eccezione. Se si è verificata un'eccezione, la variabile [ok] della riga 34 rimane impostata su [False];
- righe 41–43: se l'input era errato, viene visualizzato un messaggio di errore e il ciclo viene riavviato (riga 43);
- righe 45–48: calcoliamo le statistiche relative allo studente il cui ID è stato inserito;
- riga 46: viene utilizzato il metodo [get_stats_for_student] del livello [business]. Questo metodo genera un'eccezione se lo studente non esiste. L'eccezione viene gestita alle righe 47–48. Sappiamo che i livelli [DAO] e [business] generano l'eccezione [MyException];
14.3. Lo script principale [main]
Lo script principale [main] è il seguente (main.py):
- righe 1–4: configurazione del Python Path dell'applicazione;
- righe 6-9: importano le classi e le interfacce necessarie;
- riga 14: istanzia il livello [DAO];
- riga 16: istanzia il livello [business];
- riga 18: istanzia il livello [ui];
- riga 20: avvia l'interfaccia utente;
- righe 13–20: normalmente, da queste righe non vengono generate eccezioni. Qualsiasi eccezione che si propaga dai livelli [DAO] e [business] viene intercettata dal livello [Console]. La gestione delle eccezioni è un'arte difficile quando non si comprendono appieno i livelli utilizzati (cosa che non avviene in questo caso). In caso di dubbio, è possibile aggiungere del codice per intercettare qualsiasi tipo di eccezione che potrebbe essere generata dal codice in esecuzione. Questo è ciò che viene fatto qui, alle righe 21–23. Intercettiamo qualsiasi eccezione derivante da [BaseException], ovvero tutte le eccezioni;
- righe 24–25: la clausola [finally] qui non fa nulla. È presente solo per consentire di commentare le righe 21–23. Infatti, in modalità debug, non è consigliabile intercettare le eccezioni. In questo caso, l'interprete Python le intercetta e poi segnala il numero di riga in cui si è verificata l'eccezione. Si tratta di un'informazione essenziale. Quando le righe 21–23 sono commentate, la presenza delle righe 24–25 garantisce un blocco try/catch sintatticamente corretto. Senza di esse, Python genera un errore;
Ecco un esempio di esecuzione:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/main/main.py
Numéro de l'élève (>=1 et * pour arrêter) : 11
Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33
Numéro de l'élève (>=1 et * pour arrêter) : 1
L'erreur suivante s'est produite : MyException[10, L'élève d'identifiant 1 n'existe pas]
Numéro de l'élève (>=1 et * pour arrêter) : *
Process finished with exit code 0
14.4. Esempio 2
Questo nuovo esempio di architetture a livelli mira a dimostrare i vantaggi della programmazione basata su interfacce. Questo approccio facilita la manutenzione e il collaudo delle applicazioni. Utilizzeremo nuovamente un'architettura a tre livelli:

Ogni livello sarà implementato in due modi diversi. Vogliamo mostrare che l'implementazione di un livello può essere facilmente modificata con un impatto minimo sugli altri.
14.4.1. Il livello [dao]

L'interfaccia [InterfaceDao] è la seguente:
- righe 8–10: il metodo [do_something_in_dao_layer] è l'unico metodo dell'interfaccia;
La classe [DaoImpl1] implementa l'interfaccia [InterfaceDao] come segue:
La classe [DaoImpl2] implementa l'interfaccia [InterfaceDao] come segue:
14.4.2. Il livello [aziendale]

L'interfaccia [BusinessInterface] è la seguente:
- righe 8–10: il metodo [do_something_in_business_layer] è l'unico metodo nell'interfaccia;
La classe [AbstractBaseMétier] implementa l'interfaccia [InterfaceMétier] come segue:
- riga 8: la classe [AbstractBaseMétier] definisce due classi:
- [BusinessInterface]: la classe [AbstractBusinessBase] implementa questa interfaccia alle righe 19–22. Infatti, vediamo che non ha implementato il metodo [do_something_in_business_layer], che ha dichiarato come astratto (riga 20). Spetterà alle classi derivate implementare il metodo;
- [ABC] per accedere alle annotazioni [@abstractmethod];
- l'ordine è importante: se lo invertiamo qui, Python genera un errore di runtime;
Questa è la prima volta che utilizziamo l'ereditarietà multipla (ereditando da più classi). La classe [AbstractBaseMétier] eredita proprietà sia dalla classe [InterfaceMétier] che dalla classe [ABC].
- Righe 9–17: Definiamo la proprietà [dao], che sarà un riferimento al livello [dao];
Un'interfaccia è destinata ad essere implementata. Quando diverse implementazioni condividono proprietà, è utile collocarle in una classe padre per evitare duplicazioni. È il caso della proprietà [dao]. La classe padre è generalmente sempre astratta perché non implementa tutti i metodi dell'interfaccia.
La classe [BusinessImpl1] implementa l'interfaccia [BusinessInterface] come segue:
- riga 4: la classe [BusinessImpl1] deriva dalla classe [AbstractBusinessBase]. Eredita quindi la proprietà [dao] da questa classe;
- righe 6–9: implementazione dell'interfaccia [BusinessInterface] che la classe padre [AbstractBusinessBase] non ha implementato;
- riga 9: viene utilizzato il livello [dao];
La classe [BusinessImpl2] implementa l'interfaccia [BusinessInterface] in modo simile:
14.4.3. Il livello [ui]

L'interfaccia [InterfaceUi] è la seguente:
- righe 8–10: l'unico metodo dell'interfaccia;
La classe [AbstractBaseUi] implementa l'interfaccia [InterfaceUi] come segue:
- La classe [AbstractBaseUi] è una classe astratta (riga 20). È necessario derivarne per implementare l'interfaccia [InterfaceUi];
- righe 9–17: la classe [AbstractBaseUi] ha un riferimento al livello [business];
La classe di implementazione [UiImpl1] è la seguente:
- riga 4: la classe [UiImpl1] deriva dalla classe [AbstractBaseUi] e quindi eredita la sua proprietà [business]. Questa viene utilizzata alla riga 9;
La classe di implementazione [UiImpl2] è simile:
- Riga 4: La classe [UiImpl2] deriva dalla classe [AbstractBaseUi] e quindi eredita la sua proprietà [business]. Questa viene utilizzata alla riga 9;
14.4.4. I file di configurazione

- I file [config1, config2] configurano l'applicazione in due modi diversi;
- Il file [main] è lo script principale dell'applicazione;
Il file [config1] è il seguente:
- righe 2–16: configurazione del Python Path dell'applicazione;
- righe 18–31: istanziazione dei livelli [DAO, business, UI]. Per implementare le loro interfacce, scegliamo ogni volta la prima implementazione disponibile;
- righe 33–35: aggiungiamo i riferimenti ai livelli alla configurazione. Qui, lo script principale necessita solo del livello [ui];
Il file [config2] è simile e implementa ciascuna interfaccia con la seconda implementazione disponibile:
14.4.5. Lo script principale [main]

Lo script principale è il seguente:
Questo script accetta un parametro:
- [config1] per utilizzare la configurazione n. 1;
- [config2] per utilizzare la configurazione n. 2;
Python memorizza i parametri in una lista [sys.argv]:
- sys.argv[0] è il nome dello script, in questo caso [main]. Questo parametro è sempre presente;
- sys.argv[1] è il primo parametro passato allo script, sys.argv[2] è il secondo, …
- riga 8: recuperiamo il numero di parametri;
- righe 9–11: verifichiamo che ci sia effettivamente un argomento e che il suo valore sia [config1] o [config2]. Se così non fosse, viene visualizzato un messaggio di errore (riga 10) e usciamo dal programma (riga 11);
Una volta individuata la configurazione desiderata, dobbiamo eseguirla. Ad esempio, se è stata scelta la configurazione 1, dobbiamo eseguire il codice:
Il problema in questo caso è che la configurazione da utilizzare è memorizzata in una variabile, ovvero [sys.argv[1]. Per importare un modulo il cui nome è memorizzato in una variabile, dobbiamo utilizzare il pacchetto [importlib] (riga 2).
- Riga 14: importiamo il modulo il cui nome si trova in [sys.argv[1]
- riga 15: una volta fatto ciò, eseguiamo la funzione [configure] di questo modulo. Recuperiamo un dizionario [config] che rappresenta la configurazione dell’applicazione;
- riga 18: sappiamo che un riferimento al livello [ui] si trova in config['ui']. Lo usiamo per chiamare il metodo [do_something_in_ui_layer]. Sappiamo che questo metodo chiamerà un metodo nel livello [business], che a sua volta chiamerà un metodo nel livello [dao];
Ad esempio, la funzione [do_something_in_ui_layer] è la seguente:
- La riga 6 sopra utilizza la proprietà [business] della classe [UiImpl1], riga 1. Tuttavia, nella configurazione [config1] è stato scritto quanto segue:
# métier
métier = MétierImpl1()
métier.dao = dao
# ui
ui = UiImpl1()
ui.métier = métier
- Riga 6: La proprietà [business] di [UIImpl1] è un riferimento alla classe [BusinessImpl1] (riga 2). Pertanto, verrà eseguito il metodo [do_something_in_ui_layer] della classe [BusinessImpl1];
Nella classe [MétierUiImpl1] è scritto:
- Riga 6: il metodo chiamato dal livello [ui] a sua volta chiama un metodo della proprietà [dao] della classe [BusinessImpl1];
Tuttavia, nella configurazione [config1] era stato scritto quanto segue:
# dao
dao = DaoImpl1()
# métier
métier = MétierImpl1()
métier.dao = dao
- riga 5: la proprietà [BusinessImpl1.dao] è di tipo [DaoImpl1] (riga 2);
Ciò che vogliamo mostrare qui è che lo script [main] non deve occuparsi dei livelli [business] e [DAO]. Deve occuparsi solo del livello [UI], poiché le connessioni tra questo livello e gli altri sono state stabilite tramite la configurazione.

Per passare il parametro [config1] o [config2] allo script [main], procedere come segue:

- in [1-2], creare quella che viene chiamata una configurazione di runtime;
- in [3], assegnare un nome a questa configurazione in modo da poterla ritrovare in seguito;
- in [4], selezionare lo script da eseguire. Se si è seguita la procedura descritta in [1-2], lo script corretto è già stato selezionato;
- in [5], inserisci qui i parametri da passare allo script. Qui, passiamo la stringa [config1] per indicare allo script di utilizzare la configurazione n. 1;
- In [6], confermi la configurazione di esecuzione;

- In [1-2], visualizza i contesti di esecuzione esistenti;
- in [3], seleziona il contesto di esecuzione esistente e duplicalo [4];

- in [5], il nome assegnato alla nuova configurazione. Questa è la configurazione che esegue lo script [main] [6] passandole il parametro [config2] [7];
Le configurazioni di esecuzione sono disponibili nell'angolo in alto a destra della finestra di PyCharm:

Basta selezionare [2] o [3] e poi cliccare su [4] per eseguire lo script [main] con il parametro [config1] o [config2].
Con [config1], l'esecuzione di [main] produce i seguenti risultati:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config1
34
Process finished with exit code 0
Con [config2], l'esecuzione di [main] produce i seguenti risultati:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config2
-10
Process finished with exit code 0
Si invita il lettore a verificare questi risultati.













