Skip to content

14. Arquitetura em camadas e programação por interfaces

14.1. Introdução

Propomos escrever uma aplicação que permita a visualização das notas dos alunos de uma escola secundária. Esta aplicação pode ter uma arquitetura multicamadas:

Image

  • a camada [ui] (Interface do Utilizador) é a camada em contacto com o utilizador da aplicação;
  • A camada [métier] implementa as regras de gestão da aplicação, tais como o cálculo de um salário ou de uma fatura. Esta camada utiliza dados provenientes do utilizador através da camada [présentation] e da camada SGBD através da camada [dao];
  • a camada [dao] (Data Access Objects) gere o acesso aos dados da camada SGBD (Sistema de Gestão de Bases de Dados).

Esta é a arquitetura que foi utilizada no |curso sobre Python 2|. Também é possível introduzir uma variante:

Image

As diferenças em relação à estrutura em camadas anterior são as seguintes:

  • um script principal denominado [main], acima referido, organiza a instanciação das camadas;
  • as camadas [ui, métier, dao] já não comunicam necessariamente entre si. Se for necessário que o façam, o script [main] fornece-lhes as referências das camadas de que necessitam;

O código está aqui organizado em centros de competências com um «maestro»:

  • o «maestro» é o script principal [main];
  • as camadas [ui], [dao] e [métier] são os centros de competências;

Poderíamos chamar a esta organização uma organização orquestral.

14.2. Exemplo 1

Vamos ilustrar a arquitetura em camadas com uma aplicação de consola simples:

  • não haverá base de dados;
  • a camada [dao] irá gerir as entidades Elève, Classe, Matière e Note, permitindo a gestão das notas dos alunos;
  • a camada [métier] permitirá calcular indicadores com base nas notas de um aluno específico;
  • a camada [ui] será uma aplicação de consola que apresentará os resultados dos alunos;

O projeto PyCharm da aplicação é o seguinte:

Nota: as pastas a azul fazem parte do [Sources Root] do projeto PyCharm.

14.2.1. As entidades da aplicação

Chamaremos de entidades as classes cuja única função é encapsular dados. Para tal, poderíamos utilizar dicionários. O interesse da classe reside em permitir testar a validade dos dados armazenados no objeto e em fornecer um método que devolva a identidade do objeto sob a forma de uma cadeia de caracteres.

14.2.1.1. A entidade [Classe]

A entidade [Classe] (Classe.py) representa uma turma do colégio:


# importações
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # atributos excluídos do estado da classe
    excluded_keys = []

    # propriedades da classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador da classe
        # nome: nome da classe
        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):
        # o nome deve ser uma cadeia de caracteres não vazia
        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")

Notas

  • linha 7: a entidade [Classe] deriva da entidade [BaseEntity] analisada no parágrafo |A turma BaseEntity|;
  • linhas 11-16: uma classe é definida por um n.º id e um nom (linha 16). A propriedade [id] é fornecida pela classe [BaseEntity] e o nome pela classe [Classe];
  • linhas 18-30: getter/setter do atributo [nom];

14.2.1.2. A entidade [Matière]

A classe [Matière] (matière.py) é a seguinte:


# importações
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # atributos excluídos do estado da classe
    excluded_keys = []

    # propriedades da classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador da disciplina
        # nome: nome da disciplina
        # coeficiente: coeficiente da disciplina
        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):
        # o nome deve ser uma cadeia de caracteres não vazia
        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):
        # o coeficiente deve ser um número real >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # erro?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

Notas

  • linha 7: a classe [Classe] deriva da classe [BaseEntity];
  • linhas 11-17: uma disciplina é definida pelo seu n.º [id], pelo seu nome [nom] e pelo seu coeficiente [coefficient];
  • linhas 19-50: getters/setters dos atributos da classe;

14.2.1.3. A entidade [Elève]

A classe [Elève] (élève.py) é a seguinte:


# importações
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # atributos excluídos do estado da classe
    excluded_keys = []

    # propriedades da classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador do aluno
        # apelido: apelido do aluno
        # nome próprio: nome próprio do aluno
        # turma: turma do aluno
        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:
        # o apelido deve ser uma cadeia de caracteres não vazia
        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:
        # o nome próprio deve ser uma cadeia de caracteres não vazia
        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:
            # é esperado um tipo Classe
            if isinstance(value, Classe):
                self.__classe = value
            # ou um tipo «dict»
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # ou um 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}")

Notas

  • linha 9: a classe [Elève] deriva da classe [BaseEntity];
  • linhas 13-20: um aluno é identificado pelo seu n.º [id], o seu apelido [nom], o seu nome próprio [prénom] e a sua turma [classe]. Este último parâmetro é uma referência a um objeto [Classe];
  • linhas 22-65: getters/setters dos atributos da classe;

14.2.1.4. A entidade [Note]

A classe [Note] (note.py) é a seguinte:


# importações
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    # atributos excluídos do estado da classe
    excluded_keys = []

    # propriedades da classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador da nota
        # valor: a própria nota
        # aluno: aluno (do tipo Aluno) a quem a nota diz respeito
        # disciplina: disciplina (do tipo Disciplina) a que a nota se refere
        # o objeto «Nota» corresponde, portanto, à nota de um aluno numa disciplina
        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):
        # a nota deve ser um número real entre 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:
            # espera-se um tipo «Aluno»
            if isinstance(value, Elève):
                self.__élève = value
            # ou um tipo «dict»
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # ou um 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:
            # é esperado um tipo «Matière»
            if isinstance(value, Matière):
                self.__matière = value
            # ou um tipo «dict»
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # ou um 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}")

Notas

  • linha 8: a classe [Note] deriva da classe [BaseEntity];
  • linhas 12-20: um objeto [Note] é caracterizado pelo seu n.º [id], pelo valor da nota [valeur], por uma referência [élève] ao aluno que obteve essa nota e por uma referência à disciplina [matière] objeto da nota;
  • linhas 22-75: getters/setters dos atributos da classe;

14.2.2. Configuração da aplicação

O ficheiro [config.py] configura o ambiente do script principal [main] (1), bem como o dos testes (2). Todos estes scripts têm uma instrução [import config] no início do código. Recorde-se que a pasta que contém o script alvo do comando [python script] faz automaticamente parte do Python Path.Si; portanto, como o [config] se encontra na mesma pasta que os scripts com a instrução [import config], será encontrado. Os ficheiros [1] e [2] são, neste caso, idênticos. No entanto, isso pode não ser o caso.

O ficheiro [config.sys] é o seguinte:


def configure():
    import os

    # caminho absoluto da pasta deste 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"
    # dependências absolutas
    absolute_dependencies=[
        # as pastas locais que contêm classes e interfaces
        f"{root_dir}/02/entities",
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
    ]

    # atualização do syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # faz-se a configuração
    return {}
  • linhas 11-14: as pastas que devem fazer parte do Python Path (sys.path);
  • a pasta [f"{root_dir}/02/entities"] dá acesso às classes [BaseEntity] e [MyException];
  • A pasta [f"{script_dir}/../entities"] dá acesso às classes [Elève], [Classe], [Matière] e [Note];
  • a pasta [f"{script_dir}/../interfaces",] dá acesso às interfaces da aplicação;
  • a pasta [f"{script_dir}/../services"] dá acesso às classes que implementam as interfaces;

14.2.3. Testes das entidades

Vamos aqui escrever testes executados por uma ferramenta chamada [unittest]. O PyCharm inclui vários frameworks de teste. A escolha de um deles é feita na configuração do PyCharm:

Image

  • No [4], estão disponíveis várias estruturas de teste:

Image

14.2.3.1. A classe de testes [TestBaseEntity]

O script de teste [TestBaseEntity] será o seguinte:


import unittest

# configuramos a aplicação
import config

config = config.configure()


class TestBaseEntity (unittest.TestCase):

    def test_note1(self):
        # importações
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        # criação de uma nota a partir de uma cadeia de caracteres jSON
        note = Note().fromjson(
            '{"id": 8, "valor": 12, "aluno": {"id": 42, "apelido": "apelido4", "nome": "nome4", "turma": {"id": 2, "nome": "turma2"}}, "disciplina": {"id": 2, "nome": "disciplina2", "coeficiente": 2}}')
        # verificações
        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):
        # importações
        from Note import Note
        from Elève import Elève
        from Classe import Classe
        from Matière import Matière
        # cálculo de uma nota a partir de um dicionário
        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}})
        # verificações
        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()

Notas

  • linha 1: importa-se o módulo [unittest], que irá fornecer os diferentes métodos de teste;
  • linhas 3-6: configura-se a aplicação para que as classes necessárias aos testes sejam encontradas;
  • linha 9: uma classe de teste [unittest] deve herdar da classe [unittest.TestCase];
  • linhas 11, 27: as funções de teste devem ter um nome que comece por [test]; caso contrário, não serão reconhecidas;
  • linhas 13-16: importam-se as classes necessárias;
  • nesta classe de teste, pretende-se verificar o comportamento dos métodos [BaseEntity.fromdict] (linha 34) e [BaseEntity.fromjson] (linha 18). A classe [Note] possui propriedades que são referências a outras classes. Queremos verificar se os dois métodos anteriores criam objetos [Note] válidos;
  • linha 18: cria-se um objeto [Note] a partir de um objeto jSON;
  • linha 21: verifica-se se o objeto criado é efetivamente do tipo [Note]. O método [assertIsInstance] é um método da classe [unittest.TestCase], classe pai da classe [TestBaseEntity];
  • linha 22: verifica-se se [note.élève] é efetivamente do tipo [Elève];
  • linha 23: verifica-se se [note.élève.classe] é efetivamente do tipo [Classe];
  • linha 24: verifica-se se [note.matière] é efetivamente do tipo [Matière];
  • linhas 33-42: faz-se o mesmo com o método [BaseEntity.fromdict];

Existem várias formas de executar os testes:

  • em [1-2], executa-se [TestBaseEntity] com o framework [UnitTest];
  • no [3-5], os testes falham. O [UnitTests] indica que não encontrou nenhum teste para executar;

A falha nos testes deve-se à organização do código do [TestBaseEntity]:


import unittest

# configuração da aplicação
import config

config = config.configure()


class TestBaseEntity(unittest.TestCase):

O que está a causar problemas ao framework [UnitTest] é a presença de código executável, nas linhas 3 a 6, antes da definição da classe de teste, na linha 9.

Reorganizamos, então, o código da seguinte forma:


import unittest


class TestBaseEntity(unittest.TestCase):

    def setUp(self):
        # configuração da aplicação
        import config

        config.configure()

    def test_note1(self):
        

    def test_note2(self):
        


if __name__ == '__main__':
    unittest.main()
  • linhas 6-10: define-se uma função [setUp]. Esta função tem um papel específico: é executada antes de cada função de teste (test_note1, test_note2);

Feito isto, a execução da classe [TestBaseEntity] produz os seguintes resultados:

Desta vez, os dois métodos de teste foram executados e os testes foram bem-sucedidos.

Vamos ver o que acontece quando um teste falha. Modifiquemos o código de [test_note1] da seguinte forma:


    def test_note1(self):
        # erro intencional — verifica-se se 1 == 2
        self.assertEqual(1,2)
        # importações
        from Note import Note

  • linha 2: verifica-se se 1==2;

Os resultados da execução são então os seguintes:

É possível saber a causa do erro clicando no teste falhado [2]:

  • em [7-8], a causa do erro;

Outra forma de executar uma classe de testes é fazê-lo num terminal:


(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

A linha 6 indica que os dois testes foram bem-sucedidos (eliminou-se o erro 1==2);

Por fim, uma terceira forma de executar a classe de testes [TestBaseEntity], também num terminal, é a seguinte. Concluímos a classe de testes com as seguintes linhas 6-7;



        self.assertIsInstance(note.élève.classe, Classe)
        self.assertIsInstance(note.matière, Matière)


if __name__ == '__main__':
    unittest.main()
  • linha 6: a variável [__name__] é o nome atribuído ao script que é executado. Quando o script é aquele iniciado pelo comando [python script.py], a variável [__name__] assume o valor [__main__] (2 caracteres sublinhados antes e depois do identificador). Assim, a linha 7 só é executada quando o script [TestBaseEntity] é iniciado pelo comando [python TestBaseEntity.py]. A instrução [unittest.main()] inicia a execução do script através do framework [UnitTest]. Eis um exemplo:

(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. A classe de testes [TestEntités]

A classe de testes [TestEntités] é a seguinte:


import unittest


class TestEntités(unittest.TestCase):
    def setUp(self):
        # configura-se a aplicação
        import config

        config.configure()

    def test_code1a(self):
        # importações
        from Elève import Elève
        from MyException import MyException
        # código de erro
        code = None
        try:
            # ID inválido
            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ção
        self.assertEqual(code, 1)

    def test_code41(self):
        # importações
        from Elève import Elève
        from MyException import MyException
        # código de erro
        code = None

        try:
            # nome inválido
            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ção
        self.assertEqual(code, 41)

    def test_code42(self):
        # importações
        from Elève import Elève
        from MyException import MyException
        # código de erro
        code = None
        try:
            # nome próprio inválido
            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ção
        self.assertEqual(code, 42)

    def test_code43(self):
        # importações
        from Elève import Elève
        from MyException import MyException
        # código de erro
        code = None
        try:
            # classe inválida
            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ção
        self.assertEqual(code, 43)

    def test_code1b(self):
        # importações
        from Classe import Classe
        from MyException import MyException
        # código de erro
        code = None
        try:
            # identificador inválido
            Classe().fromdict({"id": "x", "nom": "y"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verificação
        self.assertEqual(code, 1)

    def test_code11(self):
        # importações
        from Classe import Classe
        from MyException import MyException

        # código de erro
        code = None
        try:
            # nome inválido
            Classe().fromdict({"id": 1, "nom": ""})
        except MyException as ex:
            code = ex.code
        # verificação
        self.assertEqual(code, 11)

    def test_code1c(self):
        # importações
        from Matière import Matière
        from MyException import MyException

        # código de erro
        code = None
        try:
            # identificador inválido
            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ção
        self.assertEqual(code, 1)

    def test_code21(self):
        # importações
        from Matière import Matière
        from MyException import MyException
        # código de erro
        code = None
        try:
            # nome inválido
            Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
        except MyException as ex:
            print(f"\ncode erreur={ex.code}, message={ex}")
            code = ex.code
        # verificação
        self.assertEqual(code, 21)

    def test_code22(self):
        # importações
        from Matière import Matière
        from MyException import MyException
        # código de erro
        code = None
        try:
            # coeficiente inválido
            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ção
        self.assertEqual(code, 22)

    def test_code1d(self):
        # importações
        from Note import Note
        from MyException import MyException
        # código de erro
        code = None
        try:
            # identificador inválido
            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ção
        self.assertEqual(code, 1)

    def test_code31(self):
        # importações
        from Note import Note
        from MyException import MyException

        # código de erro
        code = None
        try:
            # valor inválido
            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ção
        self.assertEqual(code, 31)

    def test_code32(self):
        # importações
        from Note import Note
        from MyException import MyException

        # código de erro
        code = None
        try:
            # aluno inválido
            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ção
        self.assertEqual(code, 32)

    def test_code33(self):
        # importações
        from Elève import Elève
        from Note import Note
        from Classe import Classe
        from MyException import MyException

        # código de erro
        code = None
        try:
            # disciplina inválida
            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ção
        self.assertEqual(code, 33)

    def test_exception(self):
        # importações
        from Elève import Elève
        # o teste deve utilizar o tipo [MyException] para ser bem-sucedido
        from MyException import MyException
        with self.assertRaises(MyException):
            # o teste
            Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})


if __name__ == '__main__':
    unittest.main()
  • O script de teste tem como objetivo testar os setters das classes: verificar se não é possível atribuir valores incorretos aos atributos das diferentes entidades;
  • linhas 11-24: verifica-se se não é possível atribuir um identificador inválido a um aluno. Como se atribui o valor «x», na linha 16, como identificador do aluno, espera-se que ocorra uma exceção. Dever-se-ia, portanto, passar para as linhas 20-22;
  • linha 21: exibição da mensagem de erro;
  • linha 22: recupera-se o código do erro (ver parágrafo |A entidade MyException|);
  • linha 24: verifica-se (assert) se o código de erro é 1. Aqui, verificam-se duas coisas:
    • que ocorreu efetivamente um erro;
    • que o código de erro é 1;
  • este processo é repetido com as funções das linhas 24-213;
  • linhas 215-222: verifica-se se uma ação lança uma exceção de um determinado tipo;
  • linha 220: indica-se que o teste foi bem-sucedido se for lançada uma exceção do tipo [MyException];

Resultados

Executa-se o script de teste:

Os resultados obtidos são os seguintes:


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

Neste caso, todos os testes foram bem-sucedidos

14.2.4. A camada [dao]

Image

A camada [dao] implementa a interface [InterfaceDao] [1]. Esta é implementada pela classe [Dao] (2). O script [tests_dao] (3) testa os métodos da camada [dao].

14.2.4.1. Interface [InterfaceDao]

Uma interface é um contrato estabelecido entre o código chamador e o código chamado. É o código chamado que disponibiliza a interface:

  • O código chamador [1] não conhece a implementação do código chamado [3]. Só sabe como o chamar. É a interface [2] que lhe indica isso. Esta define um certo número de métodos/funções a utilizar para interagir com o código chamado. Esta interface é também designada por API (Interface de Programação de Aplicações);

A camada [dao] disponibilizará a seguinte interface:

  • [get_classes] apresenta a lista das turmas do ensino básico;
  • [get_matières] apresenta a lista das disciplinas lecionadas no colégio;
  • [get_élèves] apresenta a lista de alunos do ensino básico;
  • [get_notes] apresenta a lista das notas de todos os alunos;
  • [get_notes_for_élève_by_id] apresenta as notas de um aluno específico;
  • [get_élève_by_id] apresenta um aluno identificado pelo seu número;

O código chamador utilizará apenas estes métodos. Não precisa de saber como são implementados. Os dados podem, assim, provir de diferentes fontes (armazenados em memória, de uma base de dados, de ficheiros de texto…) sem que isso tenha impacto no código chamador. A isto chama-se programação por interfaces.

O Python 3 possui um conceito semelhante ao de interface: a classe abstrata. Vamos utilizá-la. Vamos agrupar as interfaces deste exemplo na pasta [interfaces].

Definimos uma classe abstrata [InterfaceDao] (InterfaceDao.py) para a camada [dao]:


# importações
from abc import ABC, abstractmethod

# interface DAO
from Elève import Elève


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

    # lista de alunos
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # lista de disciplinas
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    # lista de notas
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    # lista de notas de um aluno
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # procurar um aluno pelo seu ID
    @abstractmethod
    def get_élève_by_id(self, élève_id: int) -> Elève:
        pass

Notas:

  • linha 2: ABC = Classe Base Abstrata. Importa-se do módulo [abc] a classe ABC, bem como o decorador [abstractmethod] utilizado nas linhas 10, 15, 20, 25, 30 e 35;
  • linha 8: a classe abstrata chama-se [InterfaceDao] e deriva da classe [ABC];
  • os métodos da classe abstrata são decorados com o decorador [@abstractmethod], o que transforma o método assim decorado num método abstrato: o seu código não está definido. No entanto, insere-se código nele: a instrução [pass], que não faz nada;
  • a classe abstrata [InterfaceDao] não pode ser instanciada. Apenas podem ser instanciadas as classes derivadas de [InterfaceDao] que tenham implementado todos os métodos de [InterfaceDao]. Assim, se criarmos duas classes, [Dao1] e [Dao2], derivadas da classe [InterfaceDao], ambas implementarão os métodos abstratos de [InterfaceDao]. Poder-se-ia dizer, assim, que elas implementam a interface [InterfaceDao];
  • as linguagens que implementam tanto interfaces como classes abstratas atribuem à interface um papel diferente do da classe abstrata. Uma interface não tem atributos e não pode ser instanciada. Uma classe pode implementar uma interface definindo todos os seus métodos;

14.2.4.2. Implementação [Dao]

A classe [Dao] (dao.py) implementa a interface [InterfaceDao] da seguinte forma:


# importação de entidades e 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


# A camada [dao] implementa a interface InterfaceDao
class Dao(InterfaceDao):
    # construtor
    # construem-se listas fixas
    def __init__(self):
        # instanciamos as classes
        classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
        classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
        self.classes = [classe1, classe2]
        # as disciplinas
        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]
        # os alunos
        é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]
        # as notas dos alunos nas diferentes disciplinas
        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
    # -----------
    

Notas:

  • linhas 1-7: importam-se as entidades e a interface [InterfaceDao];
  • linha 11: a classe [Dao] deriva da classe abstrata [InterfaceDao]. Diremos que ela implementa a interface [InterfaceDao];
  • linha 14: o construtor não tem parâmetros. Ele define de forma estática quatro listas:
    • linhas 15-18: a lista de classes;
    • linhas 19-22: a lista de disciplinas;
    • linhas 23-28: a lista de alunos;
    • linhas 29-38: a lista de notas;
  • linhas 40-44: implementação dos métodos da interface [Interface Dao]. Aqui, não os definimos para vermos a mensagem de erro emitida pelo Python;

Um programa de teste poderia ser o seguinte: [tests-dao.py]:


# configura-se a aplicação
import config

config = config.configure()

# instanciação da camada [dao]
from Dao import Dao

daoImpl = Dao()

# lista de turmas
for classe in daoImpl.get_classes():
    print(classe)

# lista de disciplinas
for matière in daoImpl.get_matières():
    print(matière)

# lista de turmas
for élève in daoImpl.get_élèves():
    print(élève)

# lista de notas
for note in daoImpl.get_notes():
    print(note)

Nota: o script [tests-dao.py] não é um teste [unittest], uma vez que não contém métodos cujo nome comece por [test_].

Os comentários são autoexplicativos. As linhas 11 a 25 utilizam a interface da camada [dao]. Não há aqui quaisquer pressupostos sobre a implementação real da camada. Na linha 9, instanciamos a camada [dao].

Os resultados da execução deste script são os seguintes:


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

Vemos que ocorre um erro logo na instância da classe [Dao] (linha 3 acima). O interpretador Python 3 indica que não consegue instanciar a classe, uma vez que não definimos os métodos abstratos [get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves].

O PyCharm também reconhece o conceito de classe abstrata e sugere-nos que definamos os métodos da mesma:

  • em [1], clique com o botão direito do rato no código;
  • em [2-3], selecione [Generate / Implement Methods] para implementar os métodos em falta da classe [Dao];
  • em [4], selecione os métodos a implementar, neste caso todos;

Feito isto, a classe [Dao] é completada pela PyCharm da seguinte forma:


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

Completamos a classe [Dao] da seguinte forma:


    # -----------
    # interface IDao
    # -----------
    
    # lista de turmas
    def get_classes(self) -> list:
        return self.classes

    # lista de disciplinas
    def get_matières(self) -> list:
        return self.matières

    # lista de alunos
    def get_élèves(self) -> list:
        return self.élèves

    # lista de notas
    def get_notes(self) -> list:
        return self.notes

    def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
        # procurar o aluno
        élève = self.get_élève_by_id(élève_id)
        # consultar as notas
        notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
        # apresenta o resultado
        return {"élève": élève, "notes": notes}

    def get_élève_by_id(self, élève_id: int) -> Elève:
        # filtrar os alunos
        élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
        # Encontrado?
        if not élèves:
            raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
        # resultado
        return élèves[0]
  • as linhas 5-19 não apresentam dificuldades;
  • linhas 29-36: o método que devolve o aluno cujo número é passado. Se o aluno não existir, é lançada uma exceção;
  • linha 31: a função [filter] permite filtrar uma lista:
    • o primeiro parâmetro é o critério de filtragem;
    • o segundo parâmetro é a lista a filtrar, neste caso a lista de alunos;
  • linha 31: o critério de filtragem da lista é implementado com a ajuda de uma função [f(e :Elève)->bool]. Esta é aplicada a cada um dos elementos da lista a filtrar. Se o elemento satisfizer o critério de filtragem, é mantido na lista filtrada; caso contrário, é excluído. Aqui, pode-se:
    • indicar o nome da função f e implementá-la noutro local. A chamada à função [filter] passa então a ser [filter(f,self.get_élèves()];
    • definir a função f. A chamada à função [filter] passa então a ser [filter(f(e :Elève){…},self.get_élèves()], em que [e] representa um elemento da lista filtrada, ou seja, um aluno. Foi isso que se fez aqui. A definição da função f seria, neste caso, [f(e :Elève){return e.id==élève_id)]: um aluno só é selecionado se o n.º [id] não for igual ao procurado. Uma função deste tipo pode ser substituída por uma função denominada lambda: [lambda e: e.id == élève_id]:
      • e: representa o parâmetro da função f, neste caso, um aluno. Pode-se utilizar o nome que se quiser;
      • e.id==élève_id é o critério de filtragem: um aluno [e] só é selecionado se o seu n.º [id] corresponder ao que está a ser procurado;
  • linha 31: a função [filter] devolve a lista filtrada num tipo que não é o tipo [list], mas que pode ser transformado no tipo [list]. É isso que fazemos aqui com a expressão [list(liste filtrée)];
  • linhas 33-34: se a lista filtrada estiver vazia, significa que o aluno procurado não existe. Nesse caso, é lançada uma exceção;
  • linha 36: se chegarmos aqui, é porque não houve nenhuma exceção. Sabemos, então, que recuperámos uma lista com um único elemento (não há dois alunos com o mesmo n.º [id]). Devolvemos, portanto, o primeiro elemento da lista;
  • linhas 21-27: o método [get_notes_for_élève_by_id] deve devolver as notas do aluno cujo n.º [id] lhe é passado;
  • linhas 22-23: começa-se por procurar o aluno com o n.º [élève_id] utilizando o método [get_élève_by_id] que acabámos de comentar. Pode ocorrer uma exceção se o aluno procurado não existir. Como não existe um try/catch em torno da instrução da linha 23, a exceção será propagada para o código chamador. É isso que se pretende;
  • linhas 24-25: assim que o aluno for recuperado, recuperam-se todas as suas notas. Faz-se isso novamente com um filtro:
    • o filtro é [filter(critère, self_getnotes()]. A lista a filtrar é, portanto, a lista de todas as notas de todos os alunos do colégio;
    • o critério de filtragem é expresso através de uma função [lambda]: lambda n: n.élève.id == élève_id. O parâmetro n é um elemento da lista a filtrar, ou seja, uma nota. O tipo [Note] possui uma propriedade [élève] que representa o aluno a quem pertence a nota. É necessário, portanto, que [n.élève.id], que representa o número desse aluno, seja igual ao número do aluno procurado;

Em seguida, executamos o script [tests-dao.py].


# configura-se a aplicação
import config

config = config.configure()

# instanciação da camada [dao]
from Dao import Dao

daoImpl = Dao()

# lista de classes
for classe in daoImpl.get_classes():
    print(classe)

# lista de disciplinas
for matière in daoImpl.get_matières():
    print(matière)

# lista de turmas
for élève in daoImpl.get_élèves():
    print(élève)

# lista de notas
for note in daoImpl.get_notes():
    print(note)

# um aluno específico
print(daoImpl.get_élève_by_id(11))

# a lista das suas notas
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}")

Obtenemos então os seguintes resultados:


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

É possível observar que, ao apresentar uma nota (para os outros objetos, o procedimento é semelhante), temos também:

  • o aluno a quem a nota pertence;
  • a disciplina a que a nota se refere;

É a função [BaseEntity.asdict] que produz este resultado (ver parágrafo «ligação»).

14.2.5. A camada [métier]

  • [InterfaceMétier] é a interface da camada [métier];
  • [Métier] é a classe de implementação da camada [métier];
  • [Testmétier] é uma classe de teste [UnitTest] da classe [Métier];

14.2.5.1. Interface [InterfaceMétier]

A camada [métier] irá implementar a seguinte interface [InterfaceMétier] (InterfaceMétier.py):


# importações
from abc import ABC, abstractmethod

from StatsForElève import StatsForElève


# interface de negócio
class InterfaceMétier(ABC):
    # cálculo de estatísticas para um aluno
    @abstractmethod
    def get_stats_for_élève(self, idElève: int) -> StatsForElève:
        pass
  • [get_stats_for_élève] apresenta as notas do aluno n.º idElève, bem como informações sobre as mesmas: média ponderada, nota mais baixa, nota mais alta. Estas informações estão encapsuladas num objeto do tipo [StatsForElève];

14.2.5.2. A entidade [StatsForElève]

O tipo [StatsForElève] (StatsForElève.py), que encapsula as estatísticas (notas, mínima, máxima, média ponderada) de um aluno, é o seguinte:


# importações
from BaseEntity import BaseEntity


# estatísticas de um aluno específico


class StatsForElève(BaseEntity):
    # atributos excluídos do relatório da turma
    excluded_keys = []

    # propriedades da turma
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador da nota
        # aluno: o aluno em questão
        # notas: as suas notas
        # moyennePondérée: a sua média ponderada pelos coeficientes das disciplinas
        # min: a sua nota mínima
        # máx.: a sua nota máxima

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

    # toString
    def __str__(self) -> str:
        # caso do aluno sem notas
        if len(self.notes) == 0:
            return f"Elève={self.élève}, notes=[]"
        # caso do aluno com notas
        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}"

Notas:

  • linha 8: a classe [StatsForElève] deriva da classe [BaseEntity];
  • linhas 13-22: as propriedades da classe;
    • um identificador [id] proveniente de [BaseEntity];
    • o aluno [élève], cujas estatísticas são encapsuladas;
    • as suas notas [notes];
    • a sua média ponderada [moyenne_pondérée];
    • a sua nota mínima [min];
    • a sua nota máxima [max];
  • não se definem getters/setters para estes atributos. Parte-se do princípio de que é a camada [métier] que cria objetos deste tipo e que esta não cria objetos inválidos;
  • linhas 23-33: a função [__str__] devolve uma cadeia de caracteres que reflete as propriedades do objeto;

14.2.5.3. A implementação [Métier]

A implementação [Métier] (Metier.py) da interface [InterfaceMétier] será a seguinte:


# importações
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from StatsForElève import StatsForElève


class Métier(InterfaceMétier):

    # construtor
    def __init__(self, dao: InterfaceDao):
        # guardamos o parâmetro
        self.__dao = dao

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

    # os indicadores relativos às notas de um aluno específico
    def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
        # Estatísticas para o aluno com o n.º idEleve
        # id_élève: n.º do aluno

        # recuperam-se as suas notas com a camada [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"]

        # paramos se não houver notas
        if len(notes) == 0:
            # retorna o resultado
            return StatsForElève().fromdict({"élève": élève, "notes": []})

        # análise das notas do aluno
        somme_pondérée = 0
        somme_coeff = 0
        max = -1
        min = 21
        for note in notes:
            # valor da nota
            valeur = note.valeur
            # coeficiente da disciplina
            coeff = note.matière.coefficient
            # soma dos coeficientes
            somme_coeff += coeff
            # soma ponderada
            somme_pondérée += valeur * coeff
            # procura do mínimo
            if valeur < min:
                min = valeur
            # procura do máximo
            if valeur > max:
                max = valeur
        # cálculo dos indicadores em falta
        moyenne_pondérée = float(somme_pondérée) / somme_coeff

        # o resultado é apresentado na forma de um 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})

Notas

  • linha 7: a classe [Métier] deriva da classe [InterfaceMétier]. É habitual dizer que ela implementa a interface [InterfaceMétier];
  • linhas 9-12: o construtor recebe como único parâmetro uma referência à camada [dao]. Na linha 10, note-se que foi atribuído o tipo [InterfaceDao] ao parâmetro [dao]. Não se espera uma implementação específica, mas simplesmente uma implementação que respeite a interface [InterfaceDao]. Neste caso, isso não tem importância, uma vez que o Python não vai ter em conta este tipo, mas é uma boa prática trabalhar com interfaces em vez de com implementações específicas. O código torna-se assim mais facilmente modificável;
  • linhas 19-60: implementação do método [get_stats_for_élève];
  • linha 19: o método recebe um único parâmetro, o n.º [idElève] do aluno cujas estatísticas se pretendem obter;
  • linha 24: solicita-se à camada [dao] as notas do aluno. Este pedido resulta numa exceção se o aluno não existir. Esta exceção não é tratada (ausência de try/catch) e, por isso, é reenviada para o código chamador;
  • linha 25: chega-se aqui se não tiver ocorrido nenhuma exceção. [notes_élève] é, então, um dicionário com duas chaves [élève, note]:
    • linha 25: recuperam-se as informações sobre o aluno (o seu nome, a sua turma, etc.);
    • linha 26: recuperam-se as notas do aluno;
  • linhas 28-31: verifica-se se o aluno tem notas. Se não tiver, não há estatísticas a calcular;
  • linha 31: devolve-se um objeto [StatsForElève] construído a partir de um dicionário com o método [BaseEntity.fromdict];
  • linhas 33-54: utilizam-se as notas do aluno para calcular as estatísticas solicitadas. Os comentários do código devem ser suficientes para a sua compreensão;
  • linhas 56-60: devolve-se um objeto [StatsForElève] construído a partir de um dicionário com o método [BaseEntity.fromdict];

14.2.5.4. Teste da camada [métier]

Um script [UnitTest] da camada [métier] poderia ser o seguinte (TestMétier.py):


# importações
import unittest


class Testmétier(unittest.TestCase):
    def setUp(self):
        # configuração da aplicação
        import config
        config.configure()

    def test_statsForEleve11(self):
        # importações
        from Dao import Dao
        from Métier import Métier
        # testam-se os indicadores do aluno 11
        dao = Dao()
        stats_for_élève = Métier(dao).get_stats_for_élève(11)
        # visualização
        print(f"\nstats={stats_for_élève}")
        # verificações
        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()

Notas

  • linhas 6-9: a função [setUp] é aqui utilizada para configurar o Python Path do teste;
  • linha 16: instanciamos a camada [dao];
  • linha 17: instanciamos a camada [métier] e utilizamos o seu método [get_stats_for_élève] para calcular as estatísticas do aluno n.º 11;
  • linha 19: apresenta-se o resultado [StatsForElève] obtido. Como [StatsForElève] deriva de [BaseEntity], é a cadeia jSON de [StatsForElève] que é aqui apresentada;
  • linha 21: verifica-se a nota mínima do aluno;
  • linha 22: verifica-se a nota máxima do aluno;
  • linha 23: verifica-se se a média ponderada é igual a 7,333, com uma precisão de 10⁻³. Em geral, não é possível comparar números reais com exatidão, pois, internamente, na maioria das vezes, estes têm apenas uma representação aproximada;

Os resultados do teste são os seguintes:


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. A fralda [ui]

Image

  • em [1], a interface da camada [ui];
  • em [2], a implementação dessa interface;
  • em [3], o script principal da aplicação;

14.2.6.1. Interface [InterfaceUi]

A interface da camada [UI] será a seguinte:


# importações
from abc import ABC, abstractmethod


# interface UI
class InterfaceUi(ABC):
    # execução da camada UI
    @abstractmethod
    def run(self: object):
        pass

Notas

  • linhas 9-10: a camada [UI] terá apenas um método, [run];

14.2.6.2. A implementação [Console]

A camada [console] é implementada pelo seguinte script [Console.py]:


# importações das camadas

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

# outras dependências
from MyException import MyException


class Console(InterfaceUi):
    # construtor
    def __init__(self: object, métier: InterfaceMétier):
        # área de negócio: a camada [métier]

        # os atributos são memorizados
        self.métier = métier


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

    def run(self):
        # diálogo com o utilizador
        fini = False
        while not fini:
            # pergunta/resposta
            réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
            # Concluído?
            if réponse == "*":
                break
            # A entrada está correta?
            ok = False
            try:
                id_élève = int(réponse, 10)
                ok = id_élève >= 1
            except ValueError as erreur:
                pass
            # dados corretos?
            if not ok:
                print("Saisie incorrecte. Recommencez...")
                continue
            # cálculo das estatísticas para o aluno selecionado
            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}")
  • linhas 3-5: importação de todas as interfaces;
  • linha 11: a classe [Console] implementa a interface [InterfaceUi];
  • linhas 12-17: o construtor da classe [Console] recebe como parâmetro uma referência à camada [métier]. Note-se que foi atribuído o tipo [InterfaceMétier] a este parâmetro para recordar que se está a trabalhar com interfaces e não com implementações específicas;
  • linha 24: implementação do método [run] da interface;
  • linha 27: um ciclo que termina quando a condição da linha 31 é verificada;
  • linha 29: introdução de um valor digitado no teclado. A função [input] recebe um parâmetro opcional: a mensagem a apresentar no ecrã para solicitar a introdução de dados. Esta é sempre recuperada como uma cadeia de caracteres. A função [strip] remove os «espaços» que a precedem ou seguem;
  • linhas 34-39: verifica-se se a entrada, um número de aluno, é válida. Tem de ser um número inteiro >=1. Recorde-se que a entrada foi feita como uma cadeia de caracteres;
  • linha 36: tenta-se converter o valor introduzido num número inteiro na base 10. A função [int] lança uma exceção se tal não for possível;
  • linha 37: só se chega aqui se não tiver ocorrido nenhuma exceção. Verifica-se se o número inteiro obtido é, de facto, >=1;
  • linhas 38-39: trata-se da exceção. Se tiver ocorrido uma exceção, a variável [ok] da linha 34 permaneceu em [False];
  • linhas 41-43: se a introdução de dados tiver sido incorreta, exibe-se uma mensagem de erro e volta-se ao início do ciclo (linha 43);
  • linhas 45-48: calculam-se as estatísticas do aluno cujo número foi introduzido;
  • linha 46: utiliza-se o método [get_stats_for_élève] da camada [métier]. Este método lança uma exceção se o aluno não existir. A exceção é tratada nas linhas 47-48. Sabe-se que as camadas [dao] e [métier] lançam a exceção [MyException];

14.3. O script principal [main]

O script principal [main] é o seguinte (main.py):


# configurar a aplicação
import config

config = config.configure()

# o syspath está configurado — já é possível efetuar as importações
from Console import Console
from Dao import Dao
from Métier import Métier

# ----------- camada [console]
try:
    # instanciação da camada [dao]
    dao = Dao()
    # instanciação da camada [métier]
    métier = Métier(dao)
    # instanciação da camada [ui]
    console = Console(métier)
    # execução da camada [console]
    console.run()
except BaseException as ex:
    # é apresentado o erro
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    pass
  • linhas 1-4: configura-se o Python Path da aplicação;
  • linhas 6-9: importam-se as classes e interfaces necessárias;
  • linha 14: instanciamento da camada [dao];
  • linha 16: instanciação da camada [métier];
  • linha 18: instanciação da camada [ui];
  • linha 20: inicia-se o diálogo com o utilizador;
  • linhas 13-20: normalmente, nenhuma exceção é lançada nestas linhas. As exceções que provêm das camadas [dao] e [métier] são interceptadas pela camada [Console]. A gestão de exceções é uma arte difícil quando não se conhece perfeitamente as camadas utilizadas (o que não é o caso aqui). Em caso de dúvida, pode-se adicionar código para interceptar qualquer tipo de exceção que possa ser lançada pelo código executado. É isso que se faz aqui, nas linhas 21-23. Interceptam-se todas as exceções derivadas de [BaseException], ou seja, todas as exceções;
  • linhas 24-25: a cláusula [finally] não tem qualquer efeito aqui. Está presente apenas para permitir comentar as linhas 21-23. Com efeito, no modo de depuração, não é aconselhável interceptar as exceções. Neste caso, é o interpretador Python que as intercepta e, ao fazê-lo, indica o número da linha onde a exceção ocorreu. Uma informação indispensável. Quando as linhas 21-23 são comentadas, a presença das linhas 24-25 permite ter um try/catch sintaticamente correto. Na sua ausência, o Python declara um erro;

Eis um exemplo de execução:


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. Exemplo 2

Este novo exemplo de arquiteturas em camadas visa demonstrar a vantagem da programação por interfaces. Esta facilita a manutenção e o teste das aplicações. Utilizaremos novamente uma arquitetura de três camadas:

Image

Cada camada será implementada de duas formas diferentes. Pretendemos demonstrar que é possível alterar facilmente a implementação de uma camada com um impacto mínimo nas restantes.

14.4.1. A camada [dao]

Image

A interface [InterfaceDao] é a seguinte:


# importações
from abc import ABC, abstractmethod


# interface DAO
class InterfaceDao(ABC):
    # um único método
    @abstractmethod
    def do_something_in_dao_layer(self, x: int, y: int) -> int:
        pass
  • linhas 8-10: o método [do_something_in_dao_layer] é o único método da interface;

A classe [DaoImpl1] implementa a interface [InterfaceDao] da seguinte forma:


from InterfaceDao import InterfaceDao


class DaoImpl1(InterfaceDao):
    # implementação InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x + y

A classe [DaoImpl2] implementa a interface [InterfaceDao] da seguinte forma:


from InterfaceDao import InterfaceDao


class DaoImpl2(InterfaceDao):
    # implementação InterfaceDao
    def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
        return x - y

14.4.2. A camada [métier]

Image

A interface [InterfaceMétier] é a seguinte:


# importações
from abc import ABC, abstractmethod


# interface de negócio
class InterfaceMétier(ABC):
    # um único método
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • linhas 8-10: o método [do_something_in_métier_layer] é o único método da interface;

A classe [AbstractBaseMétier] implementa a interface [InterfaceMétier] da seguinte forma:


# importações
from abc import ABC, abstractmethod

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


class AbstractBaseMétier(InterfaceMétier, ABC):
    # propriedades
    # __dao é uma referência na camada [dao]
    @property
    def dao(self) -> InterfaceDao:
        return self.__dao

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

    # implementação da interface [InterfaceMétier]
    @abstractmethod
    def do_something_in_métier_layer(self, x: int, y: int) -> int:
        pass
  • linha 8: a classe [AbstractBaseMétier] deriva de duas classes:
    • [InterfaceMétier]: a classe [AbstractBaseMétier] implementa esta interface nas linhas 19-22. Na verdade, verifica-se que não implementou o método [do_something_in_métier_layer], que declarou como abstrato (linha 20). Caberá às classes derivadas implementar o método;
    • [ABC] para ter acesso às anotações [@abstractmethod];
    • a ordem é importante: se a invertemos aqui, o Python reporta um erro na execução;

É a primeira vez que se utiliza a herança múltipla (herdar de várias classes). A classe [AbstractBaseMétier] herda, simultaneamente, as propriedades das classes [InterfaceMétier] e [ABC].

  • linhas 9-17: define-se a propriedade [dao], que será uma referência à camada [dao];

Uma interface destina-se a ser implementada. Quando diferentes implementações partilham propriedades, é interessante colocá-las numa classe pai, a fim de evitar a sua duplicação. É o caso, aqui, da propriedade [dao]. A classe pai é, geralmente, sempre abstrata, porque não sabe implementar todos os métodos da interface.

A classe [MétierImpl1] implementa a interface [InterfaceMétier] da seguinte forma:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl1(AbstractBaseMétier):
    # implementação da interface [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)
  • linha 4: a classe [MétierImpl1] deriva da classe [AbstractbaseMétier]. Por conseguinte, herda a propriedade [dao] dessa classe;
  • linhas 6-9: implementação da interface [InterfaceMétier], que não foi implementada pela classe pai [AbstractbaseMétier];
  • linha 9: utiliza-se a camada [dao];

A classe [MétierImpl2] implementa a interface [InterfaceMétier] de forma análoga:


from AbstractBaseMétier import AbstractBaseMétier


class MétierImpl2(AbstractBaseMétier):
    # implementação da interface [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. A camada [ui]

Image

A interface [InterfaceUi] é a seguinte:


# importações
from abc import ABC, abstractmethod


# interface Ui
class InterfaceUi(ABC):
    # um único método
    @abstractmethod
    def do_something_in_ui_layer(self, x: int, y: int) -> int:
        pass
  • linhas 8-10: o único método da interface;

A classe [AbstractBaseUi] implementa a interface [InterfaceUi] da seguinte forma:


# importações
from abc import ABC, abstractmethod

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


class AbstractBaseUi(InterfaceUi, ABC):
    # propriedades
    # o domínio de negócio é uma referência na camada [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

    # implementação da interface [InterfaceUI]
    @abstractmethod
    def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
        pass
  • a classe [AbstractBaseUi] é uma classe abstrata (linha 20). Terá de ser derivada para implementar a interface [InterfaceUi];
  • linhas 9-17: a classe [AbstractBaseUi] possui uma referência à camada [métier];

A classe de implementação [UiImpl1] é a seguinte:


from AbstractBaseUi import AbstractBaseUi


class UiImpl1(AbstractBaseUi):
    # implementação da interface [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)
  • linha 4: a classe [UiImpl1] deriva da classe [AbstractBaseUi] e, por conseguinte, herda a sua propriedade [métier]. Esta é utilizada na linha 9;

A classe de implementação [UiImpl2] é análoga:


from AbstractBaseUi import AbstractBaseUi


class UiImpl2(AbstractBaseUi):
    # implementação da interface [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)
  • linha 4: a classe [UiImpl2] deriva da classe [AbstractBaseUi] e, por conseguinte, herda a sua propriedade [métier]. Esta é utilizada na linha 9;

14.4.4. Os ficheiros de configuração

Image

  • os ficheiros [config1, config2] configuram a aplicação de duas formas diferentes;
  • o ficheiro [main] é o script principal da aplicação;

O ficheiro [config1] é o seguinte:


def configure():
    # etapa 1 ------
    # caminho absoluto da pasta deste script
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # dependências
    absolute_dependencies = [
        # pastas locais do Python Path
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # configuramos o syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # etapa 2 ------
    # configuração das camadas da aplicação
    from DaoImpl1 import DaoImpl1
    from MétierImpl1 import MétierImpl1
    from UiImpl1 import UiImpl1
    # instanciação das camadas
    # DAO
    dao = DaoImpl1()
    # logica de negócio
    métier = MétierImpl1()
    métier.dao = dao
    # interface do utilizador
    ui = UiImpl1()
    ui.métier = métier

    # colocamos as instâncias das camadas na configuração
    # aqui só é necessária a camada ui
    config = {"ui": ui}

    # fazemos a configuração
    return config
  • linhas 2-16: configuração do Python Path da aplicação;
  • linhas 18-31: instanciação das camadas [dao, métier, ui]. Para implementar as respetivas interfaces, opta-se sempre pela primeira implementação criada;
  • linhas 33-35: inserem-se as referências das camadas na configuração. Aqui, o script principal necessita apenas da camada [ui];

O ficheiro [config2] é semelhante e implementa cada interface com a segunda implementação disponível:


def configure():
    # passo 1 ---
    # caminho absoluto da pasta deste script
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # dependências
    absolute_dependencies = [
        # pastas locais do Python Path
        f"{script_dir}/../dao",
        f"{script_dir}/../ui",
        f"{script_dir}/../métier",
    ]

    # configuramos o syspath
    from myutils import set_syspath

    set_syspath(absolute_dependencies)

    # etapa 2 ------
    # configuração das camadas da aplicação
    from DaoImpl2 import DaoImpl2
    from MétierImpl2 import MétierImpl2
    from UiImpl2 import UiImpl2
    # instanciação das camadas
    # DAO
    dao = DaoImpl2()
    # logica de negócio
    métier = MétierImpl2()
    métier.dao = dao
    # interface do utilizador
    ui = UiImpl2()
    ui.métier = métier

    # colocamos as instâncias das camadas na configuração
    # aqui só é necessária a camada ui
    config = {"ui": ui}

    # geramos a configuração
    return config

14.4.5. O script principal [main]

Image

O script principal é o seguinte:


# importações
import importlib
import sys

# main ---------

# são necessários dois argumentos
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()

# configuração da aplicação
module = importlib.import_module(sys.argv[1])
config = module.configure()

# execução da camada [ui]
print(config["ui"].do_something_in_ui_layer(10, 20))

Este script recebe um parâmetro:

  • [config1] para utilizar a configuração n.º 1;
  • [config2] para utilizar a configuração n.º 2;

O Python guarda os parâmetros numa lista [sys.argv]:

  • sys.argv[0] é o nome do script, neste caso [main]. Este parâmetro está sempre presente;
  • sys.argv[1] é o primeiro parâmetro passado ao script, sys.argv[2] o segundo, …

  • linha 8: obtém-se o número de parâmetros;

  • linhas 9-11: verifica-se se existe efetivamente um parâmetro e se o seu valor é [config1] ou [config2]. Se não for esse o caso, é exibida uma mensagem de erro (linha 10) e o programa é encerrado (linha 11);

Assim que a configuração pretendida for conhecida, é necessário executá-la. Por exemplo, se tiver sido escolhida a configuração 1, é necessário executar o código:

import config1
config1.configure()

O problema aqui é que a configuração a utilizar está numa variável, a variável [len[liste1]]. Para importar um módulo cujo nome está numa variável, temos de utilizar o pacote [importlib] (linha 2).

  • linha 14: importamos o módulo cujo nome está em [sys.argv[1];
  • linha 15: feito isto, executamos a função [configure] desse módulo. Recuperamos um dicionário [config], que corresponde à configuração da aplicação;
  • linha 18: sabemos que existe uma referência da camada [ui] em config[‘ui’]. Utilizamo-la para chamar o método [do_something_in_ui_layer]. Sabemos que este método irá chamar um método da camada [métier], que, por sua vez, irá chamar um método da camada [dao];

Por exemplo, a função [do_something_in_ui_layer] é a seguinte:


class UiImpl1(AbstractBaseUi):
    # implementação da interface [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)
  • A linha 6 acima utiliza a propriedade [métier] da classe [UiImpl1], linha 1. No entanto, na configuração [config1], foi escrito:

# funções de negócio
    métier = MétierImpl1()
    métier.dao = dao
    # interface do utilizador
    ui = UiImpl1()
    ui.métier = métier
  • linha 6: a propriedade [métier] de [UIImpl1] é uma referência à classe [MétierImpl1] (linha 2). Assim, será executado o método [do_something_in_ui_layer] da classe [MétierImpl1];

Na classe [MétierUiImpl1], está escrito:


class MétierImpl1(AbstractBaseMétier):
    # implementação da interface [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)
  • na linha 6, o método chamado pela camada [ui] irá, por sua vez, chamar um método da propriedade [dao] da classe [MétierImpl1];

No entanto, na configuração [config1], está escrito:


# DAO
    dao = DaoImpl1()
    # domínio de negócio
    métier = MétierImpl1()
    métier.dao = dao
  • linha 5: a propriedade [MétierImpl1.dao] é do tipo [DaoImpl1] (linha 2);

O que se pretende demonstrar aqui é que o script [main] não precisa de se preocupar com as camadas [métier] e [dao]. Tem apenas de se preocupar com a camada [ui], uma vez que as ligações entre esta camada e as outras foram estabelecidas por configuração.

Image

Para passar o parâmetro [config1] ou [config2] para o script [main], deve proceder-se da seguinte forma:

Image

  • no [1-2], cria-se o que se denomina uma configuração de execução;
  • no [3], atribui-se um nome a esta configuração para poder localizá-la;
  • em [4], seleciona-se o script a executar. Se tiver seguido o procedimento [1-2], o script correto já terá sido selecionado;
  • Em [5], introduzem-se aqui os parâmetros a transmitir ao script. Aqui, passa-se a cadeia [config1] para solicitar ao script que utilize a configuração n.º 1;
  • em [6], valida-se a configuração de execução;

Image

  • em [1-2], solicita-se a visualização dos contextos de execução existentes;
  • no [3], seleciona-se o contexto de execução existente e duplica-se-o para [4];

Image

  • no [5], o nome atribuído à nova configuração. Será esta que executará o script [main] [6], passando-lhe o parâmetro [config2] [7];

As configurações de execução estão disponíveis no canto superior direito da janela PyCharm:

Image

Basta selecionar [2] ou [3] e, em seguida, clicar em [4] para executar o script [main] comum dos parâmetros [config1] ou [config2].

Com o [config1], a execução do [main] produz os seguintes resultados:


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

Com o [config2], a execução do [main] produz os seguintes resultados:


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

Convidamos o leitor a verificar estes resultados.