Добавление логирования по D (SOLID). Как лучше?

1,00
р.
Насколько я понимаю создавать статическое поле, хранящее 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)) } }

Всем большое спасибо за ответы и советы, читаю Марка Симана, скоро дополню реализацией/решением, чтобы как-то подытожить

Ответ
Если строго следовать принципа 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), о чём опять же хорошо написано в книге Симана. Рекомендую!