Pattern Tester / Doer: supponi che il chiamante sia conforme al modello o sii sulla difensiva e ripeti il controllo?

4

Assumi una classe semplice che implementa il pattern Tester / Doer:

public class FooCommandHandler : ICommandHandler
{
    public bool CanHandle(object command)
    {
        return command is FooCommand;
    }

    public void Handle(object command)
    {
        var fooCommand = (FooCommand)command;
        // Do something with fooCommand
    }
}

Ora, se qualcuno non si conforma al modello e chiama Handle senza verificare il comando tramite CanHandle , il codice in Handle genera un'eccezione.

Tuttavia, a seconda dell'implementazione effettiva di Handle , può trattarsi di un'intera gamma di eccezioni diverse.

La seguente implementazione controllerebbe nuovamente CanHandle in Handle e genererebbe un'eccezione descrittiva :

public void Handle(object command)
{
    if(!CanHandle(command))
        throw new TesterDoerPatternUsageViolationException("Please call CanHandle first");

    // actual implementation of handling the command.
}

Questo ha il vantaggio che l'eccezione è molto descrittiva.
Ha lo svantaggio che CanHandle è chiamato due volte per i client "buoni".

Esiste un consenso su quale variante dovrebbe essere utilizzata?

    
posta Daniel Hilgarth 26.06.2013 - 18:10
fonte

2 risposte

5

Se non puoi garantire che Handle abbia sempre qualcosa che può gestire, non hai altra scelta che ripetere il controllo chiamando CanHandle da Handle .

Ciò non significa che devi sempre subire la penalità del doppio controllo. Se metti la chiamata a CanHandle in un'istruzione Assert , le opzioni del compilatore idonee possono garantire che vengano eseguite solo nelle build di debug e rimosse dalle build di rilascio.

In questo modo gli sviluppatori vengono schiaffeggiati al polso quando sbagliano, mentre il codice di produzione non deve preoccuparsi del sovraccarico. Un avvertimento: è necessario disporre di una potente suite di test (unità) per garantire che tutte le chiamate a Handle siano controllate dall'asserzione.

In realtà, potrebbe essere meglio evitare del tutto il dilemma.

Il problema con una classe con un metodo che convalida X, e un altro metodo che elabora X è che tu rendi questa classe estremamente dipendente dai suoi clienti e devi prendere delle misure per assicurarti che le persone usino la tua classe come era intesa da usare.

Se rendi Handle in sé responsabile di restituire se il lavoro è accettabile e verrà eseguito, quindi verificare se il lavoro ricevuto è adatto diventa una parte naturale di come Handle dovrebbe essere implementato.

Il contratto di Handle ora include ovviamente il rifiuto di un lavoro inadeguato e qualsiasi test per gli implementatori dell'interfaccia ICommandHandler verificherebbe che la sua implementazione Handle risponda come desiderato sia quando viene consegnato un lavoro inadeguato adatto sia quando è consegnato.

Utilizzando questo approccio puoi eliminare completamente il metodo CanHandle dall'interfaccia. CanHandle può ancora esistere in una classe che implementa il ICommandHandler , ma se lo fa, ora è solo un dettaglio di implementazione di quella classe.

Ho scritto una discussione un po 'più ampia di questo qui: Disegnare l'enigma di CanHandle-Handle

    
risposta data 26.06.2013 - 23:04
fonte
0

Sono dell'opinione che non ci sia bisogno di questo modello specifico in C #, a causa delle caratteristiche del linguaggio. Ecco come lo implementerei:

public class CommandManager
{
    private List<ICommandHandler> handlers = new List<ICommandHandler>();

    public void RegisterHandler(ICommandHandler handler) { handlers.Add(handler); }
    public void Dispatch<T>(T command)where T : ICommand { 
        foreach (var handler in handlers.OfType<ICommandHandler<T>>()) handler.Handle(command);
    }
}

public interface ICommand { }
public class FooCommand : ICommand { }
public class BarCommand : ICommand { }


public interface ICommandHandler { }
public interface ICommandHandler<T> : ICommandHandler where T : ICommand
{
    void Handle(T command);
}

public class FooCommandHandler : ICommandHandler<FooCommand>, ICommandHandler<BarCommand>
{
    public void Handle(FooCommand command)
    {
        Console.WriteLine("Called FooCommandHandler with FooCommand");
    }
        public void Handle(BarCommand command)
    {
        Console.WriteLine("Called FooCommandHandler with BarCommand");
    }
}

public class BarCommandHandler : ICommandHandler<BarCommand>
{

    public void Handle(BarCommand command)
    {
        Console.WriteLine("Called BarCommandHandler with BarCommand");
    }
}

L'esecuzione sarebbe come questa:

void Main()
{
    var myCommands = new CommandManager();
    myCommands.RegisterHandler(new FooCommandHandler());
    myCommands.RegisterHandler(new BarCommandHandler());

    myCommands.Dispatch(new FooCommand());
    /* Output:
         Called FooCommandHandler with FooCommand
    */
    myCommands.Dispatch(new BarCommand());
    /* Output:
        Called FooCommandHandler with BarCommand
        Called BarCommandHandler with BarCommand
    */
}   

OfType<>() gestisce il tuo controllo per te: filtrerà automaticamente solo i tipi che possono gestire il comando specificato e convertiti in quel tipo in modo da poter chiamare Handle() . Aggiungi solo un singolo sovraccarico di Handle per ogni comando specifico che può gestire, quindi non devi preoccuparti che venga chiamato con un comando non valido.

    
risposta data 27.06.2013 - 00:06
fonte

Leggi altre domande sui tag