Windsor - 從容器中拉出瞬態對象
如何從容器中拉出本質上是瞬態的對象?我是否必須在容器中註冊它們並註入所需類的建構子?將所有內容都注入建構子感覺並不好。也只是對於一個類,我不想創建一個
TypedFactory並將工廠注入到需要的類中。我想到的另一個想法是根據需要“新建”它們。但我也將一個
Logger組件(通過屬性)注入到我的所有類中。因此,如果我更新它們,我將不得不手動實例化Logger這些類。我怎樣才能繼續為我的所有課程使用容器?***記錄器注入:***我的大多數類都
Logger定義了屬性,除非有繼承鏈(在這種情況下,只有基類具有此屬性,並且所有派生類都使用該屬性)。當這些通過 Windsor 容器實例化時,它們會將我的實現ILogger注入到它們中。//Install QueueMonitor as Singleton Container.Register(Component.For<QueueMonitor>().LifestyleSingleton()); //Install DataProcessor as Trnsient Container.Register(Component.For<DataProcessor>().LifestyleTransient()); Container.Register(Component.For<Data>().LifestyleScoped()); public class QueueMonitor { private dataProcessor; public ILogger Logger { get; set; } public void OnDataReceived(Data data) { //pull the dataProcessor from factory dataProcessor.ProcessData(data); } } public class DataProcessor { public ILogger Logger { get; set; } public Record[] ProcessData(Data data) { //Data can have multiple Records //Loop through the data and create new set of Records //Is this the correct way to create new records? //How do I use container here and avoid "new" Record record = new Record(/*using the data */); ... //return a list of Records } } public class Record { public ILogger Logger { get; set; } private _recordNumber; private _recordOwner; public string GetDescription() { Logger.LogDebug("log something"); // return the custom description } }問題:
- 如何在
Record不使用“new”的情況下創建新對象?QueueMonitor是Singleton,而是Data“作用域”。如何注入Data方法OnDataReceived()?
從您提供的範例中很難非常具體,但總的來說,當您將
ILogger實例注入大多數服務時,您應該問自己兩件事:
- 我記錄太多了嗎?
- 我是否違反了 SOLID 原則?
1. 我是否記錄太多
當你有很多這樣的程式碼時,你記錄的太多了:
try { // some operations here. } catch (Exception ex) { this.logger.Log(ex); throw; }編寫這樣的程式碼是出於對失去錯誤資訊的擔憂。然而,在所有地方複製這些類型的 try-catch 塊並沒有幫助。
throw更糟糕的是,我經常看到開發人員通過刪除最後一條語句來登錄並繼續:try { // some operations here. } catch (Exception ex) { this.logger.Log(ex); // <!-- No more throw. Execution will continue. }在大多數情況下,這是一個壞主意(並且聞起來像舊的 VB
ON ERROR RESUME NEXT行為),因為在大多數情況下,您根本沒有足夠的資訊來確定它是否安全繼續。通常程式碼中存在錯誤或外部資源(如數據庫)中的故障導致操作失敗。繼續意味著使用者經常得到操作成功的想法,而實際上並沒有。問問自己:更糟糕的是,向使用者顯示一條通用的錯誤消息,說明出現問題並要求他們重試,或者默默地跳過錯誤並讓使用者認為他們的請求已成功處理?想想如果使用者在兩週後發現他們的訂單從未發貨,他們會有什麼感受。你可能會失去一個客戶。或者更糟糕的是,一個病人的MRSA註冊默默地失敗,導致病人沒有被護士隔離,導致其他病人被污染,造成高成本甚至死亡。
大多數這些類型的 try-catch-log 行應該被刪除,你應該簡單地讓異常在呼叫堆棧中冒泡。
你不應該登錄嗎?你絕對應該!但如果可以,請在應用程序頂部定義一個 try-catch 塊。使用 ASP.NET,您可以實現
Application_Error事件、註冊HttpModule或定義執行日誌記錄的自定義錯誤頁面。使用 Win Forms,解決方案有所不同,但概念保持不變:定義一個單一的頂級包羅萬象。但是,有時您仍然希望擷取並記錄某種類型的異常。我過去工作的一個系統讓業務層拋出
ValidationExceptions,這將被表示層擷取。這些異常包含向使用者顯示的驗證資訊。由於這些異常會在表示層中被擷取和處理,因此它們不會冒泡到應用程序的最頂層,也不會最終出現在應用程序的包羅萬象的程式碼中。我仍然想記錄這些資訊,只是為了了解使用者輸入無效資訊的頻率,並了解是否出於正確的原因觸發了驗證。所以這不是錯誤記錄;只是記錄。我編寫了以下程式碼來執行此操作:try { // some operations here. } catch (ValidationException ex) { this.logger.Log(ex); throw; }看起來很熟悉?是的,看起來和前面的程式碼片段完全一樣,不同的是我只擷取了
ValidationException異常。但是,僅查看此程式碼段無法看出另一個差異。應用程序中只有一個地方包含該程式碼!這是一個裝飾器,這讓我想到了你應該問自己的下一個問題:2. 我是否違反了 SOLID 原則?
諸如日誌記錄、審計和安全性之類的東西稱為橫切關注點(或方面)。它們被稱為橫切,因為它們可以橫切應用程序的許多部分,並且必須經常應用於系統中的許多類。但是,當您發現您正在編寫程式碼供系統中的許多類使用時,您很可能違反了 SOLID 原則。舉個例子:
public void MoveCustomer(int customerId, Address newAddress) { var watch = Stopwatch.StartNew(); // Real operation this.logger.Log("MoveCustomer executed in " + watch.ElapsedMiliseconds + " ms."); }在這裡,您測量執行
MoveCustomer操作所需的時間並記錄該資訊。系統中的其他操作很可能需要同樣的橫切關注點。您開始為您的 、 、 和其他案例添加這樣的程式碼ShipOrder,CancelOrder這CancelShipping會導致大量程式碼重複,並最終導致維護噩夢(我去過那裡。)這段程式碼的問題可以追溯到違反SOLID原則。SOLID 原則是一組物件導向的設計原則,可幫助您定義靈活且可維護(物件導向)的軟體。該
MoveCustomer範例至少違反了其中兩個規則:
- 單一職責原則(SRP)——類應該有單一職責。然而,持有該
MoveCustomer方法的類不僅包含核心業務邏輯,而且還測量執行操作所需的時間。換句話說,它有多重責任。- 開閉原則(OCP)——它規定了一種應用程序設計,可以防止您必須對整個程式碼庫進行徹底的更改;或者,在 OCP 的詞彙表中,一個類應該對擴展開放,但對修改關閉。如果您需要向案例添加異常處理(第三項職責)
MoveCustomer,您(再次)必須更改MoveCustomer方法。但是,您不僅需要更改MoveCustomer方法,而且還需要更改許多其他方法,因為它們通常需要相同的異常處理,因此這是一個徹底的改變。這個問題的解決方案是將日誌提取到它自己的類中,並允許該類包裝原始類:
// The real thing public class MoveCustomerService : IMoveCustomerService { public virtual void MoveCustomer(int customerId, Address newAddress) { // Real operation } } // The decorator public class MeasuringMoveCustomerDecorator : IMoveCustomerService { private readonly IMoveCustomerService decorated; private readonly ILogger logger; public MeasuringMoveCustomerDecorator( IMoveCustomerService decorated, ILogger logger) { this.decorated = decorated; this.logger = logger; } public void MoveCustomer(int customerId, Address newAddress) { var watch = Stopwatch.StartNew(); this.decorated.MoveCustomer(customerId, newAddress); this.logger.Log("MoveCustomer executed in " + watch.ElapsedMiliseconds + " ms."); } }通過將裝飾器包裹在真實實例周圍,您現在可以將此測量行為添加到類中,而無需更改系統的任何其他部分:
IMoveCustomerService service = new MeasuringMoveCustomerDecorator( new MoveCustomerService(), new DatabaseLogger());然而,前面的範例確實解決了部分問題(僅解決了 SRP 部分)。在編寫如上所示的程式碼時,您必須為系統中的所有操作定義單獨的裝飾器,最終會得到像
MeasuringShipOrderDecorator,MeasuringCancelOrderDecorator和MeasuringCancelShippingDecorator. 這再次導致大量重複程式碼(違反 OCP 原則),並且仍然需要為系統中的每個操作編寫程式碼。這裡缺少的是對系統中案例的通用抽象。缺少的是一個
ICommandHandler<TCommand>介面。讓我們定義這個介面:
public interface ICommandHandler<TCommand> { void Execute(TCommand command); }讓我們將方法的方法參數儲存
MoveCustomer到它自己的(參數對象)類中,稱為MoveCustomerCommand:public class MoveCustomerCommand { public int CustomerId { get; set; } public Address NewAddress { get; set; } }讓我們把
MoveCustomer方法的行為放在一個實現的類中ICommandHandler<MoveCustomerCommand>:public class MoveCustomerCommandHandler : ICommandHandler<MoveCustomerCommand> { public void Execute(MoveCustomerCommand command) { int customerId = command.CustomerId; Address newAddress = command.NewAddress; // Real operation } }起初這可能看起來很奇怪,但是因為您現在對案例有了一個通用的抽象,您可以將您的裝飾器重寫為以下內容:
public class MeasuringCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand> { private ILogger logger; private ICommandHandler<TCommand> decorated; public MeasuringCommandHandlerDecorator( ILogger logger, ICommandHandler<TCommand> decorated) { this.decorated = decorated; this.logger = logger; } public void Execute(TCommand command) { var watch = Stopwatch.StartNew(); this.decorated.Execute(command); this.logger.Log(typeof(TCommand).Name + " executed in " + watch.ElapsedMiliseconds + " ms."); } }這個新
MeasuringCommandHandlerDecorator<T>的看起來很像MeasuringMoveCustomerDecorator,但是這個類可以被系統中的所有命令處理程序重用:ICommandHandler<MoveCustomerCommand> handler1 = new MeasuringCommandHandlerDecorator<MoveCustomerCommand>( new MoveCustomerCommandHandler(), new DatabaseLogger()); ICommandHandler<ShipOrderCommand> handler2 = new MeasuringCommandHandlerDecorator<ShipOrderCommand>( new ShipOrderCommandHandler(), new DatabaseLogger());這樣,將橫切關注點添加到您的系統中會容易得多。在您的合成根中創建一個方便的方法非常容易,該方法可以將任何創建的命令處理程序與系統中適用的命令處理程序包裝起來。例如:
private static ICommandHandler<T> Decorate<T>(ICommandHandler<T> decoratee) { return new MeasuringCommandHandlerDecorator<T>( new DatabaseLogger(), new ValidationCommandHandlerDecorator<T>( new ValidationProvider(), new AuthorizationCommandHandlerDecorator<T>( new AuthorizationChecker( new AspNetUserProvider()), new TransactionCommandHandlerDecorator<T>( decoratee)))); }此方法可按如下方式使用:
ICommandHandler<MoveCustomerCommand> handler1 = Decorate(new MoveCustomerCommandHandler()); ICommandHandler<ShipOrderCommand> handler2 = Decorate(new ShipOrderCommandHandler());但是,如果您的應用程序開始增長,那麼使用 DI 容器引導它會很有用,因為 DI 容器可以支持自動註冊。這使您不必為添加到系統的每個新命令/處理程序對更改合成根。
大多數現代、成熟的 .NET DI 容器對裝飾器都有相當不錯的支持,尤其是 Autofac ( example ) 和 Simple Injector ( example ) 使得註冊開放通用裝飾器變得容易。
另一方面,Unity 和 Castle 具有動態攔截設施(就像 Autofac 對 btw 所做的那樣)。動態攔截與裝飾有很多共同之處,但它在幕後使用動態代理生成。這可能比使用泛型裝飾器更靈活,但是在可維護性方面你要付出代價,因為你經常失去類型安全性,而攔截器總是迫使你依賴於攔截庫,而裝飾器是類型安全的並且可以在不依賴外部庫的情況下編寫。
我已經使用這些類型的設計十多年了,如果沒有它,我就無法設計我的應用程序。我寫了大量關於這些設計的文章,最近,我與人合著了一本名為Dependency Injection Principles, Practices, and Patterns的書,其中更詳細地介紹了這種 SOLID 程式風格和上述設計(參見第 10 章)。