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

28

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?

Damith Ganegoda
la source
5
Quel est le problème avec ce que vous avez actuellement? Les exigences sont-elles susceptibles de changer pour cette fonctionnalité? IE, y aura-t-il probablement de nouvelles ifdéclarations?
Darren Young
Non, je cherche une autre solution pour améliorer le code.
Damith Ganegoda
46
Vous allez à ce sujet à l'envers. Vous ne trouvez pas de modèle, puis écrivez du code pour s'adapter au modèle. Vous écrivez le code pour répondre à vos besoins, puis utilisez éventuellement un modèle pour décrire votre code.
Courses de légèreté avec Monica
1
notez que si vous pensez que votre question est en effet un double de celle-ci , alors en tant que demandeur, vous avez la possibilité de "remplacer" la réouverture récente et de la fermer seule en tant que telle. Je l'ai fait pour certaines de mes propres questions et cela fonctionne comme un charme. Voici comment je l'ai fait, 3 étapes faciles - la seule différence avec mes "instructions" est que puisque vous avez moins de 3K rep, vous devrez passer par la boîte de dialogue des drapeaux pour accéder à l'option "dupliquer"
gnat
8
@LightnessRacesinOrbit: Il y a du vrai dans ce que vous dites, mais il est parfaitement raisonnable de demander s'il existe une meilleure façon de structurer son code, et il est parfaitement raisonnable d'invoquer un modèle de conception pour décrire une meilleure structure proposée. (Pourtant, je conviens que c'est un peu un problème XY de demander un modèle de conception lorsque ce que vous voulez est une conception , qui peut ou non suivre strictement un modèle bien connu.) En outre, il est légitime que les "modèles" affecte légèrement votre code, en ce sens que si vous utilisez un modèle bien connu, il est souvent judicieux de nommer vos composants en conséquence.
ruakh

Réponses:

15

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

  1. faites-le, dans ce cas, 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 ifs 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 le polymorphisme pour obtenir ce que vous voulez, déplacer les décisions (le ifs) de votre logique métier à un lieu où les objets sont instanciés - jusqu'aux usines .

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

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

Juste en regardant 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 d' 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 mot-clé any est là pour le représenter peut être de n'importe quel type, dans un langage comme C # vous pouvez le remplacer par generics ( <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 classmot - clé serait remplacé par interface(si vous traitez avec des langages comme C #, Java et / ou PHP) ou le classmot - clé resterait, mais le Compresset les Encryptméthodes seraient définies comme un pur virtuel , 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 Processméthode.

Dans votre logique métier, où vous traitez avec la décision none / encryption / compression / both, vous allez concevoir votre objet pour qu'il dépende de l' DataProcessinginterface 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 DataServicen'a aucune idée de ce qui sera vraiment fait avec les données lorsqu'elles seront transmises au dataProcessingmembre, et il 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 (en 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 que vous allez besoin et si vous deviez refactoriser les Compressionet les Encryptionclasses et les rendre interfaces à la place, vous devez inclure leurs mises en œuvre aussi bien.

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 saisir la solution d'Ixrec , qui a au moins réussi à éliminer les blocs else ifet else, qui, à mon avis, sont même un peu pire qu'une simple if.

Tenez compte que c'est ma façon de faire une bonne conception 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 variation de façade.

Andy
la source
28
Je n'ai pas downvote mais la raison pourrait être la quantité ridicule de nouvelles classes / interfaces pour faire quelque chose que le code original a fait en 8 lignes (et l'autre réponse l'a fait en 5). À mon avis, la seule chose qu'il accomplit est d'augmenter la courbe d'apprentissage du code.
Maurycy
6
@Maurycy Ce qu'OP a demandé était d'essayer de trouver une solution à son problème en utilisant des modèles de conception communs, si une telle solution existe. Ma solution est-elle plus longue que son code ou celui d'Ixrec? C'est. Je l'admets. Ma solution résout-elle son problème en utilisant des modèles de conception et répond ainsi à sa question tout en supprimant efficacement tous les ifs nécessaires du processus? Cela fait. Ixrec ne le fait pas.
Andy
26
Je crois que l'écriture d'un code clair, fiable, concis, performant et maintenable est la voie à suivre. Si j'avais un dollar pour chaque fois que quelqu'un citait SOLID ou citait un modèle de logiciel sans articuler clairement leurs objectifs et leur justification, je serais un homme riche.
Robert Harvey
12
Je pense que j'ai deux problèmes ici. Premièrement, les interfaces Compressionet Encryptionsemblent totalement superflues. Je ne sais pas si vous suggérez qu'elles sont en quelque sorte nécessaires au processus de décoration, ou si vous sous-entendez simplement qu'elles représentent des concepts extraits. Le deuxième problème est que créer une classe comme CompressionEncryptionDecoratorconduit au même type d'explosion combinatoire que les conditions de l'OP. Je ne vois pas non plus assez clairement le motif du décorateur dans le code suggéré.
cbojar
5
Le débat sur SOLID vs simple est un peu raté: ce code n'est ni l'un ni l'autre, et il n'utilise pas non plus le motif décorateur. Le code n'est pas automatiquement SOLIDE simplement parce qu'il utilise un tas d'interfaces. L'injection de dépendances d'une interface DataProcessing est plutôt sympa; tout le reste est superflu. SOLID est une préoccupation au niveau de l'architecture visant à bien gérer le changement. OP n'a donné aucune information sur son architecture ni sur la façon dont il s'attend à ce que son code change, nous ne pouvons donc pas vraiment discuter de SOLID dans une réponse.
Carl Leth
120

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.

Ixrec
la source
18
@DamithGanegoda Non, si vous lisez attentivement mon code, vous verrez qu'il fait exactement la même chose dans ce cas. C'est pourquoi il n'y a pas elseentre mes deux déclarations if et pourquoi j'attribue à datachaque fois. Si les deux indicateurs sont vrais, alors compress () est exécuté, puis encrypt () est exécuté sur le résultat de compress (), comme vous le souhaitez.
Ixrec
14
@DavidPacker Techniquement, il en va de même pour chaque instruction if dans chaque langage de programmation. Je suis allé pour la simplicité, car cela ressemblait à un problème où une réponse très simple était appropriée. Votre solution est également valable, mais personnellement, je la garderais pour quand j'aurais plus de deux drapeaux booléens à craindre.
Ixrec
15
@DavidPacker: correct n'est pas défini par la façon dont le code adhère à certaines directives d'un auteur sur une idéologie de programmation. Correct est "le code fait-il ce qu'il est censé faire et a-t-il été implémenté dans un délai raisonnable". S'il est logique de le faire "dans le mauvais sens", alors le mauvais sens est le bon, car le temps c'est de l'argent.
whatsisname
9
@DavidPacker: Si j'étais dans la position de l'OP et que je posais cette question, Lightness Race dans le commentaire d'Orbit est ce dont j'ai vraiment besoin. «Trouver une solution en utilisant des modèles de conception» commence déjà du mauvais pied.
whatsisname
6
@DavidPacker En fait, si vous lisez la question de plus près, il n'insiste pas sur un modèle. Il dit: "Je pense au modèle Décorateur. Est-ce le bon choix, ou y a-t-il peut-être une meilleure alternative?" . Vous avez abordé la première phrase de ma citation, mais pas la seconde. D'autres personnes ont adopté l'approche que non, ce n'est pas le bon choix. Vous ne pouvez alors pas prétendre que seul le vôtre répond à la question.
Jon Bentley
12

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 exagération pour un problème aussi trivial que celui que vous proposez, mais à des fins d'illustration, il va ici:

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 cliccas 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.

Tulains Córdova
la source
Sauf si vous pouvez fournir un exemple de "une sorte d'usine qui rassemble la liste des commandes" sans code qui ressemble essentiellement à la réponse d'Ixrec, alors IMO cela ne répond pas à la question. Cela fournit un meilleur moyen d'implémenter les fonctions de compression et de cryptage, mais pas comment éviter les drapeaux.
thexacre le
@thexacre J'ai ajouté un exemple.
Tulains Córdova
Donc, dans votre écouteur d'événements de case à cocher, vous avez "si checkbox.ticked alors ajoutez la commande"? Il me semble que vous êtes en train de mélanger le drapeau si des déclarations
circulent
@thexacre Non, un auditeur pour chaque case à cocher. Dans l'événement de clic juste commands.add(new EncryptCommand()); ou commands.add(new CompressCommand());respectivement.
Tulains Córdova
Que diriez-vous de décocher la case? Dans à peu près toutes les boîtes à outils de langue / interface utilisateur que j'ai rencontrées, vous devrez toujours vérifier l'état de la case à cocher dans l'écouteur d'événements. Je suis d'accord pour dire que c'est un meilleur schéma, mais cela n'évite pas d'avoir à la base si le drapeau fait quelque chose quelque part.
thexacre du
7

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. Je pourrais peut-être un exemple C # plus tard, mais ce n'est pas aussi agréable. Ma façon de résoudre ce problème serait les étapes suivantes avec quelques explications pour les non-clojuriens:

1. Représentez 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"

Ainsi, l'écriture (transformations :encrypt)ou (:encrypt transformations)retournerait la fonction de cryptage. ((fn [data] ... ) est juste 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

3. 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
NiklasJ
la source
Comment les fonctions sont-elles remplies pour n'inclure que les fonctions qui doivent être appliquées sans utiliser essentiellement une série d'instructions if d'une manière ou d'une autre?
thexacre le
La ligne funcs-to-run-here (map options funcs)effectue le filtrage, choisissant ainsi un ensemble de fonctions à appliquer. Je devrais peut-être mettre à jour la réponse et entrer dans un peu plus de détails.
NiklasJ
5

[Essentiellement, ma réponse fait suite à la réponse de @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, le 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 indiqué 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 reporter 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.)

Nick Alexeev
la source
1

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 une composition fonctionnelle (ligne 3)

Sachin K
la source
Pourquoi est-ce meilleur (ou pire) que l'approche du PO? Et / ou que pensez-vous de l'idée du PO d'utiliser un motif de décoration?
Kasper van den Berg
cet extrait de code est meilleur et est explicite sur l'ordre (compression avant chiffrement); évite les interfaces indésirables
Rag