2013-02-12 19 views
21

Sono in procinto di rifattorizzare un mostruoso servizio WCF in qualcosa di più gestibile. Al momento della scrittura, il servizio impiega circa 9 dipendenze tramite costruttore, il che rende l'unità molto difficile da testare.Rifacimento servizio WCF "procedurale"

Il servizio sta gestendo lo stato locale tramite la macchina a stati, esegue la convalida sui parametri, genera eccezioni di errore, esegue l'operazione effettiva e genera eventi di pubblicazione tramite un pub/canale secondario. Questo codice è molto simile a tutte le altre chiamate di servizio.

Mi rendo conto che posso fare diverse di queste cose (convalida argomento, notifiche pub/sub) in modo diverso, forse tramite Aspect-Oriented Programming o comportamenti WCF, ma il mio istinto mi dice che l'approccio generale è sbagliato - questo sembra troppo "procedurale" ".

Il mio obiettivo è separare l'esecuzione dell'operazione effettiva da cose come le notifiche pub/sub e forse anche la gestione degli errori.

Mi chiedo se gli acronimi come DDD o CQRS o altre tecniche possono aiutare qui? Sfortunatamente, non conosco molto bene quei concetti che vanno oltre la definizione.

Ecco un (semplificato) esempio di una tale operazione WCF:

public void DoSomething(DoSomethingData data) 
{ 
    if (!_stateMachine.CanFire(MyEvents.StartProcessing)) 
    { 
     throw new FaultException(...); 
    } 

    if (!ValidateArgument(data)) 
    { 
     throw new FaultException(...); 
    } 

    var transitionResult = 
     _stateMachine.Fire(MyEvents.StartProcessing); 

    if (!transitionResult.Accepted) 
    { 
     throw new FaultException(...); 
    } 

    try 
    { 
     // does the actual something 
     DoSomethingInternal(data); 

     _publicationChannel.StatusUpdate(new Info 
     { 
      Status = transitionResult.NewState 
     }); 
    } 
    catch (FaultException<MyError> faultException) 
    { 
     if (faultException.Detail.ErrorType == 
      MyErrorTypes.EngineIsOffline) 
     { 
      TryFireEvent(MyServiceEvent.Error, 
       faultException.Detail); 
     } 
     throw; 
    } 
} 

risposta

43

Quello che hai lì è un grande esempio di un comando sotto mentite spoglie. È bello sapere che cosa stai facendo qui, il tuo metodo di servizio utilizza già un singolo argomento DoSomethingData. Questo è il tuo messaggio di comando.

Cosa ti manca qui è un'astrazione generale su gestori di comandi:

public interface ICommandHandler<TCommand> 
{ 
    void Handle(TCommand command); 
} 

Con un po 'di refactoring, il metodo di servizio sarebbe simile a questa:

// Vanilla dependency. 
ICommandHandler<DoSomethingData> doSomethingHandler; 

public void DoSomething(DoSomethingData data) 
{ 
    this.doSomethingHandler.Handle(data); 
} 

E naturalmente hai bisogno di un'implementazione per ICommandHandler<DoSomethingData>. Nel tuo caso sarà simile a questa:

public class DoSomethingHandler : ICommandHandler<DoSomethingData> 
{ 
    public void Handle(DoSomethingData command) 
    { 
     // does the actual something 
     DoSomethingInternal(command); 
    } 
} 

Ora si potrebbe chiedere, che dire di quelle preoccupazioni trasversali implementato come la convalida degli argomenti, la lattina fuoco, aggiornamento di stato del canale di pubblicazione e la gestione degli errori. Beh sì, sono tutti problemi trasversali, sia la tua classe di servizio WCF che la tua logica di business (lo DoSomethingHandler) non dovrebbero preoccuparsene.

Esistono diversi modi per applicare la programmazione orientata agli aspetti. Ad alcuni piace usare strumenti di tessitura del codice come PostSharp. Il rovescio della medaglia di questi strumenti è che rendono i test unitari molto più difficili, dal momento che intrecciate tutte le vostre preoccupazioni trasversali.

Il secondo modo è usare l'intercettazione. Utilizzo della generazione di proxy dinamici e qualche riflessione. C'è tuttavia una variazione di questo che mi piace di più, e cioè applicando i decoratori. La cosa bella di questo è che questo è nella mia esperienza il modo più pulito di applicare i problemi trasversali.

Diamo uno sguardo a un decoratore per la convalida:

public class WcfValidationCommandHandlerDecorator<T> : ICommandHandler<T> 
{ 
    private IValidator<T> validator; 
    private ICommandHandler<T> wrapped; 

    public ValidationCommandHandlerDecorator(IValidator<T> validator, 
     ICommandHandler<T> wrapped) 
    { 
     this.validator = validator; 
     this.wrapped = wrapped; 
    } 

    public void Handle(T command) 
    { 
     if (!this.validator.ValidateArgument(command)) 
     { 
      throw new FaultException(...); 
     } 

     // Command is valid. Let's call the real handler. 
     this.wrapped.Handle(command); 
    } 
} 

Dal momento che questo WcfValidationCommandHandlerDecorator<T> è un tipo generico, siamo in grado di avvolgere intorno ad ogni gestore di comandi. Per esempio:

var handler = new WcfValidationCommandHandlerDecorator<DoSomethingData>(
    new DoSomethingHandler(), 
    new DoSomethingValidator()); 

E si può facilmente creare un decoratore che gestisce tutte le eccezioni sollevate:

public class WcfExceptionHandlerCommandHandlerDecorator<T> : ICommandHandler<T> 
{ 
    private ICommandHandler<T> wrapped; 

    public ValidationCommandHandlerDecorator(ICommandHandler<T> wrapped) 
    { 
     this.wrapped = wrapped; 
    } 

    public void Handle(T command) 
    { 
     try 
     { 
      // does the actual something 
      this.wrapped.Handle(command); 

      _publicationChannel.StatusUpdate(new Info 
      { 
       Status = transitionResult.NewState 
      }); 
     } 
     catch (FaultException<MyError> faultException) 
     { 
      if (faultException.Detail.ErrorType == MyErrorTypes.EngineIsOffline) 
      { 
       TryFireEvent(MyServiceEvent.Error, faultException.Detail); 
      } 

      throw; 
     } 
    } 
} 

Hai visto come ho appena avvolto il codice in questo decoratore? ancora una volta siamo in grado di utilizzare questo decoratore per avvolgere l'originale:

var handler = 
    new WcfValidationCommandHandlerDecorator<DoSomethingData>(
     new WcfExceptionHandlerCommandHandlerDecorator<DoSomethingData>(
      new DoSomethingHandler()), 
    new DoSomethingValidator()); 

Naturalmente tutto questo sembra come un sacco di codice e se hai a disposizione solo un unico metodo di servizio WCF che sì, questo è probabilmente eccessivo. Ma inizia a diventare davvero interessante se ne hai una dozzina o giù di lì. Se ne hai centinaia? Bene .. Io non voglio essere lo sviluppatore che mantiene quel codice se non stai usando una tecnica come questa.

Quindi dopo alcuni minuti di refactoring si finisce con le classi di servizio WCF che dipendono solo dalle interfacce ICommandHandler<TCommand>. Tutte le preoccupazioni trasversali saranno collocate nei decoratori e, naturalmente, tutto è cablato insieme dalla libreria DI. Credo che si conosce un paio ;-)

Quando hai fatto questo, v'è probabilmente una cosa che si potrebbe migliorare, perché tutte le classi di servizio WCF inizieranno a guardare noiosamente la stessa:

// Vanilla dependency. 
ICommandHandler<FooData> handler; 

public void Foo(FooData data) 
{ 
    this.handler.Handle(data); 
} 

Sarà iniziare a diventare noioso per scrivere nuovi comandi e nuovi gestori. Avrai comunque il tuo servizio WCF da mantenere.

Che cosa si può fare, invece, è creare un servizio WCF con una singola classe con un solo metodo, in questo modo:

[ServiceKnownType("GetKnownTypes")] 
public class CommandService 
{ 
    [OperationContract] 
    public void Execute(object command) 
    { 
     Type commandHandlerType = typeof(ICommandHandler<>) 
      .MakeGenericType(command.GetType()); 

     dynamic commandHandler = Bootstrapper.GetInstance(commandHandlerType); 

     commandHandler.Handle((dynamic)command); 
    } 

    public static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider) 
    { 
     // create and return a list of all command types 
     // dynamically using reflection that this service 
     // must accept. 
    } 
} 

Ora tutto quello che hai è un servizio WCF con un unico metodo che non cambierà mai . Il numero ServiceKnownTypeAttribute punta allo GetKnownTypes. WCF chiamerà questo metodo all'avvio per vedere quali tipi deve accettare. Quando si restituisce l'elenco in base ai metadati dell'applicazione, è possibile aggiungere e rimuovere comandi al sistema, senza dover modificare una singola riga nel servizio WCF.

Probabilmente verranno aggiunti di volta in volta nuovi decoratori specifici di WCF e in genere questi dovranno essere inseriti nel servizio WCF. Gli altri decoratori sarebbero probabilmente più generici e potrebbero essere collocati nello stesso livello aziendale. Ad esempio, potrebbero essere riutilizzati dall'applicazione MVC.

La tua domanda era un po 'su CQRS ma la mia risposta non ha nulla a che fare con questo. Bene ... niente è un'esagerazione. CQRS utilizza ampiamente questo modello, ma CQRS fa un ulteriore passo avanti. CQRS riguarda domini collaborativi che ti obbligano a mettere in coda i comandi e elaborarli in modo asincrono. D'altra parte, la mia risposta riguarda solo l'applicazione dei principi di progettazione SOLID. SOLID è buono ovunque Non solo in domini collaborativi.

Se si desidera saperne di più, si prega di leggere il mio articolo sull'applicazione di command handlers. Dopodiché, vai avanti e leggi my article about applying this principle to WCF services. La mia risposta è un riassunto di quegli articoli.

Buona fortuna.

+1

Wow, grazie mille, Steven, per la tua risposta incredibilmente dettagliata! Ho pensato di implementare il modello di ICommand/ICommandHandler, volevo solo assicurarmi che fosse il modo "idiomatico" di gestirlo. Sono contento che tu abbia rassicurato che avevo ragione :) –

+0

Questo lascia solo la domanda se dovrei incapsulare l'interrogazione della macchina di stato, in qualche modo. Ma a questo punto, lo lascerò nel servizio stesso. –

+0

Non posso davvero commentarlo. Non ho idea di cosa sia questa macchina statale. Ma scommetto che puoi incapsularlo in un decoratore. Ancora meglio, quando capisci questo schema, inizi a vedere tutti i nuovi miglioramenti che puoi apportare al tuo codice. Benvenuto nella tua nuova vita ;-) – Steven