15. Практичне завдання — версія 4

Тут ми повертаємося до вправи, описаної в розділі |Версія 3|, і тепер розглядаємо її з використанням класів та інтерфейсів. Ми напишемо дві програми:
Програма 1 буде такою:

Головний скрипт [main] створить екземпляри шару [dao] та шару [métier]:
- шар [dao] буде відповідати за обробку даних, що зберігаються у текстових файлах, а згодом — у базі даних;
- рівень [métier] буде виконувати обчислення податку;
У цьому додатку не передбачено жодних дій з боку користувача: дані платників податків будуть взяті з текстового файлу, ім’я якого буде передано модулю [main].
У додатку 2 користувач самостійно вводитиме дані платників податків за допомогою клавіатури. Тоді архітектура зміниться наступним чином:

- рівень [dao] (Data Access Object) відповідає за доступ до зовнішніх даних
- рівень [métier] відповідає за бізнес-задачі, в даному випадку — за розрахунок податку. Він не оперує даними. Дані можуть надходити з двох джерел:
- рівень [dao] — для постійних даних;
- шар [ui] — для даних, що надаються користувачем.
- шар [ui] (User Interface) відповідає за взаємодію з користувачем;
- [main] є «диригентом»;
Далі шари [dao], [métier] та [ui] будуть реалізовані за допомогою окремих класів. Шари [métier] та [dao] будуть однаковими для обох додатків. Саме тому їх об’єднано в одній версії практичного завдання.
15.1. Версія 4 — додаток 1
Версія 4 обчислює податок для списку платників податків, розміщеного у текстовому файлі. Вона має таку архітектуру:

15.1.1. Об’єкти

Ентітети — це класи даних. Їхня роль полягає в інкапсуляції даних та наданні методів getter/setter, які дозволяють перевіряти їхню валідність. Ентітети передаються між рівнями. Один і той самий ентітет може переміщатися з рівня [ui] на рівень [dao] і навпаки.
15.1.1.1. Клас [ImpôtsError]
Ми будемо використовувати власний клас винятків:
# -------------------------------
# клас винятків
from MyException import MyException
class ImpôtsError(MyException):
pass
Як тільки шари [métier] та [dao] зіткнуться з проблемою, вони викличуть це виключення. Воно походить від класу [MyException]. Отже, його використовують таким чином: [raise ImpôtsError(code_erreur, msg_erreur)].
15.1.1.2. Клас [AdminData]
Клас [AdminData] інкапсулює константи, що використовуються при розрахунку податку:
from BaseEntity import BaseEntity
# дані податкової адміністрації
class AdminData(BaseEntity):
# ключі, виключені зі стану класу
excluded_keys = []
# дозволені ключі
@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"
]
- рядок 5: клас [AdminData] успадковує клас [BaseEntity], описаний у параграфі |BaseEntity|. Нагадаємо, що класи, які успадковують клас [BaseEntity], повинні визначати:
- атрибут класу [excluded_keys] (рядок 7), який перелічує властивості об’єкта, що виключаються під час перетворення об’єкта на словник;
- статичний метод [get_allowed_keys] (рядки 10–26), який повертає список властивостей, що приймаються під час ініціалізації об’єкта за допомогою словника;
Ми не використовували сетери для перевірки правильності даних, що використовуються для ініціалізації об’єкта [AdminData]. Адже цей об’єкт є унікальним і визначається конфігурацією, тому він не може містити помилок.
15.1.1.3. Клас [TaxPayer]
Клас [TaxPayer] буде моделювати платника податків:
# імпортні дані
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError
# платник податків
class TaxPayer(BaseEntity):
# моделює платника податків
# id: ідентифікатор
# одружений: так / ні
# діти: кількість дітей
# зарплата: його річна зарплата
# податок: сума податку до сплати
# надбавка: надбавка до суми податку, що підлягає сплаті
# знижка: знижка з податку, що підлягає сплаті
# знижка: знижка на податок, що підлягає сплаті
# ставка: ставка оподаткування платника податків
# ключі, виключені зі звіту про клас
excluded_keys = []
# дозволені ключі
@staticmethod
def get_allowed_keys() -> list:
return ['id', 'marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']
# властивості
@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
# сетери
@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):
# children має бути цілим числом >=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):
# зарплата має бути цілим числом >=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):
# податок повинен бути цілим числом >=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):
# знижка має бути цілим числом >=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):
# надбавка повинна бути цілим числом >=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):
# надбавка повинна бути цілим числом >=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):
# ставка повинна бути дійсним числом >=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")
Примітки:
- клас [TaxPayer] інкапсулює платника податків;
- рядок 7: клас [TaxPayer] походить від класу [BaseEntity]. Отже, він має ідентифікатор [id];
- рядок 20: жодна властивість не виключена зі стану об’єкта [AdminData];
- рядки 22–25: властивості класу. Вони описані в рядках 9–17;
- рядки 27–58: методи-геттери атрибутів класу;
- рядки 60–161: методи-сеттери атрибутів класу. Нагадаємо, що перевага класу, який інкапсулює дані, перед простим словником полягає в тому, що клас може перевіряти правильність своїх властивостей за допомогою своїх методів-сеттерів;
15.1.2. Рівень [dao]

Ми об’єднаємо реалізації шарів у папці [services]. Ці класи будуть реалізовувати інтерфейси, визначені в папці [interfaces].

15.1.2.1. Інтерфейс [InterfaceImpôtsDao]
Шар [dao] реалізує наступний інтерфейс [InterfaceImpôtsDao] (файл InterfaceImpôtsDao.py):
# імпорт
from abc import ABC, abstractmethod
# інтерфейс IImpôtsDao
from AdminData import AdminData
class InterfaceImpôtsDao(ABC):
# перелік податкових шкал
@abstractmethod
def get_admindata(self) -> AdminData:
pass
# перелік даних про платників податків
@abstractmethod
def get_taxpayers_data(self) -> dict:
pass
# запис результатів розрахунку податку
@abstractmethod
def write_taxpayers_results(self, taxpayers_results: list):
pass
Інтерфейс визначає три методи:
- [get_admindata]: метод, що отримує масив податкових шкал. Слід зауважити, що не надається жодної інформації щодо способу отримання цих даних. Далі ці дані спочатку будуть знаходитися у текстовому файлі, а потім — у базі даних. Класи, що реалізують цей інтерфейс, повинні будуть адаптуватися до способу зберігання даних. Отже, ми матимемо один клас для отримання податкових шкал із текстового файлу та інший — для отримання їх із бази даних. Обидва вони реалізовуватимуть метод [get_admindata];
- [get_taxpayers_data]: це метод, який отримує дані про платників податків. І тут ми також не вказуємо, де саме вони будуть знаходитися. Ми розглянемо лише той випадок, коли вони містяться у текстовому файлі;
- [write_taxpayers_results]: метод, який зберігатиме результати розрахунку податку. Ми не вказуємо, де саме. Ми розглянемо лише випадок, коли результати зберігаються у текстовому файлі. Параметр [taxpayers_results] буде списком результатів, які потрібно зберегти;
15.1.2.2. Клас [AbstractImpôtsDao]
Рівень [dao] буде реалізовано двома класами:
- один буде отримувати дані (платники податків, результати, податкові ставки) з текстових файлів;
- інший — отримуватиме дані (платники податків, результати) з текстових файлів, а податкові шкали — з бази даних;
Ці два класи відрізнятимуться лише способом обробки податкових шкал. Дані про платників податків та результати розрахунків податку оброблятимуться однаково. З цієї причини ми будемо обробляти їх у батьківському класі [AbstractImpôtsDao]. Особливості обробки податкових шкал будуть реалізовані у двох дочірніх класах:
- клас [ImpôtsDaoWithAdminDataInJsonFile] буде отримувати податкові шкали з текстового файлу у форматі jSON;
- клас [ImpôtsDaoWithAdminDataInDatabase] буде отримувати податкові шкали з бази даних;
Батьківський клас [AbstractImpôtsDao] матиме такий вигляд:
# імпорт
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
# базовий клас для шару [dao]
class AbstractImpôtsDao(InterfaceImpôtsDao):
# платники податків та їхні податки будуть у текстових файлах
# конструктор
def __init__(self, config: dict):
# config[taxpayersFilename]: ім’я текстового файлу платників податків
# config[resultsFilename] : ім’я файлу jSON з результатами
# config[errorsFilename]: ім’я файлу помилок
# зберігаються параметри
self.taxpayers_filename = config.get("taxpayersFilename")
self.taxpayers_results_filename = config.get("resultsFilename")
self.errors_filename = config.get("errorsFilename")
# ------------------
# інтерфейс IImpôtsDao
# ------------------
# список даних про платників податків
def get_taxpayers_data(self) -> dict:
…
# запис податків платників податків
def write_taxpayers_results(self, taxpayers: list):
…
# зчитування податкових шкал
@abstractmethod
def get_admindata(self) -> AdminData:
pass
-
рядок 13: клас [AbstractImpôtsDao] реалізує інтерфейс [InterfaceImpôtsDao]. Тому тут містяться три методи цього інтерфейсу:
- [get_taxpayers_data]: рядок 31;
- [write_taxpayers_results]: рядок 35;
- [get_admindata]: рядок 40. Цей метод не буде реалізований класом [AbstractImpôtsDao], тому він оголошений абстрактним (рядок 39);
-
рядок 16: конструктор отримує словник [config], що містить таку інформацію:
- [taxpayersFilename]: ім’я текстового файлу, що містить дані платників податків;
- [resultsFilename]: ім’я текстового файлу, до якого будуть записані результати;
- [errorsFilename]: ім’я текстового файлу, що містить перелік помилок, виявлених під час обробки файлу [taxpayersFilename];
Метод [get_taxpayers_data] є таким:
# список даних про платників податків
def get_taxpayers_data(self) -> dict:
# ініціалізація
taxpayers_data = []
datafile = None
erreurs = []
try:
# відкриття файлу даних
datafile = open(self.taxpayers_filename, "r")
# обробка поточного рядка файлу
ligne = datafile.readline()
# номер рядка
numligne = 0
while ligne != '':
# рядок «+»
numligne += 1
# видалення пробілів
ligne = ligne.strip()
# пропускаються порожні рядки та коментарі
if ligne != "" and ligne[0] != "#":
try:
# витягуються 4 поля id,marié,enfants,salaire, що утворюють рядок платника податків
(id, marié, enfants, salaire) = ligne.split(",")
# створюється новий TaxPayer
taxpayers_data.append(
TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
except BaseException as erreur:
# фіксується помилка
erreurs.append(f"Ligne {numligne}, {erreur}")
# зчитується новий запис платника податків
ligne = datafile.readline()
# записуємо помилки, якщо вони є
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)
# повертається результат
return {"taxpayers": taxpayers_data, "erreurs": erreurs}
except BaseException as erreur:
# викликається виняток ImpôtsError
raise ImpôtsError(11, f"{erreur}")
finally:
# закривається файл
if datafile:
datafile.close()
- рядок 4: дані про платників податків (сімейний стан, діти, заробітна плата) будуть розміщені у списку об’єктів типу [TaxPayer];
- рядки 8–9: відкриваємо текстовий файл платників податків у режимі читання. Його вміст має такий вигляд:
Порівняно з попередніми версіями:
- кожен рядок файлу [taxpayersFilename] починається з ідентифікатора платника податків — простого номера;
- дозволено вживати коментарі та порожні рядки;
- ми будемо обробляти помилки. Таким чином, рядки 17, 19 та 21 мають бути визнані помилковими. Помилки записуються в окремий файл;
Продовжимо розгляд коду:
- рядок 4: дані з текстового файлу переносяться до списку [taxPayersData];
- рядки 14–31: файл платників податків зчитується рядок за рядком;
- рядок 14: кінець файлу досягається, коли зчитується порожній рядок (нічого — навіть позначки кінця рядка \r\n немає);
- рядок 20: порожні рядки та коментарі ігноруються. Рядок вважається коментарем, якщо після видалення пробілів перед і після тексту першим символом є символ #;
- рядок 24: правильний рядок складається з чотирьох полів, розділених комою. Ці поля витягуються. Присвоєння даних кортежу з чотирьох елементів завершується з помилкою, якщо кількість присвоєних даних не дорівнює точно чотирьом;
- рядок 25: якщо одне з чотирьох отриманих полів [id, marié, enfants, salaire] є недійсним, то метод [BaseEntity.fromdict] викличе виняток типу [MyException];
- рядки 25–26: об’єкт [TaxPayer] додається до списку платників податків [taxpayers_data];
- рядки 27–29: можливі помилки накопичуються у списку [erreurs]. Цей список було створено у рядку 6;
- рядки 33–36: список виявлених помилок записується у текстовий файл [errorsFilename]. Помилки бувають двох типів:
- у рядку не було правильної кількості очікуваних полів;
- інформація в рядку була помилковою, і не вдалося створити об’єкт [TaxPayer];
- рядки 39–41: перехоплюється будь-яка помилка (BaseException) і передається вище, інкапсулюючи її в тип [ImpôtsError];
- рядки 42–45: у будь-якому випадку — успіху чи невдачі — текстовий файл платників податків закривається, якщо він був відкритий;
Метод [write_taxpayers_results] повинен створити файл jSON такого вигляду:
[
{
"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
},
…
]
Метод [write_taxpayers_results] має такий вигляд:
# запис податків платників податків
def write_taxpayers_results(self, taxpayers: list):
# запис результатів у файл jSON
# платники податків: список об’єктів типу TaxPayer
# (ідентифікатор, сімейний стан, діти, заробітна плата, податок, надбавка, знижка, зменшення, ставка)
# список [taxpayers] записано у текстовий файл [self.taxpayers_results_filename]
file = None
try:
# відкриття файлу результатів
file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
# створення списку для серіалізації у jSON
mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
# серіалізація jSON
json.dump(list(mapping), file, ensure_ascii=False)
except BaseException as erreur:
# повторне генерування помилки під іншим типом
raise ImpôtsError(12, f"{erreur}")
finally:
# закриття файлу, якщо він був відкритий
if file:
file.close()
- рядок 2: метод отримує список платників податків [taxpayers], який він повинен записати у текстовий файл [self.taxpayers_results_filename] у форматі jSON;
- рядок 10: створення файлу результатів UTF-8;
- рядок 12: тут ми вводимо функцію [map], синтаксис якої в даному випадку має вигляд [map (fonction, liste1)]. Функція [fonction] застосовується до кожного елемента [liste1] і створює новий елемент, який поповнює список [liste2]. Нарешті, для кожного i:
liste2[i]=fonction(liste1[i])
Тут [liste1] — це список [taxPayers], список об’єктів типу [TaxPayer]. Функція [fonction] тут представлена у вигляді так званої функції [lambda], яка описує перетворення, що виконується над елементом [taxpayer] списку [taxpayers]: кожен елемент [taxpayer] замінюється на свій словник [taxpayer.asdict()]. Врешті-решт, отриманий список [liste2] є списком словників елементів списку [taxpayers];
- рядок 12: результатом роботи функції [map] є не список [liste2], а об’єкт типу [map]. Щоб отримати [liste2], потрібно використати вираз [list(mapping)] (рядок 14);
- рядок 14: список [liste2] записується у форматі jSON у файл [self.taxpayers_results_filename];
- рядки 15–17: будь-який тип винятку перехоплюється та інкапсулюється в помилку типу [ImpôtsError] перед повторним запуском (рядок 17);
- рядки 19–21: у будь-якому випадку — чи то успішному, чи то невдалому — файл результатів закривається, якщо він був відкритий;
15.1.2.3. Клас [ImpôtsDaoWithAdminDataInJsonFile]
Клас [ImpôtsDaoWithAdminDataInJsonFile] буде похідним від класу [AbstractImpôtsDao] і реалізує метод [getAdminData], який його батьківський клас не реалізував. Він буде отримувати дані податкової адміністрації з файлу 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
}
Клас [ImpôtsDaoWithAdminDataInJsonFile] має такий вигляд:
# імпорт
import codecs
import json
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
# реалізація шару [dao], де дані податкової адміністрації містяться у файлі jSON
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
# конструктор
def __init__(self, config: dict):
# config[admindataFilename]: ім’я файлу jSON, що містить дані податкової адміністрації
# config[taxpayersFilename] : ім’я текстового файлу з даними про платників податків
# config[resultsFilename]: ім’я файлу jSON з результатами
# config[errorsFilename]: ім’я файлу з помилками
# ініціалізація класу Parent
AbstractImpôtsDao.__init__(self, config)
# читання даних податкової служби
file = None
try:
# відкриття файлу jSON з податковими даними у режимі читання
file = codecs.open(config["admindataFilename"], "r", "utf8")
# перенесення вмісту файлу jSON в об’єкт [AdminData]
self.admindata = AdminData().fromdict(json.load(file))
except BaseException as erreur:
# повторне формування помилки у вигляді типу [ImpôtsError]
raise ImpôtsError(21, f"{erreur}")
finally:
# закриття файлу, якщо він був відкритий
if file:
file.close()
# -------------
# інтерфейс
# -------------
# отримання даних від податкової служби
# метод повертає об’єкт [AdminData]
def get_admindata(self) -> AdminData:
return self.admindata
- рядок 11: клас [ImpôtsDaoWithAdminDataInJsonFile] успадковує клас [AbstractImpôtsDao]. Таким чином, він реалізує інтерфейс [InterfaceImpôtsDao];
- рядок 13: конструктор отримує як параметр словник, що містить інформацію з рядків 14–17;
- рядок 20: ініціалізується батьківський клас;
- рядок 24: відкриття файлу jSON з даними податкової адміністрації;
- рядок 25: відкрито файл UTF-8 з даними податкової адміністрації;
- рядок 27: вміст файлу зчитується та розміщується в об’єкті [self.admindata] типу [AdminData]. Ключі файлу jSON повинні відповідати властивостям, що допускаються для об’єкта [AdminData], інакше метод [fromdict] викличе виняток;
- рядки 28–30: обробка винятків. Винятки, що можуть виникнути, інкапсулюються в тип [ImpôtsError] перед тим, як їх перекинути;
- рядки 32–34: файл закривається, якщо він був відкритий;
- рядки 42–43: реалізація методу [get_admindata] інтерфейсу [InterfaceImpôtsDao];
15.1.3. Рівень [métier]

15.1.3.1. Інтерфейс [InterfaceImpôtsMétier]
Інтерфейс шару [métier] матиме такий вигляд:
# імпорт
from abc import ABC, abstractmethod
from AdminData import AdminData
from TaxPayer import TaxPayer
# інтерфейс IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
# розрахунок податку для 1 платника податків
@abstractmethod
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
pass
- інтерфейс [InterfaceImpôtsMétier] визначає єдиний метод:
- рядок 12: метод [calculate_tax] дозволяє розрахувати податок для одного платника податків [taxpayer]. [admindata] — це об’єкт [AdminData], що інкапсулює дані податкової адміністрації;
- рядок 12: метод [calculate_tax] не повертає результату. Отримані дані (податок, надбавка, знижка, пільга, ставка) містяться в параметрі [taxpayer]: до виклику ці атрибути порожні, після виклику вони ініціалізовані;
15.1.3.2. Клас [ImpôtsMétier]
Клас [ImpôtsMétier] реалізує інтерфейс [InterfaceImpôtsMétier] наступним чином:

Методи класу походять із модуля [impôts_module_02], описаного в розділі |Модуль [impots.v02.modules.impôts_module_02]|. Кількість параметрів методів було обмежено до двох:
- taxpayer(id, одружений, діти, зарплата, податок, знижка, надбавка, пільга, ставка): об’єкт, що представляє платника податків та його податок;
- admindata: об’єкт, що інкапсулює дані податкової адміністрації;
На прикладі одного методу продемонструємо внесені зміни;
# розрахунок податку — етап 1
# ----------------------------------------
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
# платник податків (ідентифікаційний номер, сімейний стан, діти, заробітна плата, податок, знижка, надбавка, пільга, ставка)
# admindata: дані податкової адміністрації
# розрахунок податку з урахуванням дітей
self.calculate_tax_2(taxpayer, admindata)
# результати містяться в taxpayer
taux1 = taxpayer.taux
surcôte1 = taxpayer.surcôte
impot1 = taxpayer.impôt
# розрахунок податку без урахування дітей
if taxpayer.enfants != 0:
# розрахунок податку для того самого платника податків без дітей
taxpayer2 = TaxPayer().fromdict(
{'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
self.calculate_tax_2(taxpayer2, admindata)
# результати знаходяться у taxpayer2
taux2 = taxpayer2.taux
surcôte2 = taxpayer2.surcôte
impot2 = taxpayer2.impôt
# застосування обмеження сімейного коефіцієнта
if taxpayer.enfants < 3:
# PLAFOND_QF_DEMI_PART євро за перших двох дітей
impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
else:
# PLAFOND_QF_DEMI_PART євро за перших двох дітей, удвічі більше — за наступних
impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
* 2 * admindata.plafond_qf_demi_part
else:
# якщо платник податків не має дітей, то податок2 = податок1
impot2 = impot1
# береться найбільший податок із відповідною ставкою та надбавкою
(impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
impot2, impot2 - impot1 + surcôte2, taux2)
# проміжні результати
taxpayer.impôt = impot
taxpayer.surcôte = surcôte
taxpayer.taux = taux
# розрахунок можливої знижки
self.get_décôte(taxpayer, admindata)
taxpayer.impôt -= taxpayer.décôte
# розрахунок можливого зменшення податку
self.get_réduction(taxpayer, admindata)
taxpayer.impôt -= taxpayer.réduction
# результат
taxpayer.impôt = math.floor(taxpayer.impôt)
- рядок 3: метод [calculate_tax] є єдиним методом інтерфейсу [InterfaceImpôtsMétier]. Він приймає два параметри:
- [tapPayer]: платник податків, для якого розраховується податок;
- [admindata] — об’єкт, що інкапсулює дані податкової адміністрації;
- результати розрахунку інкапсульовані в параметрі [taxpayer] (рядки 40–50). Отже, вміст цього об’єкта до та після виклику методу відрізняється;
15.1.4. Тестування шарів [dao] та [métier]

- [TestDaoMétier] — це тестовий клас UnitTest для шарів [dao] та [métier];
- [config] — це файл конфігурації тестів;
Конфігурація [config] має такий вигляд:
def configure():
import os
# етап 1 ------
# налаштування шляху до Python
# папка, в якій знаходиться цей файл
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"
# абсолютні залежності додатка
absolute_dependencies = [
f"{script_dir}/../entities",
f"{script_dir}/../interfaces",
f"{script_dir}/../services",
f"{root_dir}/02/entities",
]
# встановлюємо syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# крок 2 ------
# конфігурація додатка
config = {
# абсолютні шляхи до файлів додатка
"admindataFilename": f"{script_dir}/../data/input/admindata.json"
}
# інстанціювання шарів додатка
from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
from ImpôtsMétier import ImpôtsMétier
dao = ImpôtsDaoWithAdminDataInJsonFile(config)
métier = ImpôtsMétier()
# додаємо екземпляри шарів до конфігурації
config["dao"] = dao
config["métier"] = métier
# виконується конфігурація
return config
- рядки 4–23: налаштовується Python Path для тестів;
- рядки 32–41: створюються екземпляри шарів [dao] та [métier]. Їхні посилання додаються до словника [config];
- рядок 44: повертається цей словник;
Клас тесту [TestDaoMétier] має такий вигляд:
import unittest
def get_config() -> dict:
# конфігурація додатка
import config
# виконується конфігурація
return config.configure()
class TestDaoMétier(unittest.TestCase):
# виконується перед кожним методом test_
def setUp(self) -> None:
# отримуємо конфігурацію тестів
config = get_config()
# зберігаємо деяку інформацію
self.métier = config['métier']
self.admindata = config['dao'].get_admindata()
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'одружений': 'так', 'діти': 2, 'зарплата': 55555,
# 'податок': 2814, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірка
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
# {'одружений': 'так', 'діти': 2, 'зарплата': 50000,
# 'податок': 1384, 'надбавка': 0, 'знижка': 384, 'зниження': 347, 'ставка': 0,14}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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
# {'одружений': 'так', 'діти': 3, 'зарплата': 50000,
# 'податок': 0, 'надбавка': 0, 'знижка': 720, 'зниження': 0, 'ставка': 0,14}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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
# {'одружений': 'ні', 'діти': 2, 'зарплата': 100000,
# 'податок': 19884, 'надбавка': 4480, 'знижка': 0, 'зниження': 0, 'ставка': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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
# {'одружений': 'ні', 'діти': 3, 'зарплата': 100000,
# 'податок': 16782, 'надбавка': 7176, 'знижка': 0, 'зниження': 0, 'ставка': 0.41}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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
# {'одружений': 'так', 'діти': 3, 'зарплата': 100000,
# 'податок': 9200, 'надбавка': 2180, 'знижка': 0, 'зниження': 0, 'ставка': 0,3}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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
# {'одружений': 'так', 'діти': 5, 'зарплата': 100000,
# 'податок': 4230, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0,14}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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
# {'одружений': 'ні', 'діти': 0, 'зарплата': 100000,
# 'податок': 22986, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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
# {'одружений': 'так', 'діти': 2, 'зарплата': 30000,
# 'податок': 0, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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
# {'одружений': 'ні', 'діти': 0, 'зарплата': 200000,
# 'податок': 64210, 'надбавка': 7498, 'знижка': 0, 'зниження': 0, 'ставка': 0,45}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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
# {'одружений': 'так', 'діти': 3, 'зарплата': 200000,
# 'податок': 42842, 'надбавка': 17283, 'знижка': 0, 'зниження': 0, 'ставка': 0.41}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
self.métier.calculate_tax(taxpayer, self.admindata)
# перевірки
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()
Коментарі
- рядок 11: тестовий клас успадковує клас [unittest.TestCase];
- рядки 13–19: у тесті UnitTest метод [setUp] виконується перед кожним із методів [test_];
- рядок 16: отримується конфігурація зі скрипта [config], розглянутого раніше;
- рядок 18: зберігається посилання на шар [métier];
- рядок 19: у шару [dao] запитується об’єкт [AdminData], що інкапсулює дані податкової адміністрації, і він зберігається;
- рядки 21–173: 11 тестів, результати яких було перевірено на офіційному сайті податкової служби за 2019 рік |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm|;
- рядки 21–33: усі тести побудовано за одним і тим самим зразком;
- рядок 22: імпортується клас [TaxPayer];
- рядок 24: платник податків, що тестується;
- рядок 25: очікувані результати;
- рядок 26: створення об’єкта [TaxPayer] платника податків;
- рядок 27: розрахунок податку. Результат міститься в [taxpayer];
- рядки 29–33: перевірка отриманих результатів;
- рядок 29: перевіряється сума податку з точністю до євро. Тестування показало, що результати, отримані за допомогою алгоритму, описаного в цьому документі, можуть відрізнятися від офіційних цифр не більше ніж на 1 євро;
Виконання тестів дає такі результати:

15.1.5. Головний скрипт

Головний скрипт налаштовується за допомогою наступного скрипта [config]:
def configure():
import os
# етап 1 ------
# налаштування шляху до Python
# папка цього файлу
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"
# залежності додатка
absolute_dependencies = [
# локальні залежності
f"{script_dir}/../../entities",
f"{script_dir}/../../interfaces",
f"{script_dir}/../../services",
f"{root_dir}/02/entities",
]
# встановлюємо syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# крок 2 ------
# конфігурація додатка
config = {
# абсолютні шляхи до файлів додатка
"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"
}
# інстанціювання шарів додатка
from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
from ImpôtsMétier import ImpôtsMétier
dao = ImpôtsDaoWithAdminDataInJsonFile(config)
métier = ImpôtsMétier()
# додаємо екземпляри шарів до конфігурації
config["dao"] = dao
config["métier"] = métier
# завантажуємо конфігурацію
return config
Він аналогічний тому, що використовується для тестування шарів [métier] та [dao].
Основний скрипт [main.py] має такий вигляд:
# налаштовуємо додаток
import config
config = config.configure()
# імпортуємо
from ImpôtsError import ImpôtsError
# отримуємо шари додатка (вони вже інстанційовані)
dao = config["dao"]
métier = config["métier"]
try:
# отримання податкових шкал
admindata = dao.get_admindata()
# зчитування даних про платників податків
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# платників податків?
if not taxpayers:
raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# розрахунок податку платників податків
for taxpayer in taxpayers:
# «платник податків» є одночасно вхідним і вихідним параметром
# «taxpayer» буде змінено
métier.calculate_tax(taxpayer, admindata)
# запис результатів у текстовий файл
dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
# виведення повідомлення про помилку
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# завершено
print("Travail terminé...")
Примітки
- рядки 2–4: отримуємо конфігурацію додатка. Також відомо, що Python Path додатка вже сформовано;
- рядки 9–11: отримуємо посилання на шари [métier] та [dao];
- рядок 15: отримуємо дані від податкової адміністрації;
- рядок 17: отримуємо список платників податків, для яких потрібно розрахувати податок;
- рядки 19–20: якщо цей список порожній, генерується виняток;
- рядки 22–25: обчислення податку для різних об’єктів [taxpayer] за допомогою шару [métier];
- рядок 27: [taxpayers] тепер є списком об’єктів [TaxPayer], у яких атрибути (податок, знижка, надбавка, зменшення, ставка) отримали значення. Цей список записується у файл jSON;
- рядки 28–30: виявлення можливої помилки;
- рядки 31–33: виконуються в будь-якому випадку;
Виконання скрипта дає ті самі результати, що й у попередніх версіях. Файл з помилками платників податків був новим елементом у цій версії. Після виконання скрипта [main] його вміст виглядає так:
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]
Помилкові рядки були такими:
15.2. Версія 4 — додаток 2
У цій версії користувач самостійно вводить список платників податків за допомогою клавіатури. Архітектура додатка буде такою:

З’являється новий модуль: рівень [ui] (User Interface), який буде взаємодіяти з користувачем. Цей рівень матиме інтерфейс і буде реалізований класом.

15.2.1. Інтерфейс [InterfaceImpôtsUi]
# імпорт
from abc import ABC, abstractmethod
# інтерфейс InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
# виконання класу, що реалізує інтерфейс
@abstractmethod
def run(self):
pass
Інтерфейс [InterfaceImpôtsUi] матиме лише один метод, наведений у рядках 8–10. У цьому прикладі інтерфейс буде реалізовано за допомогою консольної програми, але його також можна реалізувати за допомогою графічного інтерфейсу. Параметри, що передаються методу [run], у цих двох реалізаціях будуть різними. Щоб обійти цю проблему, зазвичай застосовують такий підхід:
- не передавати параметри методу [run] (або передавати мінімальну кількість параметрів);
- передавати параметри конструктору класу, що реалізує інтерфейс. Вони можуть відрізнятися в різних реалізаціях. Ці параметри зберігаються як атрибути класу;
- забезпечити, щоб метод [run] використовував ці атрибути класу (self.x);
Цей метод дозволяє отримати дуже загальний інтерфейс, який уточнюється параметрами конструкторів кожного класу реалізації. Цей метод вже використовувався для модульної версії № 1.
15.2.2. Клас [ImpôtsConsole]
Клас [ImpôtsConsole] реалізує інтерфейс [InterfaceImpôtsUi] наступним чином:
# імпорт
import re
from InterfaceImpôtsUi import InterfaceImpôtsUi
from TaxPayer import TaxPayer
# шар [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
# конструктор
def __init__(self, config: dict):
# зберігання параметрів
self.admindata = config['dao'].get_admindata()
self.métier = config['métier']
def run(self):
# інтерактивний діалог з користувачем
fini = False
while not fini:
# чи одружений платник податків?
marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
# перевірка правильності введених даних
while marié != "oui" and marié != "non" and marié != "*":
# повідомлення про помилку
print("Tapez oui ou non ou *")
# повторити запитання
marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
# Завершено?
if marié == "*":
# діалог завершено
return
# кількість дітей
enfants = input("Nombre d'enfants : ").strip()
# перевірка правильності введених даних
if not re.match(r"^\d+$", enfants):
# повідомлення про помилку
print("Tapez un nombre entier positif ou nul")
# почати спочатку
enfants = input("Nombre d'enfants : ").strip()
# річний оклад
salaire = input("Salaire annuel : ").strip()
# перевірка правильності введених даних
if not re.match(r"^\d+$", salaire):
# повідомлення про помилку
print("Tapez un nombre entier positif ou nul")
# повторюємо
salaire = input("Salaire annuel : ").strip()
# розрахунок податку
taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
self.métier.calculate_tax(taxpayer, self.admindata)
# відображення
print(f"Impôt du contribuable = {taxpayer}\n\n")
# наступний платник податків
- рядок 9: клас [ImpôtsConsole] реалізує інтерфейс [InterfaceImpôtsUi];
- рядок 11: конструктор класу отримує параметр — словник [config] з конфігурації додатка;
- рядок 13: отримуються дані від податкової служби, що дозволяють розрахувати податок;
- рядок 14: зберігається посилання на шар [métier];
- рядок 16: реалізація методу [run] інтерфейсу;
- рядки 19–53: діалог з користувачем. Він полягає
- у запиті трьох відомостей (сімейний стан, наявність дітей, заробітна плата) про платника податків;
- у розрахунку його податку;
- у його відображенні;
- діалог завершується, коли користувач відповідає * на перше запитання;
- рядки 20–27: запитується, чи одружений платник податків, і перевіряється правильність відповіді;
- рядки 29–31: якщо користувач відповів «*» на запитання, діалог припиняється;
- рядки 32–39: запитується кількість дітей платника податків і перевіряється правильність відповіді;
- рядки 40–47: запитується річний дохід платника податків і перевіряється правильність відповіді;
- рядки 48–50: на основі цієї інформації за допомогою шару [métier] розраховується податок платника податків;
- рядок 52: відображається сума податку;
15.2.3. Головний скрипт
Головний скрипт [main] налаштовується за допомогою такого файлу [config]:
def configure():
import os
# крок 1 ------
# налаштування шляху до Python
# папка цього файлу
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"
# залежності додатка
absolute_dependencies = [
# локальні залежності
f"{script_dir}/../../entities",
f"{script_dir}/../../interfaces",
f"{script_dir}/../../services",
f"{root_dir}/02/entities",
]
# встановлюємо syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# крок 2 ------
# конфігурація додатка
config = {
# абсолютні шляхи до файлів додатка
"admindataFilename": f"{script_dir}/../../data/input/admindata.json",
}
# інстанціювання шарів додатка
from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
from ImpôtsMétier import ImpôtsMétier
from ImpôtsConsole import ImpôtsConsole
# рівень DAO
dao = ImpôtsDaoWithAdminDataInJsonFile(config)
# бізнес-шар
métier = ImpôtsMétier()
# додаємо екземпляри шарів у конфігурацію
config["dao"] = dao
config["métier"] = métier
# шар UI
ui = ImpôtsConsole(config)
config["ui"] = ui
# генеруємо конфігурацію
return config
Координаційний скрипт має такий вигляд (main.py):
# налаштовуємо додаток
import config
config = config.configure()
# імпортуємо
from ImpôtsError import ImpôtsError
# отримуємо шари додатка (вони вже інстанційовані)
ui = config["ui"]
# код
try:
# виконання шару [ui]
ui.run()
except ImpôtsError as erreur:
# виводиться повідомлення про помилку
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# виконується в будь-якому випадку
print("Travail terminé...")
- рядки 1–4: отримуємо конфігурацію додатка;
- рядок 10: отримуємо посилання на шар [ui];
- рядки 12–21: структура коду така сама, як і в попередньому додатку: код, оточений блоком try/catch для перехоплення можливих винятків;
- рядок 15: запускається шар [ui]: після цього починається діалог з користувачем;
- рядки 16–18: перехоплення можливого винятку;
Ось приклад виконання:
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