Из примеров, которые вы даете, трудно быть очень конкретным, но в целом, когда вы вводите 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
и других случаев использования, и это приводит к значительному дублированию кода и в конечном итоге к кошмару на обслуживание (я был там).
Проблема с этим кодом в том, что он нарушает принципы SOLID .Принципы SOLID - это набор принципов объектно-ориентированного проектирования, которые помогут вам определить гибкое и поддерживаемое (объектно-ориентированное) программное обеспечение.Пример MoveCustomer
нарушил как минимум два из этих правил:
- Принцип единой ответственности - классы должны нести одну ответственность.Однако класс, содержащий метод
MoveCustomer
, не только содержит основную бизнес-логику, но и измеряет время, необходимое для выполнения операции.Другими словами, он имеет несколько обязанностей . - Принцип Open-Closed (OCP) - он предписывает дизайн приложения, который предотвращает необходимость внесения радикальных измененийпо всей базе кода;или, в словаре OCP, класс должен быть открыт для расширения, но закрыт для модификации.Если вам нужно добавить обработку исключений (третью ответственность) в сценарий использования
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());
Таким образом,будет намного, намного проще добавить сквозные проблемы в вашу систему.Довольно просто создать в вашем Root Composition удобный метод, который может обернуть любой созданный обработчик команд соответствующими обработчиками команд в системе.Например:
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).).