Меня, если честно, ваши вопросы ставят в тупик. Эти функции, как и запись в БД, и ротация лог-файлов, и изменение формата вывода сообщений без изменения кода приложения, на уровне конфигурационных файлов давным-давно не являются экзотикой в фреймворках логирования.
В Java этого добра поболее — java.util.logging, log4j, slfj4, в PHP я использовал только log4php и собственно Monolog (PSR3 имплементацию). Я не вижу смысла проектировать логгеры — они уже есть. Остается их только использовать там, где нужно. А в сложных системах, да еще и в случае "гибкой архитектуры", их нужно использовать чуть менее, чем везде (слегка утрирую, но для сервисов, реализующих бизнес-логику — везде).
Если вы же и поддерживаете те приложения, архитектором которых вы являетесь, у вас не может быть другого мнения на этот счет.
Для начала я бы использовал готовый PSR3-фреймворк — Monolog. Затем, убедившись, что он не конфигурируется через внешние файлы, я бы обернул его в monolog-cascade. После чего я имел бы возможность через конфигурационный файл влиять на все логи приложения с возможностью их вывода в "никуда"/файл/базу/syslog/email/… С использованием процессора IntrospectionProcessor (подключаемого через конфиг, когда мне нужно) я имел бы возможность расширять сообщения до такого формата: [YYYY-MM-DD HH:MM:SS] main.DEBUG: MESSAGE_HERE. {"file":"/.../Main_Test.php","line":75,"class":"...\\Main_IntegrationTest","function":"test_main"}
Чего мне уже бы хватило для нахождения в лог-файле (я в основном использую их, но можно и в БД лить) сообщения с конкретной строки конкретного метода конкретного класса (вне зависимости от его уровня логирования).
Если же мне и этого мало, а мне нужно по конкретному сообщению создавать, например, таск support'у в JIRA, то я могу написать свой handler/processor/formatter и подключить его через конфиг-файл без изменения кода сервиса:
public function __construct(
\Psr\Log\LoggerInterface $logger,
...
) {
$this->_logger = $logger;
...
}
public function operation($request) {
$this->_logger->info("Operation is called.");
...
}
Если нужна еще большая гибкость, то да — это решение не подходит, и нужно применять что-то другое. Возможно даже EventDispatcher.
Если предполагается, что в сервисе могут возникнуть различные варианты обработки лог-сообщений (помимо стандартного уровня — debug, info, ...), то нужно маркировать подобные сообщения на этапе девелопмента (см. второй параметр $context в LoggerInterface). После чего на уровне конфигурирования можно перенаправлять ваши сообщения куда хотите.
Если вы сначала написали код, а потом решили, что он должен работать по-другому, то да — код нужно будет менять.
Не я, клиент :) У Magento 1 гораздо большее кол-во расширений. Для него это важно.
разве DI у Zend и DI у Magento2 как то изменят архитектуру приложения?
Моего расширения — нет. Но если бы я не ввел DI Zend'а, то — да.
Почему бы вам не выбрать архитектуру, а потом собрать ее из пакетов разных фреймворков, если уж возникают такие проблемы?
Я делаю расширения к уже существующим приложениям и встраиваюсь в них. Они и диктуют "правила игры". Может завтра клиент решит в OroCRM или WordPress встроиться — я ж не знаю :)
Но меня не столько интересует, по какому пути мы пошли, как то, почему мы пошли по этому пути. А это можно узнать только в самом сервисе — в нем же реализована бизнес-логика.
сегодня хочется логировать, завтра письма слать, послезавтра какую-то бизнес-логику навешивать
Тот же Monolog (имплементации PSR3-логгера) позволяет делать это все и многое другое без изменения кода приложения — на уровне конфигурационных файлов. Log4php позволял делать примерно такие же финты (но он из до-PSR3 эпохи). Можно хоть вообще логирование отключить, если оно не нужно, а можно логировать все подряд только от определенного namespace'а (в Log4php, по крайней мере). Можно в файл, можно в БД, можно по email'у, а можно и туда, и туда. И все это — чисто на уровне конфигов уже существующих и широко используемых логгеров — Monolog на packagist'е показывает 24М загрузок. Log4php — 624K.
IMHO, логгер вполне широкоприменимый компонент, и он вполне может соседствовать с DBA. ОК, я могу еще согласиться, что DBA не место рядом с другим сервисом в зависимостях, но PSR3-логгер уж точно может идти куда угодно и с кем угодно. Как минимум, в контексте данной статьи.
Это не мой код. Это просто один из классов Magento 2 — а их там много. Ссылка на репозиторий есть, можете сами убедиться, что это не исключительный случай. Я не говорю, что это хорошо или плохо. Я отмечаю, что это есть. Если в системе есть 100 классов, то их можно свести к 4-м зависимостям (хотя человек способен достаточно комфортно оперировать объектами в количестве 7 плюс/минус 2, т.е., для разработчика это скорее в плюс, чем в минус — итого 9). Но и тут придется увеличить глубину иерархии "сервис вызывает сервис, который вызывает сервис, вызывающий сервис". Что тоже в конце-концов, при увеличении кол-ва реализованных функций и глубины залегания нужных сервисов, приведет всех в уныние. Вообщем, я тоже за здравый смысл. Вот только он у всех здрав по разному :)
Для вас не составило. Для меня — да. Сработала инерция мышления. Изначально я вообще использовал функцию extract() и заголовок был "DI, PHPUnit и extract", а не как сейчас. Коллега Fesor навел меня на setUp(). Да, я смотрел тесты для класса AfterAddressSaveObserver.php и видел, что моки сохраняются в приватных свойствах. Да, я не заметил, что именно так там и было сделано — через приватные свойства и setUp. Да, я загуглил запрос "DI phpunit", перед тем, как написать хабр (с костылем extract, кстати).
Спасибо, что донесли до меня свое мнение. Нет, я не смогу выполнить ваши пожелания, т.к. сложные случаи имеют множество правильных решений, оптимальность которых зависит от контекста задачи и преследуемых целей, и без спецификации с вашей стороны того и другого моя статья для вас выйдет не менее пустой.
Залогирует LoggableOtherService, а мы тестируем MyService, он не занимается логированием.
Может быть мы под логированием понимаем какие-то разные вещи? Я попытался до вас донести, что в сервисе MyService предположительно реализуется некоторая логика, шаги которой (трейс) было бы хорошо иметь в случае разбора полетов (например, для PSR3-интерфейса есть имплементация, для которой есть обработчик FingersCrossedHandler, позволяющий сбрасывать в лог сообщения всех уровней, если произошла ошибка, и не писать ничего, если ошибки не было). Я полагаю, что в более-менее сложной системе обязательно нужно логировать ключевые моменты в принятии решений (в файл, БД, email'ом — это уже настройки логгера, которые делаются админом приложения). Но вот формирование внятных сообщений — это обязанности разработчика приложения. А какое ж логирование без логгера? Я еще допускаю, что на низком уровне (ближе к БД) можно опустить логирование (генерируется большой объем сообщений), но в сервисах, реализующих бизнес-логику…
Из моего примера не следует, кстати, что нужно тестировать имплементацию PSR3-интерфейса, поставляемую в конструктор сервиса, из него следует только, что этот интерфейс должен быть замокирован, чтобы сервис мог в принципе работать (а не вылетать по ошибке "Call to a member function info() on a non-object"). Вот, например, моя типовая операция:
public function getRepresentative(Request\GetRepresentative $request) {
$this->_logger->info("'Get representative account' operation is called.");
...
$this->_logger->info("'Get representative account' operation is completed.");
return $result;
}
Я не проверяю в тестах, что сервис дергает метод info у logger'а, хотя и могу, если вдруг окажется, что данная диагностика критически важна в каких-то ситуациях, когда постфактум пытаешься понять, что произошло на живой системе, в случае проблем.
И я абсолютно не вижу причин, по которым PSR3-логгер не может соседствовать в моем сервисе с DBA и/или другими сервисами.
У меня сейчас такой. Пишу код, который должен функционировать или под Magento 1, или под Magento 2. В первом случае в качестве DI я использую Zend'овский (Magento 1 обошлась без DI), во втором — собственно Magento'овский. Ну и еще по мелочи.
Придерживаясь правила "не более 4-х", мы можем выделить зависимости в отдельные сервисы, увеличив кол-во классов, и превратив 300 строк кода в даже не знаю сколько. Можете попробовать отрефакторить код примера, если у вас есть желание и время, и сравнить его с первоначальным — улучшиться ли читаемость и управляемость в результате.
В данном случае при рефакторинге придется наращивать кол-во узкоспециализированных классов, что я и называют "ростом вглубь", в отличие от "роста вширь", когда в классе используется "более 4-х" зависимостей с более широкой специализацией. Плюс "широкой специализации" — порог вхождения в проект ниже, чем порог вхождения в проект с "узкой специализацией". Плюс "узкой специализации" — позволяет строить в конечном итоге более сложные проекты, чем "широкая" (правда в этом случае нужно очень хорошо представлять себе предметную область, а "широкая" более снисходительна к экспериментам).
Тогда я не понимаю, каким образом в приведенном примере MyService залогирует, какой вариант (А, Б или В) он выбрал? Ведь у него нет логгера. Допустим, OtherService лезет в базу и логгирует каждый свой запрос. Но в этом случае логируются все запросы, вне зависимости от того, вызываются ли они из MyService'а или из какого другого места. Более того, иногда возникает необходимость залогировать сообщение с разным уровнем (debug, info, warn, error). И уровень логирования определяется логикой MyService'а. Более того, возможны варианты, когда для уточнения ситуации нужно логировать не сам запрос OtherService, а только часть его (например, ID (не)найденного объекта) в сопоставлении с результатами работы ISomeService (например, запросы к API GitHub'а). Т.е., в MyService реализуется сценарий, когда запрашиваются данные с GitHub'а, анализируются данные в БД, в случае выполнения некоторых условий данные в БД изменяются (или вызывается другой сервис, например IWalletService), если условия не выполняются, то причины логируются. Логгер нужен именно сервису MyService, т.к. логика обработки данных реализована именно в нем.
По поводу ре-инициализации моков между вызовами до меня дошло. Спасибо еще раз.
сервис лазает в базу, сервис-декоратор занимается логированием.
Но это же все равно две зависимости, разве нет? Есть ли где-то в формулировке "принципа единой ответственности" ограничение на количество зависимостей для отдельного проектируемого класса? Если нет, то этих зависимостей может быть чуть менее, чем очень много.
И я все одно не понял, почему залезть в БД и залогировать это дело в рамках одного класса — есть нехорошо, есть "нарушение принципа единой ответственности"? Ну допустим, мой класс должен находить какие-то данные в базе по несколько причудливому алгоритму, типа "Вернуть А, если не найдено, то Б, если не найдено, то В". А в логах отметить, что именно было возвращено для последующего "разбора полетов" в случае чего. У меня есть логгер и есть адаптер к БД. Почему я не могу использовать оба этих объекта в своем классе?
Как будет выглядеть спецификация, если тестируемый класс выполняет не одну полезную функцию, а, допустим, 10?
function it_do_something_valuable01() {}
// ...
function it_do_something_valuable10() {}
Меня интересует инициализация/сброс мокируемых зависимостей между определением их поведения в тестирующих функциях (будет весьма нехорошо, если мы проинициализируем моки один раз, а потом будем специализировать поведение одних и тех же моков). Что касается замечательного "принципа единой ответственности", то ему действительно следуют не все, но что делать, если иногда нужно в базу залесть и в логах об этом запись оставить? Для более-менее сложных систем количество объектов, которыми в итоге нужно манипулировать при выполнении некоторой операции может быть весьма велико. Либо мы растем вширь (кол-во зависимостей в конструкторе), либо вглубь (иерархия специализированных манипуляторов с единой ответственностью).
sl4mmer, я дал пояснение в тексте, почему так. Это отсылка к поваренной книге, указывающая на "рецептурность" описанного в статье и то, что это уже не первая статья о предмете. Кстати, в Fabric сценарии развертывания так и называются — рецепты (recipes).
Да, фигурка у "девушки" не очень изящная, но зато спектр предоставляемых услуг весьма широк, хоть и оказывает она их не очень расторопно. Тем не менее, с использованием дополнительных прилад с ней можно творить вполне себе безумные вещи. Вчера на http://www.banggood.com/ купил Arduino-наборчик "на поиграться" — она, родимая. И бегает шустро, по кэшированным страницам, по крайней мере.
Когда я говорил «слегка не так» я имел в виду именно EAV, а не то, что вы имели в виду, говоря «только так». Но в целом вы совершенно правы — все в конце-концов хранится именно в БД и, если не вдаваться в детали, не важно, по большому счету, в каком виде.
Да.
Меня, если честно, ваши вопросы ставят в тупик. Эти функции, как и запись в БД, и ротация лог-файлов, и изменение формата вывода сообщений без изменения кода приложения, на уровне конфигурационных файлов давным-давно не являются экзотикой в фреймворках логирования.
В Java этого добра поболее — java.util.logging, log4j, slfj4, в PHP я использовал только log4php и собственно Monolog (PSR3 имплементацию). Я не вижу смысла проектировать логгеры — они уже есть. Остается их только использовать там, где нужно. А в сложных системах, да еще и в случае "гибкой архитектуры", их нужно использовать чуть менее, чем везде (слегка утрирую, но для сервисов, реализующих бизнес-логику — везде).
Если вы же и поддерживаете те приложения, архитектором которых вы являетесь, у вас не может быть другого мнения на этот счет.
[YYYY-MM-DD HH:MM:SS] main.DEBUG: MESSAGE_HERE. {"file":"/.../Main_Test.php","line":75,"class":"...\\Main_IntegrationTest","function":"test_main"}Чего мне уже бы хватило для нахождения в лог-файле (я в основном использую их, но можно и в БД лить) сообщения с конкретной строки конкретного метода конкретного класса (вне зависимости от его уровня логирования).
Если же мне и этого мало, а мне нужно по конкретному сообщению создавать, например, таск support'у в JIRA, то я могу написать свой handler/processor/formatter и подключить его через конфиг-файл без изменения кода сервиса:
Если нужна еще большая гибкость, то да — это решение не подходит, и нужно применять что-то другое. Возможно даже EventDispatcher.
Т.е., в более гибком варианте поведения мне нужно будет изменить код слушателя, я правильно понял?
Если вы сначала написали код, а потом решили, что он должен работать по-другому, то да — код нужно будет менять.
Моего расширения — нет. Но если бы я не ввел DI Zend'а, то — да.
Я делаю расширения к уже существующим приложениям и встраиваюсь в них. Они и диктуют "правила игры". Может завтра клиент решит в OroCRM или WordPress встроиться — я ж не знаю :)
Тот же Monolog (имплементации PSR3-логгера) позволяет делать это все и многое другое без изменения кода приложения — на уровне конфигурационных файлов. Log4php позволял делать примерно такие же финты (но он из до-PSR3 эпохи). Можно хоть вообще логирование отключить, если оно не нужно, а можно логировать все подряд только от определенного namespace'а (в Log4php, по крайней мере). Можно в файл, можно в БД, можно по email'у, а можно и туда, и туда. И все это — чисто на уровне конфигов уже существующих и широко используемых логгеров — Monolog на packagist'е показывает 24М загрузок. Log4php — 624K.
IMHO, логгер вполне широкоприменимый компонент, и он вполне может соседствовать с DBA. ОК, я могу еще согласиться, что DBA не место рядом с другим сервисом в зависимостях, но PSR3-логгер уж точно может идти куда угодно и с кем угодно. Как минимум, в контексте данной статьи.
Спасибо, что донесли до меня свое мнение. Нет, я не смогу выполнить ваши пожелания, т.к. сложные случаи имеют множество правильных решений, оптимальность которых зависит от контекста задачи и преследуемых целей, и без спецификации с вашей стороны того и другого моя статья для вас выйдет не менее пустой.
Может быть мы под логированием понимаем какие-то разные вещи? Я попытался до вас донести, что в сервисе MyService предположительно реализуется некоторая логика, шаги которой (трейс) было бы хорошо иметь в случае разбора полетов (например, для PSR3-интерфейса есть имплементация, для которой есть обработчик FingersCrossedHandler, позволяющий сбрасывать в лог сообщения всех уровней, если произошла ошибка, и не писать ничего, если ошибки не было). Я полагаю, что в более-менее сложной системе обязательно нужно логировать ключевые моменты в принятии решений (в файл, БД, email'ом — это уже настройки логгера, которые делаются админом приложения). Но вот формирование внятных сообщений — это обязанности разработчика приложения. А какое ж логирование без логгера? Я еще допускаю, что на низком уровне (ближе к БД) можно опустить логирование (генерируется большой объем сообщений), но в сервисах, реализующих бизнес-логику…
Из моего примера не следует, кстати, что нужно тестировать имплементацию PSR3-интерфейса, поставляемую в конструктор сервиса, из него следует только, что этот интерфейс должен быть замокирован, чтобы сервис мог в принципе работать (а не вылетать по ошибке "Call to a member function info() on a non-object"). Вот, например, моя типовая операция:
Я не проверяю в тестах, что сервис дергает метод info у logger'а, хотя и могу, если вдруг окажется, что данная диагностика критически важна в каких-то ситуациях, когда постфактум пытаешься понять, что произошло на живой системе, в случае проблем.
И я абсолютно не вижу причин, по которым PSR3-логгер не может соседствовать в моем сервисе с DBA и/или другими сервисами.
Придерживаясь правила "не более 4-х", мы можем выделить зависимости в отдельные сервисы, увеличив кол-во классов, и превратив 300 строк кода в даже не знаю сколько. Можете попробовать отрефакторить код примера, если у вас есть желание и время, и сравнить его с первоначальным — улучшиться ли читаемость и управляемость в результате.
В данном случае при рефакторинге придется наращивать кол-во узкоспециализированных классов, что я и называют "ростом вглубь", в отличие от "роста вширь", когда в классе используется "более 4-х" зависимостей с более широкой специализацией. Плюс "широкой специализации" — порог вхождения в проект ниже, чем порог вхождения в проект с "узкой специализацией". Плюс "узкой специализации" — позволяет строить в конечном итоге более сложные проекты, чем "широкая" (правда в этом случае нужно очень хорошо представлять себе предметную область, а "широкая" более снисходительна к экспериментам).
За ориентир "4 зависимости" — спасибо :)
Но это же все равно две зависимости, разве нет? Есть ли где-то в формулировке "принципа единой ответственности" ограничение на количество зависимостей для отдельного проектируемого класса? Если нет, то этих зависимостей может быть чуть менее, чем очень много.
И я все одно не понял, почему залезть в БД и залогировать это дело в рамках одного класса — есть нехорошо, есть "нарушение принципа единой ответственности"? Ну допустим, мой класс должен находить какие-то данные в базе по несколько причудливому алгоритму, типа "Вернуть А, если не найдено, то Б, если не найдено, то В". А в логах отметить, что именно было возвращено для последующего "разбора полетов" в случае чего. У меня есть логгер и есть адаптер к БД. Почему я не могу использовать оба этих объекта в своем классе?
В этом случае перед каждым тестирующим методом будет происходить реинициализация всех используемых моков и самого тестируемого объекта.
Спасибо за наводку, Fesor.
Меня интересует инициализация/сброс мокируемых зависимостей между определением их поведения в тестирующих функциях (будет весьма нехорошо, если мы проинициализируем моки один раз, а потом будем специализировать поведение одних и тех же моков). Что касается замечательного "принципа единой ответственности", то ему действительно следуют не все, но что делать, если иногда нужно в базу залесть и в логах об этом запись оставить? Для более-менее сложных систем количество объектов, которыми в итоге нужно манипулировать при выполнении некоторой операции может быть весьма велико. Либо мы растем вширь (кол-во зависимостей в конструкторе), либо вглубь (иерархия специализированных манипуляторов с единой ответственностью).
Посмотрите таблицы:
на примере того же Клиента: