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:
- Tôi đăng nhập quá nhiều?
- 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:
- 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ó.
- 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
, MeasuringCancelOrderCommandDecorator
và MeasuringCancelShippingCommandDecorator
. Đ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.
@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
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? –
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