Skip to content

15. Exercício prático - versão 4

Image

Retomamos aqui o exercício descrito no parágrafo |Versão 3| e abordamo-lo agora com classes e interfaces. Iremos escrever duas aplicações:

A aplicação 1 será a seguinte:

Image

Um script principal [main] irá instanciar uma camada [dao] e uma camada [métier]:

  • a camada [dao] terá como função gerir dados armazenados em ficheiros de texto e, posteriormente, numa base de dados;
  • a camada [métier] terá como função efetuar o cálculo do imposto;

Nesta aplicação, não haverá intervenção por parte do utilizador: os dados dos contribuintes serão obtidos a partir de um ficheiro de texto cujo nome será fornecido ao módulo [main].

Na aplicação 2, será o utilizador a introduzir os dados dos contribuintes através do teclado. A arquitetura evoluirá então da seguinte forma:

Image

  • a camada [dao] (Data Access Object) encarrega-se do acesso aos dados externos
  • a camada [métier] trata dos aspetos de negócio, neste caso o cálculo do imposto. Não lida com os dados. Estes podem ter duas origens:
    • a camada [dao] para os dados persistentes;
    • a camada [ui] para os dados fornecidos pelo utilizador.
  • a camada [ui] (Interface do Utilizador) trata das interações com o utilizador;
  • a camada [main] é o «maestro»;

A seguir, as camadas [dao], [métier] e [ui] serão implementadas, cada uma delas, através de uma classe. As camadas [métier] e [dao] serão as mesmas para ambas as aplicações. É por isso que foram reunidas numa única versão do exercício prático.

15.1. Versão 4 – aplicação 1

A versão 4 calcula o imposto de uma lista de contribuintes incluída num ficheiro de texto. Apresenta a seguinte arquitetura:

Image

15.1.1. As entidades

Image

As entidades são classes de dados. A sua função é encapsular dados e disponibilizar getters/setters que permitem verificar a validade dos mesmos. As entidades são trocadas entre as camadas. Uma mesma entidade pode partir da camada [ui] para chegar à camada [dao] e vice-versa.

15.1.1.1. A classe [ImpôtsError]

Iremos utilizar uma classe de exceção proprietária:


# -------------------------------
# classe de exceção
from MyException import MyException


class ImpôtsError(MyException):
    pass

Assim que as camadas [métier] e [dao] encontrarem um problema, lançarão esta exceção. Esta deriva da classe [MyException]. Por conseguinte, é utilizada da seguinte forma: [raise ImpôtsError(code_erreur, msg_erreur)].

15.1.1.2. A classe [AdminData]

A classe [AdminData] encapsula as constantes envolvidas no cálculo do imposto:


from BaseEntity import BaseEntity


# dados da administração fiscal
class AdminData(BaseEntity):
    # chaves excluídas do estado da classe
    excluded_keys = []

    # chaves autorizadas
    @staticmethod
    def get_allowed_keys() -> list:
        return [
            "limites",
            "coeffr",
            "coeffn",
            "plafond_qf_demi_part",
            "plafond_revenus_celibataire_pour_reduction",
            "plafond_revenus_couple_pour_reduction",
            "valeur_reduc_demi_part",
            "plafond_decote_celibataire",
            "plafond_decote_couple",
            "plafond_impot_couple_pour_decote",
            "plafond_impot_celibataire_pour_decote",
            "abattement_dixpourcent_max",
            "abattement_dixpourcent_min"
        ]
  • linha 5: a classe [AdminData] estende a classe [BaseEntity] descrita no parágrafo |BaseEntity|. Recorde-se que as classes que estendem a classe [BaseEntity] devem definir:
    • um atributo de classe [excluded_keys] (linha 7) que enumera as propriedades do objeto excluídas quando este é transformado num dicionário;
    • um método estático [get_allowed_keys] (linhas 10-26) que devolve a lista das propriedades aceites quando o objeto é inicializado com um dicionário;

Não foram utilizados setters para verificar a validade dos dados utilizados para inicializar um objeto [AdminData]. Com efeito, este objeto é único e definido por configuração, pelo que não é suscetível de conter erros.

15.1.1.3. A classe [TaxPayer]

A classe [TaxPayer] irá modelar um contribuinte:


# importações
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError


# um contribuinte
class TaxPayer(BaseEntity):
    # modela um contribuinte
    # id: identificador
    # casado: sim / não
    # filhos: o número de filhos
    # salário: o seu salário anual
    # imposto: montante do imposto a pagar
    # sobretaxa: sobretaxa do imposto a pagar
    # abatimento: abatimento do imposto a pagar
    # redução: redução do imposto a pagar
    # taxa: taxa de imposto do contribuinte

    # chaves excluídas do relatório da classe
    excluded_keys = []

    # chaves autorizadas
    @staticmethod
    def get_allowed_keys() -> list:
        return ['id', 'marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']

    # propriedades
    @property
    def marié(self) -> str:
        return self.__marié

    @property
    def enfants(self) -> int:
        return self.__enfants

    @property
    def salaire(self) -> int:
        return self.__salaire

    @property
    def impôt(self) -> int:
        return self.__impôt

    @property
    def surcôte(self) -> int:
        return self.__surcôte

    @property
    def décôte(self) -> int:
        return self.__décôte

    @property
    def réduction(self) -> int:
        return self.__réduction

    @property
    def taux(self) -> float:
        return self.__taux

    # setter
    @marié.setter
    def marié(self, marié: str):
        ok = isinstance(marié, str)
        if ok:
            marié = marié.strip().lower()
            ok = marié == "oui" or marié == "non"
        if ok:
            self.__marié = marié
        else:
            raise ImpôtsError(31, f"l'attribut marié [{marié}] doit avoir l'une des valeurs oui / non")

    @enfants.setter
    def enfants(self, enfants):
        # filhos deve ser um número inteiro >=0
        try:
            enfants = int(enfants)
            erreur = enfants < 0
        except:
            erreur = True
        if not erreur:
            self.__enfants = enfants
        else:
            raise ImpôtsError(32, f"L'attribut enfants [{enfants}] doit être un entier >=0")

    @salaire.setter
    def salaire(self, salaire):
        # salário deve ser um número inteiro >=0
        try:
            salaire = int(salaire)
            erreur = salaire < 0
        except:
            erreur = True
        if not erreur:
            self.__salaire = salaire
        else:
            raise ImpôtsError(33, f"L'attribut salaire [{salaire}] doit être un entier >=0")

    @impôt.setter
    def impôt(self, impôt):
        # imposto deve ser um número inteiro >=0
        try:
            impôt = int(impôt)
            erreur = impôt < 0
        except:
            erreur = True
        if not erreur:
            self.__impôt = impôt
        else:
            raise ImpôtsError(34, f"L'attribut impôt [{impôt}] doit être un nombre >=0")

    @décôte.setter
    def décôte(self, décôte):
        # o desconto deve ser um número inteiro >= 0
        try:
            décôte = int(décôte)
            erreur = décôte < 0
        except:
            erreur = True
        if not erreur:
            self.__décôte = décôte
        else:
            raise ImpôtsError(35, f"L'attribut décôte [{décôte}] doit être un nombre >=0")

    @surcôte.setter
    def surcôte(self, surcôte):
        # o sobrepreço deve ser um número inteiro >= 0
        try:
            surcôte = int(surcôte)
            erreur = surcôte < 0
        except:
            erreur = True
        if not erreur:
            self.__surcôte = surcôte
        else:
            raise ImpôtsError(36, f"L'attribut surcôte [{surcôte}] doit être un nombre >=0")

    @réduction.setter
    def réduction(self, réduction):
        # o acréscimo deve ser um número inteiro >= 0
        try:
            réduction = int(réduction)
            erreur = réduction < 0
        except:
            erreur = True
        if not erreur:
            self.__réduction = réduction
        else:
            raise ImpôtsError(37, f"L'attribut réduction [{réduction}] doit être un nombre >=0")

    @taux.setter
    def taux(self, taux):
        # a taxa deve ser um número real >= 0
        try:
            taux = float(taux)
            erreur = taux < 0
        except:
            erreur = True
        if not erreur:
            self.__taux = taux
        else:
            raise ImpôtsError(38, f"L'attribut taux [{taux}] doit être un nombre >=0")

Notas:

  • a classe [TaxPayer] encapsula um contribuinte;
  • linha 7: a classe [TaxPayer] deriva da classe [BaseEntity]. Por conseguinte, tem um identificador [id];
  • linha 20: nenhuma propriedade é excluída do estado de um objeto [AdminData];
  • linhas 22-25: as propriedades da classe. Estas são explicadas nas linhas 9-17;
  • linhas 27-58: getters dos atributos da classe;
  • linhas 60-161: os setters dos atributos da classe. Recorde-se que a vantagem de uma classe que encapsula dados em relação a um simples dicionário é que a classe pode verificar a validade das suas propriedades através dos seus setters;

15.1.2. A camada [dao]

Image

Vamos reunir as implementações das camadas numa pasta [services]. Estas classes irão implementar as interfaces definidas na pasta [interfaces].

Image

15.1.2.1. A interface [InterfaceImpôtsDao]

A camada [dao] irá implementar a seguinte interface [InterfaceImpôtsDao] (ficheiro InterfaceImpôtsDao.py):


# importações
from abc import ABC, abstractmethod


# interface IImpôtsDao
from AdminData import AdminData


class InterfaceImpôtsDao(ABC):
    # lista das faixas de imposto
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

    # lista de dados dos contribuintes
    @abstractmethod
    def get_taxpayers_data(self) -> dict:
        pass

    # registo dos resultados do cálculo do imposto
    @abstractmethod
    def write_taxpayers_results(self, taxpayers_results: list):
        pass

A interface define três métodos:

  • [get_admindata]: é o método que obtém a tabela das faixas de imposto. Note-se que não são fornecidas quaisquer informações sobre a forma de obter estes dados. Posteriormente, estes serão encontrados primeiro num ficheiro de texto e, em seguida, numa base de dados. Caberá às classes que implementam a interface adaptarem-se ao modo de armazenamento dos dados. Teremos, portanto, uma classe para recuperar as faixas de imposto de um ficheiro de texto e outra para as recuperar de uma base de dados. Ambas implementarão o método [get_admindata];
  • [get_taxpayers_data]: é o método que obtém os dados dos contribuintes. Mais uma vez, não especificamos onde esses dados se encontram. Abordaremos apenas o caso em que se encontram num ficheiro de texto;
  • [write_taxpayers_results]: é o método que irá armazenar os resultados do cálculo do imposto. Não especificamos onde. Abordaremos apenas o caso em que os resultados são armazenados num ficheiro de texto. O parâmetro [taxpayers_results] será a lista dos resultados a armazenar;

15.1.2.2. A classe [AbstractImpôtsDao]

A camada [dao] será implementada por duas classes:

  • uma irá buscar os dados (contribuintes, resultados, escalões de imposto) em ficheiros de texto;
  • a outra irá buscar os dados (contribuintes, resultados) em ficheiros de texto e as faixas de imposto numa base de dados;

As duas classes diferirão apenas na gestão das faixas de imposto. Os dados dos contribuintes e os resultados dos cálculos do imposto serão, por sua vez, geridos da mesma forma. Por este motivo, iremos geri-los numa classe pai [AbstractImpôtsDao]. A particularidade da gestão das faixas de imposto será, por sua vez, gerida em duas classes filhas:

  • a classe [ImpôtsDaoWithAdminDataInJsonFile] irá buscar as faixas de imposto a partir de um ficheiro de texto no formato jSON;
  • a classe [ImpôtsDaoWithAdminDataInDatabase] irá buscar as faixas de imposto a partir de uma base de dados;

A classe pai [AbstractImpôtsDao] será a seguinte:


# importações
import codecs
import json
from abc import abstractmethod

from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDao import InterfaceImpôtsDao
from TaxPayer import TaxPayer


# classe base para a camada [dao]
class AbstractImpôtsDao(InterfaceImpôtsDao):
    # os contribuintes e os respetivos impostos serão incluídos em ficheiros de texto
    # construtor
    def __init__(self, config: dict):
        # config[taxpayersFilename]: o nome do ficheiro de texto dos contribuintes
        # config[resultsFilename]: o nome do ficheiro jSON dos resultados
        # config[errorsFilename]: o nome do ficheiro de erros

        #: os parâmetros são guardados
        self.taxpayers_filename = config.get("taxpayersFilename")
        self.taxpayers_results_filename = config.get("resultsFilename")
        self.errors_filename = config.get("errorsFilename")

    # ------------------
    # interface IImpôtsDao
    # ------------------

    # lista de dados dos contribuintes
    def get_taxpayers_data(self) -> dict:
        

    # registo do imposto dos contribuintes
    def write_taxpayers_results(self, taxpayers: list):
        

    # leitura das faixas de imposto
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass
  • linha 13: a classe [AbstractImpôtsDao] implementa a interface [InterfaceImpôtsDao]. Assim, encontram-se os três métodos desta interface:

    • [get_taxpayers_data]: linha 31;
    • [write_taxpayers_results]: linha 35;
    • [get_admindata]: linha 40. Este método não será implementado pela classe [AbstractImpôtsDao], pelo que é declarado como abstrato (linha 39);
  • linha 16: o construtor recebe um dicionário [config] contendo as seguintes informações:

    • [taxpayersFilename]: o nome do ficheiro de texto que contém os dados dos contribuintes;
    • [resultsFilename]: o nome do ficheiro de texto no qual serão gravados os resultados;
    • [errorsFilename]: o nome do ficheiro de texto que enumera os erros encontrados durante o processamento do ficheiro [taxpayersFilename];

O método [get_taxpayers_data] é o seguinte:


    # lista de dados dos contribuintes
    def get_taxpayers_data(self) -> dict:
        # inicializações
        taxpayers_data = []
        datafile = None
        erreurs = []
        try:
            # abertura do ficheiro de dados
            datafile = open(self.taxpayers_filename, "r")
            # processamento da linha atual do ficheiro
            ligne = datafile.readline()
            # n.º da linha
            numligne = 0
            while ligne != '':
                # uma linha de +
                numligne += 1
                # removem-se os espaços
                ligne = ligne.strip()
                # ignoram-se as linhas vazias e os comentários
                if ligne != "" and ligne[0] != "#":
                    try:
                        # recuperam-se os 4 campos id, casado, filhos, salário que constituem a linha do contribuinte
                        (id, marié, enfants, salaire) = ligne.split(",")
                        # cria-se um novo TaxPayer
                        taxpayers_data.append(
                            TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
                    except BaseException as erreur:
                        # regista-se o erro
                        erreurs.append(f"Ligne {numligne}, {erreur}")
                # lê-se uma nova linha de contribuinte
                ligne = datafile.readline()
            # registam-se os erros, caso existam
            if erreurs:
                text = f"Analyse du fichier {self.taxpayers_filename}\n\n" + "\n".join(erreurs)
                with codecs.open(self.errors_filename, "w", "utf-8") as fd:
                    fd.write(text)
            # retorna-se o resultado
            return {"taxpayers": taxpayers_data, "erreurs": erreurs}
        except BaseException as erreur:
            # lança-se uma exceção ImpôtsError
            raise ImpôtsError(11, f"{erreur}")
        finally:
            # fecha-se o ficheiro
            if datafile:
                datafile.close()
  • linha 4: os dados dos contribuintes (estado civil, filhos, salário) serão colocados numa lista de objetos do tipo [TaxPayer];
  • linhas 8-9: abre-se o ficheiro de texto dos contribuintes em modo de leitura. O seu conteúdo tem o seguinte formato:
# dados válidos: identificação, casado, filhos, salário
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# podem existir linhas vazias

# criam-se linhas com erros
# valores insuficientes
11,12
# valores em excesso
12,oui,3,200000, x, y
# valores errados
x,x,x,x

Em relação às versões anteriores:

  • cada linha do ficheiro [taxpayersFilename] começa com o identificador do contribuinte, um simples número;
  • são permitidos comentários e linhas em branco;
  • vamos tratar os erros. Assim, as linhas 17, 19 e 21 devem ser declaradas como erradas. Os erros são registados num ficheiro separado;

Continuemos a análise do código:

  • linha 4: os dados do ficheiro de texto são transferidos para a lista [taxPayersData];
  • linhas 14-31: o ficheiro dos contribuintes é lido linha a linha;
  • linha 14: chega-se ao fim do ficheiro quando se lê uma linha vazia (nada — nem sequer o símbolo de fim de linha \r\n);
  • linha 20: as linhas vazias e os comentários são ignorados. Uma linha é considerada um comentário se, após a remoção dos espaços em branco à frente e atrás do texto, o primeiro carácter for o carácter #;
  • linha 24: uma linha correta é composta por quatro campos separados por uma vírgula. Recuperamos esses campos. A atribuição de dados a uma tupla de quatro elementos falha se não houver exatamente quatro dados atribuídos;
  • linha 25: se um dos quatro campos recuperados [id, marié, enfants, salaire] for inválido, então o método [BaseEntity.fromdict] lançará uma exceção do tipo [MyException];
  • linhas 25-26: um objeto [TaxPayer] é adicionado à lista [taxpayers_data] de contribuintes;
  • linhas 27-29: os eventuais erros são acumulados numa lista [erreurs]. Esta lista foi criada na linha 6;
  • linhas 33-36: a lista de erros encontrados é registada no ficheiro de texto [errorsFilename]. Existem dois tipos de erros:
    • uma linha não tinha o número correto de campos esperados;
    • as informações da linha estavam erradas e não foi possível criar um objeto [TaxPayer];
  • linhas 39-41: qualquer erro (BaseException) é interceptado e reportado, encapsulando-o num tipo [ImpôtsError];
  • linhas 42-45: em todos os casos, seja de sucesso ou de falha, o ficheiro de texto dos contribuintes é fechado, caso tenha sido aberto;

O método [write_taxpayers_results] deve produzir um ficheiro jSON com o seguinte formato:


[
  {
    "id": 1,
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555,
    "impôt": 2814,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 0,
    "réduction": 0
  },
  {
    "id": 2,
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000,
    "impôt": 1384,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 384,
    "réduction": 347
  },
  {
    "id": 3,
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000,
    "impôt": 0,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 720,
    "réduction": 0
  },

]

O método [write_taxpayers_results] é o seguinte:


    # registo do imposto dos contribuintes
    def write_taxpayers_results(self, taxpayers: list):
        # registo dos resultados num ficheiro jSON
        # contribuintes: lista de objetos do tipo TaxPayer
        # (ID, casado, filhos, salário, imposto, sobretaxa, abatimento, redução, taxa)
        # a lista [taxpayers] está gravada no ficheiro de texto [self.taxpayers_results_filename]
        file = None
        try:
            # abertura do ficheiro de resultados
            file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
            # criação da lista a serializar em jSON
            mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
            # serialização jSON
            json.dump(list(mapping), file, ensure_ascii=False)
        except BaseException as erreur:
            # o erro é reenviado com outro tipo
            raise ImpôtsError(12, f"{erreur}")
        finally:
            # fecha-se o ficheiro, caso tenha sido aberto
            if file:
                file.close()
  • linha 2: o método recebe uma lista de contribuintes [taxpayers] que deve registar no ficheiro de texto [self.taxpayers_results_filename] no formato jSON;
  • linha 10: criação do ficheiro UTF-8 com os resultados;
  • linha 12: introduzimos aqui a função [map], cuja sintaxe neste contexto é [map (fonction, liste1)]. A função [fonction] é aplicada a cada elemento de [liste1] e produz um novo elemento que alimenta uma lista [liste2]. Por fim, para cada i:

liste2[i]=fonction(liste1[i])

Aqui, [liste1] é a lista [taxPayers], uma lista de objetos do tipo [TaxPayer]. A função [fonction] é aqui expressa sob a forma de uma função denominada [lambda], que expressa a transformação efetuada num elemento [taxpayer] da lista [taxpayers]: cada elemento [taxpayer] é substituído pelo seu dicionário [taxpayer.asdict()]. Por fim, a lista [liste2] obtida é a lista dos dicionários dos elementos da lista [taxpayers];

  • linha 12: o resultado devolvido pela função [map] não é a lista [liste2], mas sim um objeto do tipo [map]. Para obter [liste2], é necessário utilizar a expressão [list(mapping)] (linha 14);
  • linha 14: a lista [liste2] é gravada no formato jSON no ficheiro [self.taxpayers_results_filename];
  • linhas 15-17: qualquer tipo de exceção é interceptada e encapsulada num erro do tipo [ImpôtsError] antes de ser relançada (linha 17);
  • linhas 19-21: em todos os casos, seja de sucesso ou de falha, o ficheiro de resultados é fechado, caso tenha sido aberto;

15.1.2.3. Classe [ImpôtsDaoWithAdminDataInJsonFile]

A classe [ImpôtsDaoWithAdminDataInJsonFile] irá derivar da classe [AbstractImpôtsDao] e implementar o método [getAdminData] que a sua classe pai não implementou. Irá buscar os dados da administração fiscal num ficheiro jSON:


{
    "limites": [9964, 27519, 73779, 156244, 0],
    "coeffr": [0, 0.14, 0.3, 0.41, 0.45],
    "coeffn": [0, 1394.96, 5798, 13913.69, 20163.45],
    "plafond_qf_demi_part": 1551,
    "plafond_revenus_celibataire_pour_reduction": 21037,
    "plafond_revenus_couple_pour_reduction": 42074,
    "valeur_reduc_demi_part": 3797,
    "plafond_decote_celibataire": 1196,
    "plafond_decote_couple": 1970,
    "plafond_impot_couple_pour_decote": 2627,
    "plafond_impot_celibataire_pour_decote": 1595,
    "abattement_dixpourcent_max": 12502,
    "abattement_dixpourcent_min": 437
}

A classe [ImpôtsDaoWithAdminDataInJsonFile] é a seguinte:


# importações
import codecs
import json

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError


# uma implementação da camada [dao], em que os dados da administração fiscal se encontram num ficheiro jSON
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
    # construtor
    def __init__(self, config: dict):
        # config[admindataFilename]: o nome do ficheiro jSON que contém os dados da administração fiscal
        # config[taxpayersFilename]: o nome do ficheiro de texto dos contribuintes
        # config[resultsFilename]: o nome do ficheiro jSON dos resultados
        # config[errorsFilename]: o nome do ficheiro de erros

        # inicialização da classe Parent
        AbstractImpôtsDao.__init__(self, config)
        # leitura dos dados da administração fiscal
        file = None
        try:
            # abertura do ficheiro jSON com os dados fiscais em modo de leitura
            file = codecs.open(config["admindataFilename"], "r", "utf8")
            # transferência do conteúdo do ficheiro jSON para um objeto [AdminData]
            self.admindata = AdminData().fromdict(json.load(file))
        except BaseException as erreur:
            # o erro é reenviado sob a forma de um tipo [ImpôtsError]
            raise ImpôtsError(21, f"{erreur}")
        finally:
            # fecho do ficheiro, caso este tenha sido aberto
            if file:
                file.close()

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

    # recuperação dos dados da administração fiscal
    # o método devolve um objeto [AdminData]
    def get_admindata(self) -> AdminData:
        return self.admindata
  • linha 11: a classe [ImpôtsDaoWithAdminDataInJsonFile] herda da classe [AbstractImpôtsDao]. Como tal, implementa a interface [InterfaceImpôtsDao];
  • linha 13: o construtor recebe como parâmetro um dicionário que contém as informações das linhas 14 a 17;
  • linha 20: a classe pai é inicializada;
  • linha 24: abertura do ficheiro jSON com os dados da administração fiscal;
  • linha 25: o ficheiro UTF-8 com os dados da administração fiscal é aberto;
  • linha 27: o conteúdo do ficheiro é lido e colocado no objeto [self.admindata] do tipo [AdminData]. É necessário que as chaves do ficheiro jSON correspondam às propriedades aceites para um objeto [AdminData]; caso contrário, o método [fromdict] lançará uma exceção;
  • linhas 28-30: gestão de exceções. As exceções que podem ocorrer são encapsuladas num tipo [ImpôtsError] antes de serem relançadas;
  • linhas 32-34: o ficheiro é fechado, caso tenha sido aberto;
  • linhas 42-43: implementação do método [get_admindata] da interface [InterfaceImpôtsDao];

15.1.3. A camada [métier]

Image

15.1.3.1. A interface [InterfaceImpôtsMétier]

A interface da camada [métier] será a seguinte:


# importações
from abc import ABC, abstractmethod

from AdminData import AdminData
from TaxPayer import TaxPayer


# interface IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
    # cálculo do imposto para 1 contribuinte
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        pass
  • A interface [InterfaceImpôtsMétier] define um único método:
  • linha 12: o método [calculate_tax] permite calcular o imposto de um único contribuinte [taxpayer]. [admindata] é o objeto [AdminData] que encapsula os dados da administração fiscal;
  • linha 12: o método [calculate_tax] não devolve qualquer resultado. Os dados obtidos (imposto, sobretaxa, abatimento, redução, taxa) estão incluídos no parâmetro [taxpayer]: antes da chamada, estes atributos estão vazios; após a chamada, foram inicializados;

15.1.3.2. A classe [ImpôtsMétier]

A classe [ImpôtsMétier] implementa a interface [InterfaceImpôtsMétier] da seguinte forma:

Image

Os métodos da classe provêm do módulo [impôts_module_02] do parágrafo |O módulo [impots.v02.modules.impôts_module_02]|. Limitámos apenas os parâmetros dos métodos a dois:

  • taxpayer(id, casado, filhos, salário, imposto, dedução, sobretaxa, redução, taxa): o objeto que representa um contribuinte e o seu imposto;
  • admindata: o objeto que encapsula os dados da administração fiscal;

Mostramos num método as alterações assim introduzidas;


    # cálculo do imposto - fase 1
    # ----------------------------------------
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        # contribuinte (identificação, estado civil, filhos, salário, imposto, dedução, majoração, redução, taxa)
        # dados administrativos: dados da administração fiscal

        # cálculo do imposto com filhos
        self.calculate_tax_2(taxpayer, admindata)
        # os resultados encontram-se em «contribuinte»
        taux1 = taxpayer.taux
        surcôte1 = taxpayer.surcôte
        impot1 = taxpayer.impôt
        # cálculo do imposto sem filhos
        if taxpayer.enfants != 0:
            # cálculo do imposto para o mesmo contribuinte sem filhos
            taxpayer2 = TaxPayer().fromdict(
                {'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
            self.calculate_tax_2(taxpayer2, admindata)
            # os resultados encontram-se em «taxpayer2»
            taux2 = taxpayer2.taux
            surcôte2 = taxpayer2.surcôte
            impot2 = taxpayer2.impôt
            # aplicação do limite máximo do quociente familiar
            if taxpayer.enfants < 3:
                # PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos
                impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
            else:
                # PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos, o dobro para os seguintes
                impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
                         * 2 * admindata.plafond_qf_demi_part
        else:
            # se o contribuinte não tiver filhos, então imposto2 = imposto1
            impot2 = impot1

        # considera-se o imposto mais elevado, com a taxa e a sobretaxa correspondentes
        (impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
            impot2, impot2 - impot1 + surcôte2, taux2)

        # resultados parciais
        taxpayer.impôt = impot
        taxpayer.surcôte = surcôte
        taxpayer.taux = taux
        # cálculo de uma eventual redução
        self.get_décôte(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.décôte
        # cálculo de uma eventual redução de impostos
        self.get_réduction(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.réduction
        # resultado
        taxpayer.impôt = math.floor(taxpayer.impôt)
  • linha 3: o método [calculate_tax] é o único método da interface [InterfaceImpôtsMétier]. Aceita dois parâmetros:
    • [tapPayer]: o contribuinte cujo imposto está a ser calculado;
    • [admindata]: o objeto que encapsula os dados da administração fiscal;
    • os resultados do cálculo são encapsulados no parâmetro [taxpayer] (linhas 40-50). O conteúdo deste objeto não é, portanto, o mesmo antes e depois da chamada ao método;

15.1.4. Testes das camadas [dao] e [métier]

Image

  • [TestDaoMétier] é a classe de teste UnitTest das camadas [dao] e [métier];
  • [config] é o ficheiro de configuração dos testes;

A configuração [config] é a seguinte:


def configure():
    import os

    # etapa 1 ------
    # configuração do caminho do Python

    # pasta deste ficheiro
    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 da aplicação
    absolute_dependencies = [
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
        f"{root_dir}/02/entities",
    ]

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

    # etapa 2 ------
    # configuração da aplicação
    config = {
        # caminhos absolutos dos ficheiros da aplicação
        "admindataFilename"f"{script_dir}/../data/input/admindata.json"
    }

    # instanciação das camadas da aplicação
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    métier = ImpôtsMétier()

    # colocamos as instâncias das camadas na configuração
    config["dao"] = dao
    config["métier"] = métier

    # carregamos a configuração
    return config
  • linhas 4-23: configura-se o Python Path dos testes;
  • linhas 32-41: instanciam-se as camadas [dao] e [métier]. As respetivas referências são inseridas no dicionário [config];
  • linha 44: devolve-se este dicionário;

A classe de teste [TestDaoMétier] é a seguinte:


import unittest


def get_config() -> dict:
    # configuração da aplicação
    import config
    # a configuração é devolvida
    return config.configure()


class TestDaoMétier(unittest.TestCase):

    # executada antes de cada método test_
    def setUp(self) -> None:
        # recuperamos a configuração dos testes
        config = get_config()
        # armazenamos algumas informações
        self.métier = config['métier']
        self.admindata = config['dao'].get_admindata()

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'sim', 'filhos': 2, 'salário': 55555,
        # 'imposto': 2814, 'majoração': 0, 'redução': 0, 'abatimento': 0, 'taxa': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificação
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_2(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'sim', 'filhos': 2, 'salário': 50000,
        # 'imposto': 1384, 'sobretaxa': 0, 'desconto': 384, 'redução': 347, 'taxa': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertAlmostEqual(taxpayer.impôt, 1384, delta=1)
        self.assertAlmostEqual(taxpayer.décôte, 384, delta=1)
        self.assertAlmostEqual(taxpayer.réduction, 347, delta=1)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_3(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'sim', 'filhos': 3, 'salário': 50000,
        # 'imposto': 0, 'sobretaxa': 0, 'desconto': 720, 'redução': 0, 'taxa': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertEqual(taxpayer.impôt, 0)
        self.assertAlmostEqual(taxpayer.décôte, 720, delta=1)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_4(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'não', 'filhos': 2, 'salário': 100000,
        # 'imposto': 19884, 'sobretaxa': 4480, 'desconto': 0, 'redução': 0, 'taxa': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertAlmostEqual(taxpayer.impôt, 19884, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 4480, delta=1)

    def test_5(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'não', 'filhos': 3, 'salário': 100000,
        # 'imposto': 16782, 'sobretaxa': 7176, 'desconto': 0, 'redução': 0, 'taxa': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertAlmostEqual(taxpayer.impôt, 16782, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 7176, delta=1)

    def test_6(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'sim', 'filhos': 3, 'salário': 100000,
        # 'imposto': 9200, 'sobretaxa': 2180, 'desconto': 0, 'redução': 0, 'taxa': 0,3}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertAlmostEqual(taxpayer.impôt, 9200, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.3, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 2180, delta=1)

    def test_7(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'sim', 'filhos': 5, 'salário': 100000,
        # 'imposto': 4230, 'sobretaxa': 0, 'desconto': 0, 'redução': 0, 'taxa': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertAlmostEqual(taxpayer.impôt, 4230, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_8(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'não', 'filhos': 0, 'salário': 100000,
        # 'imposto': 22986, 'sobretaxa': 0, 'desconto': 0, 'redução': 0, 'taxa': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertAlmostEqual(taxpayer.impôt, 22986, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_9(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'sim', 'filhos': 2, 'salário': 30000,
        # 'imposto': 0, 'sobretaxa': 0, 'desconto': 0, 'redução': 0, 'taxa': 0}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertEqual(taxpayer.impôt, 0)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.0, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_10(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'não', 'filhos': 0, 'salário': 200000,
        # 'imposto': 64210, 'sobretaxa': 7498, 'desconto': 0, 'redução': 0, 'taxa': 0,45}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertAlmostEqual(taxpayer.impôt, 64210, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.45, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 7498, delta=1)

    def test_11(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'sim', 'filhos': 3, 'salário': 200000,
        # 'imposto': 42842, 'sobretaxa': 17283, 'desconto': 0, 'redução': 0, 'taxa': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # verificações
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)


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

Comentários

  • linha 11: a classe de teste estende a classe [unittest.TestCase];
  • linhas 13-19: num teste UnitTest, o método [setUp] é executado antes de cada um dos métodos [test_];
  • linha 16: recupera-se a configuração proveniente do script [config] analisado anteriormente;
  • linha 18: armazena-se uma referência na camada [métier];
  • linha 19: solicita-se à camada [dao] o objeto [AdminData] que encapsula os dados da administração fiscal e este é memorizado;
  • linhas 21-173: 11 testes cujos resultados foram verificados no site oficial das finanças de 2019 |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm|;
  • linhas 21-33: todos os testes foram elaborados seguindo o mesmo modelo;
  • linha 22: importa-se a classe [TaxPayer];
  • linha 24: contribuinte testado;
  • linha 25: resultados esperados;
  • linha 26: criação do objeto [TaxPayer] do contribuinte;
  • linha 27: cálculo do seu imposto. O resultado encontra-se em [taxpayer];
  • linhas 29-33: verificação dos resultados obtidos;
  • linha 29: verifica-se o montante do imposto com uma precisão de um euro. Os testes demonstraram, de facto, que os resultados obtidos pelo algoritmo deste documento podiam diferir dos valores oficiais em, no máximo, 1 euro;

A execução dos testes fornece os seguintes resultados:

Image

Testing started at 16:08 ...
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/impots/v04/tests/TestDaoMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/tests/TestDaoMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v04\tests



Ran 11 tests in 0.055s

OK

Process finished with exit code 0

15.1.5. Script principal

Image

O script principal é configurado pelo seguinte script [config]:


def configure():
    import os

    # etapa 1 ------
    # configuração do caminho do Python

    # pasta deste ficheiro
    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 da aplicação
    absolute_dependencies = [
        # dependências locais
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

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

    # etapa 2 ------
    # configuração da aplicação
    config = {
        # caminhos absolutos dos ficheiros da aplicação
        "taxpayersFilename"f"{script_dir}/../../data/input/taxpayersdata.txt",
        "resultsFilename"f"{script_dir}/../../data/output/résultats.json",
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json",
        "errorsFilename"f"{script_dir}/../../data/output/errors.txt"
    }

    # instanciação das camadas da aplicação
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    métier = ImpôtsMétier()

    # colocamos as instâncias das camadas na configuração
    config["dao"] = dao
    config["métier"] = métier

    # carregamos a configuração
    return config

É semelhante ao utilizado para o teste das camadas [métier] e [dao].

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


# configuramos a aplicação
import config

config = config.configure()

# importações
from ImpôtsError import ImpôtsError

# recuperam-se as camadas da aplicação (já estão instanciadas)
dao = config["dao"]
métier = config["métier"]

try:
    # recuperação dos escalões de imposto
    admindata = dao.get_admindata()
    # leitura dos dados dos contribuintes
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # dos contribuintes?
    if not taxpayers:
        raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # cálculo do imposto dos contribuintes
    for taxpayer in taxpayers:
        # O contribuinte é simultaneamente um parâmetro de entrada e de saída
        # o «contribuinte» vai ser alterado
        métier.calculate_tax(taxpayer, admindata)
    # gravação dos resultados num ficheiro de texto
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    # exibição do erro
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # concluído
    print("Travail terminé...")

Notas

  • linhas 2-4: recupera-se a configuração da aplicação. Sabe-se também que o Python Path da aplicação foi construído;
  • linhas 9-11: recuperam-se referências às camadas [métier] e [dao];
  • linha 15: obtêm-se os dados da administração fiscal;
  • linha 17: obtém-se a lista de contribuintes cujo imposto deve ser calculado;
  • linhas 19-20: se esta lista estiver vazia, é lançada uma exceção;
  • linhas 22-25: cálculo do imposto dos diferentes objetos [taxpayer] através da camada [métier];
  • linha 27: [taxpayers] é agora uma lista de objetos [TaxPayer] em que os atributos (imposto, desconto, sobretaxa, redução, taxa) receberam um valor. Esta lista é gravada num ficheiro jSON;
  • linhas 28-30: deteção de um eventual erro;
  • linhas 31-33: executadas em todos os casos;

A execução do script produz os mesmos resultados que nas versões anteriores. O ficheiro de erros dos contribuintes foi uma novidade nesta versão. Após a execução do script [main], o seu conteúdo é o seguinte:


Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v04\main\01/../../data/input/taxpayersdata.txt

Ligne 17, not enough values to unpack (expected 4, got 2)
Ligne 19, too many values to unpack (expected 4)
Ligne 21, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]

As linhas com erros eram as seguintes:

# dados válidos: id, casado, filhos, salário
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# podem existir linhas vazias

# são criadas linhas com erros
# valores insuficientes
11,12
# valores em excesso
12,oui,3,200000, x, y
# valores errados
x,x,x,x

15.2. Versão 4 – aplicação 2

Nesta versão, é o utilizador que introduz a lista de contribuintes através do teclado. A arquitetura da aplicação será a seguinte:

Image

Surge um novo módulo: a camada [ui] (Interface do Utilizador), que irá interagir com o utilizador. Esta camada terá uma interface e será implementada por uma classe.

Image

15.2.1. A interface [InterfaceImpôtsUi]


# importações
from abc import ABC, abstractmethod


# interface InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
    # execução da classe que implementa a interface
    @abstractmethod
    def run(self):
        pass

A interface [InterfaceImpôtsUi] terá apenas um método, o das linhas 8-10. A interface será aqui implementada com uma aplicação de consola, mas também poderia ser implementada com uma interface gráfica. Os parâmetros passados ao método [run] não seriam os mesmos nas duas implementações. Para contornar este problema, o método habitual consiste em:

  • não passar parâmetros ao método [run] (ou o mínimo de parâmetros);
  • passar parâmetros ao construtor da classe que implementa a interface. Estes podem variar de uma implementação para outra. Estes parâmetros são registados como atributos da classe;
  • assegurar que o método [run] utilize esses atributos de classe (self.x);

Este método permite dispor de uma interface muito geral, que é especificada pelos parâmetros dos construtores de cada classe de implementação. Este método já foi utilizado na versão modular n.º 1.

15.2.2. A classe [ImpôtsConsole]

A classe [ImpôtsConsole] implementa a interface [InterfaceImpôtsUi] da seguinte forma:


# importações
import re

from InterfaceImpôtsUi import InterfaceImpôtsUi
from TaxPayer import TaxPayer


# camada [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
    # construtor
    def __init__(self, config: dict):
        # armazenamos os parâmetros
        self.admindata = config['dao'].get_admindata()
        self.métier = config['métier']

    def run(self):
        # diálogo interativo com o utilizador
        fini = False
        while not fini:
            # o contribuinte é casado?
            marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # verificação da validade dos dados introduzidos
            while marié != "oui" and marié != "non" and marié != "*":
                # mensagem de erro
                print("Tapez oui ou non ou *")
                # repetir a pergunta
                marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # Concluído?
            if marié == "*":
                # diálogo concluído
                return
            # número de filhos
            enfants = input("Nombre d'enfants : ").strip()
            # verificação da validade dos dados introduzidos
            if not re.match(r"^\d+$", enfants):
                # mensagem de erro
                print("Tapez un nombre entier positif ou nul")
                # recomeçar
                enfants = input("Nombre d'enfants : ").strip()
            # salário anual
            salaire = input("Salaire annuel : ").strip()
            # verificação da validade dos dados introduzidos
            if not re.match(r"^\d+$", salaire):
                # mensagem de erro
                print("Tapez un nombre entier positif ou nul")
                # repetir
                salaire = input("Salaire annuel : ").strip()
            # cálculo do imposto
            taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
            self.métier.calculate_tax(taxpayer, self.admindata)
            # exibição
            print(f"Impôt du contribuable = {taxpayer}\n\n")
            # próximo contribuinte
  • linha 9: a classe [ImpôtsConsole] implementa a interface [InterfaceImpôtsUi];
  • linha 11: o construtor da classe recebe um parâmetro, o dicionário [config] da configuração da aplicação;
    • linha 13: recuperam-se os dados da administração fiscal que permitem o cálculo do imposto;
    • linha 14: armazena-se uma referência na camada [métier];
  • linha 16: implementação do método [run] da interface;
  • linhas 19-53: diálogo com o utilizador. Consiste
    • em solicitar as três informações (estado civil, filhos, salário) do contribuinte;
    • calcular o seu imposto;
    • em exibi-lo;
    • o diálogo termina quando o utilizador responde * à primeira pergunta;
  • linhas 20-27: pergunta-se se o contribuinte é casado e verifica-se a validade da resposta;
  • linhas 29-31: se o utilizador tiver respondido «*» à pergunta, o diálogo é interrompido;
  • linhas 32-39: pergunta-se o número de filhos do contribuinte e verifica-se a validade da resposta;
  • linhas 40-47: pergunta-se o salário anual do contribuinte e verifica-se a validade da resposta;
  • linhas 48-50: com estas informações, calcula-se, através da camada [métier], o imposto do contribuinte;
  • linha 52: o montante do imposto é apresentado;

15.2.3. O script principal

O script principal [main] é configurado pelo seguinte ficheiro [config]:


def configure():
    import os

    # etapa 1 ------
    # configuração do caminho do Python

    # pasta deste ficheiro
    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 da aplicação
    absolute_dependencies = [
        # dependências locais
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

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

    # etapa 2 ------
    # configuração da aplicação
    config = {
        # caminhos absolutos dos ficheiros da aplicação
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json",
    }

    # instanciação das camadas da aplicação
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier
    from ImpôtsConsole import ImpôtsConsole

    # camada DAO
    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    # camada de negócios
    métier = ImpôtsMétier()
    # colocamos as instâncias das camadas na configuração
    config["dao"] = dao
    config["métier"] = métier
    # camada UI
    ui = ImpôtsConsole(config)
    config["ui"] = ui

    # geramos a configuração
    return config

O script de coordenação é o seguinte (main.py):


# configuramos a aplicação
import config

config = config.configure()

# importações
from ImpôtsError import ImpôtsError

# recuperamos as camadas da aplicação (já estão instanciadas)
ui = config["ui"]

# código
try:
    # execução da camada [ui]
    ui.run()
except ImpôtsError as erreur:
    # exibe-se a mensagem de erro
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # executado em todos os casos
    print("Travail terminé...")
  • linhas 1-4: recupera-se a configuração da aplicação;
  • linha 10: recupera-se uma referência à camada [ui];
  • linhas 12-21: a estrutura do código é a mesma que na aplicação anterior: código envolto num try/catch para interromper qualquer eventual exceção;
  • linha 15: solicita-se a execução da camada [ui]: inicia-se então o diálogo com o utilizador;
  • linhas 16-18: interceção de uma eventual exceção;

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/impots/v04/main/02/main.py
Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : oui
Nombre d'enfants : 3
Salaire annuel : 200000
Impôt du contribuable = {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}


Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : *
Travail terminé...

Process finished with exit code 0