Kihagyás

6. Entwurfsmuster (Erweiterbarkeit)

Das Ziel der Übung

Ziele der Übung (anhand eines komplexeren Beispiels aus dem wirklichen Leben):

  • Anwendung einiger Designprinzipien zur Förderung von Skalierbarkeit, Wiederverwendbarkeit, Codetransparenz und Wartbarkeit: SRP, OPEN-CLOSED, DRY, KISS, etc.
  • Einige der für die Erweiterbarkeit wichtigsten Entwurfsmuster (Template Method, Strategy, Dependency Injection).
  • Zusätzliche Techniken (z.B. Delegate/Lambda-Ausdruck) zur Unterstützung von Skalierbarkeit und Wiederverwendbarkeit mit Entwurfsmustern anwenden und kombinieren.
  • Code-Refactoring-Übung.

Verwandte Präsentationen:

  • Entwurfsmuster: erweiterungsbezogene Muster (Einführung, Schablonenmethode, Strategie) und das Dependency Injection "Muster".

Voraussetzungen

Die für die Durchführung der Übung benötigten Werkzeuge:

  • Visual Studio 2022

Übung unter Linux oder macOS

Das Übungsmaterial ist grundsätzlich für Windows und Visual Studio gedacht, kann aber auch auf anderen Betriebssystemen mit anderen Entwicklungswerkzeugen (z.B. VS Code, Rider, Visual Studio für Mac) oder sogar mit einem Texteditor und CLI (Kommandozeilen)-Tools durchgeführt werden. Dies wird dadurch ermöglicht, dass die Beispiele im Kontext einer einfachen Konsolenanwendung präsentiert werden (keine Windows-spezifischen Elemente) und das .NET 8 SDK auf Linux und macOS unterstützt wird. Hallo Welt unter Linux.

Theoretischer Hintergrund, Ansatz *

Bei der Entwicklung komplexerer Anwendungen müssen wir eine Reihe von Designentscheidungen treffen, wobei mehrere Optionen zur Auswahl stehen. Wenn wir die Wartungsfreundlichkeit und die Möglichkeit der einfachen Weiterentwicklung unserer Anwendung nicht im Auge behalten, kann die Entwicklung schnell zu einem Alptraum werden. Kundenwünsche nach Änderungen und Erweiterungen erfordern eine umfangreiche kontinuierliche Neuschreibung/Änderung des Codes: Dies führt zu neuen Fehlern und erfordert einen erheblichen Arbeitsaufwand für umfangreiche Code-Neu-Tests!

Unser Ziel ist es, diesen Änderungs- und Erweiterungsbedarf zu decken, indem wir den Code an einigen wenigen, genau definierten Stellen erweitern, ohne den bestehenden Code wesentlich zu verändern. Das Schlüsselwort lautet: Erweiterung im Gegensatz zur Änderung. Damit zusammenhängend: Wenn bestimmte Logiken erweitert werden können, sind sie auch allgemeiner, und wir können sie leichter in mehr Kontexten verwenden. Langfristig gesehen kommen wir auf diese Weise schneller voran, der Code ist kürzer und doppelter Code wird vermieden (wodurch der Code leichter zu pflegen ist).

Die Entwurfsmuster zeigen bewährte Lösungen für einige häufig auftretende Entwurfsprobleme: Diese Lösungen tragen dazu bei, dass unser Code einfacher zu erweitern, zu pflegen und wiederzuverwenden ist. In dieser Übung konzentrieren wir uns auf Muster, Entwurfsprinzipien und einige Programmierwerkzeuge, die bei diesen Problemen helfen können. Aber wir wollen es nicht übertreiben: Es lohnt sich nur dann, ein bestimmtes Entwurfsmuster zu verwenden, wenn es einen echten Nutzen bringt. Andernfalls wird die Implementierung nur unnötig kompliziert. Vor diesem Hintergrund ist es nicht unser Ziel (und oft auch gar nicht möglich), alle zukünftigen Erweiterungsbedürfnisse vorherzusehen oder sehr weit im Voraus darüber nachzudenken. Der Punkt ist, dass wir, selbst wenn wir von einer einfachen Lösung ausgehen und jedes Problem erkennen, unseren Code ständig überarbeiten, um ihn an den richtigen Stellen erweiterbar und wiederverwendbar zu machen, je nach unseren aktuellen Anforderungen (funktional und nicht funktional) und unserer Voraussicht.

Es ist erwähnenswert, dass verwandte Entwurfsmuster und Sprachtools auch sehr dabei helfen, unseren Code unit-testbar zu machen: In vielen Unternehmen ist es bei der Entwicklung eines Softwareprodukts eine (legitime) Anforderung an die Entwickler, Unit-Tests mit hoher Codeabdeckung zu erstellen. Dies ist jedoch praktisch unmöglich, wenn die Einheiten/Klassen Ihres Codes zu eng gekoppelt sind.

0. Aufgabe - Kennenlernen der Aufgabe und der Erstanwendung

Klonen Sie das Repository derursprünglichen Anwendung für Labor 6:

  • Öffnen Sie eine Eingabeaufforderung
  • Navigieren Sie zu einem Ordner Ihrer Wahl, zum Beispiel c:workNEPTUN
  • Geben Sie den folgenden Befehl ein: git clone https://github.com/bmeviauab00/lab-patterns-extensibility-kiindulo.git
  • Öffnen Sie die Lösung Lab-Patterns-Extensibility.sln in Visual Studio.

Beschreibung der Aufgabe

Im Labor werden wir eine konsolenbasierte Datenverarbeitungsanwendung (genauer gesagt, eine Anonymisierungsanwendung) entsprechend den sich entwickelnden Bedürfnissen erweitern, und zwar an verschiedenen Punkten und mit verschiedenen Techniken. In der ersten Übung wird auch das Konzept der Anonymisierung vorgestellt.

Die Eingabe in die Anwendung ist eine CSV-Textdatei, in der jede Zeile Daten für eine bestimmte Person enthält. Öffnen Sie im Dateisystem die Datei us-500.csv im Ordner Data (durch Doppelklick oder mit Notepad/Notepad). Wir sehen, dass wir zwischen "", getrennt durch ein Komma, die Daten für jede Person haben (sie sind nicht real). Schauen wir uns die erste Zeile an:

"James", "Rhymes", "Benton, John B Jr", "6649 N Blue Gum St", "New Orleans", "Orleans", "LA", "70116", "504-621-8927", "504-845-1427", "30", "65", "Herzbezogen", "jRhymes@gmail.com"

Die Person in der ersten Reihe heißt James Rhymes, arbeitet für "Benton, John B Jr.", gefolgt von einigen Adressfeldern, 30 Jahre alt, 65 kg Körpergewicht. Das folgende Feld gibt Auskunft darüber, welche schwerere Krankheit Sie haben (in der Zeile oben ist es "Herzkrankheit"). Die letzte Spalte enthält die E-Mail-Adresse der Person.

Quelle und genaues Format der Daten *

Datenquelle: https://www.briandunning.com/sample-data/, mit einigen Spalten (Alter, Gewicht, Krankheit). Die Reihenfolge der Felder: Vorname, Nachname, Unternehmen, Adresse, Stadt, Landkreis (falls zutreffend), Bundesland/Provinz (falls zutreffend), Postleitzahl, Telefon 1, Telefon 2, Alter, Gewicht, Krankheit, E-Mail

Die Hauptaufgabe der Anwendung besteht darin, diese Daten entsprechend den aktuellen Bedürfnissen zu anonymisieren und sie dann in eine CSV-Textdatei auszugeben. Der Zweck der Anonymisierung besteht darin, die Daten so umzuwandeln, dass die Personen im Datensatz nicht mehr identifizierbar sind, die Daten aber weiterhin für die Erstellung von Berichten verwendet werden können. Die Anonymisierung ist ein eigener, sehr ernster und anspruchsvoller Bereich der Datenverarbeitung. Die Übung zielt nicht darauf ab, Lösungen zu entwickeln, die in einem realen Kontext anwendbar oder sogar in jeder Hinsicht sinnvoll sind. Für uns ist eigentlich nur der "Einsatz" einer Art von Datenverarbeitungsalgorithmus für die Darstellung von Mustern wichtig. Dies kann einen etwas "aufregenderen" Rahmen bieten als einfache Datenfilterung/ Sequenzierung/etc. basierte Datenverarbeitung (die von .NET standardmäßig unterstützt wird).

Einige Gedanken zur Anonymisierung

Man könnte meinen, dass die Anonymisierung ein einfaches Problem ist. So müssen Sie beispielsweise nur die Namen der Personen, die Adresse, die Hausnummer, die Telefonnummern und die E-Mail-Adresse entfernen oder mit einem Sternchen versehen, und schon sind Sie fertig. Zum Beispiel würde die erste Zeile unserer Eingabe die Ausgabe sein:

"***", "***", "Benton, John B Jr", "***", "New Orleans ", "Orleans", "LA", "70116", "***", "***", "30", "65", "herzbezogen", "***"

Dies ist jedoch bei weitem nicht der Fall, insbesondere wenn es um wirklich große Daten geht. Stellen Sie sich ein kleines Dorf vor, in dem es nicht viele Menschen gibt. Nehmen wir an, eine der auf diese Weise anonymisierten Personen ist 14 Jahre alt, aber stark übergewichtig und wiegt 95 kg. Dies ist eine seltene "Kombination", und es besteht eine gute Chance, dass keine andere Person mit diesen Parametern im Dorf lebt. Wenn einer seiner Klassenkameraden (ein Achtklässler, er ist 14) sich die "anonymisierten" Daten ansieht, wird er wissen, wer er ist (es gibt keinen anderen Achtklässler in der Schule, der so übergewichtig ist), er wird die Person identifizieren. Sie werden zum Beispiel wissen, welche Krankheit die Person hat. Lektion: Daten können im Kontext aufschlussreich sein.

Was ist die Lösung? Stadt, Alter und Körpergewicht können nicht gelöscht/gestrichen werden, da sie gemeldet werden müssen. Eine typische Lösung besteht darin, nach der Anonymisierung nicht das genaue Alter/Gewicht einzugeben, sondern Bereiche (d. h. die Daten zu verallgemeinern): z. B. für die oben genannte Person Alter 10..20 Jahre, Gewicht 80..100 kg, und diese Werte für diese Person in die Ausgabedatei einzugeben. Es ist nicht mehr möglich, die Personen zu identifizieren. Wir werden diese Technik später noch anwenden.

Ursprüngliche Anforderungen

Ursprüngliche Anforderungen an die Bewerbung:

  1. Von einem bestimmten Client empfangene Dateien (alle im gleichen Format) müssen mit dem gleichen Anonymisierungsalgorithmus in das gleiche Ausgabeformat konvertiert werden. Die Anonymisierung sollte lediglich darin bestehen, den Vor- und Nachnamen "auszubuchstabieren".
  2. Eine gewisse Datenbereinigung ist erforderlich. _ # In den Eingabedaten kann es am Anfang/Ende der Spalte, die die Stadt enthält, redundante Zeichen geben, die entfernt werden sollten (Trimm-Operation).
  3. Nach der Verarbeitung jeder Zeile sollte in die Konsole geschrieben werden, dass die Zeile verarbeitet wurde, und nach der Verarbeitung jeder Zeile sollten einige zusammenfassende Informationen angezeigt werden: wie viele Zeilen verarbeitet wurden und bei wie vielen der Stadtname gekürzt wurde.
  4. Ein entscheidender Punkt: Die Anwendung wird nur für einen kurzen Zeitraum benötigt, und wir haben nicht die Absicht, sie in Zukunft zu erweitern.

Hinweis: Um mit weniger Feldern im Code zu arbeiten und die Ausgabe transparenter zu gestalten, lassen wir bei der Verarbeitung einige weitere Felder weg.

Ein Beispiel: Die erste Zeile unserer Eingabedatei ist die erwartete Ausgabe:

***; ***; LA; New Orleans; 30; 65; Herzbezogen

1. Lösung - alles in einem (1-Start/Start)

Im Visual Studio Solution Explorer sehen Sie Ordner, deren Namen mit den Zahlen 1 bis 4 beginnen. Diese enthalten die Lösungen für jede Arbeitsiteration. Die Lösung der ersten Runde befindet sich im Ordner "1-Start" unter dem Projektnamen "Start". Schauen wir uns die Dateien im Projekt an:

  • Person.cs - Sie enthält die Daten einer Person, die für uns von Interesse ist, und wir lesen die Daten einer Person in ihre Objekte ein.
  • Programm.cs - Die gesamte Logik ist als Funktion dieses Main implementiert, "getrennt" durch Code-Einträge. Wenn die Logik ein wenig komplizierter wird, werden wir nach ein oder zwei Tagen (Stunden?) Schwierigkeiten haben, unseren eigenen Code zu überprüfen und zu verstehen. Wir sollten diese Lösung gar nicht erst in Betracht ziehen.

Insgesamt ist die Lösung sehr einfach, da dem Code keine lange Zukunft vorausgesagt wird. Aber die "skriptartige", in eine Funktion gegossene "All-in-one"-Lösung ist auch nicht der richtige Weg, sie macht es sehr schwierig, den Code zu sehen und zu verstehen. Lassen Sie uns das nicht weiter betrachten.

2. Lösung (2-OrganizedToFunctions/OrganizedToFunctions-1)

Gehen wir in Visual Studio zu der Lösung über, die sich im Projekt "OrganizedToFunctions-1" im Ordner "2-OrganizedToFunctions" befindet. Das ist viel sympathischer, weil wir die Logik in Funktionen zerlegt haben. Schauen wir uns kurz den Code an:

Anonymizer.cs

  • Laufen lassen Die Funktion ist das "Rückgrat", sie enthält die Steuerlogik und ruft die für die einzelnen Schritte zuständigen Funktionen auf.
  • ReadFromInput Person Person operation: scannt die Quelldatei, erstellt für jede Zeile ein Objekt und gibt eine Liste der gescannten Objekte zurück.
  • TrimCityNames: Führt eine Datenbereinigung durch (Abschneiden von Städtenamen).
  • Anonym: Person Person Bei jedem gescannten Objekt wird es aufgerufen und ist dafür verantwortlich, ein neues Objekt zurückzugeben, das bereits die anonymisierten Daten enthält.
  • WriteToOutput Person: schreibt die bereits anonymisierten Objekte in die Ausgabedatei.
  • PrintSummary: gibt die Zusammenfassung am Ende der Verarbeitung auf der Konsole aus.

Programm.cs

  • Anonym Laufen lassen Erzeugt ein Objekt und führt es durch den Aufruf von . Es ist ersichtlich, dass die Zeichenkette, die zur Maskierung während der Anonymisierung verwendet wird, in einem Konstruktorparameter angegeben werden muss.

Probieren wir es aus, lassen wir es laufen! Um dies zu tun, sollte "OrganizedToFunctions-1" das Startprojekt in Visual Studio sein (mit der rechten Maustaste darauf klicken und als Startprojekt festlegen), dann starten Sie es:

Ausgabe auf der Konsole

Die Ausgabedatei kann in einem Dateimanager in einem Ordner mit dem Namen "OrganizedToFunctions-1binDebugnet8.0" oder ähnlichem mit dem Namen "us-500.processed.txt" angezeigt werden. Öffnen wir sie und sehen wir uns die Daten an.

Bewertung der Lösung

  • Die Lösung ist grundsätzlich gut strukturiert und leicht zu verstehen.
  • Folgt dem **KISS (Keep It Stupid Simple)** prinzip, ohne unnötige Komplikationen. Das ist gut, denn es gibt keinen potenziellen künftigen Entwicklungsbedarf, keine Notwendigkeit, verschiedene Formate, Logiken usw. zu unterstützen.
  • Unsere Lösung folgt jedoch nicht einem der grundlegendsten und bekanntesten Gestaltungsprinzipien, nämlich dem **Single Responsibility Principle (kurz: SRP)** allgemein bekannt als SRP. Sie geht davon aus, dass eine Abteilung - vereinfacht ausgedrückt - nur eine Aufgabe hat (im Grunde nur eine Sache).

    • Anonym Unsere Abteilung hat zweifellos viele Aufgaben: Verarbeitung von Input, Datenbereinigung, Anonymisierung, Erstellung von Output usw.
    • Dieses Problem ist in unserem Fall nicht spürbar und stellt kein Problem dar, da die Umsetzung jeder dieser Aufgaben einfach ist und in eine kürzere Funktion "passt". Wären diese jedoch komplexer und in mehreren Funktionen implementiert, müssten sie definitiv in einer eigenen Klasse organisiert werden.
    Warum ist es ein Problem, wenn eine Abteilung mehr Aufgaben hat? *
    • Es ist schwieriger zu verstehen, wie es funktioniert, weil es nicht auf eine Sache konzentriert ist.
    • Wenn eine dieser Zuständigkeiten geändert werden muss, muss eine große, multidisziplinäre Abteilung geändert und erneut getestet werden.
  • Sie können automatisierte Integrationstests (Input-Output) für die Lösung schreiben, aber keine "echten" Unit-Tests.

3. Lösung (OrganizedToFunctions-2-TwoAlgorithms)

Im Gegensatz zu früheren "Plänen" haben sich neue Bedürfnisse der Nutzer ergeben. Unser Kunde hat seine Meinung geändert und verlangt für einen anderen Datensatz einen anderen Anonymisierungsalgorithmus: Das Alter der Personen soll in Banden gespeichert werden, das genaue Alter der Personen darf nicht offenbart werden. Der Einfachheit halber werden wir in diesem Fall die Namen der Personen nicht anonymisieren. Betrachten Sie dies also als eine Art "Pseudo"-Anonymisierung (es macht immer noch Sinn, aber es ist nicht ganz korrekt, es Anonymisierung zu nennen).

Unsere Lösung - die sowohl den alten als auch den neuen Algorithmus (einen nach dem anderen) unterstützt - finden Sie im VS-Lösungsprojekt OrganizedToFunctions-2-TwoAlgorithms. Anonymizer Schauen wir uns die Klasse, das Grundprinzip der Lösung an (überprüfen Sie diese im Code):

  • AnonymizerMode Anonym Wir haben einen Enum-Typ eingeführt, der den Modus (Algorithmus) angibt, in dem die Klasse verwendet wird.
  • Anonymizer Anonymize_MaskName Die Klasse verfügt über zwei Anonymisierungsoperationen: , Anonymize_AgeRange
  • Anonymizer _anonymizerMode _anonymizerMode Die Klasse speichert den zu verwendenden Algorithmus im Member: für die beiden Modi werden zwei separate Konstruktoren eingeführt, die den Wert von .
  • Anonymizer Run GetAnonymizerDescription _anonymizerMode Die Klasse prüft an mehreren Stellen (z.B. bei Operationen) auf den Wert von und gabelt sich in Abhängigkeit davon auf.
  • GetAnonymizerDescriptionIn ist dies notwendig, da die Aufgabe dieses Vorgangs darin besteht, eine einzeilige Beschreibung des Anonymisierungsalgorithmus zu erstellen, die am Ende der Verarbeitung in der "Zusammenfassung" angezeigt wird. PintSummary Sehen wir uns den Code für die Aufrufe dieser Aktion an. So sieht die Zusammenfassung auf der Konsole aus, wenn ein Altersanonymisierer mit einem Bereich von 20 Jahren verwendet wird:

    Summary - Anonymizer (Age anonymizer with range size 20): Persons: 500, trimmed: 2

Bewertung der Lösung

Insgesamt ist unsere Lösung in Bezug auf die Codequalität schlechter als zuvor. In der Vergangenheit gab es kein Problem damit, dass es in Bezug auf Anonymisierungsalgorithmen nicht erweiterbar war, da es keinen Bedarf dafür gab. Wenn aber einmal die Notwendigkeit besteht, einen neuen Algorithmus einzuführen, ist es ein Fehler, unsere Lösung in dieser Hinsicht nicht erweiterbar zu machen: Von nun an würden wir lieber damit rechnen, dass in Zukunft zusätzliche Algorithmen eingeführt werden müssen.

if``switch Warum behaupten wir, dass unser Code nicht erweitert werden kann, wenn "nur" ein neuer Enum-Wert und eine zusätzliche / Verzweigung an einer Stelle des Codes eingeführt werden soll, wenn ein neuer Algorithmus eingeführt werden soll?

⚠ Prinzip Offen/Geschlossen
Entscheidend ist, dass eine Klasse dann als erweiterbar gilt, wenn es möglich ist, ein neues Verhalten (in unserem Fall einen neuen Algorithmus) einzuführen , ohne sie in irgendeiner Weise zu verändern, indem man einfach den Code erweitert/erweitert. Anonymizer Mit anderen Worten, in unserem Fall sollte der Code von nicht angetastet werden, was eindeutig nicht der Fall ist. Dies ist das berühmte Offen/Geschlossen-Prinzip: Die Klasse sollte offen für Erweiterungen und geschlossen für Änderungen sein. Die Änderung des Codes ist problematisch, da sie wahrscheinlich neue Fehler einführt und der geänderte Code immer wieder neu getestet werden muss, was einen erheblichen Zeit- und Kostenaufwand bedeutet.

Was genau ist das Ziel und wie können wir es erreichen? Es gibt Teile unseres Klassenzimmers, die wir nicht verbrennen wollen:

  • Dies sind keine Daten, sondern **verhaltensweisen (Code, Logik)**.
  • if``switch Wir lösen das Problem nicht mit / : Wir führen "Erweiterungspunkte" ein und schaffen es irgendwie, "beliebigen" Code in ihnen laufen zu lassen.
  • Wir setzen den Code dieser variablen/fallabhängigen Teile in andere Klassen (in einer Weise, die für unsere Klasse "austauschbar" ist)!

Note

Denken Sie nicht an Zauberei, wir werden die Werkzeuge verwenden, die wir bereits kennen: Vererbung mit abstrakten/virtuellen Funktionen, oder Schnittstellen, oder Delegaten.

Anonymizer Suchen Sie nach Teilen, die fallabhängig sind, variable Logik, so dass es nicht gut ist, sie in die Klasse zu brennen:

  • Anonymize_MaskNameDas eine ist die Anonymisierungslogik selbst: /Anonymize_AgeRange
  • Die andere ist die GetAnonymizerDescription

Diese sollten von der Klasse entkoppelt werden, an diesen Stellen sollte die Klasse erweitert werden. Die folgende Abbildung veranschaulicht das allgemeine Ziel *:

Veranschaulichung des allgemeinen Lösungsprinzips

Extensibility illustration

Wir werden uns drei spezifische Entwurfsmuster und -techniken ansehen, um die oben genannten Ziele zu erreichen:

  • Vorlage Methodenentwurf Muster
  • Strategieentwurfsmuster (einschließlich Dependency Injection)
  • Delegate (optional mit Lambda-Ausdruck)

Eigentlich haben wir sie alle in unserem Studium verwendet, aber jetzt werden wir sie noch besser kennen lernen und ihre Verwendung noch umfassender üben. Die ersten beiden im Labor und die dritte in einer entsprechenden Hausaufgabe.

4. Lösung (3-TemplateMethod/TemplateMethod-1)

In diesem Schritt werden wir das Entwurfsmuster Template Method verwenden, um unsere Lösung an den erforderlichen Stellen erweiterbar zu machen.

Note

Der Name des Musters ist "irreführend": Es hat nichts mit den in C++ erlernten Template-Methoden zu tun!

Klassendiagramm der auf der Schablonenmethode basierenden Lösung

Das folgende UML-Klassendiagramm veranschaulicht die auf der Schablonenmethode basierende Lösung und konzentriert sich auf das Wesentliche:

Template Method UML osztálydiagram cél

Im Beispiel beruht die Trennung zwischen "unveränderlichen" und "veränderlichen" Teilen auf den folgenden Grundsätzen (es lohnt sich, diese im obigen Klassendiagramm zu verstehen, das auf unser Beispiel angewandt wird):

  • Die "gemeinsamen/ungemeinsamen" Teile werden in eine Vorgängerklasse eingeordnet.
  • Dabei sind die Erweiterungspunkte die Einführung von abstrakten/virtuellen Funktionen, die an den Erweiterungspunkten aufgerufen werden.
  • Ihre fallspezifische Implementierung ist in den Nachfolgeklassen untergebracht.

Der bekannte "Trick" besteht darin, dass beim Aufruf der abstrakten/virtuellen Funktionen durch den Vorgänger der fallabhängige Code des Nachfolgers aufgerufen wird.

enum if``switch Im Folgenden wird die frühere , oder /-basierte Lösung in eine Template-Methoden-basierte Lösung umgewandelt (diese wird keine Aufzählung mehr haben). Wir führen eine Vorgängerklasse und zwei vom Algorithmus abhängige Nachkommen ein.

Wir müssen unseren Code entsprechend umstrukturieren. In der VS-Lösung, im Ordner "3-TemplateMethod", enthält das Projekt "TemplateMethod-0-Begin" den Code unserer vorherigen Lösung (eine "Kopie" davon), lassen Sie uns in diesem Projekt arbeiten:

  1. Anonymizer AnonymizerBaseBenennen Sie die Klasse um in (z. B. indem Sie auf den Klassennamen in der Quelldatei zeigen und F2drücken).
  2. NameMaskingAnonymizer AgeAnonymizer Fügen Sie eine Klasse und eine Klasse zu dem Projekt hinzu (Rechtsklick auf das Projekt, Hinzufügen/Klasse).
  3. AnonymizerBaseExtrahieren Sie sie aus
  4. AnonymizerBase NameMaskingAnonymizerVerschieben Sie die entsprechenden Teile von nach :

    1. _mask Die Mitgliedsvariable .
    2. string inputFileName, string mask NameMaskingAnonymizerDer Konstruktor, umbenannt in ,
      1. _anonymizerMode = AnonymizerMode.Name; zeile wird gelöscht,
      2. this base anstelle von .

        Konstrukteurscode
        public NameMaskingAnonymizer(string inputFileName, string mask): base(inputFileName)
        {
            _mask = mask;
        }
        
  5. AnonymizerBase AgeAnonymizerVerschieben Sie die entsprechenden Teile von nach :

    1. _rangeSize Die Mitgliedsvariable .
    2. string inputFileName, string rangeSize AgeAnonymizerDer Konstruktor, umbenannt in ,
      1. _anonymizerMode = AnonymizerMode.Age; zeile wird gelöscht,
      2. this base anstelle von .

        Konstrukteurscode
        public AgeAnonymizer(string inputFileName, int rangeSize): base(inputFileName)
        {
            _rangeSize = rangeSize;
        }
        
  6. AnonymizerBaseBei :

    1. AnonymizerMode Löschen Sie den Aufzählungstyp .
    2. _anonymizerMode Wir löschen den Tag .

AnonymizerBase Suchen Sie nach Teilen, die fallabhängig sind, nach variabler Logik, so dass Sie diese nicht in die Klasse einbauen wollen, die wiederverwendbar sein soll:

  • Anonymize_MaskName``Anonymize_AgeRangeEiner ist / ,
  • GetAnonymizerDescriptiondas andere ist .

Dem Muster folgend führen wir abstrakte (oder möglicherweise virtuelle) Funktionen für diese in der Vorgängerklasse ein und rufen sie auf, und überschreiben ihre fallabhängigen Implementierungen in den Nachfolgeklassen:

  1. AnonymizerBase class abstract Machen Sie die Klasse abstrakt (das Schlüsselwort vor ).
  2. AnonymizerBaseGeben wir eine neue URL in

    protected abstract Person Anonymize(Person person person);
    

    (diese ist für die Anonymisierung zuständig).

  3. Anonymize_MaskName NameMaskingAnonymizer Anonymize Verschieben Sie die Operation in die Klasse und ändern Sie die Signatur so, dass die übergeordnete abstrakte Funktion überschrieben wird:

    protected override Person Anonymize(Person person)
    {
        return new Person(_mask, _mask, person.CompanyName,
            person.Address, person.City, person.State, person.Age, person.Weight, person.Decease);
    }
    

    mask _mask Der Funktionskörper muss nur umgeschrieben werden, um die Mitgliedsvariable anstelle des veralteten Parameters zu verwenden.

  4. Anonymisieren_Altersbereich AgeAnonymizer Anonymize In völliger Analogie zum vorherigen Schritt verschieben Sie die Operation in die Klasse und ändern ihre Signatur so, dass die übergeordnete abstrakte Funktion überschrieben wird:

    protected override Person Anonymize(Person person)
    {
        ...
    }
    

    rangeSize _rangeSize Der Funktionskörper muss nur umgeschrieben werden, um die Mitgliedsvariable anstelle des veralteten Parameters zu verwenden.

  5. AnonymizerBase Run if``else Anonymize In der Funktion der Klasse können wir nun die Aufrufe in / durch einen einfachen abstrakten Funktionsaufruf ersetzen:

    Person person;
    if (_anonymizerMode == AnonymizerMode.Name)
        person = Anonymize_MaskName(persons[i], _mask);
    else if (_anonymizerMode == AnonymizerMode.Age)
        person = Anonymize_AgeRange(persons[i], _rangeSize);
    else
        throw new NotSupportedException("The requested anonymization mode is not supported.");
    

    stattdessen:

    var person = Anonymize(persons[i]);
    

Einer unserer Verlängerungspunkte ist fertig. GetAnonymizerDescription Es gibt aber noch eine weitere, die ebenfalls fallspezifisch ist. Die Umwandlung ist der vorherigen Reihe von Schritten sehr ähnlich:

  1. AnonymizerBase GetAnonymizerDescription NameMaskingAnonymizer override NameMaskingAnonymizerDie Operation der Klasse wird nach , einschließlich des Schlüsselworts in der Signatur, kopiert, wobei nur die Logik für im Funktionskörper verbleibt:

    protected override string GetAnonymizerDescription()
    {
        return $"NameMasking anonymizer with mask {_mask}";
    }
    
  2. AnonymizerBase GetAnonymizerDescription AgeAnonymizer override AgeAnonymizerKopieren wir die Operation nach , einschließlich des Schlüsselworts in der Signatur, und lassen wir nur die Logik für im Funktionskörper:

    protected override string GetAnonymizerDescription()
    {
        return $"Age anonymizer with range size {_rangeSize}";
    }
    
  3. AnonymizerBase GetAnonymizerDescription Die Frage ist, was man mit der Operation in . NameMaskingAnonymizer Dies wird nicht abstrahiert, sondern in eine virtuelle Abhängigkeit umgewandelt, da wir hier ein sinnvolles Standardverhalten bereitstellen können: Wir geben einfach den Namen der Klasse zurück (der für die Klasse z. B. "NameMaskingAnonymizer" lauten würde). switch Auf jeden Fall wird dadurch die starre Struktur beseitigt:

    protected virtual string GetAnonymizerDescription()
    {
        return GetType().Name;
    }
    

    Reflexion

    GetType() Type Mit der vom Objektvorgänger geerbten Operation erhalten wir ein Objekt des Typs für unsere Klasse. Dies gehört zum Thema Reflexion, über das wir in einer Vorlesung am Ende des Semesters mehr erfahren werden.

Programm.cs Main AnonymizerBase Es bleibt nur noch eines zu tun: Je nach , versuchen wir nun, den Vorgänger zu replizieren (wegen der früheren Umbenennung). Stattdessen sollte es einer der beiden Abkömmlinge sein. Pl.:

NameMaskingAnonymizer anonymizer = new("us-500.csv", "***");
anonymizer.Run();

Wir sind bereit. Probieren wir es aus, um ein besseres "Gefühl" dafür zu bekommen, wie Erweiterungspunkte wirklich funktionieren (aber wenn Sie während des Praktikums wenig Zeit haben, ist das nicht besonders wichtig, wir haben ähnliche Dinge im Zusammenhang mit C++/Java in früheren Semestern getan):

  • In Visual Studio sollte das Projekt TemplateMethod-0-Begin das Startprojekt sein, wenn es nicht bereits eingestellt ist.
  • AnonymizerBase var person = Anonymize(persons[i]); Setzen Sie einen Haltepunkt in der Zeile der Klasse.
  • Wenn der Debugger hier anhält, während er läuft, ++verwenden Sie f11++zur Eingabe.
  • AgeAnonymizer Wir erfahren, dass die Operation des Nachkommens aufgerufen wird.

Wir können einen Blick auf das Klassendiagramm der Lösung werfen:

Template Method based solution class diagram *

Template Method based solution class diagram

3-TemplateMethod/TemplateMethod-1 Die Lösung für unsere bisherige Arbeit finden Sie im Projekt, falls Sie sie benötigen.

Das Muster heißt Schablonenmethode

, weil es sich - am Beispiel unserer Anwendung - um "Schablonenmethoden" handelt, die eine schablonenartige Logik, einen Rahmen, definieren, in dem bestimmte Schritte nicht gebunden sind. Run PrintSummary Ihr "Code" wird abstrakten/virtuellen Funktionen überlassen, und ihre Implementierung wird von den abgeleiteten Klassen definiert.

Bewertung der Lösung

Wir überprüfen die Lösung, um zu sehen, ob sie unsere Ziele erreicht:

  • AnonymizerBase Ist eine wiederverwendbare Klasse geworden.
  • Wenn in Zukunft eine neue Anonymisierungslogik benötigt wird, leiten wir sie einfach ab. Es handelt sich nicht um eine Änderung, sondern um eine Verlängerung.
  • Damit ist das OPEN/CLOSED-Prinzip erfüllt, d.h. wir können die Logik an den beiden im Ahnen angegebenen Stellen anpassen und erweitern, ohne den Code zu verändern.

Soll unsere Klasse an allen Stellen erweiterbar sein?

AnonymizerBase Beachten Sie, dass wir nicht alle Operationen virtuell gemacht haben (und somit die Klasse an vielen Stellen erweitern). Wir haben dies nur dort getan, wo wir glauben, dass die Logik in Zukunft erweitert werden muss.

5. Megoldás (3-TemplateMethod/TemplateMethod-2-Progress)

T.f.h ein neuer - relativ einfacher - Bedarf entsteht:

  • NameMaskinAnonimizer Für , bleibt die bisherige einfache Fortschrittsanzeige erhalten (nach jeder Zeile wird die Anzahl der erreichten Zeilen ausgegeben),

    Illustration des einfachen Fortschritts

    Illustration des einfachen Fortschritts

  • AgeAnonymizer aber für , sollte die Fortschrittsanzeige anders sein: Sie sollte - nach jeder Zeile aktualisiert - den Prozentsatz der Verarbeitung anzeigen.

    Darstellung des prozentualen Fortschritts

    Darstellung des prozentualen Fortschritts

    (Da wir derzeit nur 500 Datenzeilen haben, werden wir dies am Ende unserer Lösung nicht sehen, es wird in kürzester Zeit auf 100 % steigen)

Laufen lassen Die Lösung ist sehr einfach: Wir verwenden das Muster der Schablonenmethode in größerem Umfang in der Operation, führen einen Erweiterungspunkt in den Fortschrittsausdruck ein und überlassen die Implementierung einer virtuellen Funktion.

Springen wir direkt zur fertigen Lösung*(* Projekt*3-TemplateMethod/TemplateMethod-2-Progress* ):

  • AnonymizerBase PrintProgress neue virtuelle Funktion in der Klasse (druckt standardmäßig nichts aus)
  • Run-bei diesem Gespräch
  • NameMaskingAnonymizer- NameMaskingAnonymizerimplementierung (Override) in und

Bislang gibt es keine besonderen Lehren, die man ziehen könnte, aber im nächsten Schritt wird es welche geben.

6. Lösung (3-TemplateMethod/TemplateMethod-3-ProgressMultiple)

Es ist ein neuer - und völlig logischer - Bedarf entstanden: In Zukunft kann jeder Anonymisierungsalgorithmus mit jeder Fortschrittsdarstellung verwendet werden. Dies bedeutet derzeit vier Kreuzkombinationen:

Anonym Progress
Namensanonymisierer Einfacher Fortschritt
Namensanonymisierer Prozentualer Fortschritt
Altersanonymisierung Einfacher Fortschritt
Altersanonymisierung Prozentualer Fortschritt

Springen Sie zur fertigen Lösung*(* Projekt*3-TemplateMethod/TemplateMethod-3-ProgressMultiple* ). Haupt.cd Öffnen Sie statt des Codes das Klassendiagramm im Projekt und überprüfen Sie die Lösung auf der Grundlage dieses Diagramms (oder Sie können das Diagramm unten im Leitfaden sehen).

Template Method based solution (two aspects)

Klassendiagramm "Template Method based solution (two aspects)

Man merkt, dass etwas "nicht stimmt", denn jede Kreuzung musste einen eigenen Nachkommen hervorbringen. Es gibt sogar zusätzliche Zwischenklassen in der Hierarchie, um Code-Duplizierung zu vermeiden. Darüber hinaus:

  • Wenn wir in Zukunft einen neuen Anonymisierungsalgorithmus einführen, sollten wir (mindestens) so viele neue Klassen schreiben, wie wir Fortschrittstypen unterstützen.
  • Wenn in Zukunft eine neue Verlaufsart eingeführt wird, sollten (mindestens) so viele neue Klassen geschrieben werden, wie Anonymisierungsarten unterstützt werden.

Was hat das Problem verursacht? Die Notwendigkeit, das Verhalten unserer Klasse um mehrere Aspekte/Dimensionen zu erweitern (in unserem Beispiel Anonymisierung und Fortschritt) und diese in vielen Kreuzkombinationen zu unterstützen. Müssten wir dies unter neuen Gesichtspunkten tun (z. B. wie man scannt, wie man Output erzeugt), würde das Problem exponentiell "explodieren". In solchen Fällen ist das Entwurfsmuster Template Method nicht anwendbar.

7. Lösung (4-Strategie/Strategie-1)

In diesem Schritt werden wir das Entwurfsmuster Strategy verwenden, um unsere ursprüngliche Lösung an den erforderlichen Stellen zu erweitern. In der Stichprobe erfolgt die Trennung zwischen "unveränderten/wiederverwendbaren" und "sich ändernden" Teilen nach folgenden Grundsätzen:

  • Die "gemeinsamen/ungemeinsamen" Teile werden in einer Klasse zusammengefasst (dieses Mal jedoch nicht als "Elternklasse").
  • Im Gegensatz zur Template-Methode verwenden wir Komposition (Containment) statt Vererbung: Wir verlassen uns auf andere Objekte, die als Schnittstellen enthalten sind, um das Verhalten an Erweiterungspunkten zu implementieren (anstelle von abstrakten/virtuellen Funktionen).
  • Wir tun dies unabhängig für jeden Aspekt/Dimension des Klassenverhaltens, den wir austauschbar/erweiterbar machen wollen. Wie wir sehen werden, wird dadurch die kombinatorische Explosion vermieden, die wir im vorherigen Kapitel gesehen haben.

Dies ist in der Praxis viel einfacher, als es sich in der Schrift anfühlt (wir haben es in unseren früheren Studien schon einige Male verwendet). Um das zu verstehen, betrachten wir unser Beispiel.

Als Nächstes wollen wir das Klassendiagramm betrachten, das die strategiebasierte Lösung veranschaulicht (aufbauend auf der Erklärung, die dem Diagramm folgt).

Klassendiagramm der strategiebasierten Lösung

Das folgende UML-Klassendiagramm veranschaulicht die strategiebasierte Lösung und konzentriert sich auf das Wesentliche:

Strategy UML osztálydiagram cél

Der erste Schritt bei der Verwendung des Strategiemusters besteht darin, zu bestimmen, wie viele verschiedene Aspekte des Klassenverhaltens Sie erweiterbar machen wollen. In unserem Beispiel gibt es zwei davon - zumindest im Moment:

  • Anonymisierungsbezogenes Verhalten mit zwei Aktionen:
    • Anonymisierungslogik
    • Definition der Beschreibung der Anonymisierungslogik (Generierung eines Beschreibungsstrings)
  • Fortschreitende Behandlung mit einer Aktion:
    • Fortschritt zeigen

Der schwierige Teil ist geschafft, von nun an können Sie im Wesentlichen mechanisch nach dem Strategiemuster arbeiten:

  1. Für jeden der oben genannten Aspekte sollte eine Strategie-Schnittstelle mit den oben definierten Operationen eingeführt werden, und die entsprechenden Implementierungen sollten vorbereitet werden.
  2. Anonym In der Klasse sollte eine Mitgliedsvariable der Strategie-Schnittstelle eingeführt werden, und die aktuell konfigurierten Strategie-Implementierungsobjekte sollten in den Erweiterungspunkten über diese Mitgliedsvariablen verwendet werden.

Diese Elemente sind in dem obigen Klassendiagramm dargestellt. Kommen wir nun zum Code. Unsere Startumgebung befindet sich im Ordner "4-Strategy" im Projekt "Strategy-0-Begin", lassen Sie uns darin arbeiten. Dies ist dieselbe Lösung mit enum, die als Ausgangspunkt für das Muster Template Method verwendet wurde.

Anonymisierungsstrategie

Wir beginnen mit der Anonymisierungsstrategie/dem Anonymisierungsaspekt. Implementieren Sie die entsprechende Schnittstelle:

  1. AnonymizerAlgorithms Erstellen Sie im Projekt einen Ordner mit dem Namen (klicken Sie mit der rechten Maustaste auf das Projekt "Strategy-0-Begin" und dann auf das Menü " Add/New Folder "). Legen Sie in den nächsten Schritten jede Schnittstelle und jede Klasse in eine eigene Quelldatei mit eigenem Namen, wie üblich!
  2. IAnonymizerAlgorithmus Fügen Sie in diesem Ordner eine Schnittstelle mit dem folgenden Code hinzu:

    IAnonymizerAlgorithm.cs
    public interface IAnonymizerAlgorithm
    {
        Person Anonymize(Person person);
        string GetAnonymizerDescription() => GetType().Name;
    }
    

    GetAnonymizerDescription Wir können auch für die Operation beobachten, dass wir in modernem C#, wenn wir wollen, Standardimplementierungen für jede Schnittstellenoperation geben können!

Wir bauen jetzt eine Implementierung dieser Schnittstelle für die Anonymisierung von Namen (d.h. wir bauen eine Strategieimplementierung).

  1. NameMaskingAnonymizerAlgorithm Fügen Sie demselben Ordner eine Klasse hinzu.
  2. Anonymizer NameMaskingAnonymizerAlgorithm _mask Verschieben Sie in der Klasse die entsprechende Mitgliedsvariable nach :
  3. NameMaskingAnonymizerAlgorithmFügen Sie den folgenden Konstruktor zu :

    public NameMaskingAnonymizerAlgorithm(string mask)
    {
        _mask = mask;
    }
    
  4. IAnonymizerAlgorithmus Machen Sie die Schnittstelle verfügbar. : IAnonymizerAlgorithmus Nachdem Sie die Schnittstelle nach dem Klassennamen eingegeben haben, empfiehlt es sich, das Skelett der Operationen mit Visual Studio zu generieren: Setzen Sie den Cursor auf den Schnittstellennamen (klicken Sie ihn im Quellcode an), verwenden Sie die Tastenkombination 'ctrl' + '.', und wählen Sie dann "Schnittstelle implementieren" aus dem erscheinenden Menü. GetAnonymizerDescription Anonymize Hinweis: Da es eine Standardimplementierung für die Operation in der Schnittstelle gibt, wird nur die Operation generiert, aber das ist für uns erst einmal in Ordnung.

  5. Anonymizer Anonymize_MaskName NameMaskingAnonymizerAlgorithm``AnonymizeVerschieben Sie die Wurzel der Operation von der Klasse nach . mask _mask Der Funktionskörper muss nur umgeschrieben werden, um die Mitgliedsvariable anstelle des nicht mehr vorhandenen Parameters zu verwenden. Anonymize Anonymize_MaskNameDie Klasse wird gelöscht.
  6. GetAnonymizerDescriptionWir wenden uns nun der Implementierung des Betriebs der Strategie-Schnittstelle zu. Anonymizer GetAnonymizerDescription NameMaskingAnonymizerAlgorithmKopieren Sie die Operation der Klasse nach , wobei Sie nur die Logik der Namensanonymisierung im Funktionskörper belassen und die Operation öffentlich machen:

    public string GetAnonymizerDescription()
    {
        return $"NameMasking anonymizer with mask {_mask}";
    }  
    
  7. Damit ist unsere Strategieimplementierung für die Namensanonymisierung abgeschlossen, der vollständige Code lautet wie folgt
    NameMaskingAnonymizerAlgorithm.cs
    public class NameMaskingAnonymizerAlgorithm: IAnonymizerAlgorithm
    {
        private readonly string _mask;
    
        public NameMaskingAnonymizerAlgorithm(string mask)
        {
            _mask = mask;
        }
    
        public Person Anonymize(Person person)
        {
            return new Person(_mask, _mask, person.CompanyName,
                person.Address, person.City, person.State, person.Age, person.Weight, person.Decease);
        }
    
        public string GetAnonymizerDescription()
        {
            return $"NameMasking anonymizer with mask {_mask}";
        }
    }
    

IAnonymizerAlgorithm Im nächsten Schritt werden wir eine Implementierung unserer Strategie-Schnittstelle zur Altersanonymisierung vorbereiten.

  1. AgeAnonymizerAlgorithm Fügen Sie im gleichen Ordner eine Klasse hinzu (AnonymizerAlgorithms).
  2. Anonymizer AgeAnonymizerAlgorithm _rangeSize Verschieben Sie in der Klasse die entsprechende Mitgliedsvariable nach :
  3. AgeAnonymizerAlgorithmFügen Sie den folgenden Konstruktor zu :

    public AgeAnonymizerAlgorithm(int rangeSize)
    {
        _rangeSize = rangeSize;
    }
    
  4. IAnonymizerAlgorithm Machen Sie die Schnittstelle verfügbar. : IAnonymizerAlgorithm Anonymize Nachdem Sie die Schnittstelle nach dem Klassennamen eingegeben haben, empfiehlt es sich auch, das Skelett der Operation mit Visual Studio auf dieselbe Weise wie zuvor zu generieren.

  5. Anonymizer Anonymize_AgeRange AgeAnonymizerAlgorithm``AnonymizeVerschieben Sie die Wurzel der Operation von der Klasse nach . rangeSize _rangeSize Der Funktionskörper muss nur umgeschrieben werden, um die Mitgliedsvariable anstelle des nicht mehr vorhandenen Parameters zu verwenden. Anonymize Anonymize_AgeRangeDie Klasse wird gelöscht.
  6. GetAnonymizerDescriptionWir wenden uns nun der Implementierung des Betriebs der Strategie-Schnittstelle zu. Anonymizer GetAnonymizerDescription AgeAnonymizerAlgorithmKopieren Sie die Operation der Klasse nach , wobei Sie nur die Logik der Altersanonymisierung im Funktionskörper beibehalten und die Operation öffentlich machen:

    public string GetAnonymizerDescription()
    {
        return $"Age anonymizer with range size {_rangeSize}";
    } 
    
  7. Ezzel a kor anonimizáláshoz tartozó strategy implementációnk elkészült, a teljes kódja a következő lett
    AgeAnonymizerAlgorithm.cs
    public class AgeAnonymizerAlgorithm: IAnonymizerAlgorithm
    {
        private readonly int _rangeSize;
    
        public AgeAnonymizerAlgorithm(int rangeSize)
        {
            _rangeSize = rangeSize;
        }
    
        public Person Anonymize(Person person)
        {
            // This is whole number integer arithmetics, e.g for 55 / 20 we get 2
            int rangeIndex = int.Parse(person.Age) / _rangeSize;
            string newAge = $"{rangeIndex * _rangeSize}..{(rangeIndex + 1) * _rangeSize}";
    
            return new Person(person.FirstName, person.LastName, person.CompanyName,
                person.Address, person.City, person.State, newAge,
                person.Weight, person.Decease);
        }
    
        public string GetAnonymizerDescription()
        {
            return $"Age anonymizer with range size {_rangeSize}";
        }
    }
    

warning❗ Beachten Sie, dass die Schnittstelle und ihre Implementierungen sich nur mit der Anonymisierung befassen und keine andere Logik (z.B. Fortschrittsverarbeitung) involviert ist!

Strategie für den Fortschritt

Im nächsten Schritt stellen wir die Schnittstelle und die Implementierungen für das Fortschrittsmanagement vor:

  1. Progresses Erstellen Sie im Projekt einen Ordner mit dem Namen . In den nächsten Schritten legen Sie jede Schnittstelle und Klasse wie üblich in einer eigenen Quelldatei mit eigenem Namen ab.
  2. IProgress Fügen Sie in diesem Ordner eine Schnittstelle mit dem folgenden Code hinzu:

    Lösung
    IProgress.cs
    public interface IProgress
    {
        void Report(int count, int index);
    }
    
  3. Fügen Sie die Implementierung dieser Schnittstelle für einfache Fortschritte in denselben Ordner ein. Anonymizer PrintProgress Die Implementierung ist von der Operation unserer Klasse "abgeleitet":

    Lösung
    SimpleProgress.cs
    public class SimpleProgress: IProgress
    {
        public void Report(int count, int index)
        {
            Console.WriteLine($"{index + 1}. person processed");
        }
    }
    
  4. Nehmen Sie die Implementierung dieser Schnittstelle für den prozentualen Fortschritt in denselben Ordner auf. Wir wollen uns nicht mit der Auslegung des Gesetzes befassen. Anonym In unserer Klasse gibt es dafür keine Lösung, da wir sie nur in unserer auf Vorlagenmethoden basierenden Lösung eingeführt haben (wir haben uns den Code dort nicht angesehen, aber er ist praktisch derselbe):

    Lösung
    PercentProgress.cs
    public class PercentProgress: IProgress
    {
        public void Report(int count, int index)
        {
            int percentage = (int)((double)(index+1) / count * 100);
    
            Console.Write($"rProcessing: {percentage} %");
    
            if (index == count - 1)
                Console.WriteLine();
        }
    }
    

❗ Beachten Sie, dass sich die Schnittstelle und ihre Implementierungen nur mit der Fortschrittsverarbeitung befassen und keine andere Logik (z.B. Anonymisierung) involviert ist!

Anwendung der Strategien

Der nächste wichtige Schritt besteht darin, die anonymisierende Basisklasse mit Hilfe der oben vorgestellten Strategien wiederverwendbar und erweiterbar zu machen. Anonymizer.cs Bei :

  1. Löschen Sie Folgendes:

    • AnonymizerMode aufzählungstyp
    • _anonymizerMode _mask _rangeSize mitglied (oder und Mitglieder, wenn Sie schon einmal hier gewohnt haben)
  2. Einführung eines einzigen Strategie-Schnittstellentyp-Tags:

    private readonly IProgress _progress;
    private readonly IAnonymizerAlgorithm _anonymizerAlgorithm;
    
  3. Fügen Sie die entsprechende Verwendung am Anfang der Datei ein:

    mit Lab_Extensibility.AnonymizerAlgorithms;
    mit Lab_Extensibility.Progresses;
    
  4. _progress _anonymizerAlgorithm Der anfängliche Wert von und, der im vorigen Abschnitt eingeführt wurde, ist Null. Im Konstruktor werden diese Referenzen auf die Implementierung gesetzt, die Ihren Anforderungen entspricht. Pl.:

    public Anonymizer(string inputFileName, string mask) : this(inputFileName)
    {
        _progress = new PercentProgress();
        _anonymizerAlgorithm = new NameMaskingAnonymizerAlgorithm(mask);
    }
    
    public Anonymizer(string inputFileName, int rangeSize) : this(inputFileName)
    {
        _progress = new PercentProgress();
        _anonymizerAlgorithm = new AgeAnonymizerAlgorithm(rangeSize);
    }
    

Anonymizer _anonymizerAlgorithm In der Klasse wird die derzeit implementierte, aber von der Anonymisierung abhängige Logik der Strategieimplementierung überlassen, auf die die Mitgliedsvariable verweist:

  1. Run if``else Anonymize _anonymizerAlgorithm Als eine Funktion der Klasse werden die Aufrufe im /-Ausdruck nun an das Objekt delegiert:

    Person person;
    if (_anonymizerMode == AnonymizerMode.Name)
        person = Anonymize_MaskName(persons[i], _mask);
    else if (_anonymizerMode == AnonymizerMode.Age)
        person = Anonymize_AgeRange(persons[i], _rangeSize);
    else
        throw new NotSupportedException("The requested anonymization mode is not supported.");
    

    stattdessen:

    Person person = _anonymizerAlgorithm.Anonymize(persons[i]);
    
  2. Anonymize_MaskName Anonymize_AgeRange Falls noch nicht geschehen, löschen Sie die Funktionen und, da ihr Code bereits in die Strategieimplementierungen aufgenommen wurde, die von der Klasse entkoppelt sind.

  3. PrintSummary switch GetAnonymizerDescriptionUnsere Funktion ruft den unflexiblen , basierend auf auf. GetAnonymizerDescription _anonymizerAlgorithm Dieser Aufruf an wird durch eine Delegation an das Objekt ersetzt. PrintSummary In der Funktion (nur zur Hervorhebung der Grundzüge):

        ... GetAnonymizerDescription() ...
    

    stattdessen:

        ... _anonymizerAlgorithm.GetAnonymizerDescription() ...
    

    GetAnonymizerDescription Ein paar Zeilen weiter unten wird die Funktion aus der Klasse entfernt (ihr Code ist in den entsprechenden Strategieimplementierungen enthalten).

Anonymizer Der letzte Schritt besteht darin, den Progress-Handler in der Klasse zu ersetzen:

  1. _progress Auch hier delegieren wir die Anfrage an unser zuvor eingeführtes Objekt. Run Sie müssen eine Zeile in der Funktion ersetzen:

    PrintProgress(i);
    

    stattdessen:

    _progress.Report(persons.Count, i);
    
  2. PrintProgress Löschen Sie die Funktion, da sich ihr Code bereits in einer eigenen Strategieimplementierung befindet, die von der Klasse entkoppelt ist.

Wir sind fertig, die fertige Lösung befindet sich im Projekt "4-Strategy/Strategy-1" (wenn Sie irgendwo stecken bleiben oder der Code sich nicht kompilieren lässt, können Sie ihn damit überprüfen).

Bewertung der Lösung

Wir sind mit der Umsetzung der Strategievorlage fertig. In seiner jetzigen Form wird es jedoch fast nie verwendet. Anomymizer Überprüfen wir unsere Lösung: Ist sie wirklich wiederverwendbar, und ist es möglich, den Anonymisierungsalgorithmus und die Fortschrittsbehandlung zu ändern, ohne die Klasse zu modifizieren? Dazu müssen Sie prüfen, ob es irgendwo in der Klasse Code gibt, der von der Implementierung abhängig ist.

Leider finden wir sie. Der Konstruktor wird in die Algorithmus-Implementierung eingebrannt und die Fortschrittsimplementierung wird erstellt. Achten Sie darauf, dies im Code zu überprüfen! neu Wenn Sie den Algorithmus oder den Fortschrittsmodus ändern wollen, müssen Sie den Typ nach dem Operator in diesen Zeilen umschreiben, wodurch sich die Klasse ändert.

Viele halten sie - zu Recht - nicht einmal in ihrer jetzigen Form für eine echte strategiebasierte Lösung. Die vollständige Lösung wird im nächsten Schritt umgesetzt.

8. Megoldás (4-Strategy/Strategy-2-DI)

⚠ Dependency Injection (DI)
Die Lösung heißt Dependency Injection (kurz: DI). Die Idee ist, dass die Klasse selbst ihre Verhaltensabhängigkeiten nicht instanziiert (dies sind Strategieimplementierungen), sondern wir sie von außen an sie weitergeben, z. B. in Konstruktorparametern oder sogar in Form von Eigenschaften oder Setter-Operationen. Das wird natürlich als Schnittstellentyp bezeichnet!

Anonymizer Strukturieren Sie die Klasse entsprechend um, so dass sie die Strategieimplementierungen nicht selbst instanziiert, sondern sie als Konstruktorparameter erhält:

  1. Alle drei Konstruktoren löschen
  2. Betrachten Sie den folgenden Konstruktor:

    public Anonymizer(string inputFileName, IAnonymizerAlgorithm anonymizerAlgorithm, IProgress progress = null)
    {
        ArgumentException.ThrowIfNullOrEmpty(inputFileName);
        ArgumentNullException.ThrowIfNull(anonymizerAlgorithm);
    
        _inputFileName = inputFileName;
        _anonymizerAlgorithm = anonymizerAlgorithm;
        _progress = progress;
    }
    

    progress Wie Sie sehen können, ist der Parameter optional, da der Benutzer der Klasse möglicherweise keine Fortschrittsinformationen sehen möchte.

  3. Da die _progress-Strategie null sein kann, muss bei ihrer Verwendung ein Nulltest eingeführt werden. Der Operator "." wird durch den Operator "?." ersetzt:

    _progress?.Report(persons.Count,i);
    
  4. Anonymizer Jetzt sind wir fertig, die Klasse ist völlig unabhängig von Strategieimplementierungen. Anonymizer Es ist möglich, die Klasse mit einer beliebigen Kombination von Anonymisierungsalgorithmen und einer beliebigen Verlaufsbehandlung zu verwenden (ohne sie zu modifizieren). Anonymizer Program.cs Main Main Lassen Sie uns auch drei mit verschiedenen Kombinationen der Datei in der Funktion erstellen (der vorhandene Code wird zuerst aus der Funktion gelöscht):

    Anonymizer p1 = new("us-500.csv",
        new NameMaskingAnonymizerAlgorithm("***"),
        new SimpleProgress());
    p1.Run();
    
    Console.WriteLine("--------------------");
    
    Anonymizer p2 = new("us-500.csv",
        new NameMaskingAnonymizerAlgorithm("***"),
        new PercentProgress());
    p2.Run();
    
    Console.WriteLine("--------------------");
    
    Anonymizer p3 = new("us-500.csv",
        new AgeAnonymizerAlgorithm(20),
        new SimpleProgress());
    p3.Run();
    
  5. usingUm den Code zum Kochen zu bringen, fügen Sie am Anfang der Datei das Notwendige ein

    using Lab_Extensibility.AnonymizerAlgorithms;
    using Lab_Extensibility.Progresses;
    

Wir haben es getan, und die fertige Lösung ist im Projekt "4-Strategy/Strategy-2-DI" zu finden (wenn Sie irgendwo stecken bleiben oder der Code sich nicht kompilieren lässt, können Sie ihn damit überprüfen).

!!! Program.cs Run Hinweis "Überprüfen der Funktionsweise" In der Übung wird wahrscheinlich keine Zeit dafür sein, aber wenn Sie sich nicht sicher sind, "warum das Strategiemuster funktioniert", warum sich das Verhalten in den vier oben genannten Fällen unterscheidet: Es lohnt sich, in der Datei Haltepunkte für die vier Funktionsaufrufe zu setzen und die Funktionen im Debugger aufzurufen, um zu testen, dass immer die richtige Strategieimplementierung aufgerufen wird.

Main.cdEs gibt ein Klassendiagramm im Projekt ( ), wo Sie auch die fertige Lösung sehen können:

Klassendiagramm der strategiebasierten Lösung

Das folgende UML-Klassendiagramm veranschaulicht unsere strategiebasierte Lösung:

Strategy DI UML osztálydiagram

Bewertung der Lösung

Wir überprüfen die Lösung, um zu sehen, ob sie unsere Ziele erreicht:

  • Anonym Ist eine wiederverwendbare Klasse geworden.
  • IAnonymizerAlgorithmus Wenn in Zukunft eine neue Anonymisierungslogik benötigt wird, muss lediglich eine neue Implementierung eingeführt werden. Es handelt sich nicht um eine Änderung, sondern um eine Verlängerung/Erweiterung.
  • IProgress Wenn in Zukunft eine neue Fortschrittslogik benötigt wird, ist lediglich eine neue Implementierung erforderlich. Es handelt sich nicht um eine Änderung, sondern um eine Verlängerung.
  • Anonymizer In den beiden oben genannten Punkten ist das OPEN/CLOSED-Prinzip erfüllt, d.h. wir können die Logik von anpassen und erweitern, ohne den Code zu verändern.
  • IAnonymizerAlgorithm IProgress Program.cs Hier ist die kombinatorische Explosion, wie sie bei der Schablonenmethode auftritt, nicht zu befürchten: Jede Implementierung kann bequem mit jeder Implementierung verwendet werden, ohne dass neue Klassen für Kombinationen eingeführt werden müssen (wir haben dies in der Datei gesehen).

Zusätzliche Strategievorteile gegenüber der Template-Methode *

  • On the fly-Verhalten kann implementiert werden. Anonymizer SetAnonimizerAlgorithm SetProgress Wenn wir in der Lage sein müssten, das Anonymisierungs- oder Fortschrittsverhalten für ein bestimmtes Objekt nach seiner Erstellung zu ändern, könnten wir das leicht tun (wir müssten nur einen , oder eine Operation einführen, bei der wir die von der Klasse verwendete Strategie auf die im Parameter angegebene Implementierung setzen könnten).
  • Unterstützung der Testbarkeit von Einheiten (wir sehen uns das im Labor nicht an).

2024-05-18 Szerzők