Utilizzo eccessivo della spedizione dinamica

4

Stavo passando per il vecchio codice e ho notato uno schema che si ripete dappertutto - invio dinamico.

Anche se non vedo alcun problema con l'implementazione stessa, mi chiedevo se ci sono altri modi per gestire la spedizione dinamica.

Per fare un esempio - considera uno scenario in cui in fase di esecuzione vado a ricevere un messaggio - Cliente o Visitatore ed esegui alcune operazioni in base al tipo del messaggio. Per brevità, diciamo che tutto ciò che dobbiamo fare è stampare un tipo di messaggio corrispondente.

Quindi, passare dalle parole al codice

public abstract class Message
{
   public abstract string Id {get;}
}

public class Customer: Message
{
   public override string Id {get {return "Customer";}}
}

public class Visitor: Message
{
   public override string Id {get {return "Visitor";}}
}

E gestione parte

public interface IHandleMessage
{
  bool CanHandle(Message message);
  void Handle(Message message);
}

public class CustomerHandler: IHandleMessage
{
   public bool CanHandle(Message message)
   {
      return message.Id == "Customer";
   }

   public void Handle(Message message) {Console.WriteLine(message.Id);}
}

public class VisitorHandler: IHandleMessage
{
   public bool CanHandle(Message message)
   {
      return message.Id == "Visitor";
   }

   public void Handle(Message message) {Console.WriteLine(message.Id);}
}

public class MessageHandlersFactory
{
   private static readonly IEnumerable<IHandleMessage> _messageHandlers =
       new List<IHandleMessage> {new CustomerHandler(), new VisitorHandler()};

   public static IHandleMessage GetHandlerForMessage(Message message)
   {
       return _messageHandlers.FirstOrDefault(h => h.CanHandle(message));
   }
}

E il codice cliente è simile a questo

public class Program
{
   public static void Main(Message message)
   {
      IHandleMessage handler = MessageHandlersFactory.GetHandlerForMessage(message);

      handler.Handle(message)
   }
}

Ora come ho detto, non mi interessa questa implementazione, quello che davvero non mi piace è che questo schema si ripete dappertutto, il che mi fa pensare se questo non è un uso eccessivo di dispatch dinamico.

Generalizzare: ovunque sia necessario prendere una decisione in base al messaggio / dati / argomento in entrata, ho più o meno la stessa implementazione che ho fornito sopra.

Quindi la mia domanda è - dato l'esempio sopra ci sono altri modi per gestire questo tipo di scenari? O questo è il modo consigliato e accettato di gestire la spedizione dinamica?

    
posta Michael 15.02.2017 - 20:04
fonte

5 risposte

0

Si potrebbe sbarazzarsi del metodo CanHandle() e utilizzare semplicemente un dizionario in cui la chiave è il messaggio Id. Sarebbe semplice e pulito.

Se la logica di gestione diventa più complessa o se possono essere richiamati più gestori per un particolare messaggio, CanHandle () ha uno scopo migliore.

Ma dal tuo esempio è necessario solo un singolo gestore per ogni messaggio, quindi un dizionario di gestori sarebbe sufficiente. La factory potrebbe utilizzare il dizionario interno e restituire la classe del gestore corretta in base alla chiave (id).

    
risposta data 15.02.2017 - 21:06
fonte
2

L'obiettivo di questo è in definitiva scegliere un metodo per gestire un messaggio. Attualmente stai emulando un dizionario con CanHandle che sostituisce una corrispondenza di chiave. Ma per le condizioni presentate qui, un dizionario standard troverà il gestore in modo più efficiente.

Quando ho fatto questo, invece di usare una classe astratta e un tipo di messaggio a stringa costante, ho usato un'interfaccia marcatore e il tipo stesso come tipo di messaggio. Qualcosa di simile (dalle tue classi sopra):

public interface IMessage { }
public class Customer : IMessage { }
public class Visitor : IMessage { }

Ciò semplifica la definizione di una classe basata sul dizionario per la gestione di qualsiasi IMessage :

public class MainHandler
{
    private Dictionary<Type, Action<IMessage>> _handlers;

    // constructor
    public MainHandler(Dictionary<Type, Action<IMessage> handlers)
    {
        _handlers = handlers;
    }

    public void Handle(IMessage message)
    {
        Action<IMessage> handle = null;
        if (_handlers.TryGetValue(message.GetType(), out handle))
        {
            handle(message);
        }
        else
        {
            throw new NotImplementedException("No handler found");
        }
    }
}

Ora il trucco è far compilare il dizionario. Tu potresti fare questo usando un'altra interfaccia marcatore per il conduttore, quindi trovarli con la riflessione. Ma trovo molto più chiaro elencare manualmente i gestori. In questo modo è più facile tracciare l'utilizzo con strumenti standard, ad es. Trova tutti i riferimenti. Puoi anche utilizzare le funzioni di supporto per renderlo un po 'più semplice.

public class CustomerHandler
{
    public static void Handle(Customer message) { return; }
}
public class VisitorHandler
{
    public static void Handle(Visitor message) { return; }
}

public class Program
{
    // helper
    static KeyValuePair<Type, Action<IMessage>> ToHandle<T>(Action<T> action)
    {
        Type key = typeof(T);
        // build in cast to proper type to make it easier on handler code
        Action<IMessage> value = message => action((T)message);
        return new KeyValuePair<Type, Action<IMessage>>(key, value);
    }

    public static void Main()
    {
        // initialize things
        var mainHandler = new MainHandler(
            new List<KeyValuePair<Type, Action<IMessage>>>
            {
                // main stuff that changes here
                ToHandle<Customer>(CustomerHandler.Handle),
                ToHandle<Visitor>(VisitorHandler.Handle)

            }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));

        // probably happens in a loop
        IMessage message = ... ; // read message
        mainHandler.Handle(message);
    }
}

Se sei arrivato a questo punto, probabilmente stai pensando che i metodi di gestione statica non funzioneranno, per quanto riguarda le dipendenze? Ma puoi comunque fornire dipendenze a metodi statici passandoli come parametri.

public class CustomerHandler
{
    public static void Handle(string setting, SharedResource res, Customer message) { ... }
}

public class VisitorHandler
{
    public static void Handle(string setting, Visitor message) { ... }
}

public class Program
{
    ...

    public static void Main()
    {
        // initialize things
        var res = new SharedResource();
        var setting = ConfigHelper.LoadSettingFromSomewhere();
        var mainHandler = new MainHandler(
            new List<KeyValuePair<Type, Action<IMessage>>>
            {
                ToHandle<Customer>(x => CustomerHandler.Handle(setting, res, x)),
                ToHandle<Visitor>(x => VisitorHandler.Handle(setting, x))

            }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));

        ...
    }

Potresti anche creare e passare funzioni che potresti aver bisogno nel gestore:

// initialize things
Func<SqlConnection> getConnection = () => new SqlConnection("...");
...
    ToHandle<Customer>(x => CustomerHandler.Handle(getConnection, ... , x));

In questa implementazione, il CanHandle boilerplate è stato eliminato. I gestori possono essere definiti nel modo desiderato (nessuna interfaccia a cui conformarsi). Quindi il collegamento ti dà l'opportunità di fornire ai gestori esattamente le dipendenze di cui hanno bisogno per soddisfare il loro caso d'uso.

    
risposta data 16.02.2017 - 01:22
fonte
2

Il dispacciamento dinamico, ovvero il polimorfismo, è uno dei componenti principali della programmazione orientata agli oggetti. E in un'applicazione che usa oggetti polimorfici, mi aspetterei di vedere dispatch dinamici usati ovunque.

Tuttavia, ciò che non non aspetterei di vedere è il codice che crea immediatamente un oggetto, crea una singola chiamata polimorfica su quell'oggetto e poi getta via l'oggetto. E se questo è ciò che sta accadendo nel tuo codice, direi che il tuo modello a oggetti non ha bisogno di essere polimorfico, e probabilmente non beneficia del polimorfismo.

Per dare un esempio di ciò che considero il "buono" polimorfismo: tutti gli investimenti (azioni, obbligazioni, opzioni e c) hanno un prezzo, una quantità e un valore calcolato da questi due attributi. Tuttavia, il calcolo specifico dipende dal tipo di investimento, così come le regole per la negoziazione dell'investimento. Quindi ha senso definire un'interfaccia di base Investment , con sottoclassi concrete. Questa interfaccia definisce i comportamenti e le interazioni che il programma può avere con un investimento generico: è un'astrazione.

D'altra parte, si potrebbe definire un modello di oggetto per l'elaborazione di eventi in cui non è necessaria un'astrazione: si legge un evento e poi lo si distribuisce per l'elaborazione, con ogni evento elaborato da un insieme completamente distinto di codice. In questo caso, non c'è bisogno di un'interfaccia comune che definisca un metodo process .

Ciò non significa che un progetto orientato agli oggetti non sia ancora utile: gli oggetti sono uno strumento utile per la modularizzazione. Ma non necessariamente hanno bisogno di essere polimorfi, e il tuo metodo factory potrebbe creare l'oggetto e chiamare immediatamente la sua funzione process ..

E se fai hai bisogno di polimorfismo, tieni presente che qualsiasi alternativa come una mappa di funtori sta semplicemente spostando l'implementazione di dispatch dinamico nel tuo codice, piuttosto che lasciare che il runtime lo gestisca.

    
risposta data 17.02.2017 - 03:37
fonte
1

Penso che sia popolare per la sua estensibilità. Ma dovremmo ricordare che ad un certo punto del tuo codice istanzia gli oggetti Cliente e Visitatore.

Puoi ignorare questo nel tuo codice di esempio, ma sarebbe possibile avere:

public class Customer
{
    public Customer(IHandler<Customer> handler)
    {
        this.handler = handler;
    }

    private IHandler<Customer> handler;
    public Handle() { handler.Handle(this);}
}
    
risposta data 16.02.2017 - 16:14
fonte
0

CanHandle e Handle possono essere combinati in un unico metodo, restituendo true se gestito.

Cosa succede quando due gestori sono in grado di gestire lo stesso messaggio? Attualmente utilizzerà il primo inserito nell'elenco. Questa potrebbe essere una fonte di comportamenti diversi (ad esempio bug), se i gestori vengono creati e inseriti dinamicamente.

Gli ambienti di programmazione orientati agli oggetti spesso implementano questo tipo di distribuzione in un modo più semplice:

  • Gli identificativi dei messaggi sono numeri interi piccoli, non stringhe.
  • I gestori di messaggi sono puntatori di funzioni (delegati in .net).
  • La spedizione consiste nell'indicizzare una serie di puntatori di funzione usando l'identificativo del messaggio, trovando la funzione di gestione e invocandola.

Potresti usare un algoritmo simile.

    
risposta data 15.02.2017 - 22:02
fonte

Leggi altre domande sui tag