Skip to content

15. Anwendungsaufgabe – Version 4

Image

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:

Image

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:

Image

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

Image

15.1.1. Die Entitäten

Image

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]

Image

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

Image

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:
# Gültige Daten: ID, verheiratet, Kinder, Gehalt
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# Es können leere Zeilen vorkommen

# Es werden fehlerhafte Zeilen erstellt
# zu wenige Werte
11,12
# zu viele Werte
12,oui,3,200000, x, y
# Falsche Werte
x,x,x,x

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]

Image

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:

Image

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]

Image

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

Image

Testing started at 16:08 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/tests/TestDaoMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/tests/TestDaoMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v04\tests



Ran 11 tests in 0.055s

OK

Process finished with exit code 0

15.1.5. Hauptskript

Image

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:

# Gültige Daten: ID, verheiratet, Kinder, Gehalt
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# Es können leere Zeilen vorkommen

# Es werden fehlerhafte Zeilen erstellt
# zu wenige Werte
11,12
# zu viele Werte
12,oui,3,200000, x, y
# falsche Werte
x,x,x,x

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:

Image

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.

Image

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