web-dev-qa-db-fra.com

Existe-t-il un modèle de conception pour supprimer la nécessité de vérifier les drapeaux?

Je vais enregistrer une charge utile de chaîne dans la base de données. J'ai deux configurations globales:

  • chiffrement
  • compression

Ceux-ci peuvent être activés ou désactivés à l'aide de la configuration de manière à ce que l'un d'eux soit activé, les deux soient activés ou les deux soient désactivés.

Mon implémentation actuelle est la suivante:

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

Je pense au motif Décorateur. Est-ce le bon choix ou existe-t-il peut-être une meilleure alternative?

28
Damith Ganegoda

Lors de la conception du code, vous avez toujours deux options.

  1. faites-le, dans ce cas, à peu près n'importe quelle solution fonctionnera pour vous
  2. être pédant et concevoir une solution qui exploite les caprices de la langue et son idéologie (langues OO dans ce cas - l'utilisation du polymorphisme comme moyen de prendre la décision)

Je ne vais pas me concentrer sur le premier des deux, car il n'y a vraiment rien à dire. Si vous vouliez simplement le faire fonctionner, vous pouvez laisser le code tel quel.

Mais que se passerait-il si vous choisissiez de le faire de manière pédante et résolviez réellement le problème des modèles de conception, comme vous le vouliez?

Vous pourriez envisager le processus suivant:

Lors de la conception du code OO, la plupart des if qui se trouvent dans un code n'ont pas à être là. Naturellement, si vous souhaitez comparer deux types scalaires, tels que ints ou floats, vous êtes susceptible d'avoir un if, mais si vous souhaitez modifier les procédures en fonction de la configuration , vous pouvez utiliser polymorphisme pour obtenir ce que vous voulez, déplacer les décisions (les ifs) de votre logique métier vers un endroit où les objets sont instanciés - vers sines .

À partir de maintenant, votre processus peut passer par 4 chemins distincts:

  1. data n'est ni chiffré ni compressé (appelez rien, retournez data)
  2. data est compressé (appelez compress(data) et retournez-le)
  3. data est crypté (appelez encrypt(data) et retournez-le)
  4. data est compressé et chiffré (appelez encrypt(compress(data)) et retournez-le)

En regardant simplement les 4 chemins, vous trouvez un problème.

Vous avez un processus qui appelle 3 (théoriquement 4, si vous comptez ne rien appeler comme un) différentes méthodes qui manipulent les données, puis les renvoient. Les méthodes ont des noms différents , différentes soi-disant API publiques (la façon dont les méthodes communiquent leur comportement).

En utilisant le modèle adaptateur , nous pouvons résoudre la colision de nom (nous pouvons unifier l'API publique) qui s'est produite. En termes simples, l'adaptateur permet à deux interfaces incompatibles de fonctionner ensemble. De plus, l'adaptateur fonctionne en définissant une nouvelle interface d'adaptateur, que les classes essayant d'unir leur implémentation d'API.

Ce n'est pas un langage concret. C'est une approche générique, le tout mot-clé est là pour le représenter peut être de tout type, dans un langage comme C # vous pouvez le remplacer par des génériques ( <T>).

Je vais supposer qu'en ce moment, vous pouvez avoir deux classes responsables de la compression et du chiffrement.

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

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

Dans un monde d'entreprise, même ces classes spécifiques sont très susceptibles d'être remplacées par des interfaces, telles que le mot clé class serait remplacé par interface (si vous avez affaire à des langages comme C #, Java et/ou PHP) ou le mot clé class resterait, mais les méthodes Compress et Encrypt seraient définies comme --- pure virtual , si vous codez en C++.

Pour faire un adaptateur, nous définissons une interface commune.

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

Ensuite, nous devons fournir des implémentations de l'interface pour la rendre utile.

// 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)
        );
    }
}

En faisant cela, vous vous retrouvez avec 4 classes, chacune faisant quelque chose de complètement différent, mais chacune fournissant la même API publique. La méthode Process.

Dans votre logique métier, où vous traitez la décision none/encryption/compression/both, vous allez concevoir votre objet pour qu'il dépende de l'interface DataProcessing que nous avons conçue auparavant.

class DataService
{
    private dataProcessing : DataProcessing;

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

Le processus lui-même pourrait alors être aussi simple que cela:

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

    // ... perhaps work with the data

    return data;
}

Plus de conditionnels. La classe DataService n'a aucune idée de ce qui sera vraiment fait avec les données quand elles seront passées au membre dataProcessing, et elle s'en fiche vraiment, ce n'est pas sa responsabilité.

Idéalement, vous auriez des tests unitaires testant les 4 classes d'adaptateurs que vous avez créées pour vous assurer qu'elles fonctionnent, vous réussissez votre test. Et s'ils réussissent, vous pouvez être sûr qu'ils fonctionneront, peu importe où vous les appelez dans votre code.

Donc, en procédant de cette façon, je n'aurai plus de ifs dans mon code?

Non. Vous êtes moins susceptible d'avoir des conditions dans votre logique métier, mais elles doivent toujours être quelque part. L'endroit est vos usines.

Et c'est bien. Vous séparez les préoccupations de la création et de l'utilisation réelle du code. Si vous rendez vos usines fiables (dans Java vous pourriez même aller jusqu'à utiliser quelque chose comme le framework Guice de Google), dans votre logique métier, vous n'êtes pas inquiet de choisir la bonne classe à injecter. Parce que vous savez que vos usines fonctionnent et livreront ce qui vous est demandé.

Est-il nécessaire d'avoir toutes ces classes, interfaces, etc.?

Cela nous ramène au début.

Dans la POO, si vous choisissez le chemin pour utiliser le polymorphisme, voulez vraiment utiliser des modèles de conception, voulez exploiter les fonctionnalités du langage et/ou voulez suivre le tout est une idéologie d'objet, alors c'est le cas. Et même alors, cet exemple ne montre même pas toutes les usines dont vous aurez besoin et si vous deviez refactoriser les classes Compression et Encryption et en faire des interfaces à la place, vous devez inclure leur implémentations également.

En fin de compte, vous vous retrouvez avec des centaines de petites classes et interfaces, axées sur des choses très spécifiques. Ce qui n'est pas nécessairement mauvais, mais pourrait ne pas être la meilleure solution pour vous si tout ce que vous voulez est de faire quelque chose d'aussi simple que d'ajouter deux nombres.

Si vous voulez le faire et rapidement, vous pouvez récupérer la solution d'Ixrec , qui a au moins réussi à éliminer les blocs else if Et else, qui, à mon avis , sont même un peu pires qu'une simple if.

Prenez en considération que c'est ma façon de faire un bon design OO. Coder sur des interfaces plutôt que sur des implémentations, c'est ainsi que je l'ai fait ces dernières années et c'est l'approche avec laquelle je suis le plus à l'aise.

Personnellement, j'aime davantage la programmation if-less et j'apprécierais beaucoup plus la solution plus longue sur les 5 lignes de code. C'est la façon dont j'ai l'habitude de concevoir du code et je suis très à l'aise de le lire.


Mise à jour 2: Il y a eu une discussion folle sur la première version de ma solution. Discussion principalement provoquée par moi, pour laquelle je m'excuse.

J'ai décidé de modifier la réponse de manière à ce que ce soit l'une des façons de regarder la solution mais pas la seule. J'ai également supprimé la partie décoratrice, où je voulais plutôt la façade, que j'ai finalement décidé de laisser de côté, car un adaptateur est une variante de la façade.

16
Andy

Le seul problème que je vois avec votre code actuel est le risque d'explosion combinatoire lorsque vous ajoutez plus de paramètres, ce qui peut être facilement atténué en structurant le code plus comme ceci:

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

Je ne connais aucun "modèle de conception" ou "idiome" dont cela pourrait être considéré comme un exemple.

118
Ixrec

Je suppose que votre question ne cherche pas à être pratique, auquel cas la réponse de lxrec est la bonne, mais à en apprendre davantage sur les modèles de conception.

Évidemment, le modèle de commande est une surpuissance pour un problème aussi trivial que celui que vous proposez, mais à des fins d'illustration, voici:

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
    }
}

Comme vous le voyez, mettre les commandes/transformation dans une liste permet de les exécuter séquentiellement. De toute évidence, il exécutera les deux, ou un seul d'entre eux dépendra de ce que vous mettez dans la liste sans conditions if.

Évidemment, les conditions vont se retrouver dans une sorte d'usine qui rassemble la liste des commandes.

EDIT pour le commentaire de @ texacre:

Il existe de nombreuses façons d'éviter les conditions if dans la partie création de la solution, prenons par exemple une application GUI de bureau . Vous pouvez avoir des cases à cocher pour les options de compression et de cryptage. Dans le on clic événement de ces cases à cocher vous instanciez la commande correspondante et l'ajoutez à la liste, ou supprimez de la liste si vous désélectionnez l'option.

12
Tulains Córdova

Je pense que les "modèles de conception" sont inutilement orientés vers les "modèles oo" et évitent complètement les idées beaucoup plus simples. Nous parlons ici d'un pipeline de données (simple).

J'essaierais de le faire en clojure. Tout autre langage où les fonctions sont de première classe est probablement correct également. Peut-être que je pourrais un exemple C # plus tard, mais ce n'est pas aussi sympa. Ma façon de résoudre ce problème serait les étapes suivantes avec quelques explications pour les non-clojuriens:

1. Représente un ensemble de transformations.

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

Il s'agit d'une carte, c'est-à-dire d'une table de recherche/dictionnaire/autre, des mots-clés aux fonctions. Un autre exemple (des mots-clés aux chaînes):

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

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

Donc, en écrivant (transformations :encrypt) ou (:encrypt transformations) retournerait la fonction de cryptage. ((fn [data] ... ) n'est qu'une fonction lambda.)

2. Obtenez les options sous forme de séquence de mots-clés:

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

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

. Filtrez toutes les transformations à l'aide des options fournies.

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

Exemple:

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

4. Combinez les fonctions en une seule:

(apply comp transformations-to-run)

Exemple:

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

5. Et puis ensemble:

(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])

Le SEUL changement si nous voulons ajouter une nouvelle fonction, disons "debug-print", est le suivant:

(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

[Essentiellement, ma réponse est une suite à réponse par @Ixrec ci-dessus . ]

Une question importante: le nombre de combinaisons distinctes que vous devez couvrir va-t-il augmenter? Vous connaissez mieux votre domaine. C'est votre jugement.
Le nombre de variantes peut-il éventuellement augmenter? Eh bien, ce n'est pas inconcevable. Par exemple, vous devrez peut-être prendre en charge des algorithmes de chiffrement plus différents.

Si vous prévoyez que le nombre de combinaisons distinctes va augmenter, alors Modèle de stratégie peut vous aider. Il est conçu pour encapsuler des algorithmes et fournir une interface interchangeable avec le code appelant. Vous auriez toujours une petite quantité de logique lorsque vous créez (instanciez) la stratégie appropriée pour chaque chaîne particulière.

Vous avez commenté ci-dessus que vous ne vous attendez pas à ce que les exigences changent. Si vous ne vous attendez pas à ce que le nombre de variantes augmente (ou si vous pouvez différer ce refactoring), gardez la logique telle qu'elle est. Actuellement, vous disposez d'une petite quantité de logique gérable. (Peut-être mettre une note dans les commentaires sur une éventuelle refactorisation d'un modèle de stratégie.)

5
Nick Alexeev

Une façon de le faire dans scala serait:

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)

L'utilisation d'un modèle de décorateur pour atteindre les objectifs ci-dessus (séparation de la logique de traitement individuelle et de la façon dont ils sont connectés) serait trop verbeuse.

Dans lequel vous auriez besoin d'un modèle de conception pour atteindre ces objectifs de conception dans un paradigme de programmation OO, le langage fonctionnel offre un support natif en utilisant des fonctions de citoyens de première classe (lignes 1 et 2 dans le code) et fonctionnelles composition (ligne 3)

1
Sachin K