it-swarm.com.de

Gibt es ein Entwurfsmuster, um die Notwendigkeit zu beseitigen, nach Flags zu suchen?

Ich werde einige String-Nutzdaten in der Datenbank speichern. Ich habe zwei globale Konfigurationen:

  • verschlüsselung
  • kompression

Diese können mithilfe der Konfiguration so aktiviert oder deaktiviert werden, dass entweder nur einer von ihnen aktiviert ist, beide aktiviert sind oder beide deaktiviert sind.

Meine aktuelle Implementierung lautet wie folgt:

if (encryptionEnable && !compressEnable) {
    encrypt(data);
} else if (!encryptionEnable && compressEnable) {
    compress(data);
} else if (encryptionEnable && compressEnable) {
    encrypt(compress(data));
} else {
  data;
}

Ich denke über das Dekorationsmuster nach. Ist es die richtige Wahl oder gibt es vielleicht eine bessere Alternative?

28
Damith Ganegoda

Beim Entwerfen von Code haben Sie immer zwei Möglichkeiten.

  1. machen Sie es einfach, in diesem Fall funktioniert so ziemlich jede Lösung für Sie
  2. seien Sie pedantisch und entwerfen Sie eine Lösung, die die Macken der Sprache und ihrer Ideologie ausnutzt (in diesem Fall OO-Sprachen - die Verwendung von Polymorphismus als Mittel zur Entscheidungsfindung).

Ich werde mich nicht auf den ersten der beiden konzentrieren, weil wirklich nichts zu sagen ist. Wenn Sie es nur zum Laufen bringen möchten, können Sie den Code so lassen, wie er ist.

Aber was würde passieren, wenn Sie es auf pedantische Weise tun und das Problem mit den Entwurfsmustern tatsächlich so lösen würden, wie Sie es wollten?

Sie könnten sich den folgenden Prozess ansehen:

Beim Entwerfen von OO - Code müssen die meisten ifs, die sich in einem Code befinden, nicht vorhanden sein. Wenn Sie zwei Skalartypen vergleichen möchten, z. B. ints oder floats, haben Sie wahrscheinlich ein if, aber wenn Sie die Prozeduren basierend auf der Konfiguration ändern möchten Sie können Polymorphismus verwenden, um das zu erreichen, was Sie möchten. Verschieben Sie die Entscheidungen (die ifs) von Ihrer Geschäftslogik an einen Ort, an dem Objekte instanziiert werden - nach Fabriken .

Ab sofort kann Ihr Prozess 4 verschiedene Pfade durchlaufen:

  1. data ist weder verschlüsselt noch komprimiert (nichts aufrufen, data zurückgeben)
  2. data ist komprimiert (rufen Sie compress(data) auf und geben Sie es zurück)
  3. data ist verschlüsselt (rufen Sie encrypt(data) auf und geben Sie es zurück)
  4. data wird komprimiert und verschlüsselt (rufen Sie encrypt(compress(data)) auf und geben Sie es zurück)

Wenn Sie sich nur die 4 Pfade ansehen, finden Sie ein Problem.

Sie haben einen Prozess, der 3 (theoretisch 4, wenn Sie nichts als einen aufrufen) verschiedene Methoden aufruft, die die Daten manipulieren und dann zurückgeben. Die Methoden haben unterschiedliche Namen , unterschiedliche sogenannte öffentliche API (die Art und Weise, wie die Methoden ihr Verhalten kommunizieren).

Mit dem Adaptermuster können wir die aufgetretene Namenskollision (wir können die öffentliche API vereinen) lösen. Einfach gesagt, Adapter hilft zwei inkompatiblen Schnittstellen zusammenzuarbeiten. Außerdem definiert der Adapter eine neue Adapterschnittstelle, über die Klassen versuchen, ihre API-Implementierung zu vereinen.

Dies ist keine konkrete Sprache. Es ist ein generischer Ansatz. Das Schlüsselwort any steht für einen beliebigen Typ. In einer Sprache wie C # können Sie es durch generische Wörter ersetzen ( <T>).

Ich gehe davon aus, dass Sie derzeit zwei Klassen haben können, die für die Komprimierung und Verschlüsselung verantwortlich sind.

class Compression
{
    Compress(data : any) : any { ... }
}

class Encryption
{
    Encrypt(data : any) : any { ... }
}

In einer Unternehmenswelt werden selbst diese spezifischen Klassen sehr wahrscheinlich durch Schnittstellen ersetzt, z. B. wird das Schlüsselwort class durch interface ersetzt (sollten Sie mit Sprachen wie C #, Java und/oder PHP) oder das Schlüsselwort class würden bleiben, aber die Methoden Compress und Encrypt würden als rein virtuell definiert , sollten Sie in C++ codieren.

Um einen Adapter herzustellen, definieren wir eine gemeinsame Schnittstelle.

interface DataProcessing
{
    Process(data : any) : any;
}

Dann müssen wir Implementierungen der Schnittstelle bereitstellen, um sie nützlich zu machen.

// when neither encryption nor compression is enabled
class DoNothingAdapter : DataProcessing
{
    public Process(data : any) : any
    {
        return data;
    }
}

// when only compression is enabled
class CompressionAdapter : DataProcessing
{
    private compression : Compression;

    public Process(data : any) : any
    {
        return this.compression.Compress(data);
    }
}

// when only encryption is enabled
class EncryptionAdapter : DataProcessing
{
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(data);
    }
}

// when both, compression and encryption are enabled
class CompressionEncryptionAdapter : DataProcessing
{
    private compression : Compression;
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(
            this.compression.Compress(data)
        );
    }
}

Auf diese Weise erhalten Sie 4 Klassen, von denen jede etwas völlig anderes tut, von denen jedoch jede dieselbe öffentliche API bereitstellt. Die Process Methode.

In Ihrer Geschäftslogik, in der Sie sich mit der Entscheidung Keine/Verschlüsselung/Komprimierung/Beide befassen, entwerfen Sie Ihr Objekt so, dass es von der zuvor entworfenen DataProcessing -Schnittstelle abhängt.

class DataService
{
    private dataProcessing : DataProcessing;

    public DataService(dataProcessing : DataProcessing)
    {
        this.dataProcessing = dataProcessing;
    }
}

Der Prozess selbst könnte dann so einfach sein:

public ComplicatedProcess(data : any) : any
{
    data = this.dataProcessing.Process(data);

    // ... perhaps work with the data

    return data;
}

Keine Bedingungen mehr. Die Klasse DataService hat keine Ahnung, was wirklich mit den Daten gemacht wird, wenn sie an das Mitglied dataProcessing übergeben werden, und sie kümmert sich nicht wirklich darum, es liegt nicht in ihrer Verantwortung.

Im Idealfall würden Sie Unit-Tests durchführen lassen, um die 4 von Ihnen erstellten Adapterklassen zu testen, um sicherzustellen, dass sie funktionieren. Sie bestehen Ihren Test. Und wenn sie bestehen, können Sie ziemlich sicher sein, dass sie funktionieren, egal wo Sie sie in Ihrem Code aufrufen.

Wenn ich es so mache, werde ich nie mehr ifs in meinem Code haben?

Nein. Es ist weniger wahrscheinlich, dass Sie Bedingungen in Ihrer Geschäftslogik haben, aber sie müssen immer noch irgendwo sein. Der Ort sind Ihre Fabriken.

Und das ist gut so. Sie trennen die Belange der Erstellung und der tatsächlichen Verwendung des Codes. Wenn Sie Ihre Fabriken zuverlässig machen (in Java könnten Sie sogar so weit gehen, das Guice Framework von Google zu verwenden), sind Sie in Ihrer Geschäftslogik nicht besorgt über die Wahl der richtigen Klasse für die Injektion. Weil Sie wissen, dass Ihre Fabriken funktionieren und liefern, was gefragt wird.

Ist es notwendig, alle diese Klassen, Schnittstellen usw. zu haben?

Dies bringt uns zurück zum Anfang.

Wenn Sie in OOP den Pfad für die Verwendung von Polymorphismus auswählen, wirklich Entwurfsmuster verwenden möchten, die Merkmale der Sprache nutzen möchten und/oder dem Objekt folgen möchten, ist dies eine Objektideologie. Und selbst dann zeigt dieses Beispiel nicht einmal alle Fabriken, die Sie benötigen, und wenn Sie die Klassen Compression und Encryption umgestalten und stattdessen zu Schnittstellen machen möchten, müssen Sie deren einschließen Implementierungen auch.

Am Ende stehen Ihnen Hunderte kleiner Klassen und Schnittstellen zur Verfügung, die sich auf ganz bestimmte Dinge konzentrieren. Das ist nicht unbedingt schlecht, aber möglicherweise nicht die beste Lösung für Sie, wenn Sie nur etwas so Einfaches wie das Hinzufügen von zwei Zahlen tun möchten.

Wenn Sie es schnell erledigen möchten, können Sie sich die Lösung von Ixrec schnappen, die es zumindest geschafft hat, die Blöcke else if Und else zu entfernen. die meiner Meinung nach sogar ein bisschen schlimmer sind als ein einfaches if.

Bedenken Sie, dass dies meine Art ist, gutes OO Design zu machen. So habe ich es in den letzten Jahren gemacht, um Schnittstellen und nicht Implementierungen zu codieren, und es ist der Ansatz, mit dem ich mich am wohlsten fühle.

Ich persönlich mag die Wenn-weniger-Programmierung mehr und würde die längere Lösung über die 5 Codezeilen viel mehr schätzen. Auf diese Weise bin ich es gewohnt, Code zu entwerfen, und ich bin sehr zufrieden damit, ihn zu lesen.


Update 2: Es gab eine wilde Diskussion über die erste Version meiner Lösung. Diskussion hauptsächlich von mir verursacht, für die ich mich entschuldige.

Ich habe beschlossen, die Antwort so zu bearbeiten, dass dies eine der Möglichkeiten ist, die Lösung zu betrachten, aber nicht die einzige. Ich habe auch den Dekorateurteil entfernt, wo ich stattdessen Fassade meinte, was ich am Ende komplett weggelassen habe, weil ein Adapter eine Fassadenvariante ist.

16
Andy

Das einzige Problem, das ich bei Ihrem aktuellen Code sehe, ist das Risiko einer kombinatorischen Explosion, wenn Sie weitere Einstellungen hinzufügen, die leicht verringert werden können, indem Sie den Code folgendermaßen strukturieren:

if(compressEnable){
  data = compress(data);
}
if(encryptionEnable) {
  data = encrypt(data);
}
return data;

Mir ist kein "Designmuster" oder "Idiom" bekannt, für das dies als Beispiel angesehen werden könnte.

118
Ixrec

Ich denke, Ihre Frage sucht nicht nach Praktikabilität. In diesem Fall ist die Antwort von lxrec die richtige, sondern um etwas über Designmuster zu lernen.

Offensichtlich ist das Befehlsmuster ein Overkill für ein so triviales Problem wie das, das Sie vorschlagen, aber zur Veranschaulichung hier geht es:

public interface Command {
    public String transform(String s);
}

public class CompressCommand implements Command {
    @Override
    public String transform(String s) {
        String compressedString=null;
        //Compression code here
        return compressedString;
    }
}

public class EncryptCommand implements Command {
    @Override
    public String transform(String s) {
        String EncrytedString=null;
        // Encryption code goes here
        return null;
    }

}

public class Test {
    public static void main(String[] args) {
        List<Command> commands = new ArrayList<Command>();
        commands.add(new CompressCommand());
        commands.add(new EncryptCommand()); 
        String myString="Test String";
        for (Command c: commands){
            myString = c.transform(myString);
        }
        // now myString can be stored in the database
    }
}

Wie Sie sehen, können die Befehle/Transformationen in einer Liste nacheinander ausgeführt werden. Offensichtlich werden beide ausgeführt, oder nur einer von ihnen hängt davon ab, was Sie ohne if-Bedingungen in die Liste aufgenommen haben.

Offensichtlich werden die Bedingungen in einer Art Fabrik enden, die die Befehlsliste zusammenstellt.

BEARBEITEN Sie den Kommentar von @ texacre:

Es gibt viele Möglichkeiten, die if-Bedingungen im Erstellungsteil der Lösung zu vermeiden. Nehmen wir zum Beispiel eine Desktop-GUI-App . Sie können Kontrollkästchen für die Komprimierungs- und Verschlüsselungsoptionen einrichten. In dem on clic Bei diesen Kontrollkästchen instanziieren Sie den entsprechenden Befehl und fügen ihn der Liste hinzu oder entfernen ihn aus der Liste, wenn Sie die Option deaktivieren.

12

Ich denke, "Design Patterns" sind unnötigerweise auf "Oo Patterns" ausgerichtet und vermeiden viel einfachere Ideen. Wir sprechen hier von einer (einfachen) Datenpipeline.

Ich würde versuchen, es in Clojure zu tun. Jede andere Sprache, in der Funktionen erstklassig sind, ist wahrscheinlich ebenfalls in Ordnung. Vielleicht könnte ich später ein C # -Beispiel geben, aber es ist nicht so schön. Meine Art, dies zu lösen, wären die folgenden Schritte mit einigen Erklärungen für Nicht-Clojurianer:

1. Stellen Sie eine Reihe von Transformationen dar.

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

Dies ist eine Karte, d. H. Eine Nachschlagetabelle/ein Wörterbuch/was auch immer, von Schlüsselwörtern zu Funktionen. Ein weiteres Beispiel (Schlüsselwörter für Zeichenfolgen):

(def employees { :A1 "Alice" 
                 :X9 "Bob"})

(employees :A1) ; => "Alice"
(:A1 employees) ; => "Alice"

Also schreibe (transformations :encrypt) oder (:encrypt transformations) würde die Verschlüsselungsfunktion zurückgeben. ((fn [data] ... ) ist nur eine Lambda-Funktion.)

2. Holen Sie sich die Optionen als Folge von Schlüsselwörtern :

(defn do-processing [options data] ;function definition
  ...)

(do-processing [:encrypt :compress] data) ;call to function

. Filtern Sie alle Transformationen mit den mitgelieferten Optionen.

(let [ transformations-to-run (map transformations options)] ... )

Beispiel:

(map employees [:A1]) ; => ["Alice"]
(map employees [:A1 :X9]) ; => ["Alice", "Bob"]

4. Kombiniere Funktionen zu einer :

(apply comp transformations-to-run)

Beispiel:

(comp f g h) ;=> f(g(h()))
(apply comp [f g h]) ;=> f(g(h()))

5. Und dann zusammen :

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress])

Die EINZIGE Änderung, wenn wir eine neue Funktion hinzufügen möchten, z. B. "Debug-Print", ist die folgende:

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )
                       :debug-print (fn [data] ...) }) ;<--- here to add as option

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress :debug-print]) ;<-- here to use it
(do-processing [:compress :debug-print]) ;or like this
(do-processing [:encrypt]) ;or like this
7
NiklasJ

[Im Wesentlichen ist meine Antwort eine Fortsetzung des Antwort von @Ixrec oben . ]]

Eine wichtige Frage: Wird die Anzahl der unterschiedlichen Kombinationen, die Sie abdecken müssen, zunehmen? Sie kennen Ihre Fachdomäne besser. Dies ist Ihr Urteil.
Kann die Anzahl der Varianten möglicherweise zunehmen? Das ist nicht unvorstellbar. Beispielsweise müssen Sie möglicherweise mehr unterschiedliche Verschlüsselungsalgorithmen berücksichtigen.

Wenn Sie davon ausgehen, dass die Anzahl der unterschiedlichen Kombinationen zunehmen wird, kann Strategiemuster Ihnen helfen. Es wurde entwickelt, um Algorithmen zu kapseln und eine austauschbare Schnittstelle zum aufrufenden Code bereitzustellen. Sie hätten immer noch ein wenig Logik, wenn Sie die entsprechende Strategie für jede bestimmte Zeichenfolge erstellen (instanziieren).

Sie haben oben kommentiert dass Sie nicht erwarten, dass sich die Anforderungen ändern. Wenn Sie nicht erwarten, dass die Anzahl der Varianten zunimmt (oder wenn Sie dieses Refactoring verschieben können), behalten Sie die Logik bei. Derzeit verfügen Sie über eine kleine und überschaubare Menge an Logik. (Vielleicht machen Sie sich in den Kommentaren eine Notiz über eine mögliche Umgestaltung eines Strategiemusters.)

5
Nick Alexeev

Eine Möglichkeit, dies in scala] zu tun, wäre:

val handleCompression: AnyRef => AnyRef = data => if (compressEnable) compress(data) else data
val handleEncryption: AnyRef => AnyRef = data => if (encryptionEnable) encrypt(data) else data
val handleData = handleCompression andThen handleEncryption
handleData(data)

Die Verwendung eines Dekorationsmusters zur Erreichung der oben genannten Ziele (Trennung der einzelnen Verarbeitungslogik und deren Verkabelung) wäre zu ausführlich.

Wobei Sie ein Entwurfsmuster benötigen würden, um diese Entwurfsziele in einem OO Programmierparadigma) zu erreichen, bietet die funktionale Sprache native Unterstützung, indem Funktionen als erstklassige Bürger (Zeile 1 und 2 im Code) und funktional verwendet werden Zusammensetzung (Zeile 3)

1
Sachin K