Come deve essere chiamato un metodo che crea condizionatori dell'evento auto annullanti la sottoscrizione?

2

Sviluppo / mantenimento di una libreria interna in un'azienda in crescita con circa 12 altri sviluppatori che scrivono codice. Questa libreria è usata principalmente per il comportamento di scripting del gioco e abbiamo molti progetti diversi che ne fanno uso.

Questa libreria implementa aggregazione di eventi semplice, tipizzata in una classe statica EventBus .

Ha i metodi previsti per sottoscrivere e annullare l'iscrizione di gestori di eventi: %codice% e Subcribe<TEvent>(Action<TEvent> handler)

Il mio enigma è un "metodo di convenienza" che ho aggiunto: Unsubscribe<TEvent>(Action<TEvent> handler) ), che consente di creare gestori di eventi (anonimi) che possono decidere se desiderano ricevere ulteriori messaggi di evento restituendo un valore booleano. Essenzialmente, verranno annullati dopo che alcune condizioni sono state soddisfatte.

Esempio:

EventBus.SubscribeUntilTrue<DialogEvent.End>(e =>
{
    if (e.Dialog.Name == "MyDialog")
    {
        DoSomething();
        return true;
    }
    return false;
});

L'obiettivo di questo metodo è consentire allo sviluppatore di utilizzarlo per segnalare l'intento in modo più chiaro, ridurre lo standard di stampa ed evitare di dichiarare metodi che vengono chiamati una sola volta. Di seguito sono riportati due esempi di codice che fanno la stessa cosa dello snippet di cui sopra in un modo che a mio avviso è meno leggibile / intuitivo / manutenibile:

Primo esempio: utilizzando un metodo denominato, che ...

EventBus.Subscribe<DialogEvent.End>(EndHandler);

... è dichiarato altrove.

private void EndHandler(DialogEvent.End e)
{
    if (e.Dialog.Name == "MyDialog")
    {
        DoSomething();
        EventBus.Unsubscribe<DialogEvent.End>(EndHandler);
    }
}

Questo non è l'ideale, perché l'intento può essere comunicato solo attraverso il nome del metodo del gestore al momento della sottoscrizione e non è chiaro se vi siano altri chiamanti per il gestore. Ho il sospetto che in molti casi il nome possa facilmente diventare molto prolisso e impreciso in quanto è probabile che il codice tipico all'interno di un gestore di questo tipo cambi spesso mentre è in fase di sviluppo, pur essendo breve.

Secondo esempio: Uno sviluppatore che eviti la dichiarazione di un metodo "una volta chiamato" potrebbe utilizzare un metodo delegato / anonimo auto-cancellato che è un po 'disordinato / brutto:

Action<DialogEvent.End> endHandler = null;
endHandler = e =>
{
    if (e.Dialog.Name == "MyDialog")
    {
        DoSomething();
        EventBus.Unsubscribe(endHandler);
    }
};
EventBus.Subscribe(endHandler);

Gli sviluppatori che non hanno familiarità con questo modello possono facilmente confondersi.

Non sono sicuro che il nome SubscribeUntilTrue<TEvent>(Func<TEvent, bool> handler sia adeguato affinché le persone comprendano intuitivamente cosa fa il metodo e quale significato assegna il valore di ritorno richiesto al gestore.

È generalmente considerato un odore includere una condizione (al) nel nome di un metodo, come SubscribeUntil True . Inizialmente era chiamato solo SubscribeUntil ma dovevo spiegare a un altro sviluppatore cosa faceva e ha suggerito di aggiungere la parte True .

Ho avuto un'idea di cambiare la firma del metodo (o di aggiungere un sovraccarico) in modo che il valore di ritorno della funzione di gestione sia una struttura di tipo enum sigillata con due proprietà statiche che fungono da 'costanti' che denoterebbero l'uso previsto più chiaramente:

EventBus.SubscribeUntil<DialogEvent.End>(e =>
{
    if (e.Dialog.Name == "MyDialog")
    {
        DoSomething();
        return EventHandlerResult.Unsubscribe;
    }
    return EventHandlerResult.KeepSubscribed;
});

Questa struttura "enum-like" con solo due valori possibili ha conversioni implicite in / da bool in modo che il passaggio tra il sovraccarico utilizzato sia più conveniente.

public struct EventHandlerResult
{
    private readonly bool shouldUnsubscribe;

    public static readonly EventHandlerResult Unsubscribe = true;

    public static readonly EventHandlerResult KeepSubscribed = false;

    private EventHandlerResult(bool shouldUnsubscribe)
    {
        this.shouldUnsubscribe = shouldUnsubscribe;
    }

    public static implicit operator bool(EventHandlerResult state)
    {
        return state.shouldUnsubscribe;
    }

    public static implicit operator EventHandlerResult(bool value)
    {
        return new EventHandlerResult(value);
    }
}

Anche se questo ha alcuni problemi ovvi come;  Se cambio la firma, costringerebbe i consumatori a cambiare la firma dei gestori che hanno già scritto. Potrei comunque tenere entrambe le versioni.

Qualcuno là fuori ha qualche idea di un nome migliore per questo metodo? Inoltre, la struttura enum-like è un approccio buono / cattivo per guidare / forzare la chiarezza / l'intento del codice dei consumatori scritti della libreria?

    
posta Sindri Jóelsson 26.04.2018 - 19:49
fonte

3 risposte

1

Sembra che quando usi questo codice stai facendo questo:

Decidi che quando termina la fine dell'evento di dialogo, quando la condizione è vera, devi fare qualcosa, solo una volta, quindi annullare l'iscrizione.

Il problema con il suggerimento di Frank Hilemans di usare semplicemente un puntatore a funzione è che la condizione B possa cambiare stato nel tempo da quando decidi di quando si verifica l'evento A. Se questo non è un problema, ti preghiamo di trovare un modo per semplificare.

Altrimenti quello che stai veramente cercando qui è un aiuto semantico. Vuoi qualcosa di facile sugli occhi che i neofiti capiranno a colpo d'occhio. Mentre ci siamo, che ne dici di smettere di far scrivere alle persone il codice che segnala il risultato?

Action<DialogEvent.End> endHandler = null;

endHandler = EventBusLambdaBuilder
    .DoneWhen( e -> Condition(e) )
    .DoOnceThenUnsubscribe( e -> Something() )
    .Build() 
;

EventBus.Subscribe(endHandler);

Ora, se usi enumerazioni o meno non è un'idea che si sta diffondendo ovunque. L'implementazione è ben nascosta. Questo è un qualcosa chiamato DSL (Domain Specific Language). Ma aspetta, questo è C #. Hai argomenti denominati .

endHandler = EventBusLambdaBuilder(
    doneWhen: e -> Condition(e),
    doOnceThenUnsubscribe: e -> Something()
);

Se ciò fornisce tutta la flessibilità di cui hai bisogno, non è necessario andare fino a un DSL. Rende comunque ciò che sta per accadere chiaro senza precisare come.

    
risposta data 27.04.2018 - 20:17
fonte
1

Stai reinventando la ruota un po 'qui.

Usando System.Reactive.Linq hai il seguente nome e approccio.

Questo

EventBus.SubscribeUntil<DialogEvent.End>(e =>
{
    if (e.Dialog.Name == "MyDialog")
    {
        DoSomething();
        return EventHandlerResult.Unsubscribe;
    }
    return EventHandlerResult.KeepSubscribed;
});

Supponendo che EventBus sia impostato su IObservable diventa questo:

EventBus.Where(e=>e.Dialog.Name=="MyDialog").Take(1).Subscribe(_=>DoSomething());

Se sei preoccupato per le dipendenze, le prestazioni, ecc., potresti forse esaminare il tuo approccio Linq-to-YourEventBus.

    
risposta data 28.05.2018 - 17:52
fonte
0

Non utilizzare il nome, utilizzare il tipo di delegato

Il naming è una cosa che hai a tua disposizione. Ma hai anche il delegato e i tipi di eventi che puoi usare. Hai ragione di pensare che usare solo il nome è debole; usare il sistema dei tipi a tuo vantaggio è molto più strong.

Suggerisco di definire un tipo di delegato specifico per i gestori che possono annullare la sottoscrizione e fornire a tale delegato un mezzo semplice e facile per eseguire la disiscrizione. In questo modo, l'intenzione viene comunicata agli sviluppatori, che saranno costretti a utilizzare il tipo di delegato speciale e saranno ovvi per chiunque legga il codice. Inoltre, avrai la sicurezza aggiuntiva del controllo in fase di compilazione.

Inoltre, questa tecnica consente di annullare l'iscrizione per essere iniettata nel gestore, offrendo tutti i vantaggi dell'iniezione delle dipendenze-- il gestore, ad esempio, non avrà capito quale istanza di EventBus chiamare per annullare l'iscrizione e puoi facilmente prendere in giro l'interfaccia di annullamento in fase di test delle unità.

Ad esempio:

//Define an interface that we can pass to the handler
public interface IUnsubscribe
{
    void Unsubscribe(TransientEventHandler handler);
}

//Define the handler that accepts our new interface
public delegate  void TransientEventHandler(object sender, EventArgs e, IUnsubscribe unsubscribe);

//Implement the interface in the bus itself
public class EventBus : IUnsubscribe
{
    protected event TransientEventHandler Clicked;

    //Allow regular handlers
    public void Subscribe(EventHandler handler)
    {
        Clicked += delegate (object sender, EventArgs e, IUnsubscribe bus) { handler(sender, e); };
    }

    //Allow transient handlers
    public void Subscribe(TransientEventHandler handler)
    {
        Clicked += handler;
    }

    //Raise the event
    public void OnClicked()
    {
        if (Clicked != null)
        {
            Clicked(this, new EventArgs(), this);
        }
    }

    //Support unsubscription
    public void Unsubscribe(TransientEventHandler handler)
    {
        Clicked -= handler;
    }
}

//Now test it all
public class Program
{
    static private bool unsub = false;

    private static void SimulateClick(EventBus bus)
    {
        bus.OnClicked();
    }

    public static void MyHandler(object sender, EventArgs e, IUnsubscribe unsubscribe)
    {
        Console.WriteLine("MyHandler - Handling click");
        if (unsub)
        {
            Console.WriteLine("MyHandler - Unsubscribing");
            unsubscribe.Unsubscribe(MyHandler);
        }
    }

    public static void Main()
    {
        var e = new EventBus();
        e.Subscribe(MyHandler);

        Console.WriteLine("Main - sending click");
        SimulateClick(e);

        Console.WriteLine("Main - telling handler to unsubscribe next time it runs");
        unsub = true;

        Console.WriteLine("Main - sending click");
        SimulateClick(e);

        Console.WriteLine("Main - sending click");
        SimulateClick(e);

        Console.WriteLine("Main - sending click");
        SimulateClick(e);
    }

}

Output:

Main - sending click
MyHandler - Handling click
Main - telling handler to unsubscribe next time it runs
Main - sending click
MyHandler - Handling click
MyHandler - Unsubscribing
Main - sending click
Main - sending click

Esempio di codice su DotNetFiddle

P.S. per quanto riguarda la tua domanda iniziale, che riguardava la denominazione, suggerirei un nome come Transitorio o Temporaneo , ad es. TransientEventHandler .

    
risposta data 27.04.2018 - 23:42
fonte

Leggi altre domande sui tag