15. Anwendungsaufgabe – Version 4

Wir greifen hier die im Abschnitt |Version 3| beschriebene Übung wieder auf und bearbeiten sie nun mit Klassen und Schnittstellen. Wir werden zwei Anwendungen schreiben:
Anwendung 1 sieht wie folgt aus:

Ein Hauptskript [main] instanziiert eine Schicht [dao] und eine Schicht [métier]:
- Die Schicht [dao] hat die Aufgabe, Daten zu verwalten, die in Textdateien und später in einer Datenbank gespeichert sind;
- die Schicht [métier] ist für die Berechnung der Steuer zuständig;
In dieser Anwendung sind keine Benutzeraktionen erforderlich: Die Daten der Steuerzahler werden aus einer Textdatei abgerufen, deren Name dem Modul [main] übergeben wird.
In Anwendung 2 gibt der Benutzer die Daten der Steuerzahler über die Tastatur ein. Die Architektur entwickelt sich dann wie folgt weiter:

- Die Schicht [dao] (Data Access Object) ist für den Zugriff auf externe Daten zuständig
- Die Schicht [métier] kümmert sich um die geschäftlichen Aspekte, in diesem Fall die Steuerberechnung. Sie befasst sich nicht mit den Daten. Diese können aus zwei Quellen stammen:
- die Schicht [dao] für persistente Daten;
- die Schicht [ui] für vom Benutzer bereitgestellte Daten.
- Die Schicht [ui] (Benutzeroberfläche) ist für die Interaktionen mit dem Benutzer zuständig;
- [main] ist der Koordinator;
Im Folgenden werden die Schichten [dao], [métier] und [ui] jeweils mithilfe einer Klasse implementiert. Die Schichten [métier] und [dao] sind für beide Anwendungen identisch. Aus diesem Grund wurden sie in einer einzigen Version der Anwendungsübung zusammengefasst.
15.1. Version 4 – Anwendung 1
Version 4 berechnet die Steuer für eine Liste von Steuerpflichtigen, die in einer Textdatei gespeichert ist. Sie weist folgende Architektur auf:

15.1.1. Die Entitäten

Entitäten sind Datenklassen. Ihre Aufgabe ist es, Daten zu kapseln und Getter/Setter bereitzustellen, mit denen die Gültigkeit dieser Daten überprüft werden kann. Die Entitäten werden zwischen den Schichten ausgetauscht. Eine Entität kann von der Schicht [ui] zur Schicht [dao] gelangen und umgekehrt.
15.1.1.1. Die Klasse [ImpôtsError]
Wir verwenden eine proprietäre Ausnahmeklasse:
# -------------------------------
# Ausnahmeklasse
from MyException import MyException
class ImpôtsError(MyException):
pass
Sobald die Ebenen [métier] und [dao] auf ein Problem stoßen, lösen sie diese Ausnahme aus. Sie leitet sich von der Klasse [MyException] ab. Sie wird daher wie folgt verwendet: [raise ImpôtsError(code_erreur, msg_erreur)].
15.1.1.2. Die Klasse [AdminData]
Die Klasse [AdminData] kapselt die Konstanten, die bei der Steuerberechnung eine Rolle spielen:
from BaseEntity import BaseEntity
# Daten der Steuerbehörde
class AdminData(BaseEntity):
# aus der Klassenübersicht ausgeschlossene Schlüssel
excluded_keys = []
# zugelassene Schlüssel
@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"
]
- Zeile 5: Die Klasse [AdminData] erweitert die im Abschnitt |BaseEntity| beschriebene Klasse [BaseEntity]. Es sei daran erinnert, dass Klassen, die die Klasse [BaseEntity] erweitern, Folgendes definieren müssen:
- ein Klassenattribut [excluded_keys] (Zeile 7), das die Eigenschaften des Objekts auflistet, die bei der Umwandlung des Objekts in ein Wörterbuch ausgeschlossen werden;
- eine statische Methode [get_allowed_keys] (Zeilen 10–26), die die Liste der akzeptierten Eigenschaften zurückgibt, wenn das Objekt mit einem Wörterbuch initialisiert wird;
Es wurden keine Setter verwendet, um die Gültigkeit der Daten zu überprüfen, die zur Initialisierung eines Objekts vom Typ [AdminData] verwendet werden. Da dieses Objekt eindeutig ist und durch die Konfiguration definiert wird, ist es nicht fehleranfällig.
15.1.1.3. Die Klasse [TaxPayer]
Die Klasse [TaxPayer] modelliert einen Steuerpflichtigen:
# Importe
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError
# ein Steuerpflichtiger
class TaxPayer(BaseEntity):
# modelliert einen Steuerpflichtigen
# ID: Kennung
# verheiratet: ja / nein
# Kinder: Anzahl der Kinder
# Gehalt: sein Jahresgehalt
# Steuer: Höhe der zu zahlenden Steuer
# Zuschlag: zu zahlender Steuerzuschlag
# Ermäßigung: Ermäßigung der zu zahlenden Steuer
# Ermäßigung: Ermäßigung auf die zu zahlende Steuer
# Steuersatz: Steuersatz des Steuerpflichtigen
# aus der Klassenübersicht ausgeschlossene Schlüssel
excluded_keys = []
# zulässige Schlüssel
@staticmethod
def get_allowed_keys() -> list:
return ['id', 'marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']
# Eigenschaften
@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):
# „children“ muss eine ganze Zahl >= 0 sein
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):
# Gehalt muss eine ganze Zahl >= 0 sein
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):
# Steuer muss eine ganze Zahl >= 0 sein
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):
# Der Abschlag muss eine ganze Zahl >= 0 sein
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):
# Der Aufschlag muss eine ganze Zahl >= 0 sein
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):
# Der Aufschlag muss eine ganze Zahl >= 0 sein
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):
# Der Zinssatz muss eine reelle Zahl >= 0 sein
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")
Anmerkungen:
- Die Klasse [TaxPayer] kapselt einen Steuerpflichtigen;
- Zeile 7: Die Klasse [TaxPayer] leitet sich von der Klasse [BaseEntity] ab. Sie hat daher die Kennung [id];
- Zeile 20: Es wird keine Eigenschaft aus dem Zustand eines Objekts der Klasse [AdminData] ausgeschlossen;
- Zeilen 22–25: Die Eigenschaften der Klasse. Diese werden in den Zeilen 9–17 näher erläutert;
- Zeilen 27–58: Getter für die Attribute der Klasse;
- Zeilen 60–161: Setter für die Attribute der Klasse. Es sei daran erinnert, dass der Vorteil einer Klasse, die Daten kapseln, gegenüber einem einfachen Wörterbuch darin besteht, dass die Klasse dank ihrer Setter die Gültigkeit ihrer Eigenschaften überprüfen kann;
15.1.2. Die Schicht [dao]

Wir werden die Implementierungen der Schichten in einem Ordner namens [services] zusammenfassen. Diese Klassen werden Schnittstellen implementieren, die im Ordner [interfaces] definiert sind.

15.1.2.1. Die Schnittstelle [InterfaceImpôtsDao]
Die Schicht [dao] wird die folgende Schnittstelle [InterfaceImpôtsDao] implementieren (Datei InterfaceImpôtsDao.py):
# Importe
from abc import ABC, abstractmethod
# Schnittstelle IImpôtsDao
from AdminData import AdminData
class InterfaceImpôtsDao(ABC):
# Liste der Steuerklassen
@abstractmethod
def get_admindata(self) -> AdminData:
pass
# Liste der Steuerpflichtigen
@abstractmethod
def get_taxpayers_data(self) -> dict:
pass
# Buchung der Ergebnisse der Steuerberechnung
@abstractmethod
def write_taxpayers_results(self, taxpayers_results: list):
pass
Die Schnittstelle definiert drei Methoden:
- [get_admindata]: Diese Methode ruft das Array der Steuerklassen ab. Es sei darauf hingewiesen, dass keine Angaben dazu gemacht werden, wie diese Daten zu beschaffen sind. Im weiteren Verlauf werden sie zunächst in einer Textdatei und anschließend in einer Datenbank gefunden. Es ist Aufgabe der Klassen, die die Schnittstelle implementieren, sich an die Art der Datenspeicherung anzupassen. Es wird also eine Klasse geben, um die Steuerklassen aus einer Textdatei abzurufen, und eine weitere, um sie aus einer Datenbank abzurufen. Beide werden die Methode [get_admindata] implementieren;
- [get_taxpayers_data]: Dies ist die Methode, die die Daten der Steuerzahler abruft. Auch hier geben wir nicht an, wo diese Daten zu finden sind. Wir behandeln nur den Fall, in dem sie in einer Textdatei vorliegen;
- [write_taxpayers_results]: ist die Methode, die die Ergebnisse der Steuerberechnung speichert. Wir geben nicht an, wo. Wir behandeln nur den Fall, in dem die Ergebnisse in einer Textdatei gespeichert werden. Der Parameter [taxpayers_results] ist die Liste der zu speichernden Ergebnisse;
15.1.2.2. Die Klasse [AbstractImpôtsDao]
Die Schicht [dao] wird durch zwei Klassen implementiert:
- Die eine ruft die Daten (Steuerpflichtige, Ergebnisse, Steuerklassen) aus Textdateien ab;
- die andere ruft die Daten (Steuerpflichtige, Ergebnisse) aus Textdateien und die Steuerklassen aus einer Datenbank ab;
Die beiden Klassen unterscheiden sich lediglich in der Handhabung der Steuerklassen. Die Daten zu den Steuerpflichtigen und die Ergebnisse der Steuerberechnungen werden hingegen auf die gleiche Weise verwaltet. Aus diesem Grund werden wir sie in einer übergeordneten Klasse [AbstractImpôtsDao] verwalten. Die Besonderheit der Handhabung der Steuerklassen wird hingegen in zwei untergeordneten Klassen verwaltet:
- Die Klasse [ImpôtsDaoWithAdminDataInJsonFile] ruft die Steuerklassen aus einer Textdatei im Format jSON ab;
- die Klasse [ImpôtsDaoWithAdminDataInDatabase] ruft die Steuerklassen aus einer Datenbank ab;
Die übergeordnete Klasse [AbstractImpôtsDao] sieht wie folgt aus:
# Importe
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
# Basisklasse für die Ebene [dao]
class AbstractImpôtsDao(InterfaceImpôtsDao):
# Die Steuerpflichtigen und ihre Steuern werden in Textdateien gespeichert
# Konstruktor
def __init__(self, config: dict):
# config[taxpayersFilename]: Name der Textdatei mit den Steuerpflichtigen
# Konfiguration[resultsFilename]: Name der jSON-Datei mit den Ergebnissen
# config[errorsFilename]: Name der Fehlerdatei
#: Die Parameter werden gespeichert
self.taxpayers_filename = config.get("taxpayersFilename")
self.taxpayers_results_filename = config.get("resultsFilename")
self.errors_filename = config.get("errorsFilename")
# ------------------
# Schnittstelle IImpôtsDao
# ------------------
# Liste der Steuerpflichtigen
def get_taxpayers_data(self) -> dict:
…
# Buchung der Steuern der Steuerzahler
def write_taxpayers_results(self, taxpayers: list):
…
# Steuerklassen auslesen
@abstractmethod
def get_admindata(self) -> AdminData:
pass
-
Zeile 13: Die Klasse [AbstractImpôtsDao] implementiert die Schnittstelle [InterfaceImpôtsDao]. Daher finden sich hier die drei Methoden dieser Schnittstelle:
- [get_taxpayers_data]: Zeile 31;
- [write_taxpayers_results]: Zeile 35;
- [get_admindata]: Zeile 40. Diese Methode wird von der Klasse [AbstractImpôtsDao] nicht implementiert und ist daher als abstrakt deklariert (Zeile 39);
-
Zeile 16: Der Konstruktor erhält ein Dictionary [config], das folgende Informationen enthält:
- [taxpayersFilename]: der Name der Textdatei, die die Daten der Steuerzahler enthält;
- [resultsFilename]: der Name der Textdatei, in die die Ergebnisse geschrieben werden;
- [errorsFilename]: Name der Textdatei, in der die bei der Verarbeitung der Datei [taxpayersFilename] aufgetretenen Fehler aufgelistet sind;
Die Methode [get_taxpayers_data] lautet wie folgt:
# Liste der Steuerpflichtigen
def get_taxpayers_data(self) -> dict:
# Initialisierungen
taxpayers_data = []
datafile = None
erreurs = []
try:
# Öffnen der Datendatei
datafile = open(self.taxpayers_filename, "r")
# Auswertung der aktuellen Zeile der Datei
ligne = datafile.readline()
# Zeilennummer
numligne = 0
while ligne != '':
# eine Zeile +
numligne += 1
# Leerzeichen werden entfernt
ligne = ligne.strip()
# Leerzeilen und Kommentare werden ignoriert
if ligne != "" and ligne[0] != "#":
try:
# die 4 Felder „id“, „verheiratet“, „Kinder“ und „Gehalt“ werden extrahiert, die die Steuerzahlerzeile bilden
(id, marié, enfants, salaire) = ligne.split(",")
# Es wird ein neuer Datensatz erstellt: TaxPayer
taxpayers_data.append(
TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
except BaseException as erreur:
# Der Fehler wird vermerkt
erreurs.append(f"Ligne {numligne}, {erreur}")
# Es wird eine neue Steuerzahlerzeile gelesen
ligne = datafile.readline()
# Fehler werden protokolliert, falls vorhanden
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)
# das Ergebnis wird zurückgegeben
return {"taxpayers": taxpayers_data, "erreurs": erreurs}
except BaseException as erreur:
# Es wird eine Ausnahme ausgelöst ImpôtsError
raise ImpôtsError(11, f"{erreur}")
finally:
# Die Datei wird geschlossen
if datafile:
datafile.close()
- Zeile 4: Die Daten der Steuerzahler (Ehepartner, Kinder, Gehalt) werden in eine Liste von Objekten vom Typ [TaxPayer] eingefügt;
- Zeilen 8–9: Die Textdatei mit den Steuerpflichtigen wird im Lesemodus geöffnet. Ihr Inhalt hat folgende Form:
Im Vergleich zu früheren Versionen:
- Jede Zeile der Datei [taxpayersFilename] beginnt mit der Steuerpflichtigen-ID, einer einfachen Nummer;
- Kommentare und leere Zeilen sind zulässig;
- Fehler werden behandelt. Somit müssen die Zeilen 17, 19 und 21 als fehlerhaft gemeldet werden. Die Fehler werden in einer separaten Datei protokolliert;
Setzen wir die Untersuchung des Codes fort:
- Zeile 4: Die Daten aus der Textdatei werden in die Liste [taxPayersData] übertragen;
- Zeilen 14–31: Die Steuerzahlerdatei wird zeilenweise gelesen;
- Zeile 14: Das Ende der Datei ist erreicht, wenn eine leere Zeile gelesen wird (nichts – nicht einmal das Zeilenendezeichen \r\n);
- Zeile 20: Leere Zeilen und Kommentare werden ignoriert. Eine Zeile gilt als Kommentar, wenn nach dem Entfernen der Leerzeichen vor und nach dem Text das erste Zeichen ein # ist;
- Zeile 24: Eine korrekte Zeile besteht aus vier durch Kommas getrennten Feldern. Diese werden abgerufen. Die Zuweisung von Daten zu einem Tupel mit vier Elementen schlägt fehl, wenn nicht genau vier Daten zugewiesen werden;
- Zeile 25: Ist eines der vier abgerufenen Felder [id, marié, enfants, salaire] ungültig, löst die Methode [BaseEntity.fromdict] eine Ausnahme vom Typ [MyException] aus;
- Zeilen 25–26: Ein Objekt vom Typ [TaxPayer] wird der Liste [taxpayers_data] der Steuerzahler hinzugefügt;
- Zeilen 27–29: Eventuelle Fehler werden in einer Liste [erreurs] gesammelt. Diese Liste wurde in Zeile 6 angelegt;
- Zeilen 33–36: Die Liste der aufgetretenen Fehler wird in der Textdatei [errorsFilename] gespeichert. Es gibt zwei Arten von Fehlern:
- Eine Zeile wies nicht die richtige Anzahl der erwarteten Felder auf;
- die Informationen der Zeile waren fehlerhaft, sodass die Erstellung eines Objekts [TaxPayer] fehlgeschlagen ist;
- Zeilen 39–41: Jeder Fehler (BaseException) wird abgefangen und durch Kapselung in einen Typ [ImpôtsError] weitergeleitet;
- Zeilen 42–45: Unabhängig vom Erfolg oder Misserfolg wird die Textdatei der Steuerpflichtigen geschlossen, sofern sie geöffnet war;
Die Methode [write_taxpayers_results] muss eine Datei jSON in folgender Form erzeugen:
[
{
"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
},
…
]
Die Methode [write_taxpayers_results] lautet wie folgt:
# Eintragung der Steuer der Steuerzahler
def write_taxpayers_results(self, taxpayers: list):
# Ergebnisse in eine Datei schreiben jSON
# Steuerzahler: Liste von Objekten vom Typ TaxPayer
# (ID, verheiratet, Kinder, Gehalt, Steuer, Zuschlag, Abschlag, Ermäßigung, Steuersatz)
# Die Liste [taxpayers] wird in der Textdatei [self.taxpayers_results_filename] gespeichert
file = None
try:
# Öffnen der Ergebnisdatei
file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
# Erstellung der zu serialisierenden Liste in jSON
mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
# Serialisierung jSON
json.dump(list(mapping), file, ensure_ascii=False)
except BaseException as erreur:
# Der Fehler wird unter einem anderen Typ erneut ausgelöst
raise ImpôtsError(12, f"{erreur}")
finally:
# Die Datei wird geschlossen, falls sie geöffnet war
if file:
file.close()
- Zeile 2: Die Methode erhält eine Liste von Steuerpflichtigen [taxpayers], die sie in der Textdatei [self.taxpayers_results_filename] im Format jSON speichern muss;
- Zeile 10: Erstellung der Ergebnisdatei UTF-8;
- Zeile 12: Hier rufen wir die Funktion [map] auf, deren Syntax hier [map (fonction, liste1)] lautet. Die Funktion [fonction] wird auf jedes Element von [liste1] angewendet und erzeugt ein neues Element, das in eine Liste [liste2] aufgenommen wird. Schließlich gilt für jedes i:
liste2[i]=fonction(liste1[i])
Hier ist [liste1] die Liste [taxPayers], eine Liste von Objekten vom Typ [TaxPayer]. Die Funktion [fonction] wird hier in Form einer sogenannten Funktion [lambda] ausgedrückt, die die Transformation eines Elements [taxpayer] der Liste [taxpayers] beschreibt: Jedes Element [taxpayer] wird durch sein Wörterbuch [taxpayer.asdict()] ersetzt. Schließlich ist die erhaltene Liste [liste2] die Liste der Wörterbücher der Elemente der Liste [taxpayers];
- Zeile 12: Das von der Funktion [map] zurückgegebene Ergebnis ist nicht die Liste [liste2], sondern ein Objekt vom Typ [map]. Um [liste2] zu erhalten, muss der Ausdruck [list(mapping)] (Zeile 14) verwendet werden;
- Zeile 14: Die Liste [liste2] wird im Format jSON in der Datei [self.taxpayers_results_filename] gespeichert;
- Zeilen 15–17: Jede Art von Ausnahme wird abgefangen und in einen Fehler vom Typ [ImpôtsError] gekapselt, bevor sie erneut ausgelöst wird (Zeile 17);
- Zeilen 19–21: Unabhängig vom Ergebnis (Erfolg oder Fehler) wird die Ergebnisdatei geschlossen, sofern sie geöffnet war;
15.1.2.3. Klasse [ImpôtsDaoWithAdminDataInJsonFile]
Die Klasse [ImpôtsDaoWithAdminDataInJsonFile] wird von der Klasse [AbstractImpôtsDao] abgeleitet und die Methode [getAdminData] implementieren, die ihre übergeordnete Klasse nicht implementiert hat. Sie wird die Daten der Steuerbehörde aus einer Datei mit dem Namen jSON abrufen:
{
"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
}
Die Klasse [ImpôtsDaoWithAdminDataInJsonFile] lautet wie folgt:
# Importe
import codecs
import json
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
# Eine Implementierung der Schicht [dao], bei der die Daten der Steuerbehörde in einer Datei jSON vorliegen
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
# Konstruktor
def __init__(self, config: dict):
# Konfiguration[admindataFilename]: Name der Datei jSON, die die Daten der Steuerbehörde enthält
# config[taxpayersFilename]: Name der Textdatei mit den Steuerpflichtigen
# config[resultsFilename]: Name der Ergebnisdatei jSON
# config[errorsFilename]: Name der Fehlerdatei
# Initialisierung der Klasse „Parent“
AbstractImpôtsDao.__init__(self, config)
# Einlesen der Daten der Steuerbehörde
file = None
try:
# Öffnen der Datei jSON mit den Steuerdaten im Lesemodus
file = codecs.open(config["admindataFilename"], "r", "utf8")
# Übertragung des Inhalts der Datei jSON in ein Objekt [AdminData]
self.admindata = AdminData().fromdict(json.load(file))
except BaseException as erreur:
# Der Fehler wird in Form eines Objekts vom Typ [ImpôtsError] erneut ausgelöst
raise ImpôtsError(21, f"{erreur}")
finally:
# Schließen der Datei, falls sie geöffnet war
if file:
file.close()
# -------------
# Schnittstelle
# -------------
# Abruf der Daten von der Steuerbehörde
# Die Methode gibt ein Objekt [AdminData] zurück
def get_admindata(self) -> AdminData:
return self.admindata
- Zeile 11: Die Klasse [ImpôtsDaoWithAdminDataInJsonFile] erbt von der Klasse [AbstractImpôtsDao]. In dieser Eigenschaft implementiert sie die Schnittstelle [InterfaceImpôtsDao];
- Zeile 13: Der Konstruktor erhält als Parameter ein Dictionary, das die Informationen aus den Zeilen 14–17 enthält;
- Zeile 20: Die übergeordnete Klasse wird initialisiert;
- Zeile 24: Die Datei „jSON“ mit den Daten der Steuerbehörde wird geöffnet;
- Zeile 25: Die Datei „UTF-8“ mit den Daten der Steuerbehörde wird geöffnet;
- Zeile 27: Der Inhalt der Datei wird gelesen und in das Objekt [self.admindata] vom Typ [AdminData] gespeichert. Die Schlüssel der Datei jSON müssen mit den für ein Objekt vom Typ [AdminData] zulässigen Eigenschaften übereinstimmen, andernfalls löst die Methode [fromdict] eine Ausnahme aus;
- Zeilen 28–30: Ausnahmebehandlung. Die möglicherweise auftretenden Ausnahmen werden in einen Typ [ImpôtsError] gekapselt, bevor sie weitergeleitet werden;
- Zeilen 32–34: Die Datei wird geschlossen, falls sie geöffnet war;
- Zeilen 42–43: Implementierung der Methode [get_admindata] der Schnittstelle [InterfaceImpôtsDao];
15.1.3. Die Schicht [métier]

15.1.3.1. Die Schnittstelle [InterfaceImpôtsMétier]
Die Schnittstelle der Schicht [métier] sieht wie folgt aus:
# Importe
from abc import ABC, abstractmethod
from AdminData import AdminData
from TaxPayer import TaxPayer
# Schnittstelle IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
# Steuerberechnung für 1 Steuerpflichtigen
@abstractmethod
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
pass
- Die Schnittstelle [InterfaceImpôtsMétier] definiert eine einzige Methode:
- Zeile 12: Die Methode [calculate_tax] ermöglicht die Berechnung der Steuer für einen einzelnen Steuerpflichtigen [taxpayer]. [admindata] ist das Objekt [AdminData], das die Daten der Steuerbehörde kapselt;
- Zeile 12: Die Methode [calculate_tax] gibt kein Ergebnis zurück. Die erhaltenen Daten (Steuer, Zuschlag, Abschlag, Ermäßigung, Steuersatz) sind im Parameter [taxpayer] enthalten: Vor dem Aufruf sind diese Attribute leer, nach dem Aufruf wurden sie initialisiert;
15.1.3.2. Die Klasse [ImpôtsMétier]
Die Klasse [ImpôtsMétier] implementiert die Schnittstelle [InterfaceImpôtsMétier] wie folgt:

Die Methoden der Klasse stammen aus dem Modul [impôts_module_02] aus dem Abschnitt |Das Modul [impots.v02.modules.impôts_module_02]|. Die Parameter der Methoden wurden lediglich auf zwei beschränkt:
- taxpayer(id, verheiratet, Kinder, Gehalt, Steuer, Abschlag, Aufschlag, Ermäßigung, Steuersatz): Das Objekt, das einen Steuerzahler und dessen Steuer darstellt;
- admindata: das Objekt, das die Daten der Steuerbehörde kapselt;
Anhand einer Methode veranschaulichen wir die vorgenommenen Änderungen;
# Steuerberechnung – Phase 1
# ----------------------------------------
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
# Steuerpflichtiger (ID, verheiratet, Kinder, Gehalt, Steuer, Abschlag, Aufschlag, Ermäßigung, Steuersatz)
# Verwaltungsdaten: Daten der Steuerbehörde
# Steuerberechnung mit Kindern
self.calculate_tax_2(taxpayer, admindata)
# Die Ergebnisse befinden sich in „Steuerzahler“
taux1 = taxpayer.taux
surcôte1 = taxpayer.surcôte
impot1 = taxpayer.impôt
# Steuerberechnung ohne Kinder
if taxpayer.enfants != 0:
# Steuerberechnung für denselben Steuerpflichtigen ohne Kinder
taxpayer2 = TaxPayer().fromdict(
{'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
self.calculate_tax_2(taxpayer2, admindata)
# Die Ergebnisse befinden sich in „taxpayer2“
taux2 = taxpayer2.taux
surcôte2 = taxpayer2.surcôte
impot2 = taxpayer2.impôt
# Anwendung der Obergrenze für den Familienquotienten
if taxpayer.enfants < 3:
# PLAFOND_QF_DEMI_PART Euro für die ersten beiden Kinder
impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
else:
# PLAFOND_QF_DEMI_PART Euro für die ersten beiden Kinder, das Doppelte für die folgenden
impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
* 2 * admindata.plafond_qf_demi_part
else:
# Wenn der Steuerpflichtige keine Kinder hat, dann ist Steuer2 = Steuer1
impot2 = impot1
# Es wird die höchste Steuer mit dem dazugehörigen Steuersatz und dem entsprechenden Zuschlag herangezogen
(impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
impot2, impot2 - impot1 + surcôte2, taux2)
# Teilergebnisse
taxpayer.impôt = impot
taxpayer.surcôte = surcôte
taxpayer.taux = taux
# Berechnung eines eventuellen Steuerabzugs
self.get_décôte(taxpayer, admindata)
taxpayer.impôt -= taxpayer.décôte
# Berechnung einer möglichen Steuerermäßigung
self.get_réduction(taxpayer, admindata)
taxpayer.impôt -= taxpayer.réduction
# Ergebnis
taxpayer.impôt = math.floor(taxpayer.impôt)
- Zeile 3: Die Methode [calculate_tax] ist die einzige Methode der Schnittstelle [InterfaceImpôtsMétier]. Sie akzeptiert zwei Parameter:
- [tapPayer]: der Steuerpflichtige, für den die Steuer berechnet wird;
- [admindata]: das Objekt, das die Daten der Steuerbehörde enthält;
- Die Berechnungsergebnisse werden im Parameter [taxpayer] gekapselt (Zeilen 40–50). Der Inhalt dieses Objekts ist daher vor und nach dem Aufruf der Methode nicht identisch;
15.1.4. Tests der Schichten [dao] und [métier]

- [TestDaoMétier] ist die Testklasse UnitTest für die Schichten [dao] und [métier];
- [config] ist die Testkonfigurationsdatei;
Die Konfiguration [config] lautet wie folgt:
def configure():
import os
# Schritt 1 ------
# Konfiguration des Python-Pfads
# Verzeichnis dieser Datei
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 Abhängigkeiten der Anwendung
absolute_dependencies = [
f"{script_dir}/../entities",
f"{script_dir}/../interfaces",
f"{script_dir}/../services",
f"{root_dir}/02/entities",
]
# Festlegen des Syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# Schritt 2 ------
# Konfiguration der Anwendung
config = {
# Absolute Pfade der Anwendungsdateien
"admindataFilename": f"{script_dir}/../data/input/admindata.json"
}
# Instanziierung der Anwendungsschichten
from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
from ImpôtsMétier import ImpôtsMétier
dao = ImpôtsDaoWithAdminDataInJsonFile(config)
métier = ImpôtsMétier()
# Die Instanzen der Schichten werden in die Konfiguration aufgenommen
config["dao"] = dao
config["métier"] = métier
# die Konfiguration wird bereitgestellt
return config
- Zeilen 4–23: Der Python-Pfad für die Tests wird konfiguriert;
- Zeilen 32–41: Die Schichten [dao] und [métier] werden instanziiert. Ihre Referenzen werden in das Wörterbuch [config] aufgenommen;
- Zeile 44: Dieses Wörterbuch wird zurückgegeben;
Die Testklasse [TestDaoMétier] sieht wie folgt aus:
import unittest
def get_config() -> dict:
# Konfiguration der Anwendung
import config
# Die Konfiguration wird zurückgegeben
return config.configure()
class TestDaoMétier(unittest.TestCase):
# wird vor jeder Methode test_ ausgeführt
def setUp(self) -> None:
# Die Testkonfiguration wird abgerufen
config = get_config()
# einige Informationen werden gespeichert
self.métier = config['métier']
self.admindata = config['dao'].get_admindata()
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'verheiratet': 'ja', 'Kinder': 2, 'Gehalt': 55555,
# 'Steuer': 2814, 'Zuschlag': 0, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
self.métier.calculate_tax(taxpayer, self.admindata)
# Überprüfung
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
# {'verheiratet': 'ja', 'Kinder': 2, 'Gehalt': 50000,
# 'Steuer': 1384, 'Zuschlag': 0, 'Abschlag': 384, 'Ermäßigung': 347, 'Satz': 0,14}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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
# {'verheiratet': 'ja', 'Kinder': 3, 'Gehalt': 50000,
# 'Steuer': 0, 'Aufschlag': 0, 'Abschlag': 720, 'Ermäßigung': 0, 'Satz': 0,14}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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
# {'verheiratet': 'nein', 'Kinder': 2, 'Gehalt': 100000,
# 'Steuer': 19884, 'Aufschlag': 4480, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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
# {'verheiratet': 'nein', 'Kinder': 3, 'Gehalt': 100000,
# 'Steuer': 16782, 'Aufschlag': 7176, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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
# {'verheiratet': 'ja', 'Kinder': 3, 'Gehalt': 100000,
# 'Steuer': 9200, 'Aufschlag': 2180, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,3}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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
# {'verheiratet': 'ja', 'Kinder': 5, 'Gehalt': 100000,
# 'Steuer': 4230, 'Aufschlag': 0, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,14}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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
# {'verheiratet': 'nein', 'Kinder': 0, 'Gehalt': 100000,
# 'Steuer': 22986, 'Aufschlag': 0, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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
# {'verheiratet': 'ja', 'Kinder': 2, 'Gehalt': 30000,
# 'Steuer': 0, 'Zuschlag': 0, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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
# {'verheiratet': 'nein', 'Kinder': 0, 'Gehalt': 200000,
# 'Steuer': 64210, 'Zuschlag': 7498, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,45}
taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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
# {'verheiratet': 'ja', 'Kinder': 3, 'Gehalt': 200000,
# 'Steuer': 42842, 'Aufschlag': 17283, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
self.métier.calculate_tax(taxpayer, self.admindata)
# Prüfungen
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()
Kommentare
- Zeile 11: Die Testklasse erweitert die Klasse [unittest.TestCase];
- Zeilen 13–19: In einem Test UnitTest wird die Methode [setUp] vor jeder der Methoden [test_] ausgeführt;
- Zeile 16: Die Konfiguration aus dem zuvor behandelten Skript [config] wird abgerufen;
- Zeile 18: Es wird eine Referenz auf die Schicht [métier] gespeichert;
- Zeile 19: Von der Schicht [dao] wird das Objekt [AdminData] angefordert, das die Daten der Steuerbehörde enthält, und dieses wird gespeichert;
- Zeilen 21–173: 11 Tests, deren Ergebnisse auf der offiziellen Website der Steuerbehörde 2019 |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm| überprüft wurden;
- Zeilen 21–33: Alle Tests wurden nach dem gleichen Muster erstellt;
- Zeile 22: Die Klasse [TaxPayer] wird importiert;
- Zeile 24: getesteter Steuerzahler;
- Zeile 25: erwartete Ergebnisse;
- Zeile 26: Erstellung des Objekts [TaxPayer] für den Steuerpflichtigen;
- Zeile 27: Berechnung seiner Steuer. Das Ergebnis befindet sich in [taxpayer];
- Zeilen 29–33: Überprüfung der erzielten Ergebnisse;
- Zeile 29: Der Steuerbetrag wird auf den Euro genau überprüft. Die Tests haben nämlich gezeigt, dass die vom Algorithmus dieses Dokuments erzielten Ergebnisse um höchstens 1 Euro von den offiziellen Zahlen abweichen können;
Die Durchführung der Tests liefert folgende Ergebnisse:

15.1.5. Hauptskript

Das Hauptskript wird durch das folgende Skript [config] konfiguriert:
def configure():
import os
# Schritt 1 ------
# Konfiguration des Python-Pfads
# Verzeichnis dieser Datei
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"
# Anwendungsabhängigkeiten
absolute_dependencies = [
# Lokale Abhängigkeiten
f"{script_dir}/../../entities",
f"{script_dir}/../../interfaces",
f"{script_dir}/../../services",
f"{root_dir}/02/entities",
]
# Der Syspath wird festgelegt
from myutils import set_syspath
set_syspath(absolute_dependencies)
# Schritt 2 ------
# Konfiguration der Anwendung
config = {
# Absolute Pfade der Anwendungsdateien
"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"
}
# Instanziierung der Anwendungsschichten
from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
from ImpôtsMétier import ImpôtsMétier
dao = ImpôtsDaoWithAdminDataInJsonFile(config)
métier = ImpôtsMétier()
# Die Layer-Instanzen werden in die Konfiguration aufgenommen
config["dao"] = dao
config["métier"] = métier
# Die Konfiguration wird bereitgestellt
return config
Es entspricht dem Skript, das für die Testschichten [métier] und [dao] verwendet wird.
Das Hauptskript [main.py] lautet wie folgt:
# Konfiguration der Anwendung
import config
config = config.configure()
# Importe
from ImpôtsError import ImpôtsError
# Die Schichten der Anwendung werden abgerufen (sie sind bereits instanziiert)
dao = config["dao"]
métier = config["métier"]
try:
# Abruf der Steuerklassen
admindata = dao.get_admindata()
# Auslesen der Steuerzahlerdaten
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# der Steuerzahler?
if not taxpayers:
raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# Berechnung der Steuer der Steuerzahler
for taxpayer in taxpayers:
# „Steuerpflichtiger“ ist sowohl ein Eingabe- als auch ein Ausgabeparameter
# „taxpayer“ wird geändert
métier.calculate_tax(taxpayer, admindata)
# Schreiben der Ergebnisse in eine Textdatei
dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
# Anzeige des Fehlers
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# abgeschlossen
print("Travail terminé...")
Anmerkungen
- Zeilen 2–4: Die Konfiguration der Anwendung wird abgerufen. Es ist außerdem bekannt, dass der Python-Pfad der Anwendung bereits erstellt wurde;
- Zeilen 9–11: Es werden Referenzen auf die Schichten [métier] und [dao] abgerufen;
- Zeile 15: Die Daten der Steuerbehörde werden abgerufen;
- Zeile 17: Man ruft die Liste der Steuerzahler ab, für die die Steuer berechnet werden muss;
- Zeilen 19–20: Ist diese Liste leer, wird eine Ausnahme ausgelöst;
- Zeilen 22–25: Berechnung der Steuer für die verschiedenen Objekte [taxpayer] mithilfe der Ebene [métier];
- Zeile 27: [taxpayers] ist nun eine Liste von Objekten [TaxPayer], bei denen die Attribute (Steuer, Abschlag, Aufschlag, Ermäßigung, Steuersatz) einen Wert erhalten haben. Diese Liste wird in eine Datei jSON geschrieben;
- Zeilen 28–30: Abfangen eines möglichen Fehlers;
- Zeilen 31–33: werden in jedem Fall ausgeführt;
Die Ausführung des Skripts liefert dieselben Ergebnisse wie in den vorherigen Versionen. Die Datei mit den Steuerpflichtigenfehlern war eine Neuerung in dieser Version. Nach Ausführung des Skripts [main] lautet ihr Inhalt wie folgt:
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]
Die fehlerhaften Zeilen lauteten wie folgt:
15.2. Version 4 – Anwendung 2
In dieser Version gibt der Benutzer die Liste der Steuerzahler über die Tastatur ein. Die Architektur der Anwendung sieht wie folgt aus:

Es kommt ein neues Modul hinzu: die Schicht [ui] (Benutzeroberfläche), die mit dem Benutzer interagiert. Diese Schicht verfügt über eine Schnittstelle und wird durch eine Klasse implementiert.

15.2.1. Die Schnittstelle [InterfaceImpôtsUi]
# Importe
from abc import ABC, abstractmethod
# Schnittstelle InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
# Ausführung der Klasse, die die Schnittstelle implementiert
@abstractmethod
def run(self):
pass
Die Schnittstelle [InterfaceImpôtsUi] wird nur eine Methode haben, nämlich die in den Zeilen 8–10. Die Schnittstelle wird hier mit einer Konsolenanwendung implementiert, könnte aber auch mit einer grafischen Benutzeroberfläche implementiert werden. Die an die Methode [run] übergebenen Parameter wären in den beiden Implementierungen nicht identisch. Um dieses Problem zu umgehen, wird üblicherweise wie folgt vorgegangen:
- der Methode [run] keine Parameter (oder nur ein Minimum an Parametern) zu übergeben;
- Parameter an den Konstruktor der Klasse zu übergeben, die die Schnittstelle implementiert. Diese können von einer Implementierung zur anderen unterschiedlich sein. Diese Parameter werden als Attribute der Klasse gespeichert;
- sicherzustellen, dass die Methode [run] diese Klassenattribute (self.x) verwendet;
Diese Methode ermöglicht eine sehr allgemeine Schnittstelle, die durch die Parameter der Konstruktoren jeder Implementierungsklasse konkretisiert wird. Diese Methode wurde bereits für die modulare Version Nr. 1 verwendet.
15.2.2. Die Klasse [ImpôtsConsole]
Die Klasse [ImpôtsConsole] implementiert die Schnittstelle [InterfaceImpôtsUi] wie folgt:
# Importe
import re
from InterfaceImpôtsUi import InterfaceImpôtsUi
from TaxPayer import TaxPayer
# Schicht [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
# Konstruktor
def __init__(self, config: dict):
# Speichern der Parameter
self.admindata = config['dao'].get_admindata()
self.métier = config['métier']
def run(self):
# interaktiver Dialog mit dem Benutzer
fini = False
while not fini:
# Ist der Steuerzahler verheiratet?
marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
# Überprüfung der Gültigkeit der Eingabe
while marié != "oui" and marié != "non" and marié != "*":
# Fehlermeldung
print("Tapez oui ou non ou *")
# Frage erneut stellen
marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
# Fertig?
if marié == "*":
# Dialog beendet
return
# Anzahl der Kinder
enfants = input("Nombre d'enfants : ").strip()
# Gültigkeitsprüfung der Eingabe
if not re.match(r"^\d+$", enfants):
# Fehlermeldung
print("Tapez un nombre entier positif ou nul")
# Neu starten
enfants = input("Nombre d'enfants : ").strip()
# Jahresgehalt
salaire = input("Salaire annuel : ").strip()
# Überprüfung der Gültigkeit der Eingabe
if not re.match(r"^\d+$", salaire):
# Fehlermeldung
print("Tapez un nombre entier positif ou nul")
# Neustart
salaire = input("Salaire annuel : ").strip()
# Steuerberechnung
taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
self.métier.calculate_tax(taxpayer, self.admindata)
# Anzeige
print(f"Impôt du contribuable = {taxpayer}\n\n")
# Nächster Steuerpflichtiger
- Zeile 9: Die Klasse [ImpôtsConsole] implementiert die Schnittstelle [InterfaceImpôtsUi];
- Zeile 11: Der Konstruktor der Klasse erhält einen Parameter, das Dictionary [config] aus der Anwendungskonfiguration;
- Zeile 13: Es werden die Daten der Steuerbehörde abgerufen, die die Berechnung der Steuer ermöglichen;
- Zeile 14: Es wird eine Referenz auf die Schicht [métier] gespeichert;
- Zeile 16: Implementierung der Methode [run] der Schnittstelle;
- Zeilen 19–53: Dialog mit dem Benutzer. Dabei werden
- in der Abfrage der drei Angaben (Familienstand, Kinder, Gehalt) des Steuerpflichtigen;
- seine Steuer zu berechnen;
- diese anzuzeigen;
- der Dialog endet, sobald der Benutzer auf die erste Frage mit * antwortet;
- Zeilen 20–27: Es wird gefragt, ob der Steuerzahler verheiratet ist, und die Gültigkeit der Antwort wird überprüft;
- Zeilen 29–31: Hat der Benutzer die Frage mit „*“ beantwortet, wird der Dialog beendet;
- Zeilen 32–39: Es wird nach der Anzahl der Kinder des Steuerpflichtigen gefragt und die Gültigkeit der Antwort überprüft;
- Zeilen 40–47: Es wird nach dem Jahresgehalt des Steuerpflichtigen gefragt und die Gültigkeit der Antwort überprüft;
- Zeilen 48–50: Anhand dieser Informationen wird die Steuer des Steuerpflichtigen über die Schicht [métier] berechnet;
- Zeile 52: Der Steuerbetrag wird angezeigt;
15.2.3. Das Hauptskript
Das Hauptskript [main] wird durch die folgende Datei [config] konfiguriert:
def configure():
import os
# Schritt 1 ------
# Konfiguration des Python-Pfads
# Verzeichnis dieser Datei
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"
# Abhängigkeiten der Anwendung
absolute_dependencies = [
# Lokale Abhängigkeiten
f"{script_dir}/../../entities",
f"{script_dir}/../../interfaces",
f"{script_dir}/../../services",
f"{root_dir}/02/entities",
]
# Der Syspath wird festgelegt
from myutils import set_syspath
set_syspath(absolute_dependencies)
# Schritt 2 ------
# Konfiguration der Anwendung
config = {
# Absolute Pfade der Anwendungsdateien
"admindataFilename": f"{script_dir}/../../data/input/admindata.json",
}
# Instanziierung der Anwendungsschichten
from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
from ImpôtsMétier import ImpôtsMétier
from ImpôtsConsole import ImpôtsConsole
# DAO-Schicht
dao = ImpôtsDaoWithAdminDataInJsonFile(config)
# Geschäftsschicht
métier = ImpôtsMétier()
# Die Instanzen der Schichten werden in die Konfiguration aufgenommen
config["dao"] = dao
config["métier"] = métier
# UI-Schicht
ui = ImpôtsConsole(config)
config["ui"] = ui
# Die Konfiguration wird bereitgestellt
return config
Das Steuerungsskript lautet wie folgt (main.py):
# die Anwendung wird konfiguriert
import config
config = config.configure()
# Importe
from ImpôtsError import ImpôtsError
# die Schichten der Anwendung werden abgerufen (sie sind bereits instanziiert)
ui = config["ui"]
# Code
try:
# Ausführung der Schicht [ui]
ui.run()
except ImpôtsError as erreur:
# Die Fehlermeldung wird angezeigt
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# wird in jedem Fall ausgeführt
print("Travail terminé...")
- Zeilen 1–4: Die Konfiguration der Anwendung wird abgerufen;
- Zeile 10: Es wird eine Referenz auf die Schicht [ui] abgerufen;
- Zeilen 12–21: Die Struktur des Codes entspricht der der vorherigen Anwendung: Der Code ist von einem „try/catch“-Block umgeben, um eventuelle Ausnahmen abzufangen;
- Zeile 15: Die Schicht [ui] wird zur Ausführung aufgefordert: Der Dialog mit dem Benutzer beginnt nun;
- Zeilen 16–18: Abfangen einer möglichen Ausnahme;
Hier ein Ausführungsbeispiel:
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