從你給它的樣本是很難很具體,但在一般情況下,當你注入情況到大多數的服務,你應該問自己兩個東西:
- 我是否登錄過許多?
- 我是否違反了SOLID原則?
1.我登錄了太多
你記錄太多,當你有這樣的大量的代碼:這樣
try
{
// some operations here.
}
catch (Exception ex)
{
this.logger.Log(ex);
throw;
}
編寫代碼來自關注丟失錯誤信息。但是,將這些try-catch塊複製到所有地方並沒有幫助。更糟的是,我經常看到開發者登錄並繼續(他們刪除了最後的throw
聲明)。這真的很糟糕(而且聞起來像舊的VB ON ERROR RESUME NEXT
),因爲在大多數情況下,您根本沒有足夠的信息來確定它是否安全。通常,代碼中存在導致操作失敗的錯誤。繼續意味着用戶經常意識到操作成功,而沒有。問問自己:更糟糕的是,向用戶顯示一條通用錯誤消息,指出出現問題,或者默默跳過錯誤並讓用戶認爲他的請求已成功處理?想想如果兩週後他發現他的訂單從未發貨,用戶將會感覺如何。你可能會失去一位顧客。或者更糟糕的是,病人的註冊安靜地失敗了,導致患者不能被護理隔離,並導致其他患者的污染,造成高成本甚至死亡。
大多數這類try-catch-log行應該被刪除,你應該簡單地讓異常向上調用堆棧。
不應該登錄嗎?你絕對應該!但是,如果可以,請在應用程序的頂部定義一個try-catch塊。使用ASP.NET,您可以實現Application_Error
事件,註冊HttpModule
或定義執行日誌記錄的自定義錯誤頁面。使用Win Forms解決方案是不同的,但概念保持不變:定義一個最頂級的全能型。
但是,有時您仍然希望捕獲並記錄某種類型的異常。我過去一直在使用的系統,讓業務層拋出ValidationException
,這將被表示層捕獲。這些例外包含用於向用戶顯示的驗證信息。由於這些異常會在表示層中被捕獲和處理,因此它們不會冒泡到應用程序的最頂部,也不會在應用程序的全部代碼中結束。儘管如此,我還是想記錄這些信息,只是爲了找出用戶輸入無效信息的頻率並找出是否出於正確原因觸發了驗證。所以這不是記錄錯誤;只是記錄。我寫了下面的代碼來做到這一點:
try
{
// some operations here.
}
catch (ValidationException ex)
{
this.logger.Log(ex);
throw;
}
看起來很熟悉嗎?是的,看起來和以前的代碼片段完全一樣,區別在於我只抓到了ValidationException
s。但是,還有另外一個不同之處,那就是隻看片段就看不到。包含該代碼的應用程序中只有一個地方!這是一個裝飾者,這讓我接下來的問題,你應該問自己:
2.我違反了固體原則?
諸如日誌,審計和安全等事情被稱爲cross-cutting concerns(或方面)。它們被稱爲交叉切割,因爲它們可以跨越應用程序的許多部分,並且必須經常應用於系統中的許多類。但是,當你發現你正在爲系統中的許多類編寫代碼時,你很可能違反了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
示例違反了這些規則中的至少兩個:
- Single Responsibility Principle。持有
MoveCustomer
方法的課程不僅可以移動客戶,還可以測量執行操作所需的時間。換句話說它有多重責任。您應該將測量值提取到自己的類中。
- The Open-Closed principle(OCP)。系統的行爲應該能夠在不改變任何現有代碼的情況下進行更改。當您還需要異常處理(第三方責任)時,您(再次)必須更改
MoveCustomer
方法,這違反了OCP。
除了違反SOLID原則,我們在這裏肯定違反了DRY原則,基本上說代碼重複是不好的,mkay。
這個問題的解決方案是將記錄提取到自己的類和允許類來包裝原始類:
// The real thing
public class MoveCustomerCommand
{
public virtual void MoveCustomer(int customerId, Address newAddress)
{
// Real operation
}
}
// The decorator
public class MeasuringMoveCustomerCommandDecorator : MoveCustomerCommand
{
private readonly MoveCustomerCommand decorated;
private readonly ILogger logger;
public MeasuringMoveCustomerCommandDecorator(
MoveCustomerCommand decorated, ILogger logger)
{
this.decorated = decorated;
this.logger = logger;
}
public override void MoveCustomer(int customerId, Address newAddress)
{
var watch = Stopwatch.StartNew();
this.decorated.MoveCustomer(customerId, newAddress);
this.logger.Log("MoveCustomer executed in " +
watch.ElapsedMiliseconds + " ms.");
}
}
通過包裝周圍的真實實例的裝飾,你現在可以添加此測量行爲類,沒有系統的任何其他部分改變:
MoveCustomerCommand command =
new MeasuringMoveCustomerCommandDecorator(
new MoveCustomerCommand(),
new DatabaseLogger());
前面的例子也不過只是解決問題(僅固體部分)的一部分。當編寫如上所示的代碼時,您必須爲系統中的所有操作定義修飾器,並且最終會得到修飾器,如 MeasuringShipOrderCommandDecorator
,MeasuringCancelOrderCommandDecorator
和MeasuringCancelShippingCommandDecorator
。這又會導致大量重複代碼(違反DRY原則),並且仍然需要爲系統中的每個操作編寫代碼。這裏缺少的是對系統中用例的常見抽象。缺少的是ICommandHandler<TCommand>
接口。
讓我們來定義這個接口:
public interface ICommandHandler<TCommand>
{
void Execute(TCommand command);
}
而且我們的MoveCustomer
方法的方法參數存入名爲MoveCustomerCommand
自己的(Parameter Object)類:
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;
var newAddress = command.NewAddress;
// Real operation
}
}
這可能看起來很奇怪,但因爲我們現在有使用的情況下,一般的抽象,我們可以重寫我們的裝飾如下:
public class MeasuringCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand>
{
private ICommandHandler<TCommand> decorated;
private ILogger logger;
public MeasuringCommandHandlerDecorator(
ICommandHandler<TCommand> decorated, ILogger logger)
{
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>
看起來很像MeasuringMoveCustomerCommandDecorator
,但這個類可以重複使用系統中的所有命令處理程序:
ICommandHandler<MoveCustomerCommand> handler1 =
new MeasuringCommandHandlerDecorator<MoveCustomerCommand>(
new MoveCustomerCommandHandler(),
new DatabaseLogger());
ICommandHandler<ShipOrderCommand> handler2 =
new MeasuringCommandHandlerDecorator<ShipOrderCommand>(
new ShipOrderCommandHandler(),
new DatabaseLogger());
這樣,這將是非常非常容易橫切關注點添加到系統中。在您的Composition Root中創建一個方便的方法非常簡單,該方法可以使用系統中適用的命令處理程序包裝任何創建的命令處理程序。例如:
ICommandHandler<MoveCustomerCommand> handler1 =
Decorate(new MoveCustomerCommandHandler());
ICommandHandler<ShipOrderCommand> handler2 =
Decorate(new ShipOrderCommandHandler());
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))));
}
如果您的應用程序開始。但是長大,它可以讓痛苦來引導這一切,沒有一個容器。特別是當你的裝飾器具有泛型類型約束時。如今,大多數現代的.NET容器都有相當不錯的支持,特別是Autofac(example)和Simple Injector(example)可以很容易地註冊開放的通用裝飾器。Simple Injector甚至可以根據給定的謂詞或複雜的泛型約束條件有條件地應用裝飾器,允許裝飾類爲injected as a factory,並允許contextual context注入裝飾器,所有這些都可以非常有用。
團結和城堡另一方面有攔截設施(如Autofac做btw)。攔截與裝飾有許多共同之處,但它使用動態代理生成。這可能比使用通用裝飾器更靈活,但是當涉及到可維護性時,您將付出代價,因爲您經常會失去類型安全,攔截器總是強制您依賴攔截庫,而裝飾器是類型安全的,可以在不依賴外部庫的情況下編寫。
如果你想了解更多關於這種設計你的應用程序的方式,請閱讀這篇文章:Meanwhile... on the command side of my architecture。
我希望這會有所幫助。
@Steven我添加了一個代碼示例,顯示記錄器的使用情況。你認爲這是一個糟糕的設計? – user1178376 2012-03-27 15:27:44
你能用具體的代碼來說明嗎,更好的測試,你試圖達到什麼目的? – 2012-03-27 17:46:45
對不起構建我的問題。我添加了更多代碼來解釋我的情況。 – user1178376 2012-03-27 19:26:45