15. Ćwiczenie praktyczne – wersja 4

Powracamy tutaj do ćwiczenia opisanego w akapicie |Version 3| i rozwiązujemy je teraz przy użyciu klas i interfejsów. Napiszemy dwie aplikacje:
Aplikacja 1 będzie wyglądała następująco:

Główny skrypt [main] utworzy instancję warstwy [dao] oraz warstwy [métier]:
- warstwa [dao] będzie odpowiedzialna za zarządzanie danymi przechowywanymi w plikach tekstowych, a w dalszej kolejności w bazie danych;
- warstwa [métier] będzie odpowiedzialna za obliczanie podatku;
W tej aplikacji użytkownik nie będzie musiał wykonywać żadnych czynności: dane podatników będą pobierane z pliku tekstowego, którego nazwę poda się modułowi [main].
W aplikacji 2 to użytkownik będzie wprowadzał dane podatników za pomocą klawiatury. Architektura ulegnie wówczas następującej zmianie:

- warstwa [dao] (Data Access Object) odpowiada za dostęp do danych zewnętrznych
- warstwa [métier] zajmuje się zagadnieniami biznesowymi, w tym przypadku obliczaniem podatku. Nie zajmuje się ona danymi. Dane te mogą pochodzić z dwóch źródeł:
- warstwa [dao] dla danych trwałych;
- warstwa [ui] dla danych dostarczonych przez użytkownika.
- warstwa [ui] (interfejs użytkownika) odpowiada za interakcje z użytkownikiem;
- [main] pełni rolę koordynatora;
W dalszej części warstwy [dao], [métier] i [ui] zostaną zaimplementowane za pomocą odpowiednich klas. Warstwy [métier] i [dao] będą takie same dla obu aplikacji. Z tego powodu zostały one połączone w jednej wersji ćwiczenia aplikacyjnego.
15.1. Wersja 4 – aplikacja 1
Wersja 4 oblicza podatek dla listy podatników umieszczonej w pliku tekstowym. Ma następującą architekturę:

15.1.1. Elementy

Entities to klasy danych. Ich rolą jest hermetyzacja danych oraz udostępnianie metod getterów i setterów, które pozwalają na weryfikację ich poprawności. Entities są przekazywane między warstwami. Ta sama entitatea może przejść z warstwy [ui] do warstwy [dao] i odwrotnie.
15.1.1.1. Klasa [ImpôtsError]
Wykorzystamy własną klasę wyjątków:
# -------------------------------
# klasa wyjątków
from MyException import MyException
class ImpôtsError(MyException):
pass
Gdy tylko warstwy [métier] i [dao] napotkają problem, wygenerują ten wyjątek. Klasa ta wywodzi się z klasy [MyException]. Stosuje się ją zatem w następujący sposób: [raise ImpôtsError(code_erreur, msg_erreur)].
15.1.1.2. Klasa [AdminData]
Klasa [AdminData] zawiera stałe wykorzystywane przy obliczaniu podatku:
from BaseEntity import BaseEntity
# dane administracji podatkowej
class AdminData(BaseEntity):
# klucze wykluczone ze stanu klasy
excluded_keys = []
# klucze dozwolone
@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"
]
- wiersz 5: klasa [AdminData] rozszerza klasę [BaseEntity] opisaną w akapicie |BaseEntity|. Należy pamiętać, że klasy rozszerzające klasę [BaseEntity] muszą zdefiniować:
- atrybut klasy [excluded_keys] (wiersz 7), który zawiera listę właściwości obiektu pomijanych podczas przekształcania obiektu w słownik;
- statyczną metodę [get_allowed_keys] (wiersze 10–26), która zwraca listę właściwości akceptowanych podczas inicjalizacji obiektu za pomocą słownika;
Nie zastosowano metod ustawiających (setterów) w celu sprawdzenia poprawności danych używanych do inicjalizacji obiektu [AdminData]. Obiekt ten jest bowiem unikalny i definiowany przez konfigurację, a zatem nie może zawierać błędów.
15.1.1.3. Klasa [TaxPayer]
Klasa [TaxPayer] będzie modelować podatnika:
# importy
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError
# podatnik
class TaxPayer(BaseEntity):
# modeluje podatnika
# id: identyfikator
# stan cywilny: tak / nie
# dzieci: liczba dzieci
# wynagrodzenie: jego roczne wynagrodzenie
# podatek: kwota podatku do zapłaty
# dopłata: dopłata do podatku do zapłaty
# ulga: ulga od podatku do zapłaty
# ulga: ulga od podatku do zapłaty
# stawka: stawka podatkowa podatnika
# klucze wykluczone z zestawienia klasy
excluded_keys = []
# klucze dozwolone
@staticmethod
def get_allowed_keys() -> list:
return ['id', 'marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']
# właściwości
@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
# ustawki
@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):
# liczba dzieci musi być liczbą całkowitą >=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):
# wynagrodzenie musi być liczbą całkowitą >=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):
# podatek musi być liczbą całkowitą >=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):
# obniżka musi być liczbą całkowitą >=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):
# dopłata musi być liczbą całkowitą >=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):
# dopłata musi być liczbą całkowitą >=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):
# stawka musi być liczbą rzeczywistą >=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")
Uwagi:
- klasa [TaxPayer] zawiera podatnika;
- wiersz 7: klasa [TaxPayer] wywodzi się z klasy [BaseEntity]. Posiada zatem identyfikator [id];
- wiersz 20: żadna właściwość nie jest wykluczona ze stanu obiektu [AdminData];
- wiersze 22–25: właściwości klasy. Są one opisane w wierszach 9–17;
- wiersze 27–58: metody pobierające atrybutów klasy;
- wiersze 60–161: metody ustawiające atrybutów klasy. Przypominamy, że zaletą klasy hermetyzującej dane w porównaniu z prostym słownikiem jest to, że klasa może sprawdzać poprawność swoich właściwości za pomocą metod ustawiających;
15.1.2. Warstwa [dao]

Zgromadzimy implementacje warstw w folderze [services]. Klasy te będą implementować interfejsy zdefiniowane w folderze [interfaces].

15.1.2.1. Interfejs [InterfaceImpôtsDao]
Warstwa [dao] będzie implementować następujący interfejs [InterfaceImpôtsDao] (plik InterfaceImpôtsDao.py):
# importy
from abc import ABC, abstractmethod
# interfejs IImpôtsDao
from AdminData import AdminData
class InterfaceImpôtsDao(ABC):
# lista przedziałów podatkowych
@abstractmethod
def get_admindata(self) -> AdminData:
pass
# lista danych podatników
@abstractmethod
def get_taxpayers_data(self) -> dict:
pass
# zapis wyników obliczeń podatku
@abstractmethod
def write_taxpayers_results(self, taxpayers_results: list):
pass
Interfejs definiuje trzy metody:
- [get_admindata]: jest to metoda pobierająca tablicę przedziałów podatkowych. Należy zauważyć, że nie podano żadnych informacji dotyczących sposobu uzyskania tych danych. W dalszej części zostaną one najpierw pobrane z pliku tekstowego, a następnie z bazy danych. Klasy implementujące ten interfejs będą musiały dostosować się do sposobu przechowywania danych. Będziemy więc mieli jedną klasę do pobierania przedziałów podatkowych z pliku tekstowego i drugą do pobierania ich z bazy danych. Obie będą implementowały metodę [get_admindata];
- [get_taxpayers_data]: jest to metoda pobierająca dane podatników. Również w tym przypadku nie określamy, gdzie dane te będą się znajdować. Omówimy jedynie przypadek, w którym znajdują się one w pliku tekstowym;
- [write_taxpayers_results]: jest to metoda, która zapisuje wyniki obliczeń podatku. Nie określamy, gdzie. Rozpatrzymy wyłącznie przypadek, w którym wyniki są zapisywane w pliku tekstowym. Parametr [taxpayers_results] będzie listą wyników do zapisania;
15.1.2.2. Klasa [AbstractImpôtsDao]
Warstwa [dao] zostanie zaimplementowana przez dwie klasy:
- jedna będzie pobierać dane (podatnicy, wyniki, progi podatkowe) z plików tekstowych;
- druga będzie pobierać dane (podatnicy, wyniki) z plików tekstowych, a progi podatkowe z bazy danych;
Obie klasy będą się różnić jedynie sposobem obsługi przedziałów podatkowych. Dane podatników oraz wyniki obliczeń podatku będą natomiast obsługiwane w ten sam sposób. Z tego powodu będziemy nimi zarządzać w klasie nadrzędnej [AbstractImpôtsDao]. Specyfika obsługi przedziałów podatkowych będzie natomiast realizowana w dwóch klasach podrzędnych:
- klasa [ImpôtsDaoWithAdminDataInJsonFile] pobierze przedziały podatkowe z pliku tekstowego w formacie jSON;
- klasa [ImpôtsDaoWithAdminDataInDatabase] pobierze przedziały podatkowe z bazy danych;
Klasa nadrzędna [AbstractImpôtsDao] będzie wyglądać następująco:
# importy
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
# klasa bazowa dla warstwy [dao]
class AbstractImpôtsDao(InterfaceImpôtsDao):
# podatnicy i ich podatki będą znajdować się w plikach tekstowych
# konstruktor
def __init__(self, config: dict):
# konfiguracja[taxpayersFilename]: nazwa pliku tekstowego zawierającego podatników
# konfiguracja[resultsFilename]: nazwa pliku jSON z wynikami
# config[errorsFilename]: nazwa pliku błędów
# zapisywanie parametrów
self.taxpayers_filename = config.get("taxpayersFilename")
self.taxpayers_results_filename = config.get("resultsFilename")
self.errors_filename = config.get("errorsFilename")
# ------------------
# interfejs IImpôtsDao
# ------------------
# lista danych podatników
def get_taxpayers_data(self) -> dict:
…
# księgowanie podatku podatników
def write_taxpayers_results(self, taxpayers: list):
…
# odczyt przedziałów podatkowych
@abstractmethod
def get_admindata(self) -> AdminData:
pass
-
wiersz 13: klasa [AbstractImpôtsDao] implementuje interfejs [InterfaceImpôtsDao]. Znajdują się tam zatem trzy metody tego interfejsu:
- [get_taxpayers_data]: wiersz 31;
- [write_taxpayers_results]: wiersz 35;
- [get_admindata]: wiersz 40. Metoda ta nie zostanie zaimplementowana przez klasę [AbstractImpôtsDao], dlatego została zadeklarowana jako abstrakcyjna (wiersz 39);
-
wiersz 16: konstruktor otrzymuje słownik [config] zawierający następujące informacje:
- [taxpayersFilename]: nazwa pliku tekstowego zawierającego dane podatników;
- [resultsFilename]: nazwa pliku tekstowego, w którym zostaną zapisane wyniki;
- [errorsFilename]: nazwa pliku tekstowego zawierającego listę błędów napotkanych podczas przetwarzania pliku [taxpayersFilename];
Metoda [get_taxpayers_data] wygląda następująco:
# lista danych podatników
def get_taxpayers_data(self) -> dict:
# inicjalizacje
taxpayers_data = []
datafile = None
erreurs = []
try:
# otwarcie pliku danych
datafile = open(self.taxpayers_filename, "r")
# przetwarzanie bieżącego wiersza pliku
ligne = datafile.readline()
# numer wiersza
numligne = 0
while ligne != '':
# wiersz z +
numligne += 1
# usuwanie spacji
ligne = ligne.strip()
# pomijamy puste wiersze i komentarze
if ligne != "" and ligne[0] != "#":
try:
# pobieramy 4 pola: id, żonaty, dzieci, wynagrodzenie, które tworzą wiersz podatnika
(id, marié, enfants, salaire) = ligne.split(",")
# tworzymy nowy plik TaxPayer
taxpayers_data.append(
TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
except BaseException as erreur:
# odnotowuje się błąd
erreurs.append(f"Ligne {numligne}, {erreur}")
# odczytuje się nowy wiersz podatnika
ligne = datafile.readline()
# zapisuje się ewentualne błędy
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)
# zwracamy wynik
return {"taxpayers": taxpayers_data, "erreurs": erreurs}
except BaseException as erreur:
# rzucamy wyjątek ImpôtsError
raise ImpôtsError(11, f"{erreur}")
finally:
# zamykamy plik
if datafile:
datafile.close()
- wiersz 4: dane podatników (stan cywilny, dzieci, wynagrodzenie) zostaną umieszczone w liście obiektów typu [TaxPayer];
- wiersze 8–9: otwieramy plik tekstowy podatników w trybie odczytu. Jego zawartość ma następującą postać:
W porównaniu z poprzednimi wersjami:
- każdy wiersz pliku [taxpayersFilename] zaczyna się od identyfikatora podatnika, czyli zwykłego numeru;
- dozwolone są komentarze i puste wiersze;
- będziemy obsługiwać błędy. W związku z tym wiersze 17, 19 i 21 należy oznaczyć jako błędne. Błędy są zapisywane w osobnym pliku;
Kontynuujmy analizę kodu:
- wiersz 4: dane z pliku tekstowego są przenoszone do listy [taxPayersData];
- wiersze 14–31: plik podatników jest odczytywany wiersz po wierszu;
- wiersz 14: koniec pliku zostaje osiągnięty po odczytaniu pustego wiersza (nic – nawet znak końca wiersza \r\n);
- wiersz 20: puste wiersze i komentarze są pomijane. Wiersz jest komentarzem, jeśli po usunięciu spacji przed i po tekście pierwszym znakiem jest znak #;
- wiersz 24: poprawny wiersz składa się z czterech pól oddzielonych przecinkami. Pola te są pobierane. Przypisanie danych do krotki składającej się z czterech elementów kończy się niepowodzeniem, jeśli nie ma dokładnie czterech przypisanych danych;
- wiersz 25: jeśli jedno z czterech pobranych pól [id, marié, enfants, salaire] jest nieprawidłowe, wówczas metoda [BaseEntity.fromdict] zgłosi wyjątek typu [MyException];
- wiersze 25–26: obiekt [TaxPayer] jest dodawany do listy podatników [taxpayers_data];
- wiersze 27–29: ewentualne błędy są gromadzone na liście [erreurs]. Lista ta została utworzona w wierszu 6;
- wiersze 33–36: lista napotkanych błędów jest zapisywana w pliku tekstowym [errorsFilename]. Błędy te dzielą się na dwa rodzaje:
- wiersz nie zawierał prawidłowej liczby oczekiwanych pól;
- informacje w wierszu były błędne i nie udało się utworzyć obiektu [TaxPayer];
- wiersze 39–41: przechwytuje się każdy błąd (BaseException) i przekazuje go wyżej, zamykając go w typie [ImpôtsError];
- wiersze 42–45: niezależnie od wyniku (powodzenie lub niepowodzenie) plik tekstowy podatników zostaje zamknięty, jeśli został otwarty;
Metoda [write_taxpayers_results] musi generować plik jSON o następującej postaci:
[
{
"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
},
…
]
Metoda [write_taxpayers_results] wygląda następująco:
# zapisywanie podatku podatników
def write_taxpayers_results(self, taxpayers: list):
# zapisywanie wyników do pliku jSON
# podatnicy: lista obiektów typu TaxPayer
# (id, stan cywilny, dzieci, wynagrodzenie, podatek, dopłata, ulga, obniżka, stawka)
# lista [taxpayers] jest zapisana w pliku tekstowym [self.taxpayers_results_filename]
file = None
try:
# otwarcie pliku wyników
file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
# utworzenie listy do serializacji w formacie jSON
mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
# serializacja jSON
json.dump(list(mapping), file, ensure_ascii=False)
except BaseException as erreur:
# ponowne zgłoszenie błędu jako innego typu
raise ImpôtsError(12, f"{erreur}")
finally:
# zamykamy plik, jeśli został otwarty
if file:
file.close()
- wiersz 2: metoda otrzymuje listę podatników [taxpayers], którą musi zapisać w pliku tekstowym [self.taxpayers_results_filename] w formacie jSON;
- wiersz 10: utworzenie pliku wyników o nazwie UTF-8;
- wiersz 12: wprowadzamy tutaj funkcję [map], której składnia w tym przypadku brzmi [map (fonction, liste1)]. Funkcja [fonction] jest stosowana do każdego elementu [liste1] i generuje nowy element, który zasila listę [liste2]. Na koniec, dla każdego i:
liste2[i]=fonction(liste1[i])
W tym przypadku [liste1] to lista [taxPayers], czyli lista obiektów typu [TaxPayer]. Funkcja [fonction] jest tutaj wyrażona w postaci funkcji zwanej [lambda], która określa transformację dokonywaną na elemencie [taxpayer] z listy [taxpayers]: każdy element [taxpayer] jest zastępowany swoim słownikiem [taxpayer.asdict()]. Ostatecznie uzyskana lista [liste2] jest listą słowników elementów z listy [taxpayers];
- wiersz 12: wynikiem zwracanym przez funkcję [map] nie jest lista [liste2], lecz obiekt typu [map]. Aby uzyskać [liste2], należy użyć wyrażenia [list(mapping)] (wiersz 14);
- wiersz 14: lista [liste2] jest zapisywana w formacie jSON w pliku [self.taxpayers_results_filename];
- wiersze 15–17: każdy rodzaj wyjątku jest przechwytywany i enkapsulowany w błąd typu [ImpôtsError], a następnie ponownie uruchamiany (wiersz 17);
- wiersze 19–21: w każdym przypadku, niezależnie od powodzenia lub niepowodzenia, plik wyników jest zamykany, jeśli został otwarty;
15.1.2.3. Klasa [ImpôtsDaoWithAdminDataInJsonFile]
Klasa [ImpôtsDaoWithAdminDataInJsonFile] będzie pochodzić od klasy [AbstractImpôtsDao] i zaimplementuje metodę [getAdminData], której jej klasa nadrzędna nie zaimplementowała. Będzie ona pobierać dane z urzędu skarbowego z pliku 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
}
Klasa [ImpôtsDaoWithAdminDataInJsonFile] ma następującą postać:
# importy
import codecs
import json
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
# implementacja warstwy [dao], w której dane urzędu skarbowego znajdują się w pliku jSON
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
# konstruktor
def __init__(self, config: dict):
# konfiguracja[admindataFilename]: nazwa pliku jSON zawierającego dane urzędu skarbowego
# config[taxpayersFilename]: nazwa pliku tekstowego podatników
# config[resultsFilename]: nazwa pliku jSON z wynikami
# config[errorsFilename]: nazwa pliku błędów
# inicjalizacja klasy Parent
AbstractImpôtsDao.__init__(self, config)
# odczyt danych z urzędu skarbowego
file = None
try:
# otwarcie pliku jSON zawierającego dane podatkowe w trybie odczytu
file = codecs.open(config["admindataFilename"], "r", "utf8")
# przeniesienie zawartości pliku jSON do obiektu [AdminData]
self.admindata = AdminData().fromdict(json.load(file))
except BaseException as erreur:
# błąd jest ponownie zgłaszany w postaci typu [ImpôtsError]
raise ImpôtsError(21, f"{erreur}")
finally:
# zamknięcie pliku, jeśli został otwarty
if file:
file.close()
# -------------
# interfejs
# -------------
# pobieranie danych z urzędu skarbowego
# metoda zwraca obiekt [AdminData]
def get_admindata(self) -> AdminData:
return self.admindata
- wiersz 11: klasa [ImpôtsDaoWithAdminDataInJsonFile] dziedziczy po klasie [AbstractImpôtsDao]. W związku z tym implementuje interfejs [InterfaceImpôtsDao];
- wiersz 13: konstruktor otrzymuje jako parametr słownik zawierający informacje z wierszy 14–17;
- wiersz 20: klasa nadrzędna zostaje zainicjowana;
- wiersz 24: otwieranie pliku jSON zawierającego dane urzędu skarbowego;
- wiersz 25: otwierany jest plik UTF-8 zawierający dane z urzędu skarbowego;
- wiersz 27: zawartość pliku jest odczytywana i umieszczana w obiekcie [self.admindata] typu [AdminData]. Klucze pliku jSON muszą odpowiadać właściwościom akceptowanym dla obiektu typu [AdminData], w przeciwnym razie metoda [fromdict] wygeneruje wyjątek;
- wiersze 28–30: obsługa wyjątków. Wyjątki, które mogą wystąpić, są enkapsulowane w typie [ImpôtsError] przed ponownym zgłoszeniem;
- wiersze 32–34: plik zostaje zamknięty, jeśli był otwarty;
- wiersze 42–43: implementacja metody [get_admindata] z interfejsu [InterfaceImpôtsDao];
15.1.3. Warstwa [métier]

15.1.3.1. Interfejs [InterfaceImpôtsMétier]
Interfejs warstwy [métier] będzie wyglądał następująco:
# importy
from abc import ABC, abstractmethod
from AdminData import AdminData
from TaxPayer import TaxPayer
# interfejs IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
# obliczenie podatku dla 1 podatnika
@abstractmethod
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
pass
- Interfejs [InterfaceImpôtsMétier] definiuje jedną metodę:
- wiersz 12: metoda [calculate_tax] umożliwia obliczenie podatku dla pojedynczego podatnika [taxpayer]. [admindata] to obiekt [AdminData] zawierający dane administracji podatkowej;
- wiersz 12: metoda [calculate_tax] nie zwraca wyniku. Uzyskane dane (podatek, dopłata, ulga, obniżka, stawka) są zawarte w parametrze [taxpayer]: przed wywołaniem atrybuty te są puste, po wywołaniu zostały zainicjowane;
15.1.3.2. Klasa [ImpôtsMétier]
Klasa [ImpôtsMétier] implementuje interfejs [InterfaceImpôtsMétier] w następujący sposób:

Metody tej klasy pochodzą z modułu [impôts_module_02] z sekcji |Moduł [impots.v02.modules.impôts_module_02]|. Ograniczono jedynie liczbę parametrów metod do dwóch:
- taxpayer(id, stan cywilny, liczba dzieci, wynagrodzenie, podatek, ulga, dopłata, obniżka, stawka): obiekt reprezentujący podatnika i jego podatek;
- admindata: obiekt zawierający dane administracji podatkowej;
Na przykładzie jednej metody pokazujemy wprowadzone zmiany;
# obliczenie podatku – etap 1
# ----------------------------------------
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
# podatnik (identyfikator, stan cywilny, dzieci, wynagrodzenie, podatek, ulga, dopłata, obniżka, stawka)
# dane administracyjne: dane urzędu skarbowego
# obliczenie podatku z uwzględnieniem dzieci
self.calculate_tax_2(taxpayer, admindata)
# wyniki znajdują się w tabeli „podatnik”
taux1 = taxpayer.taux
surcôte1 = taxpayer.surcôte
impot1 = taxpayer.impôt
# obliczenie podatku bez uwzględnienia dzieci
if taxpayer.enfants != 0:
# obliczenie podatku dla tego samego podatnika bez dzieci
taxpayer2 = TaxPayer().fromdict(
{'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
self.calculate_tax_2(taxpayer2, admindata)
# wyniki znajdują się w taxpayer2
taux2 = taxpayer2.taux
surcôte2 = taxpayer2.surcôte
impot2 = taxpayer2.impôt
# zastosowanie limitu ilorazu rodzinnego
if taxpayer.enfants < 3:
# PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci
impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
else:
# PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci, dwukrotnie więcej na kolejne
impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
* 2 * admindata.plafond_qf_demi_part
else:
# jeśli podatnik nie ma dzieci, to podatek2 = podatek1
impot2 = impot1
# przyjmuje się najwyższy podatek wraz z odpowiednią stawką i dopłatą
(impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
impot2, impot2 - impot1 + surcôte2, taux2)
# wyniki częściowe
taxpayer.impôt = impot
taxpayer.surcôte = surcôte
taxpayer.taux = taux
# obliczenie ewentualnej ulgi
self.get_décôte(taxpayer, admindata)
taxpayer.impôt -= taxpayer.décôte
# obliczenie ewentualnej ulgi podatkowej
self.get_réduction(taxpayer, admindata)
taxpayer.impôt -= taxpayer.réduction
# wynik
taxpayer.impôt = math.floor(taxpayer.impôt)
- wiersz 3: metoda [calculate_tax] jest jedyną metodą interfejsu [InterfaceImpôtsMétier]. Przyjmuje dwa parametry:
- [tapPayer]: podatnik, dla którego obliczany jest podatek;
- [admindata]: obiekt zawierający dane administracji podatkowej;
- wyniki obliczeń są zawarte w parametrze [taxpayer] (wiersze 40–50). Zawartość tego obiektu nie jest zatem taka sama przed i po wywołaniu metody;
15.1.4. Testy warstw [dao] i [métier]

- [TestDaoMétier] to klasa testowa UnitTest dla warstw [dao] i [métier];
- [config] to plik konfiguracyjny testów;
Konfiguracja [config] jest następująca:
def configure():
import os
# etap 1 ------
# konfiguracja ścieżki Python
# katalog tego pliku
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"
# absolutne zależności aplikacji
absolute_dependencies = [
f"{script_dir}/../entities",
f"{script_dir}/../interfaces",
f"{script_dir}/../services",
f"{root_dir}/02/entities",
]
# ustawiamy ścieżkę systemową
from myutils import set_syspath
set_syspath(absolute_dependencies)
# krok 2 ------
# konfiguracja aplikacji
config = {
# absolutne ścieżki do plików aplikacji
"admindataFilename": f"{script_dir}/../data/input/admindata.json"
}
# instancjonowanie warstw aplikacji
from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
from ImpôtsMétier import ImpôtsMétier
dao = ImpôtsDaoWithAdminDataInJsonFile(config)
métier = ImpôtsMétier()
# umieszczamy instancje warstw w konfiguracji
config["dao"] = dao
config["métier"] = métier
# generowanie konfiguracji
return config
- wiersze 4–23: konfiguracja ścieżki Python Path dla testów;
- wiersze 32–41: instancjonuje się warstwy [dao] i [métier]. Ich odwołania umieszcza się w słowniku [config];
- wiersz 44: zwracamy ten słownik;
Klasa testowa [TestDaoMétier] wygląda następująco:
import unittest
def get_config() -> dict:
# konfiguracja aplikacji
import config
# zwracamy konfigurację
return config.configure()
class TestDaoMétier(unittest.TestCase):
# wykonywane przed każdą metodą test_
def setUp(self) -> None:
# pobieramy konfigurację testów
config = get_config()
# zapisujemy pewne informacje
self.métier = config['métier']
self.admindata = config['dao'].get_admindata()
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'żonaty': 'tak', 'dzieci': 2, 'wynagrodzenie': 55555,
# 'podatek': 2814, 'dopłata': 0, 'ulga': 0, 'obniżka': 0, 'stawka': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacja
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
# {'stan cywilny': 'żonaty', 'dzieci': 2, 'wynagrodzenie': 50000,
# 'podatek': 1384, 'dopłata': 0, 'ulga': 384, 'obniżka': 347, 'stawka': 0,14}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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
# {'stan cywilny': 'żonaty', 'dzieci': 3, 'wynagrodzenie': 50000,
# 'podatek': 0, 'dopłata': 0, 'odliczenie': 720, 'ulga': 0, 'stawka': 0,14}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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
# {'stan cywilny': 'nieżonaty/niezamężna', 'dzieci': 2, 'wynagrodzenie': 100000,
# 'podatek': 19884, 'dopłata': 4480, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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
# {'stan cywilny': 'nieżonaty/niezamężna', 'dzieci': 3, 'wynagrodzenie': 100000,
# 'podatek': 16782, 'dopłata': 7176, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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
# {'stan cywilny': 'żonaty', 'dzieci': 3, 'wynagrodzenie': 100000,
# 'podatek': 9200, 'dopłata': 2180, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,3}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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
# {'stan cywilny': 'żonaty', 'dzieci': 5, 'wynagrodzenie': 100000,
# 'podatek': 4230, 'dopłata': 0, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,14}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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
# {'stan cywilny': 'nieżonaty/niezamężna', 'dzieci': 0, 'wynagrodzenie': 100000,
# 'podatek': 22986, 'dopłata': 0, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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
# {'stan cywilny': 'żonaty', 'dzieci': 2, 'wynagrodzenie': 30000,
# 'podatek': 0, 'dopłata': 0, 'ulga': 0, 'obniżka': 0, 'stawka': 0}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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
# {'stan cywilny': 'nie', 'dzieci': 0, 'wynagrodzenie': 200000,
# 'podatek': 64210, 'dopłata': 7498, 'ulga': 0, 'obniżka': 0, 'stawka': 0,45}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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
# {'stan cywilny': 'żonaty', 'dzieci': 3, 'wynagrodzenie': 200000,
# 'podatek': 42842, 'dopłata': 17283, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
self.métier.calculate_tax(taxpayer, self.admindata)
# weryfikacje
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()
Komentarze
- wiersz 11: klasa testowa rozszerza klasę [unittest.TestCase];
- wiersze 13–19: w teście UnitTest metoda [setUp] jest wykonywana przed każdą z metod [test_];
- wiersz 16: pobierana jest konfiguracja pochodząca ze skryptu [config] omówionego wcześniej;
- wiersz 18: zapisuje się odwołanie do warstwy [métier];
- wiersz 19: żądamy od warstwy [dao] obiektu [AdminData] zawierającego dane urzędu skarbowego i zapisujemy go;
- wiersze 21–173: 11 testów, których wyniki zostały zweryfikowane na oficjalnej stronie urzędu skarbowego z 2019 r. |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm|;
- wiersze 21–33: wszystkie testy zostały opracowane według tego samego wzorca;
- wiersz 22: importujemy klasę [TaxPayer];
- wiersz 24: podatnik objęty testem;
- wiersz 25: oczekiwane wyniki;
- wiersz 26: utworzenie obiektu [TaxPayer] dla podatnika;
- wiersz 27: obliczenie podatku. Wynik znajduje się w obiekcie [taxpayer];
- wiersze 29–33: weryfikacja uzyskanych wyników;
- wiersz 29: weryfikacja kwoty podatku z dokładnością do jednego euro. Testy wykazały bowiem, że wyniki uzyskane przez algorytm opisany w niniejszym dokumencie mogą różnić się od oficjalnych danych o kwotę nie większą niż 1 euro;
Przeprowadzenie testów daje następujące wyniki:

15.1.5. Skrypt główny

Skrypt główny jest konfigurowany przez następujący skrypt [config]:
def configure():
import os
# etap 1 ------
# konfiguracja ścieżki Python
# katalog tego pliku
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"
# zależności aplikacji
absolute_dependencies = [
# zależności lokalne
f"{script_dir}/../../entities",
f"{script_dir}/../../interfaces",
f"{script_dir}/../../services",
f"{root_dir}/02/entities",
]
# ustawiamy ścieżkę systemową
from myutils import set_syspath
set_syspath(absolute_dependencies)
# krok 2 ------
# konfiguracja aplikacji
config = {
# absolutne ścieżki do plików aplikacji
"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"
}
# tworzenie instancji warstw aplikacji
from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
from ImpôtsMétier import ImpôtsMétier
dao = ImpôtsDaoWithAdminDataInJsonFile(config)
métier = ImpôtsMétier()
# umieszczamy instancje warstw w konfiguracji
config["dao"] = dao
config["métier"] = métier
# generowanie konfiguracji
return config
Jest on analogiczny do skryptu używanego do testowania warstw [métier] i [dao].
Główny skrypt [main.py] ma następującą postać:
# konfiguracja aplikacji
import config
config = config.configure()
# importy
from ImpôtsError import ImpôtsError
# pobieramy warstwy aplikacji (są one już zainicjowane)
dao = config["dao"]
métier = config["métier"]
try:
# pobieranie przedziałów podatkowych
admindata = dao.get_admindata()
# odczyt danych podatników
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# podatników?
if not taxpayers:
raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# obliczanie podatku podatników
for taxpayer in taxpayers:
# „podatnik” jest zarówno parametrem wejściowym, jak i wyjściowym
# „podatnik” zostanie zmodyfikowany
métier.calculate_tax(taxpayer, admindata)
# zapis wyników do pliku tekstowego
dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
# wyświetlenie błędu
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# zakończono
print("Travail terminé...")
Uwagi
- wiersze 2–4: pobierana jest konfiguracja aplikacji. Wiadomo również, że ścieżka Python Path aplikacji została utworzona;
- wiersze 9–11: pobierane są odwołania do warstw [métier] i [dao];
- wiersz 15: pobierane są dane z urzędu skarbowego;
- wiersz 17: pobierana jest lista podatników, dla których należy obliczyć podatek;
- wiersze 19–20: jeśli lista jest pusta, zgłaszany jest wyjątek;
- wiersze 22–25: obliczanie podatku dla poszczególnych obiektów [taxpayer] przy użyciu warstwy [métier];
- wiersz 27: [taxpayers] jest teraz listą obiektów [TaxPayer], w których atrybuty (podatek, obniżka, podwyżka, ulga, stawka) otrzymały wartości. Lista ta jest zapisywana w pliku jSON;
- wiersze 28–30: wykrycie ewentualnego błędu;
- wiersze 31–33: wykonywane w każdym przypadku;
Uruchomienie skryptu daje takie same wyniki jak w poprzednich wersjach. Plik zawierający błędy podatników był nowością w tej wersji. Po uruchomieniu skryptu [main] jego zawartość wygląda następująco:
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]
Błędne wiersze to:
15.2. Wersja 4 – aplikacja 2
W tej wersji to użytkownik, za pomocą klawiatury, podaje listę podatników. Architektura aplikacji będzie wyglądać następująco:

Pojawia się nowy moduł: warstwa [ui] (interfejs użytkownika), która będzie komunikować się z użytkownikiem. Warstwa ta będzie posiadać interfejs i zostanie zaimplementowana przez klasę.

15.2.1. Interfejs [InterfaceImpôtsUi]
# importy
from abc import ABC, abstractmethod
# interfejs InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
# wywołanie klasy implementującej interfejs
@abstractmethod
def run(self):
pass
Interfejs [InterfaceImpôtsUi] będzie miał tylko jedną metodę, tę z wierszy 8–10. Interfejs ten zostanie tutaj zaimplementowany za pomocą aplikacji konsolowej, ale można go również zaimplementować za pomocą interfejsu graficznego. Parametry przekazywane do metody [run] nie byłyby takie same w obu implementacjach. Aby obejść ten problem, standardową metodą jest:
- nie podawanie żadnych parametrów do metody [run] (lub podawanie minimalnej liczby parametrów);
- przekazywanie parametrów do konstruktora klasy implementującej interfejs. Mogą one różnić się w zależności od implementacji. Parametry te są zapisywane jako atrybuty klasy;
- sprawić, by metoda [run] korzystała z tych atrybutów klasy (self.x);
Metoda ta pozwala na uzyskanie bardzo ogólnego interfejsu, który jest doprecyzowywany przez parametry konstruktorów każdej klasy implementującej. Metoda ta została już wykorzystana w wersji modułowej nr 1.
15.2.2. Klasa [ImpôtsConsole]
Klasa [ImpôtsConsole] implementuje interfejs [InterfaceImpôtsUi] w następujący sposób:
# importy
import re
from InterfaceImpôtsUi import InterfaceImpôtsUi
from TaxPayer import TaxPayer
# warstwa [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
# konstruktor
def __init__(self, config: dict):
# zapisywanie parametrów
self.admindata = config['dao'].get_admindata()
self.métier = config['métier']
def run(self):
# interaktywny dialog z użytkownikiem
fini = False
while not fini:
# czy podatnik jest w związku małżeńskim?
marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
# sprawdzenie poprawności wprowadzonych danych
while marié != "oui" and marié != "non" and marié != "*":
# komunikat o błędzie
print("Tapez oui ou non ou *")
# powtórz pytanie
marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
# Koniec?
if marié == "*":
# rozmowa zakończona
return
# liczba dzieci
enfants = input("Nombre d'enfants : ").strip()
# sprawdzanie poprawności wprowadzonych danych
if not re.match(r"^\d+$", enfants):
# komunikat o błędzie
print("Tapez un nombre entier positif ou nul")
# spróbujmy jeszcze raz
enfants = input("Nombre d'enfants : ").strip()
# roczne wynagrodzenie
salaire = input("Salaire annuel : ").strip()
# sprawdzenie poprawności wprowadzonych danych
if not re.match(r"^\d+$", salaire):
# komunikat o błędzie
print("Tapez un nombre entier positif ou nul")
# próbujemy ponownie
salaire = input("Salaire annuel : ").strip()
# obliczenie podatku
taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
self.métier.calculate_tax(taxpayer, self.admindata)
# wyświetlenie
print(f"Impôt du contribuable = {taxpayer}\n\n")
# kolejny podatnik
- wiersz 9: klasa [ImpôtsConsole] implementuje interfejs [InterfaceImpôtsUi];
- wiersz 11: konstruktor klasy otrzymuje parametr – słownik [config] z konfiguracji aplikacji;
- wiersz 13: pobierane są dane z urzędu skarbowego umożliwiające obliczenie podatku;
- wiersz 14: zapisuje się odwołanie w warstwie [métier];
- wiersz 16: implementacja metody [run] z interfejsu;
- wiersze 19–53: interakcja z użytkownikiem. Polega ona
- w zapytaniu podatnika o trzy informacje (stan cywilny, dzieci, wynagrodzenie);
- obliczenie jego podatku;
- wyświetlenie kwoty podatku;
- dialog kończy się, gdy użytkownik odpowie * na pierwsze pytanie;
- wiersze 20–27: zadaje się pytanie, czy podatnik jest żonaty, i sprawdza się poprawność odpowiedzi;
- wiersze 29–31: jeśli użytkownik odpowiedział „*” na pytanie, dialog zostaje zakończony;
- wiersze 32–39: pyta się o liczbę dzieci podatnika i sprawdza poprawność odpowiedzi;
- wiersze 40–47: pyta się o roczne wynagrodzenie podatnika i sprawdza poprawność odpowiedzi;
- wiersze 48–50: na podstawie tych informacji warstwa [métier] oblicza podatek podatnika;
- wiersz 52: wyświetlana jest kwota podatku;
15.2.3. Skrypt główny
Główny skrypt [main] jest skonfigurowany przez następujący plik [config]:
def configure():
import os
# krok 1 ------
# konfiguracja ścieżki Python
# katalog tego pliku
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"
# zależności aplikacji
absolute_dependencies = [
# zależności lokalne
f"{script_dir}/../../entities",
f"{script_dir}/../../interfaces",
f"{script_dir}/../../services",
f"{root_dir}/02/entities",
]
# ustawiamy ścieżkę systemową
from myutils import set_syspath
set_syspath(absolute_dependencies)
# krok 2 ------
# konfiguracja aplikacji
config = {
# absolutne ścieżki do plików aplikacji
"admindataFilename": f"{script_dir}/../../data/input/admindata.json",
}
# instancjonowanie warstw aplikacji
from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
from ImpôtsMétier import ImpôtsMétier
from ImpôtsConsole import ImpôtsConsole
# warstwa DAO
dao = ImpôtsDaoWithAdminDataInJsonFile(config)
# warstwa biznesowa
métier = ImpôtsMétier()
# instancje warstw umieszczamy w konfiguracji
config["dao"] = dao
config["métier"] = métier
# warstwa UI
ui = ImpôtsConsole(config)
config["ui"] = ui
# generujemy konfigurację
return config
Skrypt koordynujący ma następującą postać (main.py):
# konfigurujemy aplikację
import config
config = config.configure()
# importy
from ImpôtsError import ImpôtsError
# pobieramy warstwy aplikacji (są już zainicjowane)
ui = config["ui"]
# kod
try:
# wykonanie warstwy [ui]
ui.run()
except ImpôtsError as erreur:
# wyświetlanie komunikatu o błędzie
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# wykonane w każdym przypadku
print("Travail terminé...")
- wiersze 1–4: pobierana jest konfiguracja aplikacji;
- wiersz 10: pobieramy odwołanie do warstwy [ui];
- wiersze 12–21: struktura kodu jest taka sama jak w poprzedniej aplikacji: kod otoczony blokiem try/catch w celu przechwycenia ewentualnych wyjątków;
- wiersz 15: uruchamiamy warstwę [ui]: rozpoczyna się wówczas dialog z użytkownikiem;
- wiersze 16–18: przechwycenie ewentualnego wyjątku;
Oto przykład wykonania:
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