Esiste un modello di progettazione per rimuovere la necessità di verificare la presenza di bandiere?


28

Ho intenzione di salvare un po 'di payload di stringa nel database. Ho due configurazioni globali:

  • crittografia
  • compressione

Questi possono essere abilitati o disabilitati usando la configurazione in modo che sia abilitato solo uno di essi, entrambi siano abilitati o entrambi siano disabilitati.

La mia attuale implementazione è questa:

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

Sto pensando al modello Decorator. È la scelta giusta o c'è forse un'alternativa migliore?


5
Cosa c'è che non va in quello che hai attualmente? È probabile che i requisiti cambino per questa funzionalità? IE, ci sono probabilmente nuove ifdichiarazioni?
Darren Young,

No, sto cercando un'altra soluzione per migliorare il codice.
Damith Ganegoda,

46
Stai andando al contrario. Non trovi uno schema quindi scrivi il codice per adattarlo allo schema. Scrivi il codice per soddisfare le tue esigenze, quindi opzionalmente usa un modello per descrivere il tuo codice.
Lightness Races con Monica

1
nota se ritieni che la tua domanda sia effettivamente un duplicato di questa , quindi come richiedente hai un'opzione per "scavalcare" la riapertura recente e chiuderla da sola come tale. L'ho fatto per alcune delle mie domande e funziona come un incantesimo. Ecco come l'ho fatto, 3 semplici passaggi - l'unica differenza con le mie "istruzioni" è che dato che hai meno di 3K rep, dovrai passare attraverso la finestra di dialogo flag per arrivare all'opzione "duplicata"
gnat

8
@LightnessRacesinOrbit: c'è un po 'di verità in quello che dici, ma è perfettamente ragionevole chiedere se c'è un modo migliore per strutturare il proprio codice, ed è perfettamente ragionevole invocare un modello di progettazione per descrivere una struttura migliore proposta. (Tuttavia, concordo sul fatto che sia un po 'un problema XY richiedere un modello di progettazione quando ciò che si desidera è un progetto , che può o meno seguire rigorosamente qualsiasi modello noto.) Inoltre, è legittimo che i "modelli" influisce leggermente sul codice, in quanto se si utilizza un modello noto, spesso ha senso nominare i componenti di conseguenza.
Ruakh

Risposte:


15

Quando si progetta il codice, sono sempre disponibili due opzioni.

  1. fallo, nel qual caso praticamente qualsiasi soluzione funzionerà per te
  2. essere pedanti e progettare una soluzione che sfrutti le stranezze della lingua e la sua ideologia (le lingue OO in questo caso - l'uso del polimorfismo come mezzo per fornire la decisione)

Non mi concentrerò sul primo dei due, perché non c'è davvero nulla da dire. Se volessi semplicemente farlo funzionare, potresti lasciare il codice così com'è.

Ma cosa succederebbe se scegliessi di farlo in modo pedante e risolvi effettivamente il problema con i modelli di design, nel modo in cui lo volevi?

Potresti guardare il seguente processo:

Quando si progetta il codice OO, la maggior parte degli ifs che si trovano in un codice non devono essere presenti. Naturalmente, se si desidera confrontare due tipi scalari, come ints o floats, è probabile che si abbia un if, ma se si desidera modificare le procedure in base alla configurazione, è possibile utilizzare il polimorfismo per ottenere ciò che si desidera, spostare le decisioni ifs) dalla tua logica aziendale a un luogo, dove gli oggetti sono istanziati - alle fabbriche .

A partire da ora, il tuo processo può passare attraverso 4 percorsi separati:

  1. datanon è né crittografato né compresso (non chiamare nulla, restituire data)
  2. dataè compresso (chiama compress(data)e restituiscilo)
  3. dataè crittografato (chiamalo encrypt(data)e restituiscilo)
  4. dataè compresso e crittografato (chiamalo encrypt(compress(data))e restituiscilo)

Solo guardando i 4 percorsi, trovi un problema.

Hai un processo che chiama 3 (teoricamente 4, se conti di non chiamare nulla come uno) metodi diversi che manipolano i dati e poi li restituiscono. I metodi hanno nomi diversi , differenti cosiddette API pubbliche (il modo in cui i metodi comunicano il loro comportamento).

Usando il modello dell'adattatore , possiamo risolvere la colisione del nome (possiamo unire l'API pubblica) che si è verificata. Detto semplicemente, l'adattatore aiuta due interfacce incompatibili a lavorare insieme. Inoltre, l'adattatore funziona definendo una nuova interfaccia dell'adattatore, le cui classi tentano di unire la loro implementazione API.

Questo non è un linguaggio concreto. È un approccio generico, qualsiasi parola chiave è lì per rappresentare può essere di qualsiasi tipo, in un linguaggio come C # puoi sostituirlo con generics ( <T>).

Suppongo che in questo momento puoi avere due classi responsabili della compressione e della crittografia.

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

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

In un mondo aziendale, è molto probabile che anche queste classi specifiche vengano sostituite da interfacce, ad esempio la classparola chiave verrebbe sostituita interface(se si ha a che fare con linguaggi come C #, Java e / o PHP) o la classparola chiave rimarrebbe, ma il Compresse i Encryptmetodi verrebbero definiti come un puro virtuale , se si codifica in C ++.

Per creare un adattatore, definiamo un'interfaccia comune.

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

Quindi dobbiamo fornire implementazioni dell'interfaccia per renderlo 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)
        );
    }
}

In questo modo, finisci con 4 classi, ognuna facendo qualcosa di completamente diverso, ma ognuna di esse fornisce la stessa API pubblica. Il Processmetodo.

Nella tua logica aziendale, in cui hai a che fare con nessuna / crittografia / compressione / entrambe le decisioni, progetterai il tuo oggetto per farlo dipendere DataProcessingdall'interfaccia che abbiamo progettato in precedenza.

class DataService
{
    private dataProcessing : DataProcessing;

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

Il processo stesso potrebbe quindi essere semplice come questo:

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

    // ... perhaps work with the data

    return data;
}

Niente più condizionali. La classe DataServicenon ha idea di cosa verrà realmente fatto con i dati quando saranno passati al dataProcessingmembro, e non gliene importa nulla, non è sua responsabilità.

Idealmente, si avrebbero test unitari per testare le 4 classi di adattatori create per assicurarsi che funzionassero, in modo da superare il test. E se passano, puoi essere abbastanza sicuro che funzioneranno indipendentemente da dove li chiami nel tuo codice.

Così facendo così non avrò mai più ifs nel mio codice?

No. È meno probabile che tu abbia condizionali nella tua logica aziendale, ma devono comunque trovarsi da qualche parte. Il posto è le tue fabbriche.

E questo va bene. Separare le preoccupazioni relative alla creazione e all'utilizzo effettivo del codice. Se rendi le tue fabbriche affidabili (in Java potresti persino arrivare a utilizzare qualcosa come il framework Guice di Google), nella tua logica aziendale non sei preoccupato di scegliere la classe giusta da iniettare. Perché sai che le tue fabbriche funzionano e forniranno ciò che ti viene chiesto.

È necessario disporre di tutte queste classi, interfacce, ecc.?

Questo ci riporta all'inizio.

In OOP, se scegli il percorso per usare il polimorfismo, vuoi davvero usare modelli di design, vuoi sfruttare le caratteristiche del linguaggio e / o vuoi seguire il tutto è un'ideologia di oggetti, allora lo è. E anche allora, questo esempio non mostra nemmeno tutte le fabbriche che si sta per necessità e se si dovesse refactoring le Compressione Encryptionclassi e renderli interfacce, invece, è necessario includere le loro implementazioni pure.

Alla fine ti ritrovi con centinaia di piccole classi e interfacce, focalizzate su cose molto specifiche. Il che non è necessariamente negativo, ma potrebbe non essere la soluzione migliore per te se tutto ciò che vuoi è fare qualcosa di semplice come aggiungere due numeri.

Se vuoi farlo in modo rapido e veloce, puoi prendere la soluzione di Ixrec , che almeno è riuscita a eliminare i blocchi else ife else, che, secondo me, sono anche un po 'peggio di una semplice if.

Prendi in considerazione questo è il mio modo di realizzare un buon design OO. Codificare le interfacce piuttosto che le implementazioni, ecco come l'ho fatto negli ultimi anni ed è l'approccio con cui mi sento più a mio agio.

Personalmente mi piace di più la programmazione if-less e apprezzerei molto di più la soluzione più lunga su 5 righe di codice. È il modo in cui sono abituato a progettare il codice e mi sento molto a mio agio nel leggerlo.


Aggiornamento 2: c'è stata una discussione sfrenata sulla prima versione della mia soluzione. Discussione principalmente causata da me, per cui mi scuso.

Ho deciso di modificare la risposta in modo che sia uno dei modi per esaminare la soluzione, ma non l'unico. Ho anche rimosso la parte del decoratore, dove intendevo invece la facciata, che alla fine ho deciso di lasciare completamente, perché un adattatore è una variazione di facciata.


28
Non ho votato a fondo ma la logica potrebbe essere la quantità ridicola di nuove classi / interfacce per fare qualcosa che il codice originale ha fatto in 8 righe (e l'altra risposta ha fatto in 5). Secondo me l'unica cosa che riesce a fare è aumentare la curva di apprendimento del codice.
Maurycy,

6
@Maurycy Ciò che OP ha chiesto è stato quello di cercare di trovare una soluzione al suo problema utilizzando modelli di progettazione comuni, se tale soluzione esiste. La mia soluzione è più lunga del suo codice Ixrec o? È. Lo ammetto. La mia soluzione risolve il suo problema usando i modelli di progettazione e quindi risponde alla sua domanda e rimuove efficacemente tutti i se necessari dal processo? Lo fa. Ixrec no.
Andy,

26
Credo che scrivere codice che sia chiaro, affidabile, conciso, performante e mantenibile sia la strada da percorrere. Se avessi un dollaro per ogni volta che qualcuno citasse SOLID o citasse uno schema software senza articolare chiaramente i suoi obiettivi e la sua logica, sarei un uomo ricco.
Robert Harvey,

12
Penso di avere due problemi che vedo qui. Il primo è che le interfacce Compressione Encryptionsembrano totalmente superflue. Non sono sicuro se stai suggerendo che sono in qualche modo necessari per il processo di decorazione, o semplicemente implicando che rappresentino concetti estratti. Il secondo problema è che la creazione di una classe simile CompressionEncryptionDecoratorporta allo stesso tipo di esplosione combinatoria dei condizionali dell'OP. Inoltre non vedo abbastanza chiaramente il motivo decorativo nel codice suggerito.
cbojar

5
Il dibattito su SOLID vs simple manca un po 'il punto: questo codice non è nessuno dei due, e non usa nemmeno il motivo decorativo. Il codice non è SOLID automaticamente solo perché utilizza un sacco di interfacce. L'iniezione di dipendenza di un'interfaccia DataProcessing è piuttosto buona; tutto il resto è superfluo. SOLID è una preoccupazione a livello di architettura volta a gestire bene il cambiamento. OP non ha fornito informazioni sulla sua architettura né su come si aspetta che il suo codice cambi, quindi non possiamo nemmeno discutere di SOLID in una risposta.
Carl Leth,

120

L'unico problema che vedo con il tuo codice attuale è il rischio di esplosione combinatoria quando aggiungi più impostazioni, che possono essere facilmente mitigate strutturando il codice in questo modo:

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

Non sono a conoscenza di alcun "modello di progettazione" o "linguaggio" di cui questo possa essere considerato un esempio.


18
@DamithGanegoda No, se leggi attentamente il mio codice vedrai che fa esattamente la stessa cosa in quel caso. Ecco perché non c'è nessuna elsetra le mie due dichiarazioni if ​​e perché mi sto assegnando dataogni volta. Se entrambi i flag sono veri, allora compress () viene eseguito, quindi encrypt () viene eseguito sul risultato di compress (), proprio come vuoi tu.
Ixrec

14
@DavidPacker Tecnicamente, così fa ogni istruzione if in ogni linguaggio di programmazione. Sono andato per semplicità, dal momento che questo sembrava un problema in cui una risposta molto semplice era appropriata. Anche la tua soluzione è valida, ma personalmente la risparmierei se dovessi preoccuparmi di più di due bandiere booleane.
Ixrec

15
@DavidPacker: correct non è definito da quanto bene il codice aderisce ad alcune linee guida di alcuni autori su alcune ideologie di programmazione. Corretto è "il codice fa quello che dovrebbe fare ed è stato implementato in un ragionevole lasso di tempo". Se ha senso farlo nel "modo sbagliato", allora il modo sbagliato è il modo giusto perché il tempo è denaro.
whatsisname

9
@DavidPacker: se fossi nella posizione dell'OP e facessi quella domanda, il commento di Lightness Race nel commento di Orbit è ciò di cui ho veramente bisogno. "Trovare una soluzione utilizzando i modelli di progettazione" sta già iniziando dal piede sbagliato.
whatsisname

6
@DavidPacker In realtà se leggi la domanda più da vicino, non insiste su uno schema. Afferma: "Sto pensando al modello Decorator. È la scelta giusta o c'è forse un'alternativa migliore?" . Hai affrontato la prima frase nella mia citazione, ma non la seconda. Altre persone hanno adottato l'approccio secondo cui no, non è la scelta giusta. Non puoi quindi affermare che solo i tuoi rispondono alla domanda.
Jon Bentley,

12

Immagino che la tua domanda non stia cercando praticità, nel qual caso la risposta di lxrec è quella corretta, ma per conoscere i modelli di progettazione.

Ovviamente il modello di comando è eccessivo per un problema così banale come quello che proponi, ma per motivi di illustrazione qui va:

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

Come vedi mettere i comandi / trasformazione in un elenco consente di eseguirli in sequenza. Ovviamente eseguirà entrambi, o solo uno di essi dipenderà da ciò che metti nella lista senza se le condizioni.

Ovviamente i condizionali finiranno in una sorta di fabbrica che mette insieme l'elenco dei comandi.

EDIT per il commento di @ texacre:

Esistono molti modi per evitare le condizioni if ​​nella parte creazionale della soluzione, ad esempio un'app GUI desktop . È possibile disporre di caselle di controllo per le opzioni di compressione e crittografia. Nel on cliccaso in cui queste caselle di controllo si istanzino il comando corrispondente e lo si aggiunga all'elenco o si rimuova dall'elenco se si deseleziona l'opzione.


A meno che non sia possibile fornire un esempio di "una sorta di factory che mette insieme l'elenco dei comandi" senza un codice che assomiglia essenzialmente alla risposta di Ixrec, allora IMO non risponde alla domanda. Ciò fornisce un modo migliore per implementare le funzioni di compressione e crittografia, ma non come evitare i flag.
thexacre

@thexacre Ho aggiunto un esempio.
Tulains Córdova,

Quindi nel tuo listener di eventi checkbox hai "se checkbox.ticked quindi aggiungi il comando"? Mi sembra che stai solo mescolando la bandiera se le dichiarazioni intorno ...
thexacre

@thexacre No, un ascoltatore per ogni casella di controllo. Nell'evento click, solo commands.add(new EncryptCommand()); o commands.add(new CompressCommand());rispettivamente.
Tulains Córdova,

Che ne dici di gestire deselezionando la casella? In quasi tutti i toolkit di lingua / interfaccia utente che ho incontrato, dovrai comunque controllare lo stato della casella di controllo nel listener di eventi. Sono d'accordo che questo è uno schema migliore, ma non evita di averne bisogno fondamentalmente se la bandiera fa qualcosa da qualche parte.
thexacre

7

Penso che i "modelli di progettazione" siano indebitamente orientati verso "modelli oo" ed evitando completamente idee molto più semplici. Ciò di cui stiamo parlando qui è una (semplice) pipeline di dati.

Proverei a farlo con il clojure. Anche qualsiasi altra lingua in cui le funzioni sono di prima classe è probabilmente ok. Forse potrei fare un esempio in C # in seguito, ma non è così bello. Il mio modo di risolvere questo sarebbe il seguente passo con alcune spiegazioni per i non clojuriani:

1. Rappresenta una serie di trasformazioni.

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

Questa è una mappa, ovvero una tabella / dizionario di ricerca / qualunque cosa, dalle parole chiave alle funzioni. Un altro esempio (parole chiave per stringhe):

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

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

Quindi, scrivendo (transformations :encrypt)o (:encrypt transformations)restituirebbe la funzione di crittografia. ( (fn [data] ... )è solo una funzione lambda.)

2. Ottieni le opzioni come una sequenza di parole chiave:

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

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

3. Filtra tutte le trasformazioni utilizzando le opzioni fornite.

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

Esempio:

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

4. Combina le funzioni in una:

(apply comp transformations-to-run)

Esempio:

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

5. E poi insieme:

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

L'UNICO cambia se vogliamo aggiungere una nuova funzione, diciamo "debug-print", è la seguente:

(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

Come viene popolata la funzione per includere solo le funzioni che devono essere applicate senza essenzialmente utilizzare una serie di istruzioni if ​​in un modo o nell'altro?
thexacre

La riga funcs-to-run-here (map options funcs)sta eseguendo il filtraggio, scegliendo quindi una serie di funzioni da applicare. Forse dovrei aggiornare la risposta e andare un po 'più in dettaglio.
NiklasJ,

5

[Essenzialmente, la mia risposta è un seguito alla risposta di @Ixrec sopra . ]

Una domanda importante: il numero di combinazioni distinte che devi coprire aumenterà? Conosci meglio il tuo dominio soggetto. Questo è il tuo giudizio da esprimere.
Il numero di varianti può eventualmente aumentare? Bene, questo non è inconcepibile. Ad esempio, potrebbe essere necessario includere algoritmi di crittografia più diversi.

Se prevedi che il numero di combinazioni distinte aumenterà, il modello di strategia può aiutarti. È progettato per incapsulare algoritmi e fornire un'interfaccia intercambiabile al codice chiamante. Avresti ancora una piccola quantità di logica quando crei (crei un'istanza) la strategia appropriata per ogni stringa particolare.

Hai commentato sopra che non ti aspetti che i requisiti cambino. Se non ti aspetti che il numero di varianti aumenti (o se puoi rinviare questo refactoring), mantieni la logica così com'è. Attualmente, hai una quantità piccola e gestibile di logica. (Forse metti un commento personale nei commenti su un possibile refactoring a un modello di strategia.)


1

Un modo per farlo in scala sarebbe:

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'uso del motivo decorativo per raggiungere gli obiettivi di cui sopra (separazione della logica di elaborazione individuale e come collegano insieme) sarebbe troppo dettagliato.

Laddove avresti bisogno di un modello di progettazione per raggiungere questi obiettivi di progettazione in un paradigma di programmazione OO, il linguaggio funzionale offre supporto nativo usando funzioni come cittadini di prima classe (riga 1 e 2 nel codice) e composizione funzionale (riga 3)


Perché è migliore (o peggiore) dell'approccio del PO? E / o cosa ne pensi dell'idea dell'OP di utilizzare un motivo decorativo?
Kasper van den Berg,

questo frammento di codice è migliore ed è esplicito sull'ordinamento (compressione prima della crittografia); evita interfacce indesiderate
Rag
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.