Добавление логирования по D (SOLID). Как лучше?
Насколько я понимаю создавать статическое поле, хранящее ILogger, будет неправильно, так как это влечёт за собой неявную зависимость. Тогда, получается, нужно передавать ILogger через конструктор в классы, которым требуется эта зависимость? Я нашел очень мало материала по этому поводу, не обессудьте, буду рад любому совету или ссылке. Я очень хочу соблюсти правильную архитектуру, соблюдающую принципы SOLID, я не претендую на правильное мнение, а лишь хочу получить совет. Спасибо!
Хотел бы дополнить вопрос. Вот фрагмент кода, в этот класс я передаю ILogger как зависимость, но внутри класса есть еще интерфейсы, тоже переданные как зависимости через конструктор. Мне в их методах понадобится логирование. Получается нужно в каждый метод внедрять зависимость от ILogger? Это представляется не очень красивым, как посоветуете? Лишний код убрал:
class PrintCommand : ICommand
{
private readonly IRepository repository;
private readonly IDataExport dataExport;
private readonly ILogger logger;
public PrintCommand(IRepository repository, IDataExport dataExport, ILogger logger)
{
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
this.dataExport = dataExport ?? throw new ArgumentNullException(nameof(dataExport));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public CommandResult Execute(string[] args)
{
// какой-то код
// В методах GetProducts и GetString мне хотелось бы тоже вести лог
var products = repository.GetProducts(shopId);
return new CommandResult(dataExport.GetString(products));
}
}
Всем большое спасибо за ответы и советы, читаю Марка Симана, скоро дополню реализацией/решением, чтобы как-то подытожить
Ответы (3 шт):
Хороший вопрос, практический и хотя мне кажется, что немного отдаёт вкусовщиной (все делают по-разному), но вполне есть объективные вещи, которые стоит учесть при ответе.
Действительно, по строгим классическим подходам а-ля DDD, принципам SOLID гораздо более правильно явно описывать зависимости и передавать на конструктор. Если у вас уже подключен готовый DI-движок, то рекомендую так и поступать.
Насчёт того, насколько строго соблюдать все заветы ООП и DDD -- вопрос зачастую имеет весьма конкретную денежную составляющую: полностью всё делать по книгам может оказаться слишком трудозатратно в человеко-часах и соответственно в стоимости продукта, поэтому этот вопрос хоть и архитектурный, но следует согласовывать ещё и с бизнесом.
Например, я в своих проектах не закладываю уход от Entity Framework. Пробовал когда-то играться с тем, как это правильно делать (если хотите посмотреть в эту сторону, то почитайте Марка Симана Dependency Injection in c#) — но потом понял, что это долго и непрактично. Если потребуется — окей, тогда и стоит делать, а так вот именно этот момент обычно проще сделать не совсем по канону.
Update. По поводу дополнения. Нет, в методах объявлять зависимости уже не нужно, раз вы указали на конструкторе. Подробнее — опять отправляю к книге М. Симана, почитайте, там очень хорошо матчасть описана. Единственное, помните также и о первой букве в SOLID: вероятно правильнее говорить об одном публичном "методе", а не о множестве публичных "методов" класса.
Если строго следовать принципа SOLID, то следует взять во внимание SRP - принцип единой ответственности.
Логирование - это отдельная ответственность, которой класс команды не должен заниматься. В команде вообще не должно быть логгера.
Вместо этого можно сделать логирующую оболочку (паттерн декоратор).
Убираем из PrintCommand логгер:
class PrintCommand : ICommand
{
private readonly IRepository repository;
private readonly IDataExport dataExport;
public PrintCommand(IRepository repository, IDataExport dataExport)
{
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
this.dataExport = dataExport ?? throw new ArgumentNullException(nameof(dataExport));
}
public CommandResult Execute(string[] args)
{
return new CommandResult(...);
}
}
Делаем декоратор:
class LoggingCommand : ICommand
{
private readonly ICommand command;
private readonly ILogger logger;
public LoggingCommand(ICommand command, ILogger logger)
{
this.command = command ?? throw new ArgumentNullException(nameof(command));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public CommandResult Execute(string[] args)
{
// логируем входные параметры
logger.Log(/* args */);
var result = command.Execute(args);
// логируем результат
logger.Log(/* result */);
return result;
}
}
Использование:
IRepository repository = ...;
IDataExport dataExport = ...;
var printCommand = new PrintCommand(repository, dataExport);
ILogger logger = ...;
var loggingPrintCommand = new LoggingCommand(printCommand, logger);
Далее передаём loggingPrintCommand вместо printCommand туда, где требуется ICommand.
Это легко реализуется с помощью инъекции зависимостей. В книге Марка Симана уделено внимание настройке DI для декораторов.
Кроме декораторов для этой же цели можно использовать наследование и инъекцию стратегий. Это три основных способа расширения возможностей классов. Но декораторы удобны тем, что их легко вкладывать один в другой и они дружат с DI.
Однако, в декораторе нужно писать оболочки для всех методов, что утомительно и нудно. На помощь может придти AOP - перехватчики (interceptors), о чём опять же хорошо написано в книге Симана. Рекомендую!
Мне в их методах понадобится логирование. Получается нужно в каждый метод внедрять зависимость от ILogger
Не в каждый метод, а в каждый класс, который должен логгировать свое поведение. Логгирование нужно внедрять не в интерфейс, а в реализацию этого интерфейса.
Что именно писать в лог - это ответственность каждого конкретного класса и метода, т.к. в логе вы скорее всего хотите видеть конкретный контекст работы этого метода, и какая-то общая обертка, которая просто будет логгировать факт вызова метода вам этого контекста не даст.
Как именно писать в лог - это ответственность конкретной реализации логгера. Какая именно реализация логгера будет использована - это DI.
Стандартное решение в .NET Core - это вставка логгера через зависимость ILogger<T> в каждый класс, который хочет что-то залогировать. Стандартный способ вставки зависимости для ILogger<T> - constructor injection (осторожно, там пример кода для исключения factory method, не для constructor injection :)
class PrintCommand : ICommand
{
private readonly IRepository repository;
private readonly IDataExport dataExport;
private readonly ILogger<PrintCommand> logger;
public PrintCommand(IRepository repository, IDataExport dataExport, ILogger<PrintCommand> logger)
{
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
this.dataExport = dataExport ?? throw new ArgumentNullException(nameof(dataExport));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public CommandResult Execute(string[] args)
{
// какой-то код
// В методах GetProducts и GetString мне хотелось бы тоже вести лог
var products = repository.GetProducts(shopId);
return new CommandResult(dataExport.GetString(products));
}
}
Если команды создаете кодом - сделайте Factory, которая создавала бы команду с логгером, или перенесите логгирование самого факта выполнения команды в код, который вызывает `Execute'.
В репозитории и dataExport, добавляйте отдельные зависимости на ILogger<SomeRepository> и ILogger<SomeDataExport>, соответственно. Логгирование в командах никак не должно пересекаться с логгированием в репозитории по коду. Инстанс логгера прокидывать не нужно.