Quando si progetta il codice, sono sempre disponibili due opzioni.
- fallo, nel qual caso praticamente qualsiasi soluzione funzionerà per te
- 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 if
s che si trovano in un codice non devono essere presenti. Naturalmente, se si desidera confrontare due tipi scalari, come int
s o float
s, è 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 if
s) 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:
data
non è né crittografato né compresso (non chiamare nulla, restituire data
)
data
è compresso (chiama compress(data)
e restituiscilo)
data
è crittografato (chiamalo encrypt(data)
e restituiscilo)
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 class
parola chiave verrebbe sostituita interface
(se si ha a che fare con linguaggi come C #, Java e / o PHP) o la class
parola chiave rimarrebbe, ma il Compress
e i Encrypt
metodi 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 Process
metodo.
Nella tua logica aziendale, in cui hai a che fare con nessuna / crittografia / compressione / entrambe le decisioni, progetterai il tuo oggetto per farlo dipendere DataProcessing
dall'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 DataService
non ha idea di cosa verrà realmente fatto con i dati quando saranno passati al dataProcessing
membro, 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ù if
s 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 Compression
e Encryption
classi 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 if
e 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.
if
dichiarazioni?