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

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:

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:

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

15.1.1. As entidades

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]

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

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

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:

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]

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

15.1.5. Script principal

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

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.

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