Skip to content

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:

Image

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

Image

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:

#  imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: class identifier
        #  name: class name
        return BaseEntity.get_allowed_keys() + ["nom"]

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

    #   setters
    @nom.setter
    def nom(self: object, nom: str):
        #  name must be a non-empty string
        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à [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:

#  imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: material identifier
        #  name: material name
        #  coefficient: subject coefficient
        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

    #   setters
    @nom.setter
    def nom(self: object, nom: str):
        #  name must be a non-empty string
        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):
        #  the coefficient must be a real number >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        #  mistake?
        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 [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:

#  imports
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: student identifier
        #  name: student's name
        #  first name: student's first name
        #  class: student's class
        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

    #   setters
    @nom.setter
    def nom(self: object, nom: str) -> str:
        #  name must be a non-empty string
        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:
        #  first name must be a non-empty string
        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:
            #  we expect a Class type
            if isinstance(value, Classe):
                self.__classe = value
            #  or a type dict
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            #  or a json type
            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 [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:

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


class Note(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: note identifier
        #  value: the note itself
        #  student: student (of type Student) concerned by the note
        #  subject: subject (of type Subject) concerned by the grade
        #  the Note object is therefore a student's grade in a subject
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    #  getters
    @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

    #  getters
    @valeur.setter
    def valeur(self: object, valeur: float):
        #  the score must be a real number between 0 and 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:
            #  we expect a Student type
            if isinstance(value, Elève):
                self.__élève = value
            #  or a type dict
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            #  or a json type
            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:
            #  we expect a Material type
            if isinstance(value, Matière):
                self.__matière = value
            #  or a type dict
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            #  or a json type
            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 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:

def configure():
    import os

    #  absolute path of this script's folder
    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"
    #  absolute dependencies
    absolute_dependencies=[
        #  local folders containing classes and interfaces
        f"{root_dir}/02/entities",
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
    ]

    #  update syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  return the config
    return {}
  • 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:

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

#  configure the application
import config

config = config.configure()


class TestBaseEntity (unittest.TestCase):

    def test_note1(self):
        #  imports
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        #  construction of a note from a jSON string
        note = Note().fromjson(
            '{"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}}')
        #  checks
        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):
        #  imports
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        #  building a note from a dictionary
        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}})
        #  checks
        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: 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]:

1
2
3
4
5
6
7
8
9
import unittest

#  configure the application
import config

config = config.configure()


class TestBaseEntity(unittest.TestCase):

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:

import unittest


class TestBaseEntity(unittest.TestCase):

    def setUp(self):
        #  configure the application
        import config

        config.configure()

    def test_note1(self):
        

    def test_note2(self):
        


if __name__ == '__main__':
    unittest.main()
  • 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:

1
2
3
4
5
6
    def test_note1(self):
        #  deliberate error - check that 1==2
        self.assertEqual(1,2)
        #  imports
        from Note import Note

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

import unittest


class TestEntités(unittest.TestCase):
    def setUp(self):
        #  configure the application
        import config

        config.configure()

    def test_code1a(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid id
            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
        #  check
        self.assertEqual(code, 1)

    def test_code41(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None

        try:
            #  invalid name
            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
        #  check
        self.assertEqual(code, 41)

    def test_code42(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid first name
            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
        #  check
        self.assertEqual(code, 42)

    def test_code43(self):
        #  imports
        from Elève import Elève
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid class
            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
        #  check
        self.assertEqual(code, 43)

    def test_code1b(self):
        #  imports
        from Classe import Classe
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid identifier
            Classe().fromdict({"id": "x", "nom": "y"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 1)

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

        #  error code
        code = None
        try:
            #  invalid name
            Classe().fromdict({"id": 1, "nom": ""})
        except MyException as ex:
            code = ex.code
        #  check
        self.assertEqual(code, 11)

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

        #  error code
        code = None
        try:
            #  invalid identifier
            Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 1)

    def test_code21(self):
        #  imports
        from Matière import Matière
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid name
            Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 21)

    def test_code22(self):
        #  imports
        from Matière import Matière
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid coefficient
            Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        #  check
        self.assertEqual(code, 22)

    def test_code1d(self):
        #  imports
        from Note import Note
        from MyException import MyException
        #  error code
        code = None
        try:
            #  invalid identifier
            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
        #  check
        self.assertEqual(code, 1)

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

        #  error code
        code = None
        try:
            #  invalid value
            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
        #  check
        self.assertEqual(code, 31)

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

        #  error code
        code = None
        try:
            #  disabled student
            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
        #  check
        self.assertEqual(code, 32)

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

        #  error code
        code = None
        try:
            #  invalid material
            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
        #  check
        self.assertEqual(code, 33)

    def test_exception(self):
        #  imports
        from Elève import Elève
        #  the test must launch type [MyException] to succeed
        from MyException import MyException
        with self.assertRaises(MyException):
            #  the test
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})


if __name__ == '__main__':
    unittest.main()
  • 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]

Image

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

#  imports
from abc import ABC, abstractmethod

#  dao interface
from Elève import Elève


class InterfaceDao(ABC):
    #  list of classes
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    #  list of students
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    #  list of materials
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    #  lIST OF NOTES
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    #  list of student grades
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    #  search for a student by id
    @abstractmethod
    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

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:

#  import entities and interfaces
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


#  dao] layer implements InterfaceDao interface
class Dao(InterfaceDao):
    #  manufacturer
    #  we build hard lists
    def __init__(self):
        #  classes are instantiated
        classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
        classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
        self.classes = [classe1, classe2]
        #  materials
        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]
        #  students
        é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]
        #  student grades in various subjects
        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]

    # -----------
    #  interface IDao
    # -----------

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

#  configure the application
import config

config = config.configure()

#  layer instantiation [dao]
from Dao import Dao

daoImpl = Dao()

#  class list
for classe in daoImpl.get_classes():
    print(classe)

#  list of materials
for matière in daoImpl.get_matières():
    print(matière)

#  class list
for élève in daoImpl.get_élèves():
    print(élève)

#  lIST OF NOTES
for note in daoImpl.get_notes():
    print(note)

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:

    # -----------
    #  interface 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] come segue:

    # -----------
    #  interface IDao
    # -----------

    #  class list
    def get_classes(self) -> list:
        return self.classes

    #  list of materials
    def get_matières(self) -> list:
        return self.matières

    #  list of students
    def get_élèves(self) -> list:
        return self.élèves

    #  lIST OF NOTES
    def get_notes(self) -> list:
        return self.notes

    def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
        #  we're looking for the student
        élève = self.get_élève_by_id(élève_id)
        #  get your notes back
        notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
        #  we return the result
        return {"élève": élève, "notes": notes}

    def get_élève_by_id(self, élève_id: int) -> Elève:
        #  filtering students
        élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
        #  found?
        if not élèves:
            raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
        #  result
        return élèves[0]
  • 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].

#  configure the application
import config

config = config.configure()

#  layer instantiation [dao]
from Dao import Dao

daoImpl = Dao()

#  class list
for classe in daoImpl.get_classes():
    print(classe)

#  list of materials
for matière in daoImpl.get_matières():
    print(matière)

#  class list
for élève in daoImpl.get_élèves():
    print(élève)

#  lIST OF NOTES
for note in daoImpl.get_notes():
    print(note)

#  a special student
print(daoImpl.get_élève_by_id(11))

#  a list of his notes
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 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):

#  imports
from abc import ABC, abstractmethod

from StatsForElève import StatsForElève


#  business interface
class InterfaceMétier(ABC):
    #  calculating statistics for a student
    @abstractmethod
    def get_stats_for_élève(self, idElève: int) -> StatsForElève:
        pass
  • [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:

#  imports
from BaseEntity import BaseEntity


#  individual student statistics


class StatsForElève(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: note identifier
        #  pupil: the pupil concerned
        #  notes: his notes
        #  moyennePondérée: average weighted by subject coefficients
        #  min: its minimum score
        #  max: its maximum rating

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

    #  toString
    def __str__(self) -> str:
        #  students without grades
        if len(self.notes) == 0:
            return f"Elève={self.élève}, notes=[]"
        #  student with grades
        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 [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:

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


class Métier(InterfaceMétier):

    #  manufacturer
    def __init__(self, dao: InterfaceDao):
        #  the parameter
        self.__dao = dao

    # -----------
    #  interface
    # -----------

    #  indicators on a particular student's grades
    def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
        #  Stats for student no. idEleve
        #  id_élève : pupil number

        #  retrieve notes with the [dao] layer
        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"]

        #  we stop if there are no notes
        if len(notes) == 0:
            #  we return the result
            return StatsForElève().fromdict({"élève": élève, "notes": []})

        #  use of student notes
        somme_pondérée = 0
        somme_coeff = 0
        max = -1
        min = 21
        for note in notes:
            #  nOTE VALUE
            valeur = note.valeur
            #  material coefficient
            coeff = note.matière.coefficient
            #  sum of coefficients
            somme_coeff += coeff
            #  weighted sum
            somme_pondérée += valeur * coeff
            #  search for min
            if valeur < min:
                min = valeur
            #  search for the max
            if valeur > max:
                max = valeur
        #  calculation of missing indicators
        moyenne_pondérée = float(somme_pondérée) / somme_coeff

        #  the result is returned as type [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]. È 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):

#  imports
import unittest


class Testmétier(unittest.TestCase):
    def setUp(self):
        #  configure the application
        import config
        config.configure()

    def test_statsForEleve11(self):
        #  imports
        from Dao import Dao
        from Métier import Métier
        #  student indicators are tested 11
        dao = Dao()
        stats_for_élève = Métier(dao).get_stats_for_élève(11)
        #  display
        print(f"\nstats={stats_for_élève}")
        #  checks
        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 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]

Image

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

#  imports
from abc import ABC, abstractmethod


#  interface UI
class InterfaceUi(ABC):
    #  execute UI layer
    @abstractmethod
    def run(self: object):
        pass

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

#  layer imports

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

#  other dependencies
from MyException import MyException


class Console(InterfaceUi):
    #  manufacturer
    def __init__(self: object, métier: InterfaceMétier):
        #  business: the [business] layer

        #  attributes are memorized
        self.métier = métier


        # -----------
        #  interface
        # -----------

    def run(self):
        #  user dialog
        fini = False
        while not fini:
            #  question/answer
            réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
            #  finished?
            if réponse == "*":
                break
            #  is the input correct?
            ok = False
            try:
                id_élève = int(réponse, 10)
                ok = id_élève >= 1
            except ValueError as erreur:
                pass
            #  correct data?
            if not ok:
                print("Saisie incorrecte. Recommencez...")
                continue
            #  calculation of statistics for the selected student
            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 [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):

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Console import Console
from Dao import Dao
from Métier import Métier

#  ----------- layer [console]
try:
    #  layer instantiation [dao]
    dao = Dao()
    #  instantiation layer [business]
    métier = Métier(dao)
    #  instantiation layer [ui]
    console = Console(métier)
    #  layer execution [console]
    console.run()
except BaseException as ex:
    #  error is displayed
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    pass
  • 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:

Image

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]

Image

L'interfaccia [InterfaceDao] è la seguente:

#  imports
from abc import ABC, abstractmethod


#  dao interface
class InterfaceDao(ABC):
    #  a single method
    @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] come segue:

1
2
3
4
5
6
7
from InterfaceDao import InterfaceDao


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

La classe [DaoImpl2] implementa l'interfaccia [InterfaceDao] come segue:

1
2
3
4
5
6
7
from InterfaceDao import InterfaceDao


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

14.4.2. Il livello [aziendale]

Image

L'interfaccia [BusinessInterface] è la seguente:

#  imports
from abc import ABC, abstractmethod


#  business interface
class InterfaceMétier(ABC):
    #  a single method
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • 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:

#  imports
from abc import ABC, abstractmethod

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


class AbstractBaseMétier(InterfaceMétier, ABC):
    #  properties
    #  __dao is a reference to the [dao] layer
    @property
    def dao(self) -> InterfaceDao:
        return self.__dao

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

    #  interface implementation [InterfaceMétier]
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • 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:

1
2
3
4
5
6
7
8
9
from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl1(AbstractBaseMétier):
    #  interface implementation [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 [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:

1
2
3
4
5
6
7
8
9
from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl2(AbstractBaseMétier):
    #  interface implementation [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:

#  imports
from abc import ABC, abstractmethod


#  ui interface
class InterfaceUi(ABC):
    #  a single method
    @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] come segue:

#  imports
from abc import ABC, abstractmethod

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


class AbstractBaseUi(InterfaceUi, ABC):
    #  properties
    #  business is a reference to the [business] layer
    @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

    #  interface implementation [InterfaceUI]
    @abstractmethod
    def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
        pass
  • 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:

1
2
3
4
5
6
7
8
9
from AbstractBaseUi import AbstractBaseUi


class UiImpl1(AbstractBaseUi):
    #  interface implementation [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 quindi eredita la sua proprietà [business]. Questa viene utilizzata alla riga 9;

La classe di implementazione [UiImpl2] è simile:

1
2
3
4
5
6
7
8
9
from AbstractBaseUi import AbstractBaseUi


class UiImpl2(AbstractBaseUi):
    #  interface implementation [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 quindi eredita la sua proprietà [business]. 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():
    #  step 1 ------
    #  absolute path of this script's folder
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  dependencies
    absolute_dependencies = [
        #  local Python Path folders
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    #  configure the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application layer configuration
    from DaoImpl1 import DaoImpl1
    from MétierImpl1 import MétierImpl1
    from UiImpl1 import UiImpl1
    #  layer instantiation
    #  dao
    dao = DaoImpl1()
    #  business
    métier = MétierImpl1()
    métier.dao = dao
    #  ui
    ui = UiImpl1()
    ui.métier = métier

    #  put the layer instances in the config
    #  only the ui layer is required here
    config = {"ui": ui}

    #  return the config
    return config
  • 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:

def configure():
    #  step 1 ---
    #  absolute path of this script's folder
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  dependencies
    absolute_dependencies = [
        #  local Python Path folders
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    #  configure the syspath
    from myutils import set_syspath

    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application layer configuration
    from DaoImpl2 import DaoImpl2
    from MétierImpl2 import MétierImpl2
    from UiImpl2 import UiImpl2
    #  layer instantiation
    #  dao
    dao = DaoImpl2()
    #  business
    métier = MétierImpl2()
    métier.dao = dao
    #  ui
    ui = UiImpl2()
    ui.métier = métier

    #  put the layer instances in the config
    #  only the ui layer is required here
    config = {"ui": ui}

    #  return the config
    return config

14.4.5. Lo script principale [main]

Image

Lo script principale è il seguente:

#  imports
import importlib
import sys

#  hand ---------

#  you need two arguments
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()

#  application configuration
module = importlib.import_module(sys.argv[1])
config = module.configure()

#  execution of the [ui] layer
print(config["ui"].do_something_in_ui_layer(10, 20))

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:

import config1
config1.configure()

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:

1
2
3
4
5
6
class UiImpl1(AbstractBaseUi):
    #  interface implementation [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 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:

1
2
3
4
5
6
class MétierImpl1(AbstractBaseMétier):
    #  interface implementation [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 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.

Image

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

Image

  • 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;

Image

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

Image

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

Image

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.