2013-02-12 39 views
21

我試圖將一個怪異的WCF服務重構成更易於管理的東西。 在撰寫本文時,該服務通過構造函數需要約9個依賴關係,這使得單元測試非常困難。重構「程序化」WCF服務

該服務正在通過狀態機處理本地狀態,對參數進行驗證,拋出錯誤異常,執行實際操作並通過發佈/訂閱通道觸發發佈事件。此代碼在所有其他服務調用中非常相似。

我意識到我可以通過Aspect-Oriented Programming或WCF行爲以不同的方式做一些事情(參數驗證,發佈/訂閱通知),但我的直覺告訴我,一般方法是錯誤的 - 這種感覺太「程序化」。

我的目標是將實際操作的執行與pub/sub通知等事情分開,甚至可能還包括錯誤處理。

我不知道像DDDCQRS或其他技術的縮略詞可以幫助嗎?不幸的是,我不太瞭解定義之外的那些概念。

這裏有一個這樣的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; 
    } 
} 

回答

43

你有什麼有一種變相的命令的一個很好的例子。很高興你在這裏做的是你的服務方法已經在一個參數DoSomethingData。這您的命令消息。

什麼你錯過這裏是在命令處理程序的一般抽象:

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

隨着重構的一點點,你的服務的方法是這樣的:

// Vanilla dependency. 
ICommandHandler<DoSomethingData> doSomethingHandler; 

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

,當然還有您需要執行ICommandHandler<DoSomethingData>。在你的情況下,它看起來就像這樣:

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

現在,你可能會想,怎麼樣的橫切關注你喜歡實現參數驗證,罐火,出版渠道狀態更新和錯誤處理。好吧,他們都是交叉問題,你的WCF服務類和你的業務邏輯(DoSomethingHandler)都不應該擔心這一點。

有幾種應用面向方面編程的方法。有些人喜歡使用PostSharp等代碼編織工具。這些工具的缺點是它們使得單元測試變得更加困難,因爲你將所有的交叉問題編織在一起。

第二種方法是通過使用攔截。使用動態代理生成和一些反射。然而,我更喜歡這種變化,那就是應用裝飾器。關於這一點的好處是,這是我的經驗,應用橫切關注的最乾淨的方式。

讓我們來看看你的驗證一個裝飾:

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); 
    } 
} 

由於這WcfValidationCommandHandlerDecorator<T>是一個通用的類型,我們可以繞到它的每一個命令處理程序。例如:

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

而且你可以爲輕鬆地創建處理任何拋出的異常裝飾器:

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; 
     } 
    } 
} 

你怎麼看我剛剛結束你的代碼在這個裝飾?我們可以再次使用這個裝飾包裹原文:

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

當然,這一切似乎像一個可怕的很多的代碼,如果你有比是一個單一的WCF服務的方法,這可能是矯枉過正。但如果你有十幾個,它開始變得非常有趣。如果你有幾百?那麼..如果你不使用這種技術,我不想成爲開發人員維護代碼庫。

因此,經過幾分鐘的重構後,您將獲得僅取決於ICommandHandler<TCommand>接口的WCF服務類。所有的交叉問題將被放置在裝飾器中,當然,所有事情都通過DI庫連接在一起。我想你知道幾個;-)

當你這樣做,有可能是一件事,你可以改善,因爲所有的WCF服務類將開始尋找乏味相同:

// Vanilla dependency. 
ICommandHandler<FooData> handler; 

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

它將開始無聊寫新的命令和新的處理程序。你仍然有你的WCF服務來維護。

你能做什麼,而不是,是創建一個類的WCF服務有一個方法,像這樣:

[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. 
    } 
} 

現在你已經是一個單一的方法WCF服務,它永遠不會改變。 ServiceKnownTypeAttribute指向GetKnownTypes。 WCF將在啓動時調用此方法以查看它必須接受的類型。當您根據應用程序元數據返回列表時,它允許您向系統添加和刪除命令,而無需更改WCF服務中的單行。

您可能會偶爾添加新的WCF特定裝飾器,而這些裝飾器通常應放置在WCF服務中。其他裝飾器可能會更通用,可能會放置在業務層本身。例如,它們可能會被您的MVC應用程序重用。

你的問題有點關於CQRS,但我的答案與它無關。那麼......沒有什麼是多餘的。 CQRS廣泛使用這種模式,但CQRS更進一步。 CQRS是關於協作域的,它強制你排隊命令並且異步處理它們。另一方面,我的回答只是應用了設計原理。無處不在。不僅在協作領域。

如果您想了解更多關於此的信息,請閱讀我關於申請command handlers的文章。之後,繼續閱讀my article about applying this principle to WCF services。我的回答是這些文章的摘要。

祝你好運。

+1

哇,非常感謝你,史蒂文,爲你的難以置信的詳細答案!我想過實現'ICommand/ICommandHandler'模式,我只是想確保這是處理這種情況的「慣用」方式。我很高興你讓我放心,我是對的:) – 2013-02-12 12:11:47

+0

這只是留下了我是否應該封裝狀態機查詢,不知何故的問題。但在這一點上,我將把它留在服務中。 – 2013-02-12 12:25:44

+0

我真的不能對此發表評論。我不知道你的狀態機是什麼。但我敢打賭,你可以將它封裝在裝飾器中。更好的是,當你掌握了這種模式時,你會發現你可以對代碼進行各種新的改進。歡迎來到你的新生活;-) – Steven 2013-02-12 12:28:24