2012-03-27 45 views
49

Làm thế nào tôi có thể kéo các đối tượng từ thùng chứa tạm thời trong tự nhiên? Tôi có phải đăng ký chúng với các container và tiêm trong constructor của lớp cần thiết? Tiêm tất cả mọi thứ vào constructor không cảm thấy tốt. Cũng chỉ cho một lớp tôi không muốn tạo ra một TypedFactory và tiêm nhà máy vào lớp cần thiết.Windsor - kéo đối tượng thoáng qua từ thùng chứa

Một ý nghĩ khác đến với tôi là "mới" trên cơ sở cần thiết. Nhưng tôi cũng đang tiêm một thành phần Logger (thông qua thuộc tính) vào tất cả các lớp của tôi. Vì vậy, nếu tôi mới họ lên, tôi sẽ phải tự khởi tạo các Logger trong những lớp học. Làm cách nào để tiếp tục sử dụng vùng chứa cho TẤT CẢ các lớp học của tôi?

Logger tiêm: Hầu hết các lớp học của tôi có Logger tài sản quy định, trừ trường hợp có chuỗi thừa kế (trong trường hợp đó chỉ là lớp cơ sở có tài sản này, và tất cả các lớp học bắt nguồn sử dụng). Khi chúng được khởi tạo qua thùng chứa Windsor, chúng sẽ được thực hiện ILogger tiêm vào chúng.

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

Câu hỏi:

  1. Làm thế nào để tạo ra đối tượng mới Record mà không sử dụng "mới"?

  2. QueueMonitorSingleton, trong khi Data là "Phạm vi". Làm thế nào tôi có thể tiêm Data vào phương thức OnDataReceived()?

+0

@Steven Tôi đã thêm mẫu mã hiển thị mức sử dụng Trình ghi nhật ký. Bạn có nghĩ đây là thiết kế tồi không? – user1178376

+1

Bạn có thể minh họa bằng mã bê tông, thậm chí tốt hơn một bài kiểm tra, những gì bạn đang cố gắng đạt được không? –

+0

Xin lỗi vì đã xây dựng câu hỏi của tôi kém. Tôi đã thêm nhiều mã để giải thích trường hợp của mình. – user1178376

Trả lời

211

Từ các mẫu mà bạn cung cấp cho nó khó có thể rất cụ thể, nhưng nói chung, khi bạn tiêm ILogger trường hợp vào hầu hết các dịch vụ, bạn nên tự hỏi mình hai điều:

  1. Tôi đăng nhập quá nhiều?
  2. Tôi có vi phạm các nguyên tắc SOLID không?

1. Tôi có đăng nhập quá nhiều

Bạn đang đăng nhập quá nhiều, khi bạn có rất nhiều mã như thế này:

try 
{ 
    // some operations here. 
} 
catch (Exception ex) 
{ 
    this.logger.Log(ex); 
    throw; 
} 

đang Viết như thế này xuất phát từ sự quan tâm mất thông tin lỗi. Tuy nhiên, việc sao chép các loại khối try-catch này lại không giúp được gì. Thậm chí tệ hơn, tôi thường thấy các nhà phát triển đăng nhập và tiếp tục (họ xóa câu lệnh throw cuối cùng). Điều này thực sự tồi tệ (và có mùi giống như VB ON ERROR RESUME NEXT cũ), bởi vì trong hầu hết các trường hợp, bạn không có đủ thông tin để xác định xem nó có an toàn hay không. Thường thì có một lỗi trong mã khiến cho thao tác thất bại. Để tiếp tục có nghĩa là người dùng thường nhận được ý tưởng rằng các hoạt động đã thành công, trong khi nó đã không. Hãy tự hỏi: điều gì tồi tệ hơn, hiển thị cho người dùng thông báo lỗi chung nói rằng có điều gì đó không ổn hoặc âm thầm bỏ qua lỗi và cho phép người dùng nghĩ rằng yêu cầu của anh ta đã được xử lý thành công? Hãy suy nghĩ về cách người dùng sẽ cảm thấy như thế nào nếu anh ta phát hiện ra hai tuần sau đó rằng đơn hàng của anh ta chưa bao giờ được giao. Bạn có thể mất một khách hàng. Hoặc tệ hơn, việc đăng ký MRSA của bệnh nhân không thành công, khiến cho bệnh nhân không bị cách ly bởi điều dưỡng và dẫn đến sự nhiễm bẩn của các bệnh nhân khác, gây ra chi phí cao hoặc thậm chí có thể tử vong.

Hầu hết các loại đường try-catch-log này sẽ bị xóa và bạn chỉ cần để cho bong bóng ngoại lệ lên ngăn xếp cuộc gọi.

Bạn không nên đăng nhập? Bạn hoàn toàn nên! Nhưng nếu bạn có thể, hãy xác định một khối try-catch ở đầu ứng dụng. Với ASP.NET, bạn có thể thực hiện sự kiện Application_Error, đăng ký HttpModule hoặc xác định trang lỗi tùy chỉnh ghi nhật ký. Với Win Forms, giải pháp là khác nhau, nhưng khái niệm vẫn như cũ: Xác định một đầu duy nhất nắm bắt tất cả.

Đôi khi, bạn vẫn muốn bắt và ghi lại một loại ngoại lệ nhất định. Một hệ thống tôi đã làm việc trong quá khứ, cho phép lớp kinh doanh ném ValidationException s, mà sẽ bị bắt bởi lớp trình bày. Những ngoại lệ đó chứa thông tin xác thực để hiển thị cho người dùng. Vì những ngoại lệ đó sẽ bị bắt và xử lý trong lớp trình bày, chúng sẽ không xuất hiện ở phần trên cùng của ứng dụng và không kết thúc trong mã catch-all của ứng dụng. Tuy nhiên, tôi muốn đăng nhập thông tin này, chỉ để tìm hiểu tần suất người dùng nhập thông tin không hợp lệ và tìm hiểu xem các xác thực hợp lệ được kích hoạt vì lý do đúng hay không. Vì vậy, đây không phải là lỗi đăng nhập; chỉ cần đăng nhập. Tôi đã viết mã sau đây để làm điều này:

try 
{ 
    // some operations here. 
} 
catch (ValidationException ex) 
{ 
    this.logger.Log(ex); 
    throw; 
} 

Có vẻ quen thuộc? Có, trông giống hệt đoạn mã trước, với sự khác biệt là tôi chỉ bắt được ValidationException s. Tuy nhiên, có một sự khác biệt, không thể nhìn thấy bằng cách chỉ nhìn vào đoạn mã. Chỉ có một nơi trong ứng dụng có chứa mã đó! Đó là một trang trí, đưa tôi đến câu hỏi tiếp theo bạn nên tự hỏi mình:

2. Tôi có vi phạm các nguyên tắc SOLID không?

Những thứ như đăng nhập, kiểm tra và bảo mật, được gọi là cross-cutting concerns (hoặc các khía cạnh). Chúng được gọi là cross-cutting, bởi vì chúng có thể cắt ngang qua nhiều phần của ứng dụng của bạn và thường phải được áp dụng cho nhiều lớp trong hệ thống. Tuy nhiên, khi bạn tìm thấy bạn đang viết mã cho việc sử dụng của họ trong nhiều lớp học trong hệ thống, bạn rất có thể vi phạm các nguyên tắc SOLID. Ví dụ: ví dụ sau:

public void MoveCustomer(int customerId, Address newAddress) 
{ 
    var watch = Stopwatch.StartNew(); 

    // Real operation 

    this.logger.Log("MoveCustomer executed in " + 
     watch.ElapsedMiliseconds + " ms."); 
} 

Ở đây chúng tôi đo thời gian cần để thực hiện thao tác MoveCustomer và chúng tôi ghi lại thông tin đó. Rất có thể các hoạt động khác trong hệ thống cần mối quan tâm ngang nhau này. Bạn sẽ bắt đầu thêm mã như thế này cho các phương thức ShipOrder, CancelOrder, CancelShipping, v.v. của bạn kết thúc dẫn đến nhiều sự sao chép mã và cuối cùng là một cơn ác mộng bảo trì.

Vấn đề ở đây là vi phạm nguyên tắc SOLID. Nguyên tắc SOLID là một tập hợp các nguyên tắc thiết kế hướng đối tượng giúp bạn xác định phần mềm linh hoạt và có thể bảo trì. Ví dụ MoveCustomer vi phạm ít nhất hai trong số các quy tắc sau:

  1. Single Responsibility Principle. Lớp học giữ phương pháp MoveCustomer không chỉ di chuyển khách hàng mà còn đo thời gian cần thiết để thực hiện thao tác. Nói cách khác, nó có nhiều trách nhiệm. Bạn nên trích xuất phép đo thành lớp riêng của nó.
  2. Open-Closed principle (OCP). Hành vi của hệ thống sẽ có thể được thay đổi mà không thay đổi bất kỳ dòng mã hiện có nào. Khi bạn cũng cần xử lý ngoại lệ (trách nhiệm thứ ba) bạn (lại) phải thay đổi phương thức MoveCustomer, đó là một vi phạm OCP.

Bên cạnh vi phạm nguyên tắc SOLID, chúng tôi chắc chắn vi phạm nguyên tắc DRY ở đây, về cơ bản nói rằng sao chép mã không tốt, mkay.

Giải pháp cho vấn đề này là để trích xuất các đăng nhập vào lớp học riêng của mình và cho phép lớp đó để bọc các lớp ban đầu:

// 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."); 
    } 
} 

By gói trang trí xung quanh các trường hợp thực tế, bây giờ bạn có thể thêm đo này hành vi đối với lớp học, mà không cần bất kỳ phần nào khác của hệ thống thay đổi:

MoveCustomerCommand command = 
    new MeasuringMoveCustomerCommandDecorator(
     new MoveCustomerCommand(), 
     new DatabaseLogger()); 

Ví dụ trước đã giải quyết một phần vấn đề (chỉ phần SOLID). Khi viết mã như được hiển thị ở trên, bạn sẽ phải xác định trang trí cho tất cả các hoạt động trong hệ thống và bạn sẽ kết thúc với các trang trí như MeasuringShipOrderCommandDecorator, MeasuringCancelOrderCommandDecoratorMeasuringCancelShippingCommandDecorator. Điều này dẫn đến nhiều mã trùng lặp (vi phạm nguyên tắc DRY), và vẫn cần viết mã cho mọi hoạt động trong hệ thống. Điều thiếu sót ở đây là sự trừu tượng phổ biến trong các trường hợp sử dụng trong hệ thống. Thiếu thông tin là giao diện ICommandHandler<TCommand>.

Hãy xác định giao diện này:

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

Và chúng ta hãy lưu trữ các đối số phương pháp của phương pháp MoveCustomer vào (Parameter Object) lớp riêng của mình gọi là MoveCustomerCommand:

public class MoveCustomerCommand 
{ 
    public int CustomerId { get; set; } 
    public Address NewAddress { get; set; } 
} 

Và chúng ta hãy đưa hành vi của các MoveCustomer phương pháp trong một lớp thực hiện ICommandHandler<MoveCustomerCommand>:

public class MoveCustomerCommandHandler : ICommandHandler<MoveCustomerCommand> 
{ 
    public void Execute(MoveCustomerCommand command) 
    { 
     int customerId = command.CustomerId; 
     var newAddress = command.NewAddress; 
     // Real operation 
    } 
} 

này có thể trông lạ, nhưng vì bây giờ chúng ta có một sự trừu tượng chung đối với trường hợp sử dụng, chúng ta có thể viết lại trang trí của chúng tôi như sau:

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

này mới MeasuringCommandHandlerDecorator<T> trông giống như MeasuringMoveCustomerCommandDecorator, nhưng lớp này có thể tái sử dụng cho tất cả các xử lý lệnh trong hệ thống:

ICommandHandler<MoveCustomerCommand> handler1 = 
    new MeasuringCommandHandlerDecorator<MoveCustomerCommand>(
     new MoveCustomerCommandHandler(), 
     new DatabaseLogger()); 

ICommandHandler<ShipOrderCommand> handler2 = 
    new MeasuringCommandHandlerDecorator<ShipOrderCommand>(
     new ShipOrderCommandHandler(), 
     new DatabaseLogger()); 

Bằng cách này nó sẽ được nhiều, dễ dàng hơn nhiều để thêm mối quan tâm xuyên suốt vào hệ thống. Việc tạo một phương thức thuận tiện trong Composition Root của bạn có thể bao bọc bất kỳ trình xử lý lệnh đã tạo nào với các trình xử lý lệnh có thể áp dụng trong hệ thống. Ví dụ:

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

Nếu ứng dụng của bạn bắt đầu phát triển, có thể gây đau đớn khi khởi động tất cả mà không có thùng chứa. Đặc biệt là khi trang trí của bạn có những ràng buộc kiểu chung chung.

DI Container hiện đại nhất cho .NET có hỗ trợ khá phong nha cho trang trí hiện nay và đặc biệt là Autofac (example) và Injector đơn giản (example) giúp bạn dễ dàng đăng ký trang trí mở chung.Injector đơn giản thậm chí cho phép các trang trí được áp dụng có điều kiện dựa trên một biến vị ngữ hoặc các kiểu ràng buộc kiểu generic phức tạp, cho phép lớp trang trí là injected as a factory và cho phép contextual context được đưa vào trang trí, tất cả chúng có thể thực sự hữu ích theo thời gian.

Unity và Castle mặt khác có các cơ sở đánh chặn (như Autofac làm để btw). Interception có rất nhiều điểm chung với trang trí, nhưng nó sử dụng thế hệ proxy động dưới vỏ bọc. Điều này có thể linh hoạt hơn làm việc với trang trí chung, nhưng bạn sẽ trả giá khi nói đến khả năng bảo trì, bởi vì bạn thường sẽ loại bỏ an toàn và chặn luôn luôn buộc bạn phải phụ thuộc vào thư viện đánh chặn, trong khi trang trí là loại an toàn và có thể được viết mà không cần phụ thuộc vào thư viện bên ngoài.

Đọc bài viết này nếu bạn muốn tìm hiểu thêm về cách thiết kế ứng dụng của bạn: Meanwhile... on the command side of my architecture.

Tôi hy vọng điều này sẽ hữu ích.

+7

Câu trả lời tuyệt vời Steven. Tôi đồng ý với mọi thứ bạn nói ở đây, tôi cũng không đồng ý với việc bắt ngoại lệ chỉ để đăng nhập và quay lại khi tôi cảm thấy có một điểm trung tâm trong miền ứng dụng để thực hiện việc này, tuy nhiên tôi cảm thấy có những lúc bạn muốn bắt một ngoại lệ cụ thể như SQLException để thử lại hành động. – OutOFTouch

+1

SimpleInjector trông rất tuyệt và nằm ở đầu danh sách của tôi để sử dụng, duy trì công việc tốt. Dưới đây là ví dụ về một người trang trí với Windsor http://mikehadlow.blogspot.com/2010/01/10-advanced-windsor-tricks-4-how-to.html cho bất kỳ ai quan tâm. – OutOFTouch

+3

@OutOFTouch: Thực hiện thử lại một thao tác là rất phù hợp cho AOP, nhưng bạn không muốn bao bọc những thứ như vậy về mọi hoạt động của db, nhưng tại các ranh giới giao dịch. Như một vấn đề của thực tế, [bài đăng blog này của tôi] (http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91) cho thấy điều này (hãy xem 'DeadlockRetryCommandHandlerDecorator') . – Steven

Các vấn đề liên quan