Ответ 1
Из приведенных вами примеров трудно быть очень конкретным, но в целом, когда вы ILogger
экземпляры ILogger
в большинство служб, вам следует задать себе две вещи:
- Я слишком много вхожу?
- Я нарушаю твердые принципы?
1. Я слишком много вхожу
Вы слишком много регистрируетесь, когда у вас много такого кода:
try
{
// some operations here.
}
catch (Exception ex)
{
this.logger.Log(ex);
throw;
}
Написание такого кода происходит из-за потери информации об ошибках. Дублирование таких видов блоков try-catch повсеместно не помогает. Хуже того, я часто вижу, как разработчики регистрируются и продолжают (они удаляют последний оператор throw
). Это действительно плохо (и пахнет как старое поведение VB ON ERROR RESUME NEXT
), потому что в большинстве ситуаций у вас просто недостаточно информации, чтобы определить, безопасно ли продолжать. Часто в коде имеется ошибка или сбой во внешнем ресурсе, например в базе данных, что приводит к сбою операции. Продолжение означает, что пользователь часто получает представление о том, что операция прошла успешно, а это не так. Спросите себя: что хуже, показывая пользователю общее сообщение об ошибке, в котором говорится, что что-то пошло не так, и попросите его повторить попытку, или молча пропускаете ошибку и позволяете пользователю думать, что его запрос был успешно обработан? Подумайте, что будет чувствовать пользователь, если через две недели узнает, что его заказ так и не был отправлен. Вероятно, вы потеряете клиента. Или, что еще хуже, регистрация MRSA у пациента молчаливо завершается неудачей, в результате чего пациент не подвергается карантину во время кормления, что приводит к загрязнению других пациентов, что приводит к высокой стоимости или даже смерти
Большинство этих видов строк try-catch-log должны быть удалены, и вы должны просто позволить исключению пузыриться в стеке вызовов.
Вы не должны войти? Вы обязательно должны! Но если вы можете, определите один блок try-catch в верхней части приложения. В ASP.NET вы можете реализовать событие Application_Error
, зарегистрировать HttpModule
или определить пользовательскую страницу ошибок, которая ведет журнал. С WinForms решение отличается, но концепция остается неизменной: определите одну единственную вершину, которая наиболее универсальна.
Иногда, однако, вы все равно хотите перехватить и записать исключение определенного типа. Система, над которой я работал в прошлом, позволяла бизнес-уровню генерировать исключения ValidationExceptions, которые будут обнаруживаться на уровне представления. Эти исключения содержали информацию для проверки для отображения пользователю. Так как эти исключения будут пойманы и обработаны на уровне представления, они не будут пузыриться до самой верхней части приложения и не попадут в универсальный код приложения. Тем не менее я хотел зарегистрировать эту информацию, просто чтобы узнать, как часто пользователь вводил неверную информацию, и чтобы выяснить, были ли проверки инициированы по правильной причине. Так что это не было регистрацией ошибок; просто вход Я написал следующий код для этого:
try
{
// some operations here.
}
catch (ValidationException ex)
{
this.logger.Log(ex);
throw;
}
Выглядит знакомо? Да, выглядит точно так же, как и в предыдущем фрагменте кода, с той разницей, что я перехватывал только исключения ValidationException
. Однако было еще одно отличие, которое нельзя увидеть, просто взглянув на этот фрагмент. В приложении было только одно место, содержащее этот код! Это был декоратор, который подводит меня к следующему вопросу, который вы должны задать себе:
2. Нарушаю ли я твердые принципы?
Такие вещи, как ведение журнала, аудит и безопасность, называются сквозными проблемами (или аспектами). Они называются сквозными, потому что они могут разрезать многие части вашего приложения и часто должны применяться ко многим классам в системе. Однако, когда вы обнаружите, что пишете код для их использования во многих классах системы, вы, скорее всего, нарушаете принципы 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
и других вариантов использования, и это приводит к значительному дублированию кода и в конечном итоге к CancelShipping
на обслуживание (я был там).
Проблема с этим кодом в том, что он нарушает принципы SOLID. Принципы SOLID - это набор принципов объектно-ориентированного проектирования, которые помогут вам определить гибкое и поддерживаемое (объектно-ориентированное) программное обеспечение. MoveCustomer
нарушил как минимум два из этих правил:
- Принцип единой ответственности - классы должны нести одну ответственность. Однако класс, содержащий метод
MoveCustomer
, не только содержит основную бизнес-логику, но и измеряет время, необходимое для выполнения операции. Другими словами, он имеет несколько обязанностей. - Принцип Open-Closed (OCP) - он предписывает дизайн приложения, который не позволяет вам вносить быстрые изменения по всей базе кода; или, в словаре OCP, класс должен быть открыт для расширения, но закрыт для модификации. В случае, если вам нужно добавить обработку исключений (третью ответственность) в
MoveCustomer
использованияMoveCustomer
, вам (снова) придется изменить методMoveCustomer
. Но вам нужно изменить не только методMoveCustomer
, но и многие другие методы, чтобы сделать это радикальным изменением. OCP тесно связан с принципом DRY.
Решение этой проблемы состоит в том, чтобы извлечь запись в свой собственный класс и позволить этому классу обернуть исходный класс:
// 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 command =
new MeasuringMoveCustomerDecorator(
new MoveCustomerService(),
new DatabaseLogger());
Тем не менее, предыдущий пример только решил часть проблемы (только часть SRP). При написании кода, как показано выше, вам нужно будет определить отдельные декораторы для всех операций в системе, и в итоге вы получите декораторы, такие как MeasuringShipOrderDecorator
, MeasuringCancelOrderDecorator
и MeasuringCancelShippingDecorator
. Это снова приводит к большому количеству повторяющегося кода (нарушение принципа OCP) и все еще требует написания кода для каждой операции в системе. Здесь не хватает общей абстракции над вариантами использования в системе.
Отсутствует интерфейс ICommandHandler<TCommand>
.
Давайте определим этот интерфейс:
public interface ICommandHandler<TCommand>
{
void Execute(TCommand command);
}
И давайте сохраним аргументы метода MoveCustomer
в своем собственном (Parameter Object) классе с именем 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-контейнер может использовать автоматическую регистрацию. Это избавляет вас от необходимости вносить изменения в корень композиции для каждой новой пары команда/обработчик, которую вы добавляете в систему. Особенно, когда ваши декораторы имеют ограничения общего типа, DI-контейнер будет чрезвычайно полезен.
В настоящее время большинство современных DI-контейнеров для .NET имеют достаточно приличную поддержку для декораторов, и особенно Autofac (пример) и Simple Injector (пример) упрощают регистрацию декораторов открытого типа. Simple Injector даже позволяет декораторам применяться условно на основе заданного предиката или сложных ограничений общего типа, позволяя декорированному классу быть введенным как фабрика и позволяя контекстному контексту вставляться в декораторы, и все это может быть действительно полезным со временем ко времени.
Unity и Castle, с другой стороны, имеют динамические средства перехвата (как, кстати, делает Autofac). Динамический перехват имеет много общего с оформлением, но он использует динамическую генерацию прокси под крышками. Это может быть более гибким, чем работа с универсальными декораторами, но вы платите цену, когда дело доходит до удобства сопровождения, потому что вы часто теряете безопасность типов и перехватчики всегда вынуждают вас зависеть от библиотеки перехвата, в то время как декораторы безопасны от типов и могут быть написано без зависимости от внешней библиотеки.
Прочтите эту статью, если вы хотите узнать больше об этом способе разработки вашего приложения: Между тем... на командной стороне моей архитектуры.
ОБНОВЛЕНИЕ: Я также стал соавтором книги " Принципы, практики и шаблоны внедрения зависимостей", в которой более подробно рассматриваются этот стиль программирования SOLID и конструкция, описанная выше (см. Главу 10).