Il modello di strategia può essere implementato senza una ramificazione significativa?

12

Il modello di strategia funziona bene per evitare enormi se ... altrimenti costruisce e semplifica l'aggiunta o la sostituzione di funzionalità. Tuttavia, lascia ancora un difetto a mio parere. Sembra che in ogni implementazione debba ancora esserci un costrutto ramificato. Potrebbe essere una fabbrica o un file di dati. Ad esempio prendi un sistema di ordinazione.

di 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 ora c'è solo un posto dove aggiungere un nuovo tipo di ordine, ma questa sezione di codice non è ancora estensibile. Tirarlo fuori in un file di dati aiuta un po '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 il codice boilerplate per elaborare il file di dati - concesso, più facilmente unità testabile e codice relativamente stabile, ma comunque complessità aggiuntiva.

Inoltre, questo tipo di costrutto non integra bene i test di integrazione. Ogni singola strategia può essere più semplice da testare ora, ma ogni nuova strategia che aggiungi è una complessità aggiuntiva da testare. È inferiore a quello che avresti se non avessi usato il pattern, ma è ancora lì.

C'è un modo per implementare il modello di strategia che attenua questa complessità? O è così semplice come si ottiene, e cercando di andare oltre aggiungerebbe solo un altro livello di astrazione per poco o nessun vantaggio?

    
posta Michael K 01.05.2012 - 20:57
fonte

7 risposte

15

Certo che no. Anche se utilizzi un contenitore IoC, dovrai avere delle condizioni da qualche parte, decidendo quale implementazione concreta da 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 Ristrutturazione di Fowler , che se vedi un interruttore / caso o una catena di se / Els nel mezzo di un altro codice, dovresti considerare che un odore e guardare per spostarlo al proprio 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 dire che un interruttore / caso è cattivo. Questo non è il caso. Ma dovrebbe risiedere da solo, se possibile.

    
risposta data 01.05.2012 - 21:15
fonte
10

Can the Strategy pattern be implemented without significant branching?

, utilizzando un hashmap / dizionario in cui si registra ogni implementazione della strategia. Il metodo factory diventerà qualcosa come

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

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

factory.register(NEW_ORDER, NewOrder.class);

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

Il metodo di registrazione 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 (ad esempio il calcolo del prezzo a seconda del cliente, del tempo, ....) questa soluzione di hashmap combinata con un contenitore IoC potrebbe essere una buona soluzione.

    
risposta data 02.05.2012 - 10:48
fonte
2

"Strategia" significa dover scegliere tra algoritmi alternativi una volta , non più, ma anche 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, reflection, un valutatore di file di dati o un costrutto switch / case per implementare ciò, non cambia la situazione.

    
risposta data 02.05.2012 - 08:20
fonte
1

Il modello strategico viene utilizzato quando si specificano i comportamenti possibili e si utilizza al meglio quando si progetta un gestore all'avvio. Specificare quale istanza della strategia da utilizzare può essere fatta tramite un mediatore, il contenitore IoC standard, un factory come quello che descrivi, o semplicemente utilizzando quello giusto 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 diversi metodi in base ai dati, suggerirei di utilizzare i costrutti del polimorfismo forniti dalla lingua in uso; non il modello di strategia.

    
risposta data 01.05.2012 - 21:16
fonte
1

Nel parlare con altri sviluppatori in cui lavoro, ho trovato un'altra soluzione interessante, specifica per Java, ma sono sicuro che l'idea funziona in altre lingue. Costruisci un enum (o una mappa, non importa troppo quale) usando i riferimenti di classe e crea un'istanza usando il 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;
   }
}

Questo riduce drasticamente il codice di fabbrica:

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

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

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

    
risposta data 27.07.2012 - 16:24
fonte
1

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

Ad 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;
    }
}
    
risposta data 21.02.2017 - 13:35
fonte
0

Penso che ci dovrebbe essere un limite su quanti rami sono accettabili.

Ad esempio, se ho più di otto casi all'interno della mia istruzione switch, quindi rivalutare il mio codice e cercare ciò che posso ri-fattore. Molto spesso scopro che alcuni casi possono essere raggruppati in una fabbrica separata. La mia ipotesi qui è che esiste una fabbrica che costruisce strategie.

In ogni caso, non puoi evitare questo e da qualche parte dovrai controllare lo stato o il tipo dell'oggetto con cui stai lavorando. Quindi costruirai una strategia per questo. Buona vecchia separazione delle preoccupazioni.

    
risposta data 01.05.2012 - 21:18
fonte

Leggi altre domande sui tag