Le modèle de stratégie peut-il être mis en œuvre sans ramification significative?

14

Le modèle de stratégie fonctionne bien pour éviter les énormes constructions if ... else et faciliter l'ajout ou le remplacement de fonctionnalités. Cependant, cela laisse toujours un défaut à mon avis. Il semble que dans chaque implémentation, il doit encore y avoir une construction de branchement. Il peut s'agir d'une usine ou d'un fichier de données. Prenons par exemple un système de commande.

Usine:

// All of these classes implement OrderStrategy
switch (orderType) {
case NEW_ORDER: return new NewOrder();
case CANCELLATION: return new Cancellation();
case RETURN: return new Return();
}

Le code après cela n'a pas à s'inquiéter, et il n'y a qu'un seul endroit pour ajouter un nouveau type de commande maintenant, mais cette section de code n'est toujours pas extensible. Le retirer dans un fichier de données améliore la lisibilité (discutable, je sais):

<strategies>
   <order type="NEW_ORDER">com.company.NewOrder</order>
   <order type="CANCELLATION">com.company.Cancellation</order>
   <order type="RETURN">com.company.Return</order>
</strategies>

Mais cela ajoute encore du code standard pour traiter le fichier de données - accordé, plus facilement testable unitaire et code relativement stable, mais une complexité supplémentaire néanmoins.

De plus, ce type de construction ne teste pas bien l'intégration. Chaque stratégie individuelle peut être plus facile à tester maintenant, mais chaque nouvelle stratégie que vous ajoutez est une complexité supplémentaire à tester. C'est moins que ce que vous auriez si vous n'aviez pas utilisé le modèle, mais il est toujours là.

Existe-t-il un moyen de mettre en œuvre le modèle de stratégie qui atténue cette complexité? Ou est-ce aussi simple que cela, et essayer d'aller plus loin ne ferait qu'ajouter une autre couche d'abstraction pour peu ou pas d'avantages?

Michael K
la source
Hmmmmm .... il pourrait être possible de simplifier les choses avec des choses comme eval... pourrait ne pas fonctionner en Java mais peut-être dans d'autres langages?
FrustratedWithFormsDesigner
1
@FrustratedWithFormsDesigner La réflexion est le mot magique en Java
Ratchet Freak
2
Vous aurez toujours besoin de ces conditions quelque part. En les poussant dans une usine, vous respectez simplement DRY, car sinon l'instruction if ou switch aurait pu apparaître à plusieurs endroits.
Daniel B
1
La faille que vous mentionnez est souvent appelée violation du principe ouvert-fermé
k3b
la réponse acceptée dans une question connexe suggère un dictionnaire / carte comme alternative à if-else et switch
gnat

Réponses:

16

Bien sûr que non. Même si vous utilisez un conteneur IoC, vous devrez avoir des conditions quelque part, décider de l'implémentation concrète à injecter. Telle est la nature du modèle de stratégie.

Je ne vois pas vraiment pourquoi les gens pensent que c'est un problème. Il y a des déclarations dans certains livres, comme le refactoring de Fowler , que si vous voyez un commutateur / boîtier ou une chaîne de if / elses au milieu d'un autre code, vous devriez considérer cela comme une odeur et chercher à le déplacer vers sa propre méthode. Si le code dans chaque cas est plus d'une ligne, peut-être deux, alors vous devriez envisager de faire de cette méthode une méthode d'usine, renvoyant des stratégies.

Certaines personnes ont pris cela pour signifier qu'un interrupteur / boîtier est mauvais. Ce n'est pas le cas. Mais il devrait résider seul, si possible.

pdr
la source
1
Je ne le vois pas non plus comme une mauvaise chose. Cependant, je cherche toujours des moyens de réduire la quantité de code que le toucher toucherait, ce qui a suscité la question.
Michael K
Les instructions de commutation (et les longs blocs if / else-if) sont mauvaises et doivent être évitées si possible afin de maintenir votre code maintenable. Cela dit "si possible" reconnaît qu'il y a des cas où le commutateur doit exister, et dans ces cas, essayez de le garder à un seul endroit, et celui qui rend le maintien moins d'une corvée (c'est si facile de manquez accidentellement 1 des 5 emplacements du code dont vous aviez besoin pour rester synchronisé lorsque vous ne l'isolez pas correctement).
Shadow Man
10

Le modèle de stratégie peut-il être mis en œuvre sans ramification significative?

Oui , en utilisant une table de hachage / dictionnaire où s'inscrit chaque implémentation de la stratégie. La méthode d'usine deviendra quelque chose comme

Class strategyType = allStrategies[orderType];
return runtime.create(strategyType);

Chaque implémentation de stratégie doit enregistrer une usine avec son orderType et quelques informations sur la création d'une classe.

factory.register(NEW_ORDER, NewOrder.class);

Vous pouvez utiliser le constructeur statique pour l'enregistrement, si votre langue le prend en charge.

La méthode register ne fait rien de plus que d'ajouter une nouvelle valeur à la table de hachage:

void register(OrderType orderType, Class class)
{
   allStrategies[orderType] = class;
}

[mise à jour 2012-05-04]
Cette solution est beaucoup plus complexe que la "solution de commutation" d'origine que je préférerais la plupart du temps.

Cependant, dans un environnement où les stratégies changent souvent (c.-à-d. Calcul des prix en fonction du client, du temps, ....), cette solution de hachage associée à un conteneur IoC pourrait être une bonne solution.

k3b
la source
3
Alors maintenant, vous avez tout un Hashmap of Strategies, pour éviter un changement / cas? En quoi les rangées de factory.register(NEW_ORDER, NewOrder.class);nettoyant, ou moins une violation de l'OCP, sont-elles exactement que les rangées de case NEW_ORDER: return new NewOrder();?
pdr
5
Ce n'est pas du tout plus propre. C'est contre-intuitif. Il sacrifie le principe de la bête stimulante pour améliorer le principe ouvert-fermé. Il en va de même pour l'inversion de contrôle et l'injection de dépendance: celles-ci sont plus difficiles à comprendre qu'une solution simple. La question était "implémenter ... sans branchement significatif" et non "comment créer une solution plus intuitive"
k3b
1
Je ne vois pas non plus que vous ayez évité les branchements. Vous venez de changer la syntaxe.
pdr
1
N'y a-t-il pas un problème avec cette solution dans les langages qui ne chargent pas les classes jusqu'à ce qu'elles soient nécessaires? Le code statique ne s'exécutera pas tant que la classe n'est pas chargée, et la classe ne sera jamais chargée car aucune autre classe n'y fait référence.
kevin cline
2
Personnellement, j'aime cette méthode, car elle vous permet de traiter vos stratégies comme des données mutables , au lieu de les traiter comme du code immuable.
Tacroy
2

La «stratégie» consiste à devoir choisir entre des algorithmes alternatifs au moins une fois , pas moins. Quelque part dans votre programme, quelqu'un doit prendre une décision - peut-être l'utilisateur ou votre programme. Si vous utilisez un IoC, une réflexion, un évaluateur de fichier de données ou une construction switch / case pour implémenter cela ne change pas la situation.

Doc Brown
la source
Je ne suis pas d'accord avec votre déclaration d' une fois. Les stratégies peuvent être échangées au moment de l'exécution. Par exemple, après avoir échoué à traiter une demande à l'aide d'une ProcessingStrategy standard, une VerboseProcessingStrategy pourrait être choisie et le traitement réexécuté.
dmux
@dmux: bien sûr, j'ai modifié ma réponse en conséquence.
Doc Brown
1

Le modèle de stratégie est utilisé lorsque vous spécifiez des comportements possibles et il est préférable de l'utiliser lors de la désignation d'un gestionnaire au démarrage. Spécifier quelle instance de la stratégie à utiliser peut être effectuée via un médiateur, votre conteneur IoC standard, une usine comme vous le décrivez, ou simplement en utilisant la bonne en fonction du contexte (car souvent, la stratégie est fournie dans le cadre d'une utilisation plus large de la classe qui le contient).

Pour ce type de comportement où différentes méthodes doivent être appelées en fonction des données, je suggère d'utiliser les constructions de polymorphisme fournies par le langage en question; pas le modèle de stratégie.

Telastyn
la source
1

En discutant avec d'autres développeurs où je travaille, j'ai trouvé une autre solution intéressante - spécifique à Java, mais je suis sûr que l'idée fonctionne dans d'autres langages. Construisez une énumération (ou une carte, peu importe laquelle) en utilisant les références de classe et instanciez en utilisant la réflexion.

enum FactoryType {
   Type1(Type1.class),
   Type2(Type2.class);

   private Class<? extends Type> clazz;

   private FactoryType(Class<? extends Type> clazz) {
      this.clazz = clazz;
   }

   public Class<? extends Type> getTypeClass() {
      return clazz;
   }
}

Cela réduit considérablement le code d'usine:

public Type create(FactoryType type) throws Exception {
   return type.getTypeClass().newInstance();
}

(Veuillez ignorer la mauvaise gestion des erreurs - exemple de code :))

Ce n'est pas le plus flexible, car une construction est toujours requise ... mais elle réduit le changement de code à une seule ligne. J'aime que les données soient séparées du code d'usine. Vous pouvez même faire des choses comme définir des paramètres en utilisant des classes / interfaces abstraites pour fournir une méthode commune ou en créant des annotations au moment de la compilation pour forcer des signatures de constructeur spécifiques.

Michael K
la source
Je ne sais pas ce qui a causé le retour à la page d'accueil, mais c'est une bonne réponse et dans Java 8, vous pouvez maintenant créer des références de constructeur sans réflexion.
JimmyJames
1

L'extensibilité peut être améliorée en faisant en sorte que chaque classe définisse son propre type d'ordre. Ensuite, votre usine sélectionne celle qui correspond.

Par exemple:

public interface IOrderStrategy
{
    OrderType OrderType { get; }
}

public class NewOrder : IOrderStrategy
{
    public OrderType OrderType { get; } = OrderType.NewOrder;
}

public class OrderFactory
{
    private IEnumerable<IOrderStrategy> _strategies;

    public OrderFactory(IEnumerable<IOrderStrategy> strategies) // Injected by IoC container
    {
        _strategies = strategies;
    }

    public IOrderStrategy Create(OrderType orderType)
    {
        IOrderStrategy strategy = _strategies.FirstOrDefault(s => s.OrderType == orderType);

        if (strategy == null)
            throw new ArgumentException("Invalid order type.", nameof(orderType));

        return strategy;
    }
}
Eric Eskildsen
la source
1

Je pense qu'il devrait y avoir une limite au nombre de succursales acceptables.

Par exemple, si j'ai plus de huit cas à l'intérieur de mon instruction switch, je réévalue mon code et cherche ce que je peux re-factoriser. Très souvent, je trouve que certains cas peuvent être regroupés dans une usine distincte. Mon hypothèse ici est qu'il existe une usine qui élabore des stratégies.

Quoi qu'il en soit, vous ne pouvez pas éviter cela et quelque part, vous devrez vérifier l'état ou le type de l'objet avec lequel vous travaillez. Vous allez ensuite construire une stratégie pour cela. Bonne vieille séparation des préoccupations.

CodeART
la source