Обновить
8K+
53
Alex Gusev@flancer

Я кодирую, потому что я кодирую…

4,1
Рейтинг
99
Подписчики
Отправить сообщение
для конкретного лог-сообщения вам нужно поменять уровень с debug на warning.

это гораздо более гибкий вариант поведения

Т.е., в более гибком варианте поведения мне нужно будет изменить код слушателя, я правильно понял?
Если предполагается, что в сервисе могут возникнуть различные варианты обработки лог-сообщений (помимо стандартного уровня — 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'овский. Ну и еще по мелочи.
Вот, например, вполне реальный пример, в котором придерживаются принципа "<50 per method". А это — его конструктор.

    public function __construct(
        Vat $customerVat,
        HelperAddress $customerAddress,
        Registry $coreRegistry,
        GroupManagementInterface $groupManagement,
        ScopeConfigInterface $scopeConfig,
        ManagerInterface $messageManager,
        Escaper $escaper,
        AppState $appState
    ) {
        $this->_customerVat = $customerVat;
        $this->_customerAddress = $customerAddress;
        $this->_coreRegistry = $coreRegistry;
        $this->_groupManagement = $groupManagement;
        $this->scopeConfig = $scopeConfig;
        $this->messageManager = $messageManager;
        $this->escaper = $escaper;
        $this->appState = $appState;
    }

Придерживаясь правила "не более 4-х", мы можем выделить зависимости в отдельные сервисы, увеличив кол-во классов, и превратив 300 строк кода в даже не знаю сколько. Можете попробовать отрефакторить код примера, если у вас есть желание и время, и сравнить его с первоначальным — улучшиться ли читаемость и управляемость в результате.

В данном случае при рефакторинге придется наращивать кол-во узкоспециализированных классов, что я и называют "ростом вглубь", в отличие от "роста вширь", когда в классе используется "более 4-х" зависимостей с более широкой специализацией. Плюс "широкой специализации" — порог вхождения в проект ниже, чем порог вхождения в проект с "узкой специализацией". Плюс "узкой специализации" — позволяет строить в конечном итоге более сложные проекты, чем "широкая" (правда в этом случае нужно очень хорошо представлять себе предметную область, а "широкая" более снисходительна к экспериментам).

За ориентир "4 зависимости" — спасибо :)
Тогда я не понимаю, каким образом в приведенном примере MyService залогирует, какой вариант (А, Б или В) он выбрал? Ведь у него нет логгера. Допустим, OtherService лезет в базу и логгирует каждый свой запрос. Но в этом случае логируются все запросы, вне зависимости от того, вызываются ли они из MyService'а или из какого другого места. Более того, иногда возникает необходимость залогировать сообщение с разным уровнем (debug, info, warn, error). И уровень логирования определяется логикой MyService'а. Более того, возможны варианты, когда для уточнения ситуации нужно логировать не сам запрос OtherService, а только часть его (например, ID (не)найденного объекта) в сопоставлении с результатами работы ISomeService (например, запросы к API GitHub'а). Т.е., в MyService реализуется сценарий, когда запрашиваются данные с GitHub'а, анализируются данные в БД, в случае выполнения некоторых условий данные в БД изменяются (или вызывается другой сервис, например IWalletService), если условия не выполняются, то причины логируются. Логгер нужен именно сервису MyService, т.к. логика обработки данных реализована именно в нем.
По поводу ре-инициализации моков между вызовами до меня дошло. Спасибо еще раз.

сервис лазает в базу, сервис-декоратор занимается логированием.

Но это же все равно две зависимости, разве нет? Есть ли где-то в формулировке "принципа единой ответственности" ограничение на количество зависимостей для отдельного проектируемого класса? Если нет, то этих зависимостей может быть чуть менее, чем очень много.

И я все одно не понял, почему залезть в БД и залогировать это дело в рамках одного класса — есть нехорошо, есть "нарушение принципа единой ответственности"? Ну допустим, мой класс должен находить какие-то данные в базе по несколько причудливому алгоритму, типа "Вернуть А, если не найдено, то Б, если не найдено, то В". А в логах отметить, что именно было возвращено для последующего "разбора полетов" в случае чего. У меня есть логгер и есть адаптер к БД. Почему я не могу использовать оба этих объекта в своем классе?
Как-то так:

private $mLogger;
private $mDba;
private $mService;
...
private $obj;

private function setUp() {
    $this->mLogger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
    $this->mDba = $this->getMockBuilder('Zend_Db_Adapter_Pdo_Abstract')->getMockForAbstractClass();
    $this->mService = $this->getMockBuilder('Vendor\Module\ISomeService')->disableOriginalConstructor()->getMock();
    ...
    $this->obj = new Demo($this->mLogger, $this->mDba, $this->mService, ...);
} 

В этом случае перед каждым тестирующим методом будет происходить реинициализация всех используемых моков и самого тестируемого объекта.

Спасибо за наводку, Fesor.
Как будет выглядеть спецификация, если тестируемый класс выполняет не одну полезную функцию, а, допустим, 10?

function it_do_something_valuable01() {}
// ...
function it_do_something_valuable10() {}

Меня интересует инициализация/сброс мокируемых зависимостей между определением их поведения в тестирующих функциях (будет весьма нехорошо, если мы проинициализируем моки один раз, а потом будем специализировать поведение одних и тех же моков). Что касается замечательного "принципа единой ответственности", то ему действительно следуют не все, но что делать, если иногда нужно в базу залесть и в логах об этом запись оставить? Для более-менее сложных систем количество объектов, которыми в итоге нужно манипулировать при выполнении некоторой операции может быть весьма велико. Либо мы растем вширь (кол-во зависимостей в конструкторе), либо вглубь (иерархия специализированных манипуляторов с единой ответственностью).
sl4mmer, я дал пояснение в тексте, почему так. Это отсылка к поваренной книге, указывающая на "рецептурность" описанного в статье и то, что это уже не первая статья о предмете. Кстати, в Fabric сценарии развертывания так и называются — рецепты (recipes).
Да, фигурка у "девушки" не очень изящная, но зато спектр предоставляемых услуг весьма широк, хоть и оказывает она их не очень расторопно. Тем не менее, с использованием дополнительных прилад с ней можно творить вполне себе безумные вещи. Вчера на http://www.banggood.com/ купил Arduino-наборчик "на поиграться" — она, родимая. И бегает шустро, по кэшированным страницам, по крайней мере.
Когда я говорил «слегка не так» я имел в виду именно EAV, а не то, что вы имели в виду, говоря «только так». Но в целом вы совершенно правы — все в конце-концов хранится именно в БД и, если не вдаваться в детали, не важно, по большому счету, в каком виде.
Потому что в базовой реализации для работы с сущностями и их атрибутами была заложена EAV-модель.

Посмотрите таблицы:
image

на примере того же Клиента:
image
Из вас всю информацию приходится прям клещами вытягивать.

Ну я же не знаю, чего не знаете вы :) С моей колокольни все это вполне укладывается в следствие из «слабосвязанные команды разработчиков»

В целом достаточно верно.

В таком случае нужно сразу сказать что дополнительные поля из плагинов не должны быть обязательными для заполнения, иначе они могут поломать базовый функционал.

Не совсем так, возможен вариант, когда плагин добавляет обязательные поля в БД, а также на web-форму добавления/редактировния соотв. сущности (в UI).

Создание отдельной таблицы с дополнительными полями необходимыми для плагина и сделать связь с базовой таблицей OneToOne.

Отличная идея! Без преувеличения (просто в Magento это сделано слегка не так). Я бы даже смотрел в сторону создания отдельной таблицы для каждой колонки (атрибута сущности; так сказать, максимальное приближение к «6-й нормальной форме»), но количество неизведанного геморроя заставляет останавливаться на 3NF (вернее, не отрываться от нее достаточно далеко).

Нет возможности получить сущность плагина из базовой сущности.

Именно так. Поэтому и производные от DataObject, структурирующие «просто данные» под текущие условия использования.

Нужно создавать еще одну таблицу и делать JOIN для выбора дополнительных данных

Это да, есть такая беда. Можно для облегчения жизни задействовать views (а в postgres'е даже и materialized views), но без автоматизации сборки/разборки объектов по-атрибутно это будет условно-досрочное облегчение. А уж как БД админы будут благодарны девелоперам…

Кстати, если во втором варианте положить, что все объекты данных наследуют от DataObject (CustomerRef extends Customer extends DataObject), то это и будет то, о чем я и пытался сказать:

$customer->getId();
$customer->getRef(); // works, but autocomplete doesn't
$customer_ref->getId();
$customer_ref->getRef();


Другое дело, что обращение к ref-атрибуту в контексте базовой сущности не имеет смысла — этот атрибут вводится только в ref-плагине. Но если базовый функционал загрузил все доступные атрибуты сущности «Клиент» и передал их в ref-плагин, то в ref-плагине достаточно создать новый ref-объект на основании данных базового и autocomplete заработает (т.к. это уже будет другой объект с теми же данными, можно сказать — другая проекция данных на типизированный объект). В PHP можно даже просто проаннотировать тип переменной/аргумента, без реального изменения его типа, и autocomplete будет работать. Создание нового объекта на основании данных базового — это для строго типизированных языков.

надо отметь что отсутствие возможности получить поля плагина из базового класса не всегда является минусом. В некоторых случаях это позволяет избежать ошибок.

Я бы даже сказал, что так должно быть всегда, когда это делается не из контекста самого плагина (или других плагинов, на нем базирующихся).
Я согласен, что объектное (структурированное) представление данных — весьма удобно. Более того, я настаиваю, что это правильно (у меня был опыт работы с базами документов Lotus Domino, с тех пор я поклонник RDBMS). Но данные — это данные.

Они могут лежать в БД, представлены в виде JSON/XML/binary/encoded Base64, могут обрабатываться программой в виде ассоциативного массива, множества POJO-объектов, «однойбольшойдлиннойстроки» и т.п. Приложения постоянно трансформируют данные из одного формата в другой, начиная от UI'я и заканчивая БД и в обратную сторону. JSON/XML/YAML/… — это форматы передачи данных между различными (а иногда и разнородными) системами. У каждого из них своя «заточка», своя ниша. По-большому счету, исходный код — это тоже данные. Как правило, текстовые. Я встречался с решениями, когда код хранился в БД, извлекался и выполнялся по мере необходимости (то ли Liferay, то ли Odoo/OpenERP). BPML — это XML-данные или инструкции по обработке (код) в формате XML?

Иногда, особенно когда разработчики плохо представляют себе моделируемую предметную область или просто не в состоянии представить себе ее полностью (а чем сложнее система, тем выше вероятность возникновения такой ситуации), жесткая типизация объектов-данных может дать больше минусов, чем плюсов (особенно, если система нуждается в постоянной подстройке под постоянно изменяющиеся требования). И вот тут уже нужна универсализация.

Я с вами согласен, что идеальный плагин должен стать частью системы, но что делать с плагинами неидеальными? Которые core-разработчики не берут в свой код, а у этих плагинов, между прочим, весьма обширный круг пользователей? И сколько «идеальных плагинов» выдержит core и его разработчики, пока не начнут задаваться вопросом «а не сильно ль мы раздулись?»

По поводу выборки контента из JSON'а — я более чем уверен, что в вашем случае обработка идет не прямо по тестовым данным regexp'ом, а по их преобразованному (проиндексированному) аналогу (тому же ассоциативному массиву).

Информация

В рейтинге
1 354-й
Откуда
Рига, Латвия, Латвия
Дата рождения
Зарегистрирован
Активность

Специализация

Фулстек разработчик, Архитектор программного обеспечения
Ведущий
JavaScript
HTML
CSS
Node.js
Vue.js
Веб-разработка
Progressive Web Apps
PostgreSQL
MySQL
GitHub