Pull to refresh
48
9.3
Alex Gusev @flancer

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

Send message
Тогда я не понимаю, каким образом в приведенном примере 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 добавляет реферальную программу и новый атрибут ref к сущности Клиент, разработчик плагина 2 — аутентификацию по email'у и новый атрибут email). Я, как разработчик базового функционала ничего не знаю какие плагины разрабатываются сейчас или будут разрабатываться в будущем. Разработчики плагинов также не знакомы друг с другом и не имеют представления, какие еще плагины будут стоять рядом с их собственными. Но, разумеется, все разработчики имеют представление о базовом функционале и базовых структурах данных (сущностях/атрибутах). В какой комбинации весь этот зоопарк будет собран на стороне клиента — зависит от клиента, его постоянно меняющихся запросов и выхода новых плагинов. Количество сущностей (таблиц в БД) и набору их атрибутов (колонки таблиц) изменяется с установкой каждого нового плагина. Единая команда разработчиков есть только у базисного функционала, остальные разработчики могут строить свои плагины либо на основании базисного функционала, либо на основании других плагинов (по большому счету независимы друг от друга). Вот в такой среде и функционирует DataObject.
1. Ваш «семантически эквивалентный код» ни в коем разе не опровергает мой демонстрационный пример, что предложенный подход сокращает кол-во строк кода минимум в 2 раза для программ на PHP.
2. Если это невозможно сделать на PHP, то попробуйте изложить ваши мысли на Java/C/JavaScript/Python. У меня есть кое-какой опыт в этих языках и, надеюсь, я смогу понять, что вы хотите донести.
3.Т.е., вы утверждаете, что название «функциональная парадигма» не отражает сути?
4. Согласен, разница есть. Считайте, что я привел пример для случая, когда разработчик базового функционала ничего не знает о разработчиках плагинов, а разработчики плагинов знают только базовый функционал и не могут никаким образом повлиять ни на него, ни друг на друга.
5. Спешу вас расстроить а) PHP вполне позволяет строго использовать типизацию, б) вы все еще не поняли, что даже самая расстрогая типизация не дает возможности создавать изолированным командам разработчиков общее приложение, даже наоборот — мешает.
6. В самом общем случае вообще ничего нет — есть и такая теория.
Дополнение по пункту 3: первых 4 плагина как раз и занимаются поиском подобных конфликтов. Которые возникают как раз вследствие того, что большинство разработчиков плагинов друг с другом элементарно не знакомы.
1. Нет, подход не исключительно для PHP. Но я не могу сказать, какая разница, потому что я не понимаю, что вы написали. Раз уж вы задаете вопросы по статье, я полагаю, вы ее прочитали. Вопросов по PHP у вас не возникло — я делаю вывод, что этот язык вам знаком. А раз знаком, значит вы можете изложить свои мысли на нем. В конце концов, джентельмены во время дуэли оружие не меняют, а мы начали с PHP.
2. Я сделал вывод о том, что «функциональная парадигма делает акцент на вычислениях» на основании названия этой самой парадигмы, вы в свою очередь сказали, что «название обманчиво» и привели в качестве примера LISP. Я обратил ваше внимание на то, что мы говорим не про LISP, а именно про «функциональную парадигму» и в данном конкретном случае название вполне соответствует сути:
Функциона́льное программи́рование — раздел дискретной математики и парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании).

А теперь вы предлагаете мне показать функциональный язык (из мейнстрима), в котором нет обширной поддержки структур данных.. Похоже, вы просто потеряли нить обсуждения.
3. Потому что при распределенной разработке вполне возможен вариант, когда разработчики не могут договориться друг с другом просто в силу того, что они не знакомы друг с другом. Более того, я спорадически интегрирую такие решения друг с другом. Это Magento.
4. Но это и не означает обратного.
5. Если вы не видите выигрыша, то вам не и стоит этого делать. Если вдруг увидите выигрыш — тогда и применяйте.
Спасибо за пример.

Вот в этом месте лишние атрибуты. Разработчик базового функционала заложил только $id. Атрибут $ref заложил разработчик плагина 1, атрибут $email — разработчик плагина 2. Разработчики друг с другом не знакомы. Поэтому базовый класс выглядит по идее как-то так:
class Customer implements CustomerInterface
{
    private $id;

    public function getId(): int
    {
        return $this->id;
    }

    public function setId(int $id)
    {
        $this->id = $id;
        return $this;
    }
}

После чего у интегратора и начинается свистопляска с различными переопределениями. Акцентирую еще раз — весь фокус в том, что разработчики плагинов ничего не знают друг про друга, каждый из них знает только структуру базового объекта и свои собственные дополнения.

По поводу сохранения. В Magento при обращении к БД считывается структура таблиц со всеми атрибутами и при сохранении фильтруются «лишние» из $data. Это дело кэшируется, поэтому иногда получается весьма забавно, когда поля в таблице есть, а сохранить в них ничего нельзя. Но все лечится путем удаления кэша.
Смотрите, в базовой имплементации есть объект Customer, есть два плагина, разработчики которых не знают друг о друге, но знают, что есть базовая имплементация Customer'а. Каждый из разработчиков добавляет по одному атрибуту к базовой сущности Customer:
CREATE TABLE Customer (
  Id int NOT NULL AUTO_INCREMENT COMMENT 'this is attribute from base implementation',
  Ref varchar(255) DEFAULT NULL COMMENT 'this attribute is added by plugin 1',
  Email varchar(50) DEFAULT NULL COMMENT 'this attribute is added by plugin 2',
  PRIMARY KEY (Id)
)

и создает свои собственные расширения базового объекта CustomerRef & CustomerEmail:
/**
 * This is base object.
 *
 * @method int getId()
 * @method void setId(int $data)
 */
class Customer extends DataObject {
}

/**
 * This is extended customer (plugin 1).
 *
 * @method string getRef()
 * @method void setRef(string $data)
 */
class CustomerRef extends Customer {
}

/**
 * This is extended customer (plugin 2).
 *
 * @method string getEmail()
 * @method void setEmail(string $data)
 */
class CustomerEmail extends Customer {
}

допустим, есть внешний класс для выполнения операций с БД (разделение инструкций и данных, детали его реализации на данный момент не важны). В этом случае в базовой имплементации подгружается объект со всеми своими атрибутами и используется в таком виде в базовом workflow. В местах, где включаются обработчики плагинов (допустим, по событию), они преобразовывают базовые данные в понятный для себя вид и работают со «знакомыми» атрибутами, игноря атрибуты незнакомые. В конце базового workflow происходит сохранение объекта в БД:
        // data loader (base impl.)
        $base = $repo->load('Customer', 21);
        
        // plugin1 code on event 1
        $cust1 = new CustomerRef($base);
        $id1 = $cust1->getId();
        $ref = $cust1->getRef();

        // plugin2 code on event 2
        $cust2 = new CustomerEmail($base);
        $id2 = $cust2->getId();
        $cust2->setEmail('any@email.com');

        // data saver (base impl.)
        $repo->save('Customer', $cust2);

По сути дела производный от DataObject класс в некотором роде и является для среды выполнения адаптером к данным, хранимым в ассоциативном массиве. Все то же самое, при желании, можно изобразить и просто на ассоциативном массиве, только без autocomplete'а в IDE и без возможности поиска Find Usages.
1. Сравнил. Это точно не PHP.
2. Я имел в виду не LISP, я имел в виду именно «функциональное программирование».
3. «Строго типизированный» DTO не удовлетворяет условию «распределенной разработки» — там по определению не может быть строгой типизации.
4. Практика «игнорирую то, чего не знаю» весьма хорошо сочетается с «универсальным контейнером».
5. То, что это ассоциативный массив никак не отменяет того, что он может использоваться в качестве «универсального контейнера данных».
Ну вот! Так это то, о чем я и писал!!! Делая класс на базе DataObject вы делаете его:
а) типизируемым;
б) дополняемым;

Меня в Magento весьма сильно напрягало, что в таких структурах никогда точно не знаешь, что лежит, и сильно радовало, что в них всегда можно положить все, что угодно. Проблема в том, что в некоторых случаях множественное наследование не работает. Например, когда два-три расширения переопределяют один и тот же класс основного функционала. Разработчик каждого плагина не знает о существовании других плагинов, да и не должен. А я, как интегратор, должен сам решать в каком порядке мне выстроить иерархию наследования в конечном приложении. И иногда это бывает довольно забавной задачей, если учесть, что порой приходится совмещать в одном приложении по 15-20 сторонних плагинов.

В подобной ситуации вот такой DataObject + «гарвардский» подход (отделение данных от инструкций, функциональное программирование, если кому удобнее) может дать весьма ощутимые бонусы в виде конвейеризации обработчиков некоей «структуры данных».
По поводу использования обработчиков (SQL-процедур/функций и триггеров) рядом с данными есть различные мнения. Нет универсального решения — «каждому решению присуще сожаление» (с) Применяемый способ решения зависит от условий задачи. Обычный массив можно использовать в качестве «универсального контейнера», а можно для тех же целей его доработать (как это было сделано в той же Magento) и использовать его более эффективно. Сравните:
    $transId = $data['Sales'][3]['Payments'][0]['Transactions'][0]['Id'];
    $transId = $data['/Sales/3/Payments/0/Transactions/0/Id'];

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

Вы правы в том, что все данные одной не очень большой базы можно воткнуть в обычный ассоциативный массив. И да, это будет универсальный контейнер. А если его использовать без добавления инструкций по обработке, то это и будет универсальный контейнер данных. Вот только не будет отражена структура «известных данных», что изрядно усложнит разработку. Хотя можно сделать «типизатор» данных, который для любого произвольного массива будет извлекать данные нужной структуры. Это тоже путь.

Information

Rating
758-th
Location
Рига, Латвия, Латвия
Date of birth
Registered
Activity

Specialization

Fullstack Developer
Lead
From 3,000 €
JavaScript
HTML
CSS
Node.js
Vue.js
Web development
Progressive Web Apps
PostgreSQL
MySQL
GitHub