Skip to content

14. Architettura a livelli e programmazione tramite interfacce

14.1. Introduzione

Ci proponiamo di scrivere un’applicazione che consenta di visualizzare i voti degli studenti di una scuola media. Questa applicazione può avere un’architettura a più livelli:

Image

  • il livello [ui] (interfaccia utente) è il livello a contatto con l’utente dell’applicazione;
  • Il livello [métier] 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 dal SGBD tramite il livello [dao];
  • il livello [dao] (Data Access Objects) gestisce l'accesso ai dati del SGBD (Sistema di gestione dei database).

Si tratta dell'architettura utilizzata nel |corso su Python 2|. È inoltre possibile introdurre una variante:

Image

Le differenze rispetto alla precedente struttura a livelli sono le seguenti:

  • uno script principale denominato [main], come indicato sopra, organizza l’istanziazione dei livelli;
  • gli strati [ui, métier, dao] non comunicano più necessariamente tra loro. Se devono farlo, lo script [main] fornisce loro i riferimenti degli strati di cui hanno bisogno;

Il codice è qui organizzato in centri di competenza con un «direttore d’orchestra»:

  • il «direttore d’orchestra» è lo script principale [main];
  • i livelli [ui], [dao] e [métier] sono i centri di competenza;

Si potrebbe definire questa organizzazione come un’organizzazione orchestrale.

14.2. Esempio 1

Illustreremo l’architettura a livelli con una semplice applicazione da console:

  • non ci sarà alcun database;
  • il livello [dao] gestirà le entità Elève, Classe, Matière, Note, consentendo di gestire i voti degli studenti;
  • il livello [métier] consentirà di calcolare indicatori relativi ai voti di uno studente specifico;
  • il livello [ui] sarà un'applicazione da console che visualizzerà i risultati degli studenti;

Il progetto PyCharm dell'applicazione è il seguente:

Nota: le cartelle in blu fanno parte del progetto [Sources Root] del progetto PyCharm.

14.2.1. Le entità dell'applicazione

Chiameremo «entità» quelle classi il cui unico ruolo è incapsulare i dati. A tal fine si potrebbero utilizzare dei dizionari. L’utilità della classe è quella di consentire di verificare la validità dei dati memorizzati nell’oggetto e di fornire un metodo che restituisca l’identità dell’oggetto sotto forma di stringa di caratteri.

14.2.1.1. L'entità [Classe]

L'entità [Classe] (Classe.py) rappresenta una classe della scuola media:


# importazioni
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # attributi esclusi dallo stato della classe
    excluded_keys = []

    # proprietà della classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificatore della classe
        # nome: nome della classe
        return BaseEntity.get_allowed_keys() + ["nom"]

    # getter
    @property
    def nom(self: object) -> str:
        return self.__nom

    # setter
    @nom.setter
    def nom(self: object, nom: str):
        # il nome deve essere una stringa non vuota
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non vide")

Note

  • riga 7: l'entità [Classe] deriva dall'entità [BaseEntity] esaminata nel paragrafo |La classe BaseEntity|;
  • righe 11-16: una classe è definita da un n. id e da un nom (riga 16). La proprietà [id] è fornita dalla classe [BaseEntity] e il nome dalla classe [Classe];
  • righe 18-30: getter/setter dell’attributo [nom];

14.2.1.2. L'entità [Matière]

La classe [Matière] (matière.py) è la seguente:


# importazioni
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # attributi esclusi dallo stato della classe
    excluded_keys = []

    # proprietà della classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificativo della materia
        # nome: nome della materia
        # coefficiente: coefficiente della materia
        return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]

    # getter
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def coefficient(self: object) -> float:
        return self.__coefficient

    # setter
    @nom.setter
    def nom(self: object, nom: str):
        # il nome deve essere una stringa non vuota
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non vide")

    @coefficient.setter
    def coefficient(self, coefficient: float):
        # il coefficiente deve essere un numero reale >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # errore?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

Note

  • riga 7: la classe [Classe] deriva dalla classe [BaseEntity];
  • righe 11-17: una materia è definita dal suo n. [id], dal suo nome [nom] e dal suo coefficiente [coefficient];
  • righe 19-50: getter/setter degli attributi della classe;

14.2.1.3. L'entità [Elève]

La classe [Elève] (élève.py) è la seguente:


# importazioni
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # attributi esclusi dallo stato della classe
    excluded_keys = []

    # proprietà della classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificativo dello studente
        # cognome: cognome dello studente
        # nome: nome dello studente
        # classe: classe dello studente
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    # getters
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def prénom(self: object) -> str:
        return self.__prénom

    @property
    def classe(self: object) -> Classe:
        return self.__classe

    # setter
    @nom.setter
    def nom(self: object, nom: str) -> str:
        # il cognome deve essere una stringa non vuota
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @prénom.setter
    def prénom(self: object, prénom: str) -> str:
        # il nome deve essere una stringa non vuota
        if Utils.is_string_ok(prénom):
            self.__prénom = prénom
        else:
            raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @classe.setter
    def classe(self: object, value):
        try:
            # si richiede un tipo Classe
            if isinstance(value, Classe):
                self.__classe = value
            # oppure un tipo "dict"
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # oppure un tipo json
            elif isinstance(value,str):
                self.__classe = Classe().fromjson(value)
        except BaseException as erreur:
            raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou dict ou json. Erreur : {erreur}")

Note

  • riga 9: la classe [Elève] deriva dalla classe [BaseEntity];
  • righe 13-20: uno studente è identificato dal proprio numero [id], dal proprio cognome [nom], dal proprio nome [prénom] e dalla propria classe [classe]. Quest’ultimo parametro è un riferimento a un oggetto [Classe];
  • righe 22-65: getter/setter degli attributi della classe;

14.2.1.4. L'entità [Note]

La classe [Note] (note.py) è la seguente:


# importazioni
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    # attributi esclusi dallo stato della classe
    excluded_keys = []

    # proprietà della classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificativo della nota
        # valore: il voto stesso
        # studente: studente (di tipo Studente) a cui si riferisce il voto
        # materia: materia (di tipo Materia) a cui si riferisce il voto
        # l’oggetto «Voto» rappresenta quindi il voto di uno studente in una materia
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    # getter
    @property
    def valeur(self: object) -> float:
        return self.__valeur

    @property
    def élève(self: object) -> Elève:
        return self.__élève

    @property
    def matière(self: object) -> Matière:
        return self.__matière

    # getter
    @valeur.setter
    def valeur(self: object, valeur: float):
        # il voto deve essere un numero reale compreso tra 0 e 20
        if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
            self.__valeur = valeur
        else:
            raise MyException(31,
                              f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")

    @élève.setter
    def élève(self: object, value):
        try:
            # si richiede un tipo «Studente»
            if isinstance(value, Elève):
                self.__élève = value
            # oppure un tipo dict
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # oppure un tipo json
            elif isinstance(value, str):
                self.__élève = Elève().fromjson(value)
        except BaseException as erreur:
            raise MyException(32,
                              f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict ou json. Erreur : {erreur}")

    @matière.setter
    def matière(self: object, value):
        try:
            # si richiede un tipo "Materia"
            if isinstance(value, Matière):
                self.__matière = value
            # o un tipo dict
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # o un tipo json
            elif isinstance(value, str):
                self.__matière = Matière().fromjson(value)
        except BaseException as erreur:
            raise MyException(33,
                              f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict ou json. Erreur : {erreur}")

Note

  • riga 8: la classe [Note] deriva dalla classe [BaseEntity];
  • righe 12-20: un oggetto [Note] è caratterizzato dal proprio n. [id], dal valore del voto [valeur], da un riferimento [élève] allo studente che ha ottenuto tale voto e da un riferimento alla materia [matière] oggetto del voto;
  • righe 22-75: getter/setter degli attributi della classe;

14.2.2. Configurazione dell’applicazione

Il file [config.py] configura l’ambiente dello script principale [main] (1) e quello dei test (2). Tutti questi script contengono un’istruzione [import config] all’inizio del codice. Si ricorda che la cartella contenente lo script oggetto del comando [python script] fa automaticamente parte di Python Path.Si; pertanto, poiché [config] si trova nella stessa cartella degli script con l’istruzione [import config], verrà individuato. I file [1] e [2] sono qui identici. Potrebbe non essere così.

Il file [config.sys] è il seguente:


def configure():
    import os

    # percorso assoluto della cartella contenente questo script
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # root_dir
    root_dir="C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
    # dipendenze assolute
    absolute_dependencies=[
        # cartelle locali contenenti classi e interfacce
        f"{root_dir}/02/entities",
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
    ]

    # aggiornamento del syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # si rende la configurazione
    return {}
  • righe 11-14: le cartelle che devono far parte del Python Path (sys.path);
  • la cartella [f"{root_dir}/02/entities"] consente l’accesso alle classi [BaseEntity] e [MyException];
  • la cartella [f"{script_dir}/../entities"] consente di accedere alle classi [Elève], [Classe], [Matière], [Note];
  • la cartella [f"{script_dir}/../interfaces",] consente di accedere alle interfacce dell’applicazione;
  • la cartella [f"{script_dir}/../services"] consente di accedere 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. La scelta di uno di essi avviene nella configurazione di PyCharm:

Image

  • In [4] sono disponibili diversi framework di test:

Image

14.2.3.1. La classe di test [TestBaseEntity]

Lo script di test [TestBaseEntity] sarà il seguente:


import unittest

# si configura l'applicazione
import config

config = config.configure()


class TestBaseEntity (unittest.TestCase):

    def test_note1(self):
        # importazioni
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        # creazione di una nota a partire da una stringa jSON
        note = Note().fromjson(
            '{"id": 8, "valore": 12, "studente": {"id": 42, "cognome": "cognome4", "nome": "nome4", "classe": {"id": 2, "nome": "classe2"}}, "materia": {"id": 2, "nome": "materia2", "coefficiente": 2}}')
        # verifiche
        self.assertIsInstance(note, Note)
        self.assertIsInstance(note.élève, Elève)
        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)


    def test_note2(self):
        # importazioni
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        # calcolo di un voto a partire da un dizionario
        note = Note().fromdict(
            {"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}})
        # verifiche
        self.assertIsInstance(note, Note)
        self.assertIsInstance(note.élève, Elève)
        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)

if __name__ == '__main__':
    unittest.main()

Note

  • riga 1: si importa il modulo [unittest] che fornirà i vari metodi di test;
  • righe 3-6: si configura l'applicazione in modo che vengano individuate le classi necessarie per i test;
  • 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: si importano le classi necessarie;
  • in questa classe di test, si vuole 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. Si vuole verificare che i due metodi precedenti creino oggetti [Note] validi;
  • riga 18: si crea un oggetto [Note] a partire da un oggetto jSON;
  • riga 21: si verifica che l’oggetto creato sia effettivamente di tipo [Note]. Il metodo [assertIsInstance] è un metodo della classe [unittest.TestCase], classe padre della classe [TestBaseEntity];
  • riga 22: si verifica che [note.élève] sia effettivamente di tipo [Elève];
  • riga 23: si verifica che [note.élève.classe] sia effettivamente di tipo [Classe];
  • riga 24: si verifica che [note.matière] sia effettivamente del tipo [Matière];
  • righe 33-42: si procede allo stesso modo con il metodo [BaseEntity.fromdict];

Esistono diversi modi per eseguire i test:

  • in [1-2], si esegue [TestBaseEntity] con il framework [UnitTest];
  • in [3-5], i test falliscono. [UnitTests] indica che non ha trovato alcun test da eseguire;

Il fallimento dei test è dovuto all’organizzazione del codice di [TestBaseEntity]:


import unittest

# si configura l'applicazione
import config

config = config.configure()


class TestBaseEntity(unittest.TestCase):

Ciò che crea problemi al framework [UnitTest] è la presenza di codice eseguibile, righe 3-6, prima della definizione della classe di test, riga 9.

Si riorganizza quindi il codice nel modo seguente:


import unittest


class TestBaseEntity(unittest.TestCase):

    def setUp(self):
        # si configura l'applicazione
        import config

        config.configure()

    def test_note1(self):
        

    def test_note2(self):
        


if __name__ == '__main__':
    unittest.main()
  • righe 6-10: si definisce una funzione [setUp]. Questa funzione ha un ruolo particolare: viene eseguita prima di ogni funzione di test (test_note1, test_note2);

Fatto ciò, l’esecuzione della classe [TestBaseEntity] fornisce i seguenti risultati:

Questa volta entrambi i metodi di test sono stati eseguiti e i test hanno avuto esito positivo.

Vediamo cosa succede quando un test fallisce. Modifichiamo il codice di [test_note1] nel modo seguente:


    def test_note1(self):
        # errore intenzionale - si verifica che 1==2
        self.assertEqual(1,2)
        # importazioni
        from Note import Note

  • riga 2: si verifica che 1==2;

I risultati dell’esecuzione sono quindi i seguenti:

È possibile conoscere 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 hanno avuto esito positivo (è stato eliminato l'errore 1==2);

Infine, un terzo modo per eseguire la classe di test [TestBaseEntity], sempre in un terminale, è il seguente. Si conclude 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 che viene eseguito. Quando lo script è quello avviato dal comando [python script.py], la variabile [__name__] assume il valore [__main__] (2 caratteri sottolineati 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 [TestEntités]

La classe di test [TestEntités] è la seguente:


import unittest


class TestEntités(unittest.TestCase):
    def setUp(self):
        # si configura l'applicazione
        import config

        config.configure()

    def test_code1a(self):
        # importazioni
        from Elève import Elève
        from MyException import MyException
        # codice di errore
        code = None
        try:
            # ID non valido
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 1)

    def test_code41(self):
        # importazioni
        from Elève import Elève
        from MyException import MyException
        # codice di errore
        code = None

        try:
            # nome non valido
            Elève().fromdict({"id": 1, "nom": "", "prénom": "z", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 41)

    def test_code42(self):
        # importazioni
        from Elève import Elève
        from MyException import MyException
        # codice di errore
        code = None
        try:
            # nome non valido
            Elève().fromdict({"id": 1, "nom": "y", "prénom": "", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 42)

    def test_code43(self):
        # importazioni
        from Elève import Elève
        from MyException import MyException
        # codice di errore
        code = None
        try:
            # classe non valida
            Elève().fromdict({"id": 1, "nom": "y", "prénom": "z", "classe": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 43)

    def test_code1b(self):
        # importazioni
        from Classe import Classe
        from MyException import MyException
        # codice di errore
        code = None
        try:
            # ID non valido
            Classe().fromdict({"id": "x", "nom": "y"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 1)

    def test_code11(self):
        # importazioni
        from Classe import Classe
        from MyException import MyException

        # codice di errore
        code = None
        try:
            # nome non valido
            Classe().fromdict({"id": 1, "nom": ""})
        except MyException as ex:
            code = ex.code
        # verifica
        self.assertEqual(code, 11)

    def test_code1c(self):
        # importazioni
        from Matière import Matière
        from MyException import MyException

        # codice di errore
        code = None
        try:
            # ID non valido
            Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 1)

    def test_code21(self):
        # importazioni
        from Matière import Matière
        from MyException import MyException
        # codice di errore
        code = None
        try:
            # nome non valido
            Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 21)

    def test_code22(self):
        # importazioni
        from Matière import Matière
        from MyException import MyException
        # codice di errore
        code = None
        try:
            # coefficiente non valido
            Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 22)

    def test_code1d(self):
        # importazioni
        from Note import Note
        from MyException import MyException
        # codice di errore
        code = None
        try:
            # ID non valido
            Note().fromdict({"id": "x", "valeur": "x", "élève": "y", "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 1)

    def test_code31(self):
        # importazioni
        from Note import Note
        from MyException import MyException

        # codice di errore
        code = None
        try:
            # valore non valido
            Note().fromdict({"id": 1, "valeur": "x", "élève": "y", "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 31)

    def test_code32(self):
        # importazioni
        from Note import Note
        from MyException import MyException

        # codice di errore
        code = None
        try:
            # studente non valido
            Note().fromdict({"id": 1, "valeur": 10, "élève": "y", "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 32)

    def test_code33(self):
        # importazioni
        from Elève import Elève
        from Note import Note
        from Classe import Classe
        from MyException import MyException

        # codice di errore
        code = None
        try:
            # materia non valida
            classe = Classe().fromdict({"id": 1, "nom": "x"})
            élève = Elève().fromdict({"id": 1, "nom": "a", "prénom": "b", "classe": classe})
            Note().fromdict({"id": 1, "valeur": 10, "élève": élève, "matière": "z"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verifica
        self.assertEqual(code, 33)

    def test_exception(self):
        # importazioni
        from Elève import Elève
        # per il superamento del test è necessario avviare il tipo [MyException]
        from MyException import MyException
        with self.assertRaises(MyException):
            # il test
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})


if __name__ == '__main__':
    unittest.main()
  • Lo script di test ha lo scopo di verificare i setter delle classi: verificare che non sia possibile assegnare valori errati agli attributi delle diverse entità;
  • righe 11-24: si verifica che non sia possibile assegnare un identificativo non valido a uno studente. Poiché alla riga 16 si assegna il valore 'x' come identificativo dello studente, ci si aspetta che venga generata un'eccezione. Si dovrebbe quindi passare alle righe 20-22;
  • riga 21: visualizzazione del messaggio di errore;
  • riga 22: si recupera il codice dell’errore (cfr. paragrafo |L’entità MyException|);
  • riga 24: si verifica (assert) che il codice di errore sia 1. Qui si verificano due cose:
    • che si sia effettivamente verificato un errore;
    • che il codice di errore sia 1;
  • questo processo viene ripetuto con le funzioni delle righe 24-213;
  • righe 215-222: si verifica che un'azione generi un'eccezione di un determinato tipo;
  • riga 220: si indica che il test ha esito positivo se genera un'eccezione di tipo [MyException];

Risultati

Si esegue 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

In questo caso, tutti i test hanno avuto esito positivo

14.2.4. Il livello [dao]

Image

Il livello [dao] implementa l'interfaccia [InterfaceDao] [1]. Quest'ultima è 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 stipulato 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]. Conosce solo il modo in cui richiamarlo. È l'interfaccia [2] a indicarglielo. Quest'ultima definisce una serie di metodi/funzioni da utilizzare per interagire con il codice chiamato. Questa interfaccia è anche denominata API (Application Programming Interface);

Il livello [dao] fornirà la seguente interfaccia:

  • [get_classes] restituisce l’elenco delle classi della scuola media;
  • [get_matières] restituisce l'elenco delle materie insegnate nella scuola media;
  • [get_élèves] restituisce l'elenco degli studenti della scuola media;
  • [get_notes] restituisce l'elenco dei voti di tutti gli studenti;
  • [get_notes_for_élève_by_id] restituisce i voti di uno studente specifico;
  • [get_élève_by_id] restituisce un alunno identificato dal suo numero;

Il codice chiamante utilizzerà solo questi metodi. Non deve sapere come sono implementati. I dati possono quindi provenire da diverse fonti (hardcoded, da un database, da file di testo…) senza che ciò abbia alcun impatto sul codice chiamante. Questo approccio è noto come programmazione tramite interfacce.

Python 3 dispone di un concetto simile a quello di interfaccia: la classe astratta. La useremo. Raggrupperemo le interfacce di questo esempio nella cartella [interfaces].

Definiamo una classe astratta [InterfaceDao] (InterfaceDao.py) per il livello [dao]:


# importazioni
from abc import ABC, abstractmethod

# interfaccia Dao
from Elève import Elève


class InterfaceDao(ABC):
    # elenco delle classi
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # elenco degli studenti
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # elenco delle materie
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    # elenco dei voti
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    # elenco dei voti di uno studente
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # cerca uno studente in base al suo ID
    @abstractmethod
    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

Note:

  • riga 2: ABC = Classe base astratta. Dal modulo [abc] vengono importate la classe ABC e 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 così decorato un metodo astratto: il suo codice non è definito. Tuttavia, vi si inserisce del codice: l’istruzione [pass] che non fa nulla;
  • la classe astratta [InterfaceDao] non può essere istanziata. Possono esserlo solo le classi derivate da [InterfaceDao] che hanno implementato tutti i metodi di [InterfaceDao]. Se quindi si creano due classi [Dao1] e [Dao2] derivate dalla classe [InterfaceDao], entrambe implementeranno i metodi astratti di [InterfaceDao]. Si potrebbe quindi dire che esse implementano l’interfaccia [InterfaceDao];
  • i linguaggi che implementano sia le interfacce che le classi astratte attribuiscono all’interfaccia un ruolo diverso da quello della classe astratta. Un’interfaccia non ha attributi e non può essere istanziata. Una classe può implementare un’interfaccia definendone tutti i metodi;

14.2.4.2. Implementazione [Dao]

La classe [Dao] (dao.py) implementa l’interfaccia [InterfaceDao] nel modo seguente:


# importazione di entità e interfacce
from Classe import Classe
from Elève import Elève
from InterfaceDao import InterfaceDao
from Matière import Matière
from MyException import MyException
from Note import Note


# Il livello [dao] implementa l'interfaccia InterfaceDao
class Dao(InterfaceDao):
    # costruttore
    # si creano elenchi fissi
    def __init__(self):
        # si istanziano le classi
        classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
        classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
        self.classes = [classe1, classe2]
        # le materie
        matière1 = Matière().fromdict({"id": 1, "nom": "matière1", "coefficient": 1})
        matière2 = Matière().fromdict({"id": 2, "nom": "matière2", "coefficient": 2})
        self.matières = [matière1, matière2]
        # gli studenti
        élève11 = Elève().fromdict({"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": classe1})
        élève21 = Elève().fromdict({"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": classe1})
        élève32 = Elève().fromdict({"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": classe2})
        élève42 = Elève().fromdict({"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": classe2})
        self.élèves = [élève11, élève21, élève32, élève42]
        # i voti degli studenti nelle diverse materie
        note1 = Note().fromdict({"id": 1, "valeur": 10, "élève": élève11, "matière": matière1})
        note2 = Note().fromdict({"id": 2, "valeur": 12, "élève": élève21, "matière": matière1})
        note3 = Note().fromdict({"id": 3, "valeur": 14, "élève": élève32, "matière": matière1})
        note4 = Note().fromdict({"id": 4, "valeur": 16, "élève": élève42, "matière": matière1})
        note5 = Note().fromdict({"id": 5, "valeur": 6, "élève": élève11, "matière": matière2})
        note6 = Note().fromdict({"id": 6, "valeur": 8, "élève": élève21, "matière": matière2})
        note7 = Note().fromdict({"id": 7, "valeur": 10, "élève": élève32, "matière": matière2})
        note8 = Note().fromdict({"id": 8, "valeur": 12, "élève": élève42, "matière": matière2})
        self.notes = [note1, note2, note3, note4, note5, note6, note7, note8]

    # -----------
    # interfaccia IDao
    # -----------
    

Note:

  • righe 1-7: si importano le entità e l’interfaccia [InterfaceDao];
  • riga 11: la classe [Dao] deriva dalla classe astratta [InterfaceDao]. Si dirà che essa implementa l'interfaccia [InterfaceDao];
  • riga 14: il costruttore non ha parametri. Crea in modo statico quattro liste:
    • 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 [Interface Dao]. In questo caso non li definiamo per vedere il messaggio di errore generato da Python;

Un programma di test potrebbe essere il seguente: [tests-dao.py]:


# si configura l'applicazione
import config

config = config.configure()

# istanza del livello [dao]
from Dao import Dao

daoImpl = Dao()

# elenco delle classi
for classe in daoImpl.get_classes():
    print(classe)

# elenco delle materie
for matière in daoImpl.get_matières():
    print(matière)

# elenco delle classi
for élève in daoImpl.get_élèves():
    print(élève)

# elenco dei voti
for note in daoImpl.get_notes():
    print(note)

Nota: lo script [tests-dao.py] non è un test [unittest] poiché non contiene metodi il cui nome inizi con [test_].

I commenti sono autoesplicativi. Le righe 11-25 utilizzano l'interfaccia del livello [dao]. Non vi sono ipotesi sull'effettiva implementazione del livello. Alla riga 9 si istanzia 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

Si nota che si verifica un errore già al momento dell’istanziazione della classe [Dao] (riga 3 sopra). L’interprete Python 3 ci segnala che non è in grado di istanziare la classe, poiché non abbiamo definito i metodi astratti [get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves].

Anche PyCharm supporta il concetto di classe astratta e ci propone di definirne i metodi:

  • in [1], clicca con il tasto destro sul codice;
  • in [2-3], selezionare [Generate / Implement Methods] per implementare i metodi mancanti della classe [Dao];
  • in [4], selezionare i metodi da implementare, in questo caso tutti;

Fatto ciò, la classe [Dao] viene completata da PyCharm nel modo seguente:


    # -----------
    # interfaccia IDao
    # -----------

    def get_classes(self: object) -> list:
        pass

    def get_élèves(self: object) -> list:
        pass

    def get_matières(self: object) -> list:
        pass

    def get_notes(self: object) -> list:
        pass

    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

Completiamo la classe [Dao] nel modo seguente:


    # -----------
    # interfaccia IDao
    # -----------
    
    # elenco delle classi
    def get_classes(self) -> list:
        return self.classes

    # elenco delle materie
    def get_matières(self) -> list:
        return self.matières

    # elenco degli studenti
    def get_élèves(self) -> list:
        return self.élèves

    # elenco dei voti
    def get_notes(self) -> list:
        return self.notes

    def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
        # ricerca dello studente
        élève = self.get_élève_by_id(élève_id)
        # si recuperano i suoi voti
        notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
        # restituisce il risultato
        return {"élève": élève, "notes": notes}

    def get_élève_by_id(self, élève_id: int) -> Elève:
        # filtrare gli studenti
        élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
        # Trovato?
        if not élèves:
            raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
        # risultato
        return élèves[0]
  • le righe 5-19 non presentano difficoltà;
  • righe 29-36: il metodo che restituisce lo studente il cui numero viene passato. Se lo studente non esiste, viene generata un’eccezione;
  • riga 31: la funzione [filter] consente di filtrare un elenco:
    • il primo parametro è il criterio di filtro;
    • il secondo parametro è l’elenco da filtrare, in questo caso l’elenco degli studenti;
  • riga 31: il criterio di filtraggio dell’elenco viene implementato tramite una funzione [f(e :Elève)->bool]. Questa viene applicata a ciascuno degli elementi dell’elenco da filtrare. Se l’elemento soddisfa il criterio di filtraggio, viene mantenuto nell’elenco filtrato, altrimenti ne viene escluso. A questo punto è possibile:
    • specificare il nome della funzione f e implementarla altrove. La chiamata alla funzione [filter] diventa quindi [filter(f,self.get_élèves()];
    • fornire la definizione della funzione f. La chiamata alla funzione [filter] diventa quindi [filter(f(e :Elève){…},self.get_élèves()], dove [e] rappresenta un elemento dell’elenco filtrato, ovvero uno studente. È ciò che è stato fatto in questo caso. La definizione della funzione f in questo caso sarebbe [f(e :Elève){return e.id==élève_id)]: uno studente viene selezionato solo se il numero [id] non è uguale a quello cercato. Una funzione di questo tipo può essere sostituita da una funzione detta lambda: [lambda e: e.id == élève_id]:
      • e: rappresenta il parametro della funzione f, in questo caso uno studente. Si può utilizzare il nome che si desidera;
      • e.id==élève_id è il criterio di filtraggio: uno studente [e] viene selezionato solo se il suo n. [id] corrisponde a quello cercato;
  • riga 31: la funzione [filter] restituisce l’elenco filtrato in un tipo che non è il tipo [list], ma che può essere convertito nel tipo [list]. È ciò che facciamo qui con l’espressione [list(liste filtrée)];
  • righe 33-34: se l’elenco filtrato è vuoto, significa che lo studente cercato non esiste. In tal caso viene generata un’eccezione;
  • riga 36: se si arriva qui, significa che non si è verificata alcuna eccezione. Si sa quindi di aver recuperato un elenco con un solo elemento (non ci sono due studenti con lo stesso numero [id]). Si restituisce quindi il primo elemento dell’elenco;
  • righe 21-27: il metodo [get_notes_for_élève_by_id] deve restituire i voti dello studente a cui è stato passato il numero [id];
  • righe 22-23: si inizia cercando lo studente con n. [élève_id] utilizzando il metodo [get_élève_by_id] che abbiamo appena commentato. Se lo studente cercato non esiste, potrebbe verificarsi un'eccezione. Poiché non è presente un blocco try/catch attorno all’istruzione della riga 23, l’eccezione verrà segnalata al codice chiamante. Questo è proprio ciò che si desidera;
  • righe 24-25: una volta recuperato lo studente, si recuperano tutti i suoi voti. Lo si fa nuovamente con un filtro:
    • il filtro è [filter(critère, self_getnotes()]. L’elenco da filtrare è quindi l’elenco di tutti i voti di tutti gli studenti della scuola media;
    • il criterio di filtraggio è espresso tramite una funzione [lambda]: lambda n: n.élève.id == élève_id. Il parametro n è un elemento dell’elenco da filtrare, quindi un voto. Il tipo [Note] ha una proprietà [élève] che rappresenta lo studente a cui appartiene il voto. È quindi necessario che [n.élève.id], che rappresenta il numero di matricola di tale studente, sia uguale al numero dello studente ricercato;

Quindi eseguiamo lo script [tests-dao.py].


# si configura l'applicazione
import config

config = config.configure()

# istanziazione del livello [dao]
from Dao import Dao

daoImpl = Dao()

# elenco delle classi
for classe in daoImpl.get_classes():
    print(classe)

# elenco delle materie
for matière in daoImpl.get_matières():
    print(matière)

# elenco delle classi
for élève in daoImpl.get_élèves():
    print(élève)

# elenco dei voti
for note in daoImpl.get_notes():
    print(note)

# un alunno specifico
print(daoImpl.get_élève_by_id(11))

# l'elenco dei suoi voti
dict1 = daoImpl.get_notes_for_élève_by_id(11)
print(f"élève n° 11 = {dict1['élève']}")
for note in dict1["notes"]:
    print(f"note de l'élève n° 11 = {note}")

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 può notare che quando si visualizza un voto (per gli altri oggetti il procedimento è simile), si ottiene anche:

  • lo studente proprietario della nota;
  • la materia a cui fa riferimento la nota;

È la funzione [BaseEntity.asdict] a produrre questo risultato (cfr. paragrafo "link").

14.2.5. Il livello [métier]

  • [InterfaceMétier] è l'interfaccia del livello [métier];
  • [Métier] è la classe di implementazione del livello [métier];
  • [Testmétier] è una classe di test [UnitTest] della classe [Métier];

14.2.5.1. Interfaccia [InterfaceMétier]

Il livello [métier] implementerà la seguente interfaccia [InterfaceMétier] (InterfaceMétier.py):


# importazioni
from abc import ABC, abstractmethod

from StatsForElève import StatsForElève


# interfaccia di business
class InterfaceMétier(ABC):
    # calcolo delle statistiche per uno studente
    @abstractmethod
    def get_stats_for_élève(self, idElève: int) -> StatsForElève:
        pass
  • [get_stats_for_élève] restituisce i voti dello studente n. idElève e le relative informazioni: media ponderata, voto più basso, voto più alto. Queste informazioni sono incapsulate in un oggetto di tipo [StatsForElève];

14.2.5.2. L’entità [StatsForElève]

Il tipo [StatsForElève] (StatsForElève.py) che racchiude le statistiche (voti, minimo, massimo, media ponderata) di uno studente è il seguente:


# importazioni
from BaseEntity import BaseEntity


# statistiche relative a uno studente specifico


class StatsForElève(BaseEntity):
    # attributi esclusi dal riepilogo della classe
    excluded_keys = []

    # proprietà della classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificativo del voto
        # studente: lo studente in questione
        # voti: i suoi voti
        # moyennePondérée: la sua media ponderata in base ai coefficienti delle materie
        # min: il suo voto minimo
        # max: il suo voto massimo

        return BaseEntity.get_allowed_keys() + ["élève", "notes", "moyenne_pondérée", "min", "max"]

    # toString
    def __str__(self) -> str:
        # caso dello studente senza voti
        if len(self.notes) == 0:
            return f"Elève={self.élève}, notes=[]"
        # caso dello studente con voti
        str = ""
        for note in self.notes:
            str += f"{note.valeur} "
        return f"Elève={self.élève}, notes=[{str.strip()}], max={self.max}, min={self.min}, " \
               f"moyenne pondérée={self.moyenne_pondérée:4.2f}"

Note:

  • riga 8: la classe [StatsForElève] deriva dalla classe [BaseEntity];
  • righe 13-22: le proprietà della classe;
    • un identificativo [id] derivante da [BaseEntity];
    • lo studente [élève], di cui si incapsulano le statistiche;
    • i suoi voti [notes];
    • la sua media ponderata [moyenne_pondérée];
    • il suo punteggio minimo [min];
    • il suo valore massimo [max];
  • non vengono definiti getter/setter per questi attributi. Si parte dal presupposto che sia il livello [métier] a creare oggetti di questo tipo e che esso non crei oggetti non validi;
  • righe 23-33: la funzione [__str__] restituisce una stringa di caratteri che riporta le proprietà dell’oggetto;

14.2.5.3. L'implementazione [Métier]

L'implementazione [Métier] (Metier.py) dell'interfaccia [InterfaceMétier] sarà la seguente:


# importazioni
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from StatsForElève import StatsForElève


class Métier(InterfaceMétier):

    # costruttore
    def __init__(self, dao: InterfaceDao):
        # si memorizza il parametro
        self.__dao = dao

    # -----------
    # interfaccia
    # -----------

    # indicatori relativi ai voti di uno studente specifico
    def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
        # Statistiche per lo studente n. idEleve
        # id_élève: numero dello studente

        # si recuperano i suoi voti con il livello [dao]
        notes_élève = self.__dao.get_notes_for_élève_by_id(id_élève)
        élève = notes_élève["élève"]
        notes = notes_élève["notes"]

        # ci si ferma se non ci sono voti
        if len(notes) == 0:
            # si restituisce il risultato
            return StatsForElève().fromdict({"élève": élève, "notes": []})

        # elaborazione dei voti dello studente
        somme_pondérée = 0
        somme_coeff = 0
        max = -1
        min = 21
        for note in notes:
            # valore del voto
            valeur = note.valeur
            # coefficiente della materia
            coeff = note.matière.coefficient
            # somma dei coefficienti
            somme_coeff += coeff
            # somma ponderata
            somme_pondérée += valeur * coeff
            # ricerca del minimo
            if valeur < min:
                min = valeur
            # ricerca del massimo
            if valeur > max:
                max = valeur
        # calcolo degli indicatori mancanti
        moyenne_pondérée = float(somme_pondérée) / somme_coeff

        # il risultato viene restituito sotto forma di un tipo [StatsForElève]
        return StatsForElève(). \
            fromdict({"élève": élève, "notes": notes,
                      "moyenne_pondérée": moyenne_pondérée,
                      "min": min, "max": max})

Note

  • riga 7: la classe [Métier] deriva dalla classe [InterfaceMétier]. Si è preso l’abitudine di dire che essa implementa l’interfaccia [InterfaceMétier];
  • righe 9-12: il costruttore riceve come unico parametro un riferimento al livello [dao]. Alla riga 10, si noti che al parametro [dao] è stato assegnato il tipo [InterfaceDao]. Non ci si aspetta un’implementazione specifica, ma semplicemente un’implementazione che rispetti l’interfaccia [InterfaceDao]. In questo caso non ha importanza, poiché Python non terrà conto di questo tipo, ma è buona norma lavorare con le interfacce piuttosto che con implementazioni specifiche. Il codice risulta così più facilmente modificabile;
  • righe 19-60: implementazione del metodo [get_stats_for_élève];
  • riga 19: il metodo riceve un unico parametro, il numero [idElève] dello studente di cui si desiderano le statistiche;
  • riga 24: si richiedono al livello [dao] i voti dello studente. Questa richiesta genera un’eccezione se lo studente non esiste. L’eccezione non viene gestita (assenza di try/catch) e viene quindi segnalata al codice chiamante;
  • riga 25: si arriva qui se non si è verificata alcuna eccezione. [notes_élève] è quindi un dizionario con due chiavi [élève, note]:
    • riga 25: si recuperano le informazioni sullo studente (il suo nome, la sua classe, …);
    • riga 26: si recuperano i suoi voti;
  • righe 28-31: si verifica se lo studente ha dei voti. Se non ne ha, non ci sono statistiche da calcolare;
  • riga 31: si restituisce un oggetto [StatsForElève] costruito a partire da un dizionario con il metodo [BaseEntity.fromdict];
  • righe 33-54: si utilizzano i voti dello studente per calcolare le statistiche richieste. I commenti nel codice dovrebbero essere sufficienti per comprenderlo;
  • righe 56-60: si restituisce un oggetto [StatsForElève] costruito a partire da un dizionario con il metodo [BaseEntity.fromdict];

14.2.5.4. Test del livello [métier]

Uno script [UnitTest] del livello [métier] potrebbe essere il seguente (TestMétier.py):


# importazioni
import unittest


class Testmétier(unittest.TestCase):
    def setUp(self):
        # si configura l'applicazione
        import config
        config.configure()

    def test_statsForEleve11(self):
        # importazioni
        from Dao import Dao
        from Métier import Métier
        # si verificano gli indicatori dello studente 11
        dao = Dao()
        stats_for_élève = Métier(dao).get_stats_for_élève(11)
        # visualizzazione
        print(f"\nstats={stats_for_élève}")
        # verifiche
        self.assertEqual(stats_for_élève.min, 6)
        self.assertEqual(stats_for_élève.max, 10)
        self.assertAlmostEqual(stats_for_élève.moyenne_pondérée, 7.333, delta=1e-3)


if __name__ == '__main__':
    unittest.main()

Note

  • righe 6-9: la funzione [setUp] viene qui utilizzata per configurare il Python Path del test;
  • riga 16: si istanzia il livello [dao];
  • riga 17: si istanzia il livello [métier] e si utilizza il suo metodo [get_stats_for_élève] per calcolare le statistiche dello studente n. 11;
  • riga 19: si visualizza il risultato [StatsForElève] ottenuto. Poiché [StatsForElève] deriva da [BaseEntity], qui viene visualizzata la stringa jSON di [StatsForElève];
  • riga 21: si verifica il voto minimo dello studente;
  • riga 22: si verifica il suo voto massimo;
  • riga 23: si verifica che la media ponderata sia pari a 7,333 con un'approssimazione di 10⁻³. In generale, non è possibile confrontare i numeri reali in modo esatto poiché, internamente, essi hanno spesso solo una rappresentazione approssimativa;

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. Lo strato [ui]

Image

  • in [1], l'interfaccia del livello [ui];
  • in [2], l'implementazione di tale interfaccia;
  • in [3], lo script principale dell'applicazione;

14.2.6.1. Interfaccia [InterfaceUi]

L'interfaccia del livello [UI] sarà la seguente:


# importazioni
from abc import ABC, abstractmethod


# interfaccia UI
class InterfaceUi(ABC):
    # esecuzione del livello UI
    @abstractmethod
    def run(self: object):
        pass

Note

  • righe 9-10: il livello [UI] avrà un solo metodo, [run];

14.2.6.2. L'implementazione [Console]

Il livello [console] è implementato dal seguente script [Console.py]:


# importazioni dei livelli

from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi

# altre dipendenze
from MyException import MyException


class Console(InterfaceUi):
    # costruttore
    def __init__(self: object, métier: InterfaceMétier):
        # dominio: il livello [métier]

        # si memorizzano gli attributi
        self.métier = métier


        # -----------
        # interfaccia
        # -----------

    def run(self):
        # dialogo utente
        fini = False
        while not fini:
            # domanda / risposta
            réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
            # finito?
            if réponse == "*":
                break
            # L'inserimento è corretto?
            ok = False
            try:
                id_élève = int(réponse, 10)
                ok = id_élève >= 1
            except ValueError as erreur:
                pass
            # dato corretto?
            if not ok:
                print("Saisie incorrecte. Recommencez...")
                continue
            # calcolo delle statistiche per lo studente selezionato
            try:
                print(self.métier.get_stats_for_élève(id_élève))
            except MyException as erreur:
                print(f"L'erreur suivante s'est produite : {erreur}")
  • 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 [métier]. Si noti che a questo parametro è stato assegnato il tipo [InterfaceMétier] per ricordare che si sta lavorando con interfacce piuttosto che con implementazioni specifiche;
  • riga 24: implementazione del metodo [run] dell'interfaccia;
  • riga 27: un ciclo che termina quando viene verificata la condizione della riga 31;
  • riga 29: immissione di un dato digitato dalla tastiera. La funzione [input] riceve un parametro facoltativo: il messaggio da visualizzare sullo schermo per richiedere l’immissione. Quest’ultima viene sempre recuperata come stringa di caratteri. La funzione [strip] rimuove gli «spazi» che precedono o seguono la stringa;
  • righe 34-39: si verifica che l’input, un numero di studente, sia valido. Deve trattarsi di un numero intero >=1. Si ricorda che l’input è stato effettuato come stringa di caratteri;
  • riga 36: si tenta di convertire il dato immesso in un numero intero in base 10. La funzione [int] genera un'eccezione se ciò non è possibile;
  • riga 37: si arriva a questa riga solo se non si è verificata alcuna eccezione. Si verifica che il numero intero ottenuto sia effettivamente >=1;
  • righe 38-39: si gestisce l’eccezione. Se si è verificata un’eccezione, la variabile [ok] della riga 34 è rimasta a [False];
  • righe 41-43: se l’inserimento è errato, viene visualizzato un messaggio di errore e si torna all’inizio (riga 43);
  • righe 45-48: si calcolano le statistiche dello studente di cui è stato inserito il numero;
  • riga 46: si utilizza il metodo [get_stats_for_élève] del livello [métier]. Quest’ultimo genera un’eccezione se lo studente non esiste. L’eccezione viene gestita alle righe 47-48. Si sa che i livelli [dao] e [métier] generano l’eccezione [MyException];

14.3. Lo script principale [main]

Lo script principale [main] è il seguente (main.py):


# si configura l'applicazione
import config

config = config.configure()

# il syspath è configurato - è possibile eseguire le importazioni
from Console import Console
from Dao import Dao
from Métier import Métier

# ----------- livello [console]
try:
    # istanziazione del livello [dao]
    dao = Dao()
    # istanziazione del livello [métier]
    métier = Métier(dao)
    # istanza del livello [ui]
    console = Console(métier)
    # esecuzione del livello [console]
    console.run()
except BaseException as ex:
    # viene visualizzato l'errore
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    pass
  • righe 1-4: si configura il Python Path dell’applicazione;
  • righe 6-9: si importano le classi e le interfacce necessarie;
  • riga 14: istanziamento del livello [dao];
  • riga 16: istanziazione del livello [métier];
  • riga 18: istanziazione del livello [ui];
  • riga 20: si avvia la finestra di dialogo con l'utente;
  • righe 13-20: normalmente da queste righe non viene generata alcuna eccezione. Quelle che provengono dai livelli [dao] e [métier] vengono intercettate dal livello [Console]. La gestione delle eccezioni è un'arte complessa quando non si conoscono perfettamente i livelli utilizzati (in questo caso non è così). In caso di dubbi, è possibile aggiungere del codice per intercettare qualsiasi tipo di eccezione che possa essere generata dal codice in esecuzione. È ciò che viene fatto qui, righe 21-23. Si intercettano tutte le eccezioni derivanti da [BaseException], ovvero tutte le eccezioni;
  • righe 24-25: la clausola [finally] qui non ha alcuna funzione. È presente solo per poter commentare le righe 21-23. Infatti, in modalità debug non è opportuno interrompere le eccezioni. In questo caso, è l’interprete Python a intercettarle e a fornire il numero della riga in cui si è verificata l’eccezione. Un’informazione indispensabile. Quando le righe 21-23 vengono commentate, la presenza delle righe 24-25 consente di avere un try/catch sintatticamente corretto. In loro assenza, Python segnala 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 tramite interfacce. Ciò facilita la manutenzione e il collaudo delle applicazioni. Utilizzeremo nuovamente un’architettura a tre livelli:

Image

Ogni livello verrà implementato in due modi diversi. Vogliamo dimostrare che è possibile modificare facilmente l’implementazione di un livello con un impatto minimo sugli altri.

14.4.1. Il livello [dao]

Image

L’interfaccia [InterfaceDao] è la seguente:


# importazioni
from abc import ABC, abstractmethod


# interfaccia DAO
class InterfaceDao(ABC):
    # un unico metodo
    @abstractmethod
    def do_something_in_dao_layer(self, x: int, y: int) -> int:
        pass
  • righe 8-10: il metodo [do_something_in_dao_layer] è l’unico metodo dell’interfaccia;

La classe [DaoImpl1] implementa l’interfaccia [InterfaceDao] nel modo seguente:


from InterfaceDao import InterfaceDao


class DaoImpl1(InterfaceDao):
    # implementazione InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x + y

La classe [DaoImpl2] implementa l'interfaccia [InterfaceDao] nel modo seguente:


from InterfaceDao import InterfaceDao


class DaoImpl2(InterfaceDao):
    # implementazione InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x - y

14.4.2. Il livello [métier]

Image

L’interfaccia [InterfaceMétier] è la seguente:


# importazioni
from abc import ABC, abstractmethod


# interfaccia di business
class InterfaceMétier(ABC):
    # un unico metodo
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • righe 8-10: il metodo [do_something_in_métier_layer] è l'unico metodo dell'interfaccia;

La classe [AbstractBaseMétier] implementa l’interfaccia [InterfaceMétier] nel modo seguente:


# importazioni
from abc import ABC, abstractmethod

from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier


class AbstractBaseMétier(InterfaceMétier, ABC):
    # proprietà
    # __dao è un riferimento sul livello [dao]
    @property
    def dao(self) -> InterfaceDao:
        return self.__dao

    @dao.setter
    def dao(self, dao: InterfaceDao):
        self.__dao = dao

    # implementazione dell'interfaccia [InterfaceMétier]
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • riga 8: la classe [AbstractBaseMétier] deriva da due classi:
    • [InterfaceMétier]: la classe [AbstractBaseMétier] implementa questa interfaccia alle righe 19-22. In realtà si nota che non ha implementato il metodo [do_something_in_métier_layer], che ha dichiarato astratto (riga 20). Spetterà alle classi derivate implementare il metodo;
    • [ABC] per poter accedere alle annotazioni [@abstractmethod];
    • l’ordine è significativo: se qui lo si inverte, Python genera un errore in fase di esecuzione;

È la prima volta che si utilizza l’ereditarietà multipla (ereditare da più classi). La classe [AbstractBaseMétier] eredita, contemporaneamente, le proprietà delle classi [InterfaceMétier] e [ABC].

  • righe 9-17: si definisce la proprietà [dao] che sarà un riferimento al livello [dao];

Un'interfaccia è destinata ad essere implementata. Quando diverse implementazioni condividono delle proprietà, è opportuno inserirle in una classe padre per evitare di duplicarle. È il caso, in questo contesto, della proprietà [dao]. La classe padre è in genere sempre astratta, poiché non è in grado di implementare tutti i metodi dell'interfaccia.

La classe [MétierImpl1] implementa l’interfaccia [InterfaceMétier] nel modo seguente:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl1(AbstractBaseMétier):
    # implementazione dell'interfaccia [InterfaceMétier]
    def do_something_in_métier_layer(self:AbstractBaseMétier, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.dao.do_something_in_dao_layer(x, y)
  • riga 4: la classe [MétierImpl1] deriva dalla classe [AbstractbaseMétier]. Eredita quindi la proprietà [dao] da questa classe;
  • righe 6-9: implementazione dell'interfaccia [InterfaceMétier] che non è stata implementata dalla classe padre [AbstractbaseMétier];
  • riga 9: si utilizza il livello [dao];

La classe [MétierImpl2] implementa l'interfaccia [InterfaceMétier] in modo analogo:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl2(AbstractBaseMétier):
    # implementazione dell'interfaccia [InterfaceMétier]
    def do_something_in_métier_layer(self:AbstractBaseMétier, x: int, y: int) -> int:
        x -= 1
        y -= 1
        return self.dao.do_something_in_dao_layer(x, y)

14.4.3. Il livello [ui]

Image

L’interfaccia [InterfaceUi] è la seguente:


# importazioni
from abc import ABC, abstractmethod


# interfaccia Ui
class InterfaceUi(ABC):
    # un unico metodo
    @abstractmethod
    def do_something_in_ui_layer(self, x: int, y: int) -> int:
        pass
  • righe 8-10: l'unico metodo dell'interfaccia;

La classe [AbstractBaseUi] implementa l'interfaccia [InterfaceUi] nel modo seguente:


# importazioni
from abc import ABC, abstractmethod

from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi


class AbstractBaseUi(InterfaceUi, ABC):
    # proprietà
    # il business è un riferimento sul livello [métier]
    @property
    def métier(self) -> InterfaceMétier:
        return self.__métier

    @métier.setter
    def métier(self, métier: InterfaceMétier):
        self.__métier = métier

    # implementazione dell'interfaccia [InterfaceUI]
    @abstractmethod
    def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
        pass
  • la classe [AbstractBaseUi] è una classe astratta (riga 20). Dovrà essere derivata per implementare l’interfaccia [InterfaceUi];
  • righe 9-17: la classe [AbstractBaseUi] possiede un riferimento al livello [métier];

La classe di implementazione [UiImpl1] è la seguente:


from AbstractBaseUi import AbstractBaseUi


class UiImpl1(AbstractBaseUi):
    # implementazione dell'interfaccia [InterfaceUi]
    def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.métier.do_something_in_métier_layer(x, y)
  • riga 4: la classe [UiImpl1] deriva dalla classe [AbstractBaseUi] e ne eredita quindi la proprietà [métier]. Quest’ultima viene utilizzata alla riga 9;

La classe di implementazione [UiImpl2] è analoga:


from AbstractBaseUi import AbstractBaseUi


class UiImpl2(AbstractBaseUi):
    # implementazione dell'interfaccia [InterfaceUi]
    def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
        x -= 1
        y -= 1
        return self.métier.do_something_in_métier_layer(x, y)
  • riga 4: la classe [UiImpl2] deriva dalla classe [AbstractBaseUi] e ne eredita quindi la proprietà [métier]. Questa viene utilizzata alla riga 9;

14.4.4. I file di configurazione

Image

  • i file [config1, config2] configurano l’applicazione in due modi diversi;
  • il file [main] è lo script principale dell’applicazione;

Il file [config1] è il seguente:


def configure():
    # fase 1 ------
    # percorso assoluto della cartella di questo script
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # dipendenze
    absolute_dependencies = [
        # cartelle locali del Python Path
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # si configura il syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # fase 2 ------
    # configurazione dei livelli dell'applicazione
    from DaoImpl1 import DaoImpl1
    from MétierImpl1 import MétierImpl1
    from UiImpl1 import UiImpl1
    # istanziazione dei livelli
    # DAO
    dao = DaoImpl1()
    # logica di business
    métier = MétierImpl1()
    métier.dao = dao
    # interfaccia utente
    ui = UiImpl1()
    ui.métier = métier

    # si inseriscono le istanze dei livelli nella configurazione
    # qui è necessario solo il livello ui
    config = {"ui": ui}

    # si genera la configurazione
    return config
  • righe 2-16: configurazione del Python Path dell’applicazione;
  • righe 18-31: istanziamento dei livelli [dao, métier, ui]. Per implementarne le interfacce, si sceglie ogni volta la prima implementazione creata;
  • righe 33-35: si inseriscono i riferimenti ai livelli nella configurazione. In questo caso, lo script principale necessita solo del livello [ui];

Il file [config2] è analogo e implementa ciascuna interfaccia con la seconda implementazione disponibile:


def configure():
    # fase 1 ---
    # percorso assoluto della cartella di questo script
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # dipendenze
    absolute_dependencies = [
        # cartelle locali del Python Path
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # si configura il syspath
    from myutils import set_syspath

    set_syspath(absolute_dependencies)

    # fase 2 ------
    # configurazione dei livelli dell'applicazione
    from DaoImpl2 import DaoImpl2
    from MétierImpl2 import MétierImpl2
    from UiImpl2 import UiImpl2
    # istanziazione dei livelli
    # DAO
    dao = DaoImpl2()
    # logica di business
    métier = MétierImpl2()
    métier.dao = dao
    # interfaccia utente
    ui = UiImpl2()
    ui.métier = métier

    # si inseriscono le istanze dei livelli nella configurazione
    # qui è necessario solo il livello ui
    config = {"ui": ui}

    # si genera la configurazione
    return config

14.4.5. Lo script principale [main]

Image

Lo script principale è il seguente:


# importazioni
import importlib
import sys

# main ---------

# sono necessari due argomenti
nb_args = len(sys.argv)
if nb_args != 2 or (sys.argv[1] != "config1" and sys.argv[1] != "config2"):
    print(f"Syntaxe : {sys.argv[0]} config1 ou config2")
    sys.exit()

# configurazione dell'applicazione
module = importlib.import_module(sys.argv[1])
config = module.configure()

# esecuzione del livello [ui]
print(config["ui"].do_something_in_ui_layer(10, 20))

Questo script riceve un parametro:

  • [config1] per utilizzare la configurazione n. 1;
  • [config2] per utilizzare la configurazione n. 2;

Python salva i parametri in una lista denominata [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: si recupera il numero di parametri;

  • righe 9-11: si verifica che sia presente un parametro e che il suo valore sia [config1] oppure [config2]. In caso contrario, viene visualizzato un messaggio di errore (riga 10) e si esce dal programma (riga 11);

Una volta individuata la configurazione desiderata, occorre eseguirla. Ad esempio, se è stata scelta la configurazione 1, occorre eseguire il codice:

import config1
config1.configure()

Il problema in questo caso è che la configurazione da utilizzare si trova in una variabile, la variabile [sys.argv[1]. Per importare un modulo il cui nome è contenuto in una variabile, dobbiamo utilizzare il pacchetto [importlib] (riga 2).

  • riga 14: si importa il modulo il cui nome è contenuto in [sys.argv[1];
  • riga 15: fatto ciò, si esegue la funzione [configure] di questo modulo. Si recupera un dizionario [config] che rappresenta la configurazione dell’applicazione;
  • riga 18: sappiamo che in config[‘ui’] è presente un riferimento al livello [ui]. Lo utilizziamo per chiamare il metodo [do_something_in_ui_layer]. Sappiamo che questo metodo richiamerà un metodo del livello [métier], il quale a sua volta richiamerà un metodo del livello [dao];

Ad esempio, la funzione [do_something_in_ui_layer] è la seguente:


class UiImpl1(AbstractBaseUi):
    # implementazione dell'interfaccia [InterfaceUi]
    def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.métier.do_something_in_métier_layer(x, y)
  • la riga 6 sopra riportata utilizza la proprietà [métier] della classe [UiImpl1], riga 1. Tuttavia, nella configurazione [config1] è stato scritto:

# funzionalità
    métier = MétierImpl1()
    métier.dao = dao
    # interfaccia utente
    ui = UiImpl1()
    ui.métier = métier
  • riga 6: la proprietà [métier] di [UIImpl1] è un riferimento alla classe [MétierImpl1] (riga 2). Pertanto, verrà eseguito il metodo [do_something_in_ui_layer] della classe [MétierImpl1];

Nella classe [MétierUiImpl1] è scritto:


class MétierImpl1(AbstractBaseMétier):
    # implementazione dell'interfaccia [InterfaceMétier]
    def do_something_in_métier_layer(self: AbstractBaseMétier, x: int, y: int) -> int:
        x += 1
        y += 1
        return self.dao.do_something_in_dao_layer(x, y)
  • alla riga 6, il metodo chiamato dal livello [ui] a sua volta chiama un metodo della proprietà [dao] della classe [MétierImpl1];

Tuttavia, nella configurazione [config1], è stato scritto:


# DAO
    dao = DaoImpl1()
    # business
    métier = MétierImpl1()
    métier.dao = dao
  • riga 5: la proprietà [MétierImpl1.dao] è di tipo [DaoImpl1] (riga 2);

Ciò che si vuole dimostrare qui è che lo script [main] non deve occuparsi dei livelli [métier] e [dao]. Deve occuparsi solo del livello [ui], poiché i collegamenti tra questo livello e gli altri sono stati creati tramite configurazione.

Image

Per passare il parametro [config1] o [config2] allo script [main], si procederà nel modo seguente:

Image

  • in [1-2], si crea quella che viene chiamata una configurazione di esecuzione;
  • in [3], si assegna un nome a questa configurazione per poterla ritrovare;
  • in [4], si seleziona lo script da eseguire. Se si è seguita la procedura [1-2], lo script corretto è già stato selezionato;
  • in [5], qui si inseriscono i parametri da trasmettere allo script. Qui si passa la stringa [config1] per richiedere allo script di utilizzare la configurazione n. 1;
  • in [6], si conferma la configurazione di esecuzione;

Image

  • in [1-2], si richiede la visualizzazione dei contesti di esecuzione esistenti;
  • in [3], si seleziona il contesto di esecuzione esistente e lo si duplica in [4];

Image

  • in [5], si assegna un nome alla nuova configurazione. Sarà questa a eseguire lo script [main] [6] passandole il parametro [config2] [7];

Le configurazioni di esecuzione sono disponibili in alto a destra nella finestra PyCharm:

Image

È sufficiente selezionare [2] o [3], quindi premere [4] per eseguire lo script [main] conuno dei due parametri [config1] o [config2].

Con [config1], l’esecuzione di [main] fornisce 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.