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 agora o abordamos utilizando classes e interfaces. Escreveremos dois aplicativos:

A aplicação 1 será a seguinte:

Image

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

  • a camada [dao] terá a função de gerenciar dados armazenados em arquivos de texto e, posteriormente, em um banco de dados;
  • a camada [métier] terá a função de calcular o imposto;

Nesta aplicação, não haverá ações por parte do usuário: os dados dos contribuintes serão obtidos de um arquivo de texto cujo nome será fornecido ao módulo [main].

Na aplicação 2, será o usuário quem digitará os dados dos contribuintes. A arquitetura evoluirá, então, da seguinte forma:

Image

  • a camada [dao] (Data Access Object) é responsável pelo acesso aos dados externos
  • a camada [métier] lida com as questões de negócio, neste caso, o cálculo do imposto. Ela 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 usuário.
  • a camada [ui] (Interface do Usuário) lida com as interações com o usuário;
  • [main] é o coordenador;

A seguir, as camadas [dao], [métier] e [ui] serão implementadas, cada uma, por meio de uma classe. As camadas [métier] e [dao] serão as mesmas para as duas aplicações. É por isso que elas foram reunidas em uma mesma 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 contida em um arquivo de texto. Ela possui a seguinte arquitetura:

Image

15.1.1. As entidades

Image

As entidades são classes de dados. Sua função é encapsular dados e fornecer getters/setters que permitem verificar a validade desses dados. As entidades são trocadas entre as camadas. Uma mesma entidade pode partir da camada [ui] e chegar até a camada [dao] e vice-versa.

15.1.1.1. A classe [ImpôtsError]

Utilizaremos 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, elas lançarão essa exceção. Ela deriva da classe [MyException]. Portanto, ela é utilizada da seguinte maneira: [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 relatório 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|. Vale lembrar que as classes que estendem a classe [BaseEntity] devem definir:
    • um atributo de classe [excluded_keys] (linha 7) que lista as propriedades do objeto excluídas quando o objeto é transformado em um dicionário;
    • um método estático [get_allowed_keys] (linhas 10-26) que retorna a lista das propriedades aceitas 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]. De fato, esse objeto é único e definido por configuração e, portanto, não está sujeito a erros.

15.1.1.3. A classe [TaxPayer]

A classe [TaxPayer] 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: seu salário anual
    # imposto: valor do imposto a pagar
    # sobretaxa: sobretaxa do imposto a pagar
    # abatimento: abatimento do imposto a pagar
    # redução: redução sobre o imposto a pagar
    # alíquota: alíquota de imposto do contribuinte

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

    # chaves permitidas
    @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 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):
        # 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 acréscimo 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]. Portanto, ela possui o 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. Vale lembrar que a vantagem de uma classe que encapsula dados em relação a um simples dicionário é que a classe pode verificar a validade de suas propriedades por meio de seus setters;

15.1.2. A camada [dao]

Image

Vamos reunir as implementações das camadas em uma pasta [services]. Essas classes implementarão interfaces definidas na pasta [interfaces].

Image

15.1.2.1. A interface [InterfaceImpôtsDao]

A camada [dao] implementará a seguinte interface [InterfaceImpôtsDao] (arquivo 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

    # lançamento 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 de faixas de imposto. Observe-se que não são fornecidas informações sobre como obter esses dados. Posteriormente, eles serão encontrados primeiro em um arquivo de texto e, em seguida, em um banco de dados. Caberá às classes que implementam a interface se adaptarem ao modo de armazenamento dos dados. Teremos, portanto, uma classe para recuperar as faixas de imposto de um arquivo de texto e outra para recuperá-las de um banco 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 eles serão encontrados. Trataremos apenas o caso em que eles estejam em um arquivo 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 em um arquivo de texto. O parâmetro [taxpayers_results] será a lista dos resultados a serem armazenados;

15.1.2.2. A classe [AbstractImpôtsDao]

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

  • uma irá buscar os dados (contribuintes, resultados, faixas de imposto) em arquivos de texto;
  • a outra irá buscar os dados (contribuintes, resultados) em arquivos de texto e as faixas de imposto em um banco de dados;

As duas classes diferirão apenas no gerenciamento das faixas de imposto. Os dados dos contribuintes e os resultados dos cálculos do imposto serão, por sua vez, gerenciados da mesma forma. Por esse motivo, vamos gerenciá-los em uma classe pai [AbstractImpôtsDao]. A particularidade do gerenciamento das faixas de imposto será, por sua vez, tratada em duas classes filhas:

  • a classe [ImpôtsDaoWithAdminDataInJsonFile] irá buscar as faixas de imposto em um arquivo de texto no formato jSON;
  • a classe [ImpôtsDaoWithAdminDataInDatabase] irá buscar as faixas de imposto em um banco 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 seus impostos estarão em arquivos de texto
    # construtor
    def __init__(self, config: dict):
        # config[taxpayersFilename]: o nome do arquivo de texto dos contribuintes
        # config[resultsFilename]: o nome do arquivo jSON dos resultados
        # config[errorsFilename]: o nome do arquivo de erros

        #: os parâmetros são salvos
        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:
        

    # lançamento 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]. Portanto, encontramos os três métodos dessa interface:

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

    • [taxpayersFilename]: o nome do arquivo de texto que contém os dados dos contribuintes;
    • [resultsFilename]: o nome do arquivo de texto no qual serão gravados os resultados;
    • [errorsFilename]: o nome do arquivo de texto que lista os erros encontrados durante o processamento do arquivo [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 arquivo de dados
            datafile = open(self.taxpayers_filename, "r")
            # processamento da linha atual do arquivo
            ligne = datafile.readline()
            # número da linha
            numligne = 0
            while ligne != '':
                # uma linha de +
                numligne += 1
                # remoção de espaços em branco
                ligne = ligne.strip()
                # ignora-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 compõem 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:
                        # registra-se o erro
                        erreurs.append(f"Ligne {numligne}, {erreur}")
                # lê-se uma nova linha de contribuinte
                ligne = datafile.readline()
            # registram-se os erros, se houver
            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 arquivo
            if datafile:
                datafile.close()
  • linha 4: os dados dos contribuintes (estado civil, filhos, salário) serão colocados em uma lista de objetos do tipo [TaxPayer];
  • linhas 8-9: abre-se o arquivo de texto dos contribuintes em modo de leitura. Seu conteúdo tem o seguinte formato:
# 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
# pode haver linhas vazias

# criamos linhas com erros
# valores insuficientes
11,12
# valores em excesso
12,oui,3,200000, x, y
# valores incorretos
x,x,x,x

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

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

Vamos continuar a análise do código:

  • linha 4: os dados do arquivo de texto são transferidos para a lista [taxPayersData];
  • linhas 14-31: o arquivo dos contribuintes é lido linha por linha;
  • linha 14: chega-se ao fim do arquivo quando se lê uma linha vazia (nada — nem mesmo o caractere 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 à frente e atrás do texto, o primeiro caractere for o símbolo #;
  • linha 24: uma linha correta é composta por quatro campos separados por uma vírgula. Esses campos são recuperados. 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, 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 em uma lista [erreurs]. Essa lista foi criada na linha 6;
  • linhas 33-36: a lista de erros encontrados é registrada no arquivo de texto [errorsFilename]. Existem dois tipos de erros:
    • uma linha não possuía o número correto de campos esperados;
    • as informações da linha estavam incorretas e não foi possível construir um objeto [TaxPayer];
  • linhas 39-41: todo erro (BaseException) é interceptado e reportado, encapsulado em um tipo [ImpôtsError];
  • linhas 42-45: em todos os casos, seja de sucesso ou de falha, o arquivo de texto dos contribuintes é fechado, caso tenha sido aberto;

O método [write_taxpayers_results] deve gerar um arquivo jSON no 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:


    # registro do imposto dos contribuintes
    def write_taxpayers_results(self, taxpayers: list):
        # gravação dos resultados em um arquivo jSON
        # contribuintes: lista de objetos do tipo TaxPayer
        # (ID, casado, filhos, salário, imposto, sobretaxa, abatimento, redução, alíquota)
        # a lista [taxpayers] está gravada no arquivo de texto [self.taxpayers_results_filename]
        file = None
        try:
            # abertura do arquivo de resultados
            file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
            # criação da lista a ser serializada em jSON
            mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
            # serialização jSON
            json.dump(list(mapping), file, ensure_ascii=False)
        except BaseException as erreur:
            # relançamento do erro com outro tipo
            raise ImpôtsError(12, f"{erreur}")
        finally:
            # fecha-se o arquivo, caso ele tenha sido aberto
            if file:
                file.close()
  • linha 2: o método recebe uma lista de contribuintes [taxpayers], que deve gravar no arquivo de texto [self.taxpayers_results_filename] no formato jSON;
  • linha 10: criação do arquivo UTF-8 com os resultados;
  • linha 12: aqui introduzimos a função [map], cuja sintaxe neste caso é [map (fonction, liste1)]. A função [fonction] é aplicada a cada elemento de [liste1] e gera 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] é expressa aqui na forma de uma função denominada [lambda], que expressa a transformação realizada em um elemento [taxpayer] da lista [taxpayers]: cada elemento [taxpayer] é substituído por 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 retornado pela função [map] não é a lista [liste2], mas um objeto do tipo [map]. Para obter [liste2], é preciso usar a expressão [list(mapping)] (linha 14);
  • linha 14: a lista [liste2] é gravada no formato jSON no arquivo [self.taxpayers_results_filename];
  • linhas 15-17: qualquer tipo de exceção é interceptada e encapsulada em um 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 arquivo de resultados é fechado caso tenha sido aberto;

15.1.2.3. Classe [ImpôtsDaoWithAdminDataInJsonFile]

A classe [ImpôtsDaoWithAdminDataInJsonFile] derivará da classe [AbstractImpôtsDao] e implementará o método [getAdminData], que sua classe pai não implementou. Ela irá buscar os dados da administração fiscal em um arquivo 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], na qual os dados da administração fiscal estão em um arquivo jSON
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
    # construtor
    def __init__(self, config: dict):
        # config[admindataFilename]: o nome do arquivo jSON que contém os dados da administração fiscal
        # config[taxpayersFilename]: o nome do arquivo de texto dos contribuintes
        # config[resultsFilename]: o nome do arquivo jSON dos resultados
        # config[errorsFilename]: o nome do arquivo de erros

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

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

    # recuperação dos dados da administração fiscal
    # o método retorna um objeto [AdminData]
    def get_admindata(self) -> AdminData:
        return self.admindata
  • linha 11: a classe [ImpôtsDaoWithAdminDataInJsonFile] herda da classe [AbstractImpôtsDao]. Por isso, ela implementa a interface [InterfaceImpôtsDao];
  • linha 13: o construtor recebe como parâmetro um dicionário contendo as informações das linhas 14 a 17;
  • linha 20: a classe pai é inicializada;
  • linha 24: abertura do arquivo jSON com os dados da administração fiscal;
  • linha 25: o arquivo UTF-8 com os dados da administração fiscal é aberto;
  • linha 27: o conteúdo do arquivo é lido e colocado no objeto [self.admindata] do tipo [AdminData]. É necessário que as chaves do arquivo jSON correspondam às propriedades aceitas para um objeto [AdminData]; caso contrário, o método [fromdict] lançará uma exceção;
  • linhas 28-30: gerenciamento de exceções. As exceções que podem ocorrer são encapsuladas em um tipo [ImpôtsError] antes de serem relançadas;
  • linhas 32-34: o arquivo é 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 retorna nenhum resultado. Os dados obtidos (imposto, sobretaxa, abatimento, redução, alíquota) estão incluídos no parâmetro [taxpayer]: antes da chamada, esses atributos estão vazios; após a chamada, eles foram inicializados;

15.1.3.2. A classe [ImpôtsMétier]

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

Image

Os métodos da classe são provenientes do módulo [impôts_module_02] do parágrafo |O módulo [impots.v02.modules.impôts_module_02]|. Limitamos os parâmetros dos métodos a apenas dois:

  • taxpayer(id, casado, filhos, salário, imposto, abatimento, sobretaxa, redução, alíquota): o objeto que representa um contribuinte e seu imposto;
  • admindata: o objeto que encapsula os dados da administração tributária;

Mostramos em um método as alterações assim realizadas;


    # cálculo do imposto – fase 1
    # ----------------------------------------
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        # contribuinte (ID, casado, filhos, salário, imposto, abatimento, acréscimo, redução, alíquota)
        # dados administrativos: dados da administração tributária

        # cálculo do imposto com filhos
        self.calculate_tax_2(taxpayer, admindata)
        # os resultados estão em taxpayer
        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 estão 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 alto, com a alíquota 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]. Ele aceita dois parâmetros:
    • [tapPayer]: o contribuinte cujo imposto está sendo calculado;
    • [admindata]: o objeto que encapsula os dados da administração tributária;
    • os resultados do cálculo são encapsulados no parâmetro [taxpayer] (linhas 40-50). O conteúdo desse objeto, portanto, não é 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 arquivo 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 arquivo
    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 essenciais do aplicativo
    absolute_dependencies = [
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
        f"{root_dir}/02/entities",
    ]

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

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

    # instanciação das camadas do aplicativo
    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

    # geramos a configuração
    return config
  • linhas 4-23: configura-se o Python Path dos testes;
  • linhas 32-41: instanciamos as camadas [dao] e [métier]. Colocamos suas referências no dicionário [config];
  • linha 44: retorna-se esse 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 é restaurada
    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, 'sobretaxa': 0, 'abatimento': 0, 'redução': 0, 'alíquota': 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, 'abatimento': 384, 'redução': 347, 'alíquota': 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, 'alíquota': 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': 100.000,
        # 'imposto': 19884, 'sobretaxa': 4480, 'desconto': 0, 'redução': 0, 'alíquota': 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, 'alíquota': 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': 100.000,
        # 'imposto': 9200, 'sobretaxa': 2180, 'desconto': 0, 'redução': 0, 'alíquota': 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, 'alíquota': 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, 'alíquota': 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, 'abatimento': 0, 'redução': 0, 'alíquota': 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, 'alíquota': 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, 'alíquota': 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: em um 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 é armazenado;
  • linhas 21-173: 11 testes cujos resultados foram verificados no site oficial da Receita Federal 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 imposto. O resultado está em [taxpayer];
  • linhas 29-33: verificação dos resultados obtidos;
  • linha 29: verifica-se o valor do imposto com precisão de um euro. Os testes demonstraram, de fato, que os resultados obtidos pelo algoritmo deste documento podem 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 arquivo
    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 do aplicativo
    absolute_dependencies = [
        # dependências locais
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

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

    # etapa 2 ------
    # configuração do aplicativo
    config = {
        # caminhos absolutos dos arquivos do aplicativo
        "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

    # geramos a configuração
    return config

Ele é 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 (elas já estão instanciadas)
dao = config["dao"]
métier = config["métier"]

try:
    # recuperação das faixas 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 é tanto um parâmetro de entrada quanto de saída
        # o contribuinte será modificado
        métier.calculate_tax(taxpayer, admindata)
    # gravação dos resultados em um arquivo 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 do aplicativo. Sabe-se também que o Python Path do aplicativo 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 essa lista estiver vazia, é gerada uma exceção;
  • linhas 22-25: cálculo do imposto dos diferentes objetos [taxpayer] por meio da camada [métier];
  • linha 27: [taxpayers] passa a ser uma lista de objetos [TaxPayer] em que os atributos (imposto, desconto, sobretaxa, redução, alíquota) receberam um valor. Essa lista é gravada em um arquivo jSON;
  • linhas 28-30: detecção de um possível erro;
  • linhas 31-33: executadas em todos os casos;

A execução do script produz os mesmos resultados que nas versões anteriores. O arquivo de erros dos contribuintes foi uma novidade nesta versão. Após a execução do script [main], 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
# pode haver linhas vazias

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

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

Nesta versão, é o usuário que, por meio do teclado, fornece a lista de contribuintes. A arquitetura da aplicação será a seguinte:

Image

Surge um novo módulo: a camada [ui] (Interface do Usuário), que se comunicará com o usuário. Essa 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 a 10. A interface será implementada aqui com um aplicativo de console, 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 esse problema, o procedimento habitual é:

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

Esse método permite ter uma interface muito geral, que é especificada pelos parâmetros dos construtores de cada classe de implementação. Esse 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 maneira:


# 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):
        # armazenamento dos parâmetros
        self.admindata = config['dao'].get_admindata()
        self.métier = config['métier']

    def run(self):
        # diálogo interativo com o usuário
        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 inseridos
            while marié != "oui" and marié != "non" and marié != "*":
                # mensagem de erro
                print("Tapez oui ou non ou *")
                # repete a pergunta
                marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # Concluído?
            if marié == "*":
                # diálogo encerrado
                return
            # número de filhos
            enfants = input("Nombre d'enfants : ").strip()
            # verificação da validade da entrada
            if not re.match(r"^\d+$", enfants):
                # mensagem de erro
                print("Tapez un nombre entier positif ou nul")
                # vamos tentar novamente
                enfants = input("Nombre d'enfants : ").strip()
            # salário anual
            salaire = input("Salaire annuel : ").strip()
            # verificação da validade dos dados inseridos
            if not re.match(r"^\d+$", salaire):
                # mensagem de erro
                print("Tapez un nombre entier positif ou nul")
                # tentando novamente
                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 do aplicativo;
    • 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 usuário. Consiste
    • em solicitar as três informações (estado civil, filhos, salário) do contribuinte;
    • calcular seu imposto;
    • exibi-lo;
    • o diálogo termina quando o usuário responde * à primeira pergunta;
  • linhas 20-27: pergunta-se se o contribuinte é casado e verifica-se a validade da resposta;
  • linhas 29-31: se o usuário respondeu ‘*’ à pergunta, o diálogo é encerrado;
  • 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 essas informações, calcula-se, por meio da camada [métier], o imposto do contribuinte;
  • linha 52: o valor do imposto é exibido;

15.2.3. O script principal

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


def configure():
    import os

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

    # pasta deste arquivo
    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 do aplicativo
    absolute_dependencies = [
        # dependências locais
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

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

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

    # instanciação das camadas do aplicativo
    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 coordenador é o seguinte (main.py):


# configuramos o aplicativo
import config

config = config.configure()

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

# recuperamos as camadas do aplicativo (elas 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 do aplicativo;
  • linha 10: recupera-se uma referência à camada [ui];
  • linhas 12-21: a estrutura do código é a mesma da aplicação anterior: código envolto por um try/catch para interceptar qualquer exceção eventual;
  • linha 15: solicita-se que a camada [ui] seja executada: o diálogo com o usuário então se inicia;
  • linhas 16-18: interceptação de uma possível exceção;

Aqui está 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