Lors de la conception du code, vous avez toujours deux options.
- faites-le, dans ce cas, n'importe quelle solution fonctionnera pour vous
- ê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
s qui se trouvent dans un code n'ont pas à être là. Naturellement, si vous souhaitez comparer deux types scalaires, tels que int
s ou float
s, 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 if
s) 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:
data
n'est ni chiffré ni compressé (appelez rien, retournez data
)
data
est compressé (appelez-le compress(data)
et retournez-le)
data
est crypté (appelez-le encrypt(data)
et retournez-le)
data
est 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 class
mot - clé serait remplacé par interface
(si vous traitez avec des langages comme C #, Java et / ou PHP) ou le class
mot - clé resterait, mais le Compress
et les Encrypt
mé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 Process
mé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' DataProcessing
interface 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 lorsqu'elles seront transmises au dataProcessing
membre, 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 if
s 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 Compression
et les Encryption
classes 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 if
et 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.
if
déclarations?