Il modello di strategia può essere implementato senza ramificazioni significative?


14

Il modello di strategia funziona bene per evitare enormi se ... altro costrutti e rendere più facile aggiungere o sostituire funzionalità. Tuttavia, a mio avviso lascia ancora un difetto. Sembra che in ogni implementazione ci sia ancora bisogno di un costrutto ramificato. Potrebbe essere una fabbrica o un file di dati. Ad esempio, prendi un sistema di ordinazione.

Fabbrica:

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

Il codice dopo questo non deve preoccuparsi, e c'è solo un posto dove aggiungere un nuovo tipo di ordine ora, ma questa sezione di codice non è ancora estendibile. Estraerlo in un file di dati aiuta in qualche modo la leggibilità (discutibile, lo so):

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

Ma questo aggiunge ancora codice boilerplate per elaborare il file di dati - concesso, più facilmente testabile dall'unità e codice relativamente stabile, ma comunque ulteriore complessità.

Inoltre, questo tipo di costrutto non verifica bene l'integrazione. Ogni singola strategia può essere più facile da testare ora, ma ogni nuova strategia che aggiungi è una complessità aggiuntiva da testare. È meno di quello che avresti se non avessi usato il modello, ma è ancora lì.

C'è un modo per attuare il modello strategico che mitiga questa complessità? O è semplicemente così semplice, e provare ad andare oltre aggiungerebbe un altro livello di astrazione per poco o nessun beneficio?


Hmmmmm .... potrebbe essere possibile semplificare le cose con cose come eval... potrebbe non funzionare in Java ma forse in altre lingue?
FrustratedWithFormsDesigner

1
@FrustratedWithFormsDesigner riflesso è la parola magica di java
maniaco del cricchetto

2
Avrai ancora bisogno di quelle condizioni da qualche parte. Spingendoli in una fabbrica, stai semplicemente rispettando DRY, poiché altrimenti l'istruzione if o switch potrebbe apparire in più punti.
Daniel B,

1
Il difetto che stai menzionando è spesso chiamato violazione del principio aperto-chiuso
k3b

la risposta accettata nella domanda correlata suggerisce dizionario / mappa come alternativa a if-else e switch
gnat

Risposte:


16

Ovviamente no. Anche se usi un contenitore IoC, dovrai avere condizioni da qualche parte, decidere quale implementazione concreta iniettare. Questa è la natura del modello di strategia.

Non vedo davvero perché la gente pensi che questo sia un problema. Ci sono dichiarazioni in alcuni libri, come il Refactoring di Fowler , che se vedi un interruttore / caso o una catena di if / elses nel mezzo di un altro codice, dovresti considerare quell'odore e cercare di spostarlo nel suo metodo. Se il codice all'interno di ciascun caso è più di una riga, forse due, allora dovresti considerare di rendere quel metodo un Metodo di fabbrica, restituendo Strategie.

Alcune persone hanno preso questo per significare che un interruttore / caso è male. Questo non è il caso. Ma dovrebbe risiedere da solo, se possibile.


1
Non lo vedo neanche come una cosa negativa. Tuttavia, sono sempre alla ricerca di modi per ridurre la quantità di codice che manterrebbe toccare, il che ha portato alla domanda.
Michael K,

Le istruzioni switch (e i blocchi long if / else-if) sono errati e dovrebbero essere evitati se possibile per mantenere il codice mantenibile. Detto questo "se possibile" riconosce che ci sono alcuni casi in cui lo switch deve esistere, e in quei casi, cerca di tenerlo giù in un unico posto, e uno che lo rende meno faticoso (è così facile perdere accidentalmente 1 dei 5 punti del codice che era necessario mantenere sincronizzati quando non lo si isola correttamente).
Shadow Man

10

Il modello di strategia può essere implementato senza ramificazioni significative?

, utilizzando una hashmap / dizionario in cui si registra ogni implementazione di strategia. Il metodo di fabbrica diventerà qualcosa di simile

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

Ogni implementazione di strategia deve registrare una factory con il suo orderType e alcune informazioni su come creare una classe.

factory.register(NEW_ORDER, NewOrder.class);

Puoi usare il costruttore statico per la registrazione, se la tua lingua lo supporta.

Il metodo register non fa altro che aggiungere un nuovo valore all'hashmap:

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

[aggiornamento 2012-05-04]
Questa soluzione è molto più complessa della "soluzione switch" originale che preferirei la maggior parte del tempo.

Tuttavia, in un ambiente in cui le strategie cambiano spesso (ovvero il calcolo del prezzo a seconda del cliente, del tempo, ...) questa soluzione hashmap combinata con un contenitore IoC potrebbe essere una buona soluzione.


3
Quindi ora hai un'intera hashmap di strategie, per evitare un cambio / caso? In che modo esattamente le file factory.register(NEW_ORDER, NewOrder.class);più pulite, o meno una violazione di OCP, rispetto alle file di case NEW_ORDER: return new NewOrder();?
pdr

5
Non è affatto più pulito. È controintuitivo. Sacrifica il principio del tenero-stimolo-stupido per migliorare il principio aperto-chiuso. Lo stesso vale per l'inversione del controllo e l'iniezione di dipendenza: sono più difficili da comprendere di una semplice soluzione. La domanda era "implementare ... senza diramazioni significative" e non "come creare una soltuion più intuitiva"
k3b

1
Non vedo neanche tu abbia evitato la ramificazione. Hai appena cambiato la sintassi.
pdr,

1
Non esiste un problema con questa soluzione in lingue che non caricano le classi fino a quando non sono necessarie? Il codice statico non verrà eseguito fino a quando la classe non verrà caricata e la classe non verrà mai caricata perché nessun'altra classe fa riferimento ad essa.
Kevin Cline,

2
Personalmente mi piace questo metodo, perché ti permette di trattare le tue strategie come dati mutabili , invece di trattarle come codice immutabile.
Tacroy,

2

La "strategia" consiste nel dover scegliere tra algoritmi alternativi almeno una volta , non meno. Da qualche parte nel tuo programma qualcuno deve prendere una decisione - forse l'utente o il tuo programma. Se si utilizza un IoC, la riflessione, un valutatore di file di dati o un costrutto switch / case per implementare ciò non cambia la situazione.


Non sarei d'accordo con la tua affermazione di una volta. Le strategie possono essere scambiate in fase di esecuzione. Ad esempio, dopo che non è stato possibile elaborare una richiesta utilizzando una ProcessingStrategy standard, è possibile scegliere VerboseProcessingStrategy ed eseguire nuovamente l'elaborazione.
dmux

@dmux: certo, ho modificato la mia risposta di conseguenza.
Doc Brown,

1

Il modello di strategia viene utilizzato quando si specificano possibili comportamenti e viene utilizzato al meglio quando si designa un gestore all'avvio. Specificare quale istanza della strategia da utilizzare può essere eseguita tramite un mediatore, il contenitore IoC standard, una fabbrica come la descrivi o semplicemente usando quella giusta in base al contesto (poiché spesso la strategia viene fornita come parte di un uso più ampio della classe che lo contiene).

Per questo tipo di comportamento in cui è necessario chiamare metodi diversi in base ai dati, suggerirei di usare i costrutti del polimorfismo forniti dal linguaggio in questione; non il modello di strategia.


1

Parlando con altri sviluppatori in cui lavoro, ho trovato un'altra soluzione interessante, specifica per Java, ma sono sicuro che l'idea funzioni in altre lingue. Costruisci un enum (o una mappa, non importa troppo quale) usando i riferimenti di classe e crea un'istanza usando reflection.

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

Ciò riduce drasticamente il codice di fabbrica:

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

(Si prega di ignorare la scarsa gestione degli errori - codice di esempio :))

Non è il più flessibile, poiché è ancora necessaria una build ... ma riduce la modifica del codice a una riga. Mi piace che i dati siano separati dal codice di fabbrica. Puoi persino fare cose come impostare i parametri usando classi / interfacce astratte per fornire un metodo comune o creare annotazioni in fase di compilazione per forzare firme di costruttori specifiche.


Non sei sicuro di cosa abbia causato il ritorno alla home page ma questa è una buona risposta e in Java 8 ora puoi creare riferimenti a Costruttore senza riflessione.
JimmyJames,

1

L'estensibilità può essere migliorata facendo in modo che ciascuna classe definisca il proprio tipo di ordine. Quindi la tua fabbrica seleziona quella che corrisponde.

Per esempio:

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

1

Penso che dovrebbe esserci un limite al numero di filiali accettabili.

Ad esempio, se ho più di otto casi all'interno della mia istruzione switch, rivaluto il mio codice e cerco ciò che posso ricodificare. Molto spesso trovo che alcuni casi possano essere raggruppati in una fabbrica separata. La mia ipotesi qui è che esiste una fabbrica che costruisce strategie.

Ad ogni modo, non puoi evitarlo e da qualche parte dovrai controllare lo stato o il tipo di oggetto con cui stai lavorando. Costruirai quindi una strategia per questo. Buona vecchia separazione delle preoccupazioni.

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.