Однако, с развитием Интернета и бизнеса в нем, на сайте нередко начинают происходить сложные бизнес-процессы, для которых никакие CMS не предназначаны.
Пример бизнес-процессов:
- Применить промокод
- Отменить заказ
- Рассчитать размер вознаграждения продавцу
Разработчики сайтов, как правило, не видят никаких таких процессов более высокого уровня и продолжают работать на низком уровне как знают: с таблицами БД и прочими примитивами. Все это размазано тонким слоем по всей системе: в контроллере, в модели, в футере сайта. Рано или поздно, система становится такой большой, что уже не помещается в разум одного разработчика-создателя и проект начинает рассыпаться. Создатель уже не помнит все места в коде, где нужно прописать небольшие изменения в бизнес-логике, чтобы везде все работало единообразно. Система перестает быть консолидированной, вызывая одно и то же действие в разных местах, можно получить разный результат.
Как быть? Разрабатывать E-commerce сайты в стиле Enterprise: делить все на слои, хранить бизнес-логику в отдельном слое приложения, инкапсулировать изменчивость. И вообще, следовать принципам SOLID при написании кода.
На PHP в 2017 тоже можно писать качественный Enterprise, это уже не просто шаблонизатор. В статье рассказывается про некоторые вещи, которые обязательно применять при разработке Enterprise на примере PHP и Yii2 фреймворка.
- 1. Модульная слоистая архитектура
- 2. Инкапсуляция бизнес-логики
- 3. Абстракция и интерфейсы для нее
- 4. Инверсия зависимости
- 5. Управление изменчивостью
- 6. СОЛИД
1. Модульная слоистая архитектура
В настоящий момент мейнстримом является слоистая архитектура в ООП стиле. Программист видит информационную систему в виде слоев:
- Слой вью (представления);
- Доменный и сервисный слой (слой с бизнес-логикой);
- Слой моделей;
- Слой базы данных;
- Слои контроллеров c роутингом;
Система построена правильно, если каждый слой инкапсулирован (изолирован) от другого, имеет сколько-нибудь четкие очертания. Чтобы что-то поправить на слоистом бекенде сайта, нет необходимости изучать весь проект полностью, достаточно изучить один нужный слой.
Наиболее гибкими можно назвать те системы, которые состоят из наибольшего количества отдельных, изолированных друг от друга компонентов (модулей). Очень здорово, когда в случае поломки не нужно пытаться склеить разбитый монолит, а достаточно заменить один маленький осколок. Еще более здорово, когда однажды написанный код прекрасно встает в любые другие системы. Но есть одна проблема — как правильно связать все эти маленькие компоненты, чтобы приложение выглядело как единое целое, а не как куча костылей и велосипедов? В борьбе за ответ на этот вопрос родился принцип IoC (Inversion of control или инверсия управления). Принцип гласит о том, что любой ваш класс или компонент не должен жестко зависеть от других, ваш код должен работать с тем, что ему передало приложение. Код должен быть слабосвязанным и каждая его структурная частичка (класс\метод\компонент) должна выполнять лишь одну обязанность. Продолжим про инверсию зависимости в пункте 4, а пока отвлечемся на бизнес-логику и абстракцию.
2. Инкапсуляция бизнес-логики
Самое главное в модульной архитектуре Enterprise — инкапсулировать бизнес-логику. Очень просто оступиться и размазать ее не только по приложению, но еще и по модулям, тогда с развитием бизнеса поддержка такого приложения превратится в боль. Именно в части бизнес-логики чаще всего происходят сбои. Если система монолитна, переписать только больную часть нельзя, приходится переписывать вообще все, ведь бизнес-логика никак не отделена от всего остального.
Если мы отделим бизнес-логику, то получим:
- Тонкий слой контроллеров и модулей;
- Возможность тестировать в изоляции важнейшие бизнес-процессы;
- Переносимость кода.
Итак, живой пример: мы хотим написать универсальный модуль «заказ», который должен свободно встать в любое приложение. Начнем с того, что выделим в предметной области «заказ» бизнес-логику и определимся, от чего она здесь зависит. Бизнес-логику будем описывать очень простым способом: терминами, название которых совпадает с профессиональным языком общения (профессионалами в данном вопросе являются клиенты супермаркетов и продавцы).
- Отменить заказ;
- Добавить элементы в заказ;
- Отменить элемент заказа;
- Присвоить клиента заказу ;
- Присвоить продавца заказу;
- Изменить статус;
- Подсчитать сумму заказа;
- Подсчитать общее количество элементов в заказе.
Напрашивается создание класса с такими методами. Однако, есть более гибкое решение — создать для каждого действия свой отдельный класс. Например, Filling для загрузки элементов заказа из корзины.
В логике работы с моделями благодаря ActiveRecord имеются похожие штуки:
- save;
- delete;
- load;
- link и т.д.
Очень важно в уме разделять эти слои, не смотря на то, что по коду они почти неразделимы. Также очень важно понимать, что слой моделей не работает с базой, для этого существует отдельный слой (ActiveQuery). Благодаря позднему статическому связыванию, мы можем обратиться к слою БД из слоя модели, сохранив связь между ними:
$model = new Order; //Это слой моделей
$db = Order::find(); //А это уже слой БД
$db = $db->where(['id' => '1'])->one(); //Снова слой моделей
Чтобы закрепить, давай проведем соответствия «название метода» — «слой»:
- getFullName — выносим в модель ActiveRecord, слой моделей
- getUserByName — в ActiveQuery, слой работы с БД
- showFields — в виджет или вью-файл, слой отображения
- cancelCashboxTransaction — бизнес логика, сервисный или доменный слой
Вернемся к бизнес-слою. В Yii2 для подобных классов (как Filling) в модуле можно создать отдельную папку logic. Но есть одно «но»… В этой статье мы говорим про Enterprise. И будет глупо использовать данные рекомендации для обычного блога или новостного сайта. Для более простых решений бизнес-логику удобно хранить прямо в модели, а еще лучше — в сервисе.
Запуск же бизнес-процессов можно осуществлять разными способами, в зависимости от выбранной архитектуры. Наиболее удобным мне видится способ запуска через промежуточный синглтон в сервисном слое.
$order = Order::fineOne(1);
yii::$app->order->cancel($order);
Как видите, в коде выше мы работает отдельно с базой данных и отдельно с бизнес-логикой. В принципе, можно попробовать вынести вызов cancel на событие afterDelete, но это уменьшит очевидность для следующего разработчика, который будет работать с кодом.
Компонент Order.php:
<?php
namespace pistol88\order;
//...
use pistol88\order\logic\Filling;
use pistol88\order\logic\OrderCancel;
use pistol88\order\logic\OrderRecovery;
//...
class Order extends Component
{
//...
public function Filling(OrderInterface $order)
{
return yii::createObject(['class' => Filling::class, 'order' => $order])->execute();
}
public function cancel(OrderInterface $order)
{
return yii::createObject(['class' => OrderCancel::class, 'order' => $order])->execute();
}
public function recovery(OrderInterface $order)
{
return yii::createObject(['class' => OrderRecovery::class, 'order' => $order])->execute();
}
//...
Его можно добавить в секцию components конфига и использовать глобально откуда угодно.
Как видим, все методы работают с абстракциями, а не с конкретным заказом. Это нужно для реализации полиморфизма, чтобы код можно было свободно переносить и внедрять в любые проекты. И благодаря поддержки принципа полиморфизма, мы смогли как-бы инкапсулировать бизнес-логику модуля от самого модуля с контроллерами, моделями и т.д.
3. Абстракция и интерфейсы для нее
Теперь об абстракции. Для описания бизнес-логики мы используем методологию ООП, важнейшим принципом которой является абстракция. Бизнес-логика обязательно должна работать не с каким-то конкретным Order и Element, а с абстрактным (в качестве вакуума выступает Yii). Это сильно упростит всем разработчикам понимание, что делает эта самая логика. Только приложение знает, какой конкретно заказ (с какими полями) создается в системе и что является элементом заказа. Где-то элементом будет продукт питания, а где-то целый автомобиль. Наш заказ будет работать в любом случае, в этом суть полиморфизма. Для бизнес-логики предметной области «заказ» важны лишь пара свойств, которые следует описать в интерфейсе. Создаем папку interfaces и кладем туда интерфейс корзины, элемента и заказа.
Рассмотрим абстракцию на примере элемента корзины. Модуль «заказ» должен работать с любой корзиной, которая коллекционирует элементы, подходящие под интерфейс CartElement. Он содержит лишь несколько геттеров и сеттеров. Это значит, что бизнес-логика «заказа» работает лишь с ними, они создают примитивную абстракцию. По умолчанию вместе с модулем поставляется и реализация данного интерфейса в виде AR модели Element. Но модуль ни в коем случае не навязывает именно ее, конечный пользователь может «подсунуть» другую модель через Di-container.
А вот как эта абстракция используется в бизнес-логике Filling:
<?php
namespace pistol88\order\logic;
use pistol88\order\interfaces\Cart;
use pistol88\order\interfaces\OrderElement;
use yii;
class Filling
{
public $order;
protected $cart;
protected $element;
public function __construct(Cart $cart, OrderElement $element, $config = [])
{
$this->cart = $cart;
$this->element = $element;
}
public function execute()
{
foreach($this->cart->elements as $element) {
$elementModel = $this->element;
$elementModel = new $elementModel;
$elementModel->setOrderId($this->order->id);
$elementModel->setAssigment($this->order->is_assigment);
$elementModel->setModelName($element->getModelName());
///Еще куча вызовов set
}
//...
}
}
Бизнес-логика заявляет в конструкторе, что ждет на вход тип Cart и OrderElement. Магия Yii2 в своем внутреннем реестре ищет подходящий тип, имплементирующий интерфейс типа и передает его на вход. Сам Filling работает с абстракцией.
Обратите внимание, проектируя бизнес-логику, я беру эти интерфейсы из реальной (не компьютерной) жизни. Я понятия не имею, как будет развиваться система, в которой установлен данный модуль, какая там будет база данных, что там будут заказывать. Но я имею представление о реальной жизни и точно знаю, что модуль не должен выходить за ее рамки, иначе код начнет бродить в байтах и битах. В бизнес-логике не может появиться никаких таблиц БД и работы с байтами. Вы видели в реальной жизни при создании заказов в магазине какие-то там таблицы? Их там нет. Все эти таблицы БД есть в предметной области «веб-приложение» на уровень ниже, предметная область «заказ» оперирует совершенно другими сущностями, находящимися на самом верху.
4. Инверсия зависимости
Самая популярная на сегодня реализация принципа IoC (Inversion of Control, инверсия управления) — Dependency Injection (внедрение зависимостей). Здесь все очень просто. Допустим, есть модули:
- Корзина;
- Заказ, зависящий от корзины.
Каждый модуль написал независимый разработчик. При этом они могут прекрасно работать вместе или порознь. Лишь только приложение, в котором они установлены, знает что от чего зависит (конкретно, а не абстрактно). Это приложение должно как-то связать эти модули, при этом связь должна осуществляться не в где-то там в удобном сейчас месте, а в особом, специально выделенном под это контейнере. Этот контейнер так и называется: «IoC-контейнер». Он содержит данные о связях, приложение «инжектит» эти связи в отдельные классы модулей.
Понимаю, что для многих начинающих программистов все написанное выше — очередная демагогия и буря в стакане, чрезмерное усложнение простых вещей. Хочу показать, насколько все просто на самом деле и как сильно это упрощает жизнь при долговременной поддержке проектов с развитием бизнеса на примере Yii2. На практике ниже все станет ясно.
Вернемся к Filling, который зависит от некоего типа Cart (класс реализации обязательно должен содержать методы getElements и truncate). Ключевое слово тут — «некий», а не «конкретный». Мы видим четкую зависимость конкретного «заказа» от некой «корзины». Только приложение знает, каким образом устроена в нем корзина — может, это отдельный модуль, а может быть нативная реализация. В любом случае, приложение должно каким-то образом «заинжектить» эту корзину в заказ, нужно связать интерфейс Cart с реализацией.
Допустим, мы работаем с приложением — онлайн пиццерией. Значит, внедряя зависимости, мы будем работать на стыке трех предметных областей:
- Магазин;
- Заказ;
- Корзина.
Очевидно, что связь всего этого должна находиться где-то в магазине. Именно там содержится контейнер зависимости, нам нужно только добавить одну запись в репозиторий контейнера. Делается это следующим образом:
yii::$container->set('pistol88\order\interfaces\Cart', 'app\objects\Cart');
yii::$container — тот самый контейнер, который существует в любом приложении Yii2 из коробки. Все очень просто, первым параметров передается интерфейс, вторым — реализация. Осталось только разобраться, в каком месте вставлять эту строку. Вариантов несколько:
- В index.php (точка входа) приложения
- В config.php перед return. Я рекомендую именно этот вариант.
В качестве реализаций для интерфейсов мы передаем объект Cart, который не делает ничего:
<?php
namespace app\objects;
class Cart extends \pistol88\cart\Cart implements \pistol88\order\interfaces\Cart
{
}
Это пустой класс (так повезло), который просто указывает, что в качестве Cart в данной системе выступает \pistol88\cart\Cart, который четко имплементирует нужный «заказу» интерфейс. Изначальный Cart даже не знает, что он имплементирует что-то там в системе. Если бы нам не повезло и у Cart не было бы getElements, пришлось бы его реализовать в app\objects\Cart. После того, как мы заинжектим зависимость, данный Cart магическим образом передается на конструтор Filling.
Еще раз обратим внимание на факт: модули yii2-cart и yii2-order никак не связаны друг с другом, никак не зависят друг от друга. Это значит, что модуль следует расшифровке одной из букв аббревиатуры SOLID (D, Принцип инверсии зависимостей), и это здорово. Значит, модуль достаточно универсален и может быть использован в любом инстансе Yii2. Было бы совсем круто, если бы модуль не зависел даже от фреймворка, но тогда придется отказаться от готовой админки в его составе, поставлять эту админку и все виджеты отдельным модулем, что красиво, но неудобною.
Очень важное замечание: такой способ внедрения зависимости подойдет только для очень крупных E-comerce проектов. Есть более простой и менее магический способ внедрения зависимости. При подключении модуля\компонента можно просто передать ему нужный класс в качестве свойства. Сложности с yii::$container нужны, по сути, только для того, чтобы сделать как можно более тонкими и простыми все слои (сервисный, моделей) за счет выделения еще одного слоя с зависимостями.
PS: как правильно заметили в комментариях, yii2-cart тоже должен следовать нужному интерфейсу, чтобы разработчик не вздумал его переписать и все сломать. Способ такой связи продумаю позже (если такое вообще возможно в Yii).
5. Управление изменчивостью
Под влиянием WordPress, я очень полюбил делать хуки вообще везде. Я считаю, именно благодаря хукам WordPress завоевал мир, хуки закрыли большинство возражений «а что если...», «а можно ли...». Да, можно всё, если в месте, о котором идет речь, есть хук. Это просто идеальный способ быстро изменить поведение стороннего модуля или отдельного участка системы, не трогая код ядра.
В Enterprise решениях очень важно иметь возможность управлять изменчивостью. Один и тот же инстанс с модулями использует множество бизнесов и только в каком-то идеальном мире все эти бизнесы работают одинаково. В настоящий момент реальность такова, что покупая готовый продукт, бизнес вкладывает еще x10 денег в его переписывание под себя. Чтобы сократить эти издержки, приложение должно содержать хуки.
В Yii2 хуки удобнее всего делать через события и поведения. Давайте на примере модуля pistol88/yii2-cart посмотрим, как можно реализовать управление изменчивостью.
Имеем:
- Десять разных ИМ, продающих автомобильные шины
- Все 10 ИМ используют один скелетон и одни модули
Проблема:
- У 5 из 10 ИМ используется нетипичное округление суммы заказа: где-то до 5 рублей в большую, где-то до 10 в меньшую
Решение проблемы заложено в сервисе, где расположена вся бизнес-логика. В момент вызова getCost вызываются триггеры:
//..
use pistol88\cart\events\Cart as CartEvent;
//..
public function getCost($withTriggers = true)
{
//...
$cartEvent = new CartEvent(['cart' => $this->cart, 'cost' => $cost]);
if($withTriggers) {
$this->trigger(self::EVENT_CART_COST, $cartEvent);
$this->trigger(self::EVENT_CART_ROUNDING, $cartEvent);
}
//..
А в конфиге приложения события триггера self::EVENT_CART_ROUNDING («cart_rounding») прослушиваются, происходит прием DataProvider и внесение изменчивости в эти данные:
'cart' => [
'class' => 'pistol88\cart\Cart',
//..
'on cart_models_rounding' => function($event) {
$event->cost = ceil($event->cost/10)*10;
}
//'as RoundBehavior' => ... //Как альтернатива "on" с переносимым поведением
],
В getCost принимается уже измененный $cost из датапровайдера, обработанного коллбек функцией конфига:
$cartEvent = new CartEvent(['cart' => $this->cart, 'cost' => $cost]);
if($withTriggers) {
$this->trigger(self::EVENT_CART_COST, $cartEvent);
$this->trigger(self::EVENT_CART_ROUNDING, $cartEvent);
}
$cost = $cartEvent->cost;
$this->cost = $cost;
return $this->cost;
Как результат: мы не трогали сам модуль и скелетон, мы всего-лишь внесли туда немного изменчивости через хук в конфиге, можно дальше какое-то время свободно разрабатывать общий для всех функционал, не разделяя всех клиентов на ветки.
При работе с бизнесом крайне важно понимать, что он либо постоянно развивается и поэтому живет, либо имеет какую-то уникальную особенность. В идеале такие вещи программировать через изменчивость, не создавая отдельного инстанса, это значительно снизит расходы софтверной компании на интеграции решений.
6. СОЛИД
Все так сложно лишь ради того, чтобы следовать принципам SOLID. Не хочу даже давать ссылку куда-то, везде кошмарное описание, попробуйте погуглить самостоятельно. SOLID направлен на то, чтобы повысить шансы проекта прожить достаточно долго без необходимости переписывать все с нуля. Все принципы — всего-лишь теория. Единственный способ понять их сейчас — это практика.
Попробую немного упростить этот набор принципов. Проект имеет шансы прожить достаточно долго, только если в нем соблюдены такие принципы:
- Отсутствие дублирования кода. Важно писать код так, чтобы его куски не приходилось каждый раз переносить вручную и адаптировать под «здесь и сейчас», все должно быть по щелчку пальцев, одной настройкой и в одном месте.
- Легкая переносимость кода. Любой единожды написанный функционал должен быть инкапсулирован от системы. Тогда этот функционал можно взять и просто перенести куда угодно, не нужно тратить кучу времени на интеграцию и обрубание лишнего.
- Слабая связанность кода. Функциональные части системы должны быть слабо связаны между собой. «Мохнатые уши» не должны зависеть от конкретной кошки и ее головы. Если мы однажды описали уши, то они должны быть полиморфны, то есть применимы к любому животному, которое соответствует типу Animal.
Только следуя принципам SOLID, IT миру можно обрести гармонию с миром бизнеса.
PS: все модули, описанные в этой статье, находятся в стадии глубокой разработки. Непрофессионалам не рекомендую использовать их на production в настоящий момент.
Комментарии (70)
lair
13.03.2017 00:19+10Разрабатывать E-commerce сайты в стиле Enterprise
А при чем тут Enterprise, простите?
В настоящий момент мейнстримом является слоистая архитектура в ООП стиле. Программист видит информационную систему в виде слоев (сверху вниз):
Во-первых, эта ваша "слоистая архитектура" уже давно и неоднократно раскритикована. Куда вы crosscutting concerns положите?
Во-вторых, в вашем списке явно перепутан порядок.
Наиболее гибкими можно назвать те системы, которые состоят из наибольшего количества отдельных, изолированных друг от друга компонентов (модулей).
Нет, наиболее гибкие — это те системы, в которых есть, гм, гибкость. Можно иметь множество модулей, но система все равно не будет гибкой, потому что ни один из этих "модулей" нельзя заменить.
Очень здорово, когда в случае поломки не нужно пытаться склеить разбитый монолит, а достаточно заменить один маленький осколок. [...] Но есть одна проблема — как правильно связать все эти маленькие компоненты, чтобы приложение выглядело как единое целое, а не как куча костылей и велосипедов?
Единое целое — это монолит и есть. Вы уж определитесь, чего вы хотите.
Например, LoadElements для загрузки элементов заказа из корзины.
Во-первых, название отвратительное. Во-вторых, это разве бизнес-логика?
$model = new Order; //Это слой моделей $db = Order::find(); //А это уже слой БД $db = $db->where(['id' => '1'])->one(); //Снова слой моделей
Вы только что нарушили первый и главый постулат "слоистой", как вы выражаетесь, архитектуры: слой может вызывать только следующий слой, но никогда не перескакивать через него. Если ваша структура слоев имеет вид "бизнес -> модель -> БД", то бизнес может вызывать только модель.
Запуск же бизнес-процессов можно осуществлять разными способами, в зависимости от выбранной архитектуры. Наиболее удобным мне видится способ запуска через промежуточный синглтон в сервисном слое.
Синглтоны — зло.
Для описания бизнес-логики мы используем методологию ООП, важнейшим принципом которой является абстракция.
(а) кто вам это сказал и (б) что вы понимаете под абстракцией?
Самая популярная на сегодня реализация принципа IoC (Inversion of Control, Инверсия зависимости) — Dependency Injection (внедрение зависимостей).
Вообще-то, нет. Прямо начиная с того, что Inversion of Control — это инверсия управления, а не инверсия зависимости.
Еще раз обратим внимание на факт: модули yii2-cart и yii2-order никак не связаны друг с другом, никак не зависят друг от друга. Это значит, что модуль следует расшифровке одной из букв аббревиатуры SOLID (D, Принцип инверсии зависимостей), и это здорово.
А то, что в
yii2-order
объявлен интерфейсpistol88\order\interfaces\Cart
, имплементируемыйyii2-cart
— это не зависимость, да?
При этом, кстати, наличие зависимости не означает, что DIP нарушен — вы не можете построить решение без зависимостей; DIP определяет, откуда и куда направлены зависимости.
Под влиянием WordPress, я очень полюбил делать хуки вообще везде. Я считаю, именно благодаря хукам WordPress завоевал мир, хуки закрыли большинство возражений «а что если...», «а можно ли...». Да, можно всё, если в месте, о котором идет речь, есть хук. Это просто идеальный способ быстро изменить поведение стороннего модуля или отдельного участка системы, не трогая код ядра.
… и прекрасный способ сделать систему непонятной и неанализируемой без отладки. Что произойдет, если я вызову вот этот метод? А хрен знает, какой хук вызовется, то и произойдет.
pistol
13.03.2017 00:39Спасибо за развернутую критику!
Про crosscutting concerns почитаю. А в каком порядке вы видите порядок слоев?
имплементируемый yii2-cart — это не зависимость, да?
yii2-cart его не иммплементирует. Его имплементирует другой Cart, расположенный в приложении и наследующий yii2-cart.
и прекрасный способ сделать систему непонятной и неанализируемой без отладки
Согласен, WP в этом плане ужасен.lair
13.03.2017 00:47А в каком порядке вы видите порядок слоев?
Ну есть же типовая картинка: UI и API (в смысле, публично предоставляемые) зависят от бизнеса (и да, UI и API — это два разных, не связанных друг с другом слоя), бизнес зависит от БД и инфраструктурных сервисов (типа email, и снова БД и сервисы друг с другом никак не связаны). В принципе, на этом пауке уже видно, что идея слоев для архитектуры в целом плоха.
yii2-cart его не иммплементирует. Его имплементирует другой Cart, расположенный в приложении и наследующий yii2-cart.
А вы говорите, просто и понятно. Расскажите тогда, как именно связаны эти два модуля, и что обеспечивает выполнение контракта между ними.
Согласен, WP в этом плане ужасен.
Это не WP ужасен, это общее место модели хуков/событий в целом.
pistol
13.03.2017 01:05Расскажите тогда, как именно связаны эти два модуля
Через контейнер в приложении и интерфейс.
Слой вью действительно забыл наверх поставить.lair
13.03.2017 01:06Через контейнер в приложении и интерфейс.
Прекрасно, и где этот интерфейс определен?
pistol
13.03.2017 01:16В приложении.
lair
13.03.2017 01:17Эмм. У вас приложение использует компоненты (которые, поскольку компоненты, ничего не знают о приложении), которые реализуют/используют интерфейс, определенный в приложении? У вас большая проблема с зависимостями.
pistol
13.03.2017 01:19Сам интерфейс поставляется модуле, а его реализация с Cart — в приложении.
lair
13.03.2017 01:22Итак, у вас есть интерфейс
Cart
, который поставляется в модулеorders
, правильно? И реализация которого нужнаorders
для функционирования?pistol
13.03.2017 01:28Да, зависимость на абстракцию получается. А как передать элементы корзины Ордеру, не имея зависимости вообще?
lair
13.03.2017 01:31А как передать элементы корзины Ордеру, не имея зависимости вообще?
Динамическое связывание, вот это всё.
Но я повторюсь, DIP — он не про то, чтобы не было зависимостей, он про то, куда зависимости направлены. Дословно: "High-level modules should not depend on low-level modules. Both should depend on abstractions." (это первая половина, там еще вторая есть). У вас это нарушено (только у вас модули одного уровня, но не суть).
lair
13.03.2017 01:28Ну да, реализация
Cart
находится в самом модулеyii2-order
:pistol88\order\drivers\Pistol88Cart
. И она зависит отyii2-cart
:extends \pistol88\cart\Cart
. Так что все-таки у вас модули зависят друг от друга напрямую. При этом мне интересно: а что же гарантирует, что\pistol88\cart\Cart
реализует\pistol88\order\interfaces\Cart
— т.е., что ваш код вообще работает?pistol
13.03.2017 01:31Ну… Это скорее некий examples, сборник готовых связей с разными модулями. Его лучше перенести в другое место, он просто для удобства. В сообществе yii много новичков программирования, они хотят только копипастить по инструкции и чтобы все сразу работало. В readme четко написано по это.
этом мне интересно: а что же гарантирует, что \pistol88\cart\Cart реализует \pistol88\order\interfaces\Cart — т.е., что ваш код вообще работает?
Проверка на тип в конструкторе LoadData.lair
13.03.2017 01:34Это скорее некий examples, сборник готовых связей с разными модулями. Его лучше перенести в другое место, он просто для удобства.
После чего ваши модули станут не связанными вообще, и DIP к ним не будет применим.
Проверка на тип в конструкторе LoadData.
Покажите код, я не смог найти.
pistol
13.03.2017 01:36LoadElements, туплю в 5 ночи.
https://github.com/pistol88/yii2-order/blob/master/logic/LoadElements.phplair
13.03.2017 01:43+1Там проверяется, что переданный компонент — это
pistol88\order\interfaces\Cart
. При этом, как мы помним, компонентом будетpistol88\order\drivers\Pistol88Cart
, который действительно реализуетpistol88\order\interfaces\Cart
. Проблема однако же в том, что эта реализация сделана через наследование от\pistol88\cart\Cart
, и вот он ничего проpistol88\order\interfaces\Cart
не знает.
Иными словами, нет ничего, что помешало бы разработчику
\pistol88\cart\Cart
поменять реализацию так, что она станет несовместимой сpistol88\order\interfaces\Cart
, и все сломать.pistol
13.03.2017 01:49Теперь понял про что вы, спасибо. Если создать такой же интерфейс в yii2-cart, это решит проблему?
lair
13.03.2017 01:50Если создать такой же интерфейс в yii2-cart, это решит проблему?
Что значит "такой же"? С таким же контрактом? Нет, не решит, потому что между интерфейсами в
yii2-cart
иyii2-order
не будет никакой связи, у вас все равно не будет проверок.
pistol
13.03.2017 01:38После чего ваши модули станут не связанными вообще, и DIP к ним не будет применим.
Они и так не связаны вообще. Только потенциально, через интерфейс. Папка drivers создана только для того, чтобы в Ридми понятно описать принцип связи.lair
13.03.2017 01:40+1Они и так не связаны вообще.
Значит, DIP к ним не применим.
Только потенциально, через интерфейс.
До тех пор, пока это интерфейс, определенный в одном из модулей (как это у вас), DIP у вас нарушен.
pistol
13.03.2017 00:45И про перескакивание через слой поясните :) Где мы перескочили через слой?
lair
13.03.2017 00:49Вот здесь и перескочили:
$model = new Order; //Это слой моделей $db = Order::find(); //А это уже слой БД $db = $db->where(['id' => '1'])->one(); //Снова слой моделей
Один и тот же код, находящийся в одном и том же слое, вызывает два разных слоя, которые, при этом, знают друг о друге.
Fesor
14.03.2017 00:06(а) кто вам это сказал и (б) что вы понимаете под абстракцией?
Ну я по пункту (а) пройдусь.
В целом ради абстракции ООП и придумывалось. Точнее это имплементация Actor Model, где каждый "актор" это какая-то абстракция которая выполняет определенную роль. То есть инкапсуляция, полиморфизмы всякие и т.д. были задолго до ООП (изоляцией состояния еще в 60-х начали баловаться) и являются просто хорошими принципами.
lair
14.03.2017 00:11В целом ради абстракции ООП и придумывалось.
Это, скажем так, неоднозначный вопрос. Скажем, для Кея "ООП придумывалось" именно ради идеальной инкапсуляции, где каждый объект сам решает, как он обрабатывает какие сообщения: "OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things."
Поэтому вопрос "кто сказал" — он как раз очень важный.
Fesor
14.03.2017 02:19Поэтому вопрос "кто сказал" — он как раз очень важный.
Кто сказал — вопрос совершенно не важный. Важно что имелось ввиду.
По сути приведенная вами цитата описывает как раз таки actor model. Late binding и hinding of state-process лишь говорят что акторы представляют собой абстракции (снаружи мы знаем и хотим знать только о поведении, а все остальное инкапсулировано внутри). То есть без грамотной декомпозиции и построения хороших абстракций не выйдет построить хорошую систему.
lair
14.03.2017 11:24Late binding и hinding of state-process лишь говорят что акторы представляют собой абстракции (снаружи мы знаем и хотим знать только о поведении, а все остальное инкапсулировано внутри).
И вот в этот момент мы и приходим к вопросу (б) ("что вы понимаете под абстракцией?"). Потому что "снаружи мы знаем только о поведении" — это в моем понимании инкапсуляция, а не абстракция.
Fesor
14.03.2017 11:49это в моем понимании инкапсуляция, а не абстракция.
Ну как бы суть инкапсуляции в том что мы детали реализации прячем. Что как бы является синонимом абстракции. Так что "идеальную инкапсуляция" можно трактовать как "качественную абстракцию". Хотя вашу точку зрения по этому вопросу я бы очень хотел услышать.
lair
14.03.2017 12:01Ну как бы суть инкапсуляции в том что мы детали реализации прячем. Что как бы является синонимом абстракции.
А является ли?
В моем понимании абстракция — это когда мы выделяем существенные для нас признаки, отбрасывая несущественные; в частности, когда мы говорим "поток (данных) — это что-то, куда мы можем записать и откуда мы можем прочитать", и это — абстракция, которая может быть конкретизирована до "потока, в котором есть перемещение на позицию" и вплоть до "потока поверх буфера в памяти".
У Кея ровно одна абстракция — все есть объект, всему можно послать сообщение. Вся последующая декомпозиция и построение прикладных абстракций вида "сюда мы можем слать такие сообщения, а туда — такие" — это личное дело каждого программиста (и далеко не всегда — программиста). Так что — в моем понимании — ООП дает нам механизм для построения прикладных абстракций, но я совершенно не уверен, что оно ради этого придумывалось.
Но это сугубо терминологический спор, и именно поэтому я всегда начинаю такие разговоры с вопроса "что вы понимаете под абстракцией".
europanzer
15.03.2017 09:39Под cross-cutting concerns имеются ввиду в том числе транзакции БД?
Подскажите статью/эксперта раскритиковавшего слоистую архитектуру.lair
15.03.2017 11:36Под cross-cutting concerns имеются ввиду в том числе транзакции БД?
Нет, аутентификация/авторизация, аудит, логгирование и так далее.
Подскажите статью/эксперта раскритиковавшего слоистую архитектуру.
http://www.ben-morris.com/the-problem-with-tiered-or-layered-architecture/
http://johannesbrodwall.com/2014/07/10/the-madness-of-layered-architecture/
http://blog.ploeh.dk/2012/02/09/IsLayeringWorththeMapping/
http://www.michaelnygard.com/blog/2015/04/bad-layering/
europanzer
15.03.2017 11:55Спасибо большое. Почитаю.Вместо слоистой рекомендуете гексагональную? Или в гексагональной та же проблема?
Fesor
15.03.2017 19:51Давайте думать...
Под архитектурой мы как правило понимаем определенный набор принципов и ограничений, которые позволяют нам добиться каких-то характеристик системы. Например если мы возьмем гексагональную архитектуру, она налагает на нашу слоеную архитектуру ограничение в виде направления зависимостей внутри слоев (строго снаружи внутрь) и предлагает использовать инверсию зависимостей для этого (то есть на границе слоев будут порты и адаптеры, которые соединяют слои вместе). Как результат, логика приложения не будет зависеть от инфраструктуры что дает определенную гибкость.
Но эта гибкость нужна далеко не всегда, потому тащить ее по дефолту смысла особо нет. Иногда намного выгоднее реализовать часть системы напрямую завязанную на инфраструктуру что бы удовлетворить каким-то нефункциональным требованиям и уменьшить издержки на поддержку/разработку. А иногда это будет плохой идеей.
Потому перед тем как выбирать архитектуру, нужно прекрасно понимать какие характеристики системы вам нужны для каждого конкретного случая. И уже потом смотреть какие виды архитектур чего решают. Можно вообще комбинировать идеи например чтобы максимально эффективно решать проблемы.
funca
13.03.2017 02:11+2Архитектура это в большей степени проектирование, чем программирование. Демонстрировать архитектуру кодом не очень выгодно по нескольким причинам. Программный код содержит много других деталей, что ухудшает восприятие. Код не обязан отражать все архитектурные нюансы. Код должен быть эффективным. Поэтому в энтерпрайзе такие штуки чаще моделируются и документируются, а не кодятся.
Подход «кодом вперед» скорее из области, хм… кодинга. Когда нет возможности отдельно заниматься проектированием, попробуйте TDD. Тесты реально помогают снижать количество ошибок, указывая в том числе и на некоторые архитектурные просчеты.
sspat
13.03.2017 14:25+5Иронично, но единственный кусок кода в статье, который действительно является бизнес-логикой, вы вынесли в…
'on cart_models_rounding' => function($event) { $event->cost = ceil($event->cost/10)*10; }
… конфигурационный файл приложения, где он будет сидеть рядом с настройками подключения к бд и DI-контейнера.pistol
13.03.2017 15:33Это же конфиг. Оттуда функция берется в компоненте сервисного слоя, так что фактически эта логика сидит в компоненте.
sspat
13.03.2017 17:18+4Одной из задач слоистой архитектуры является изоляция бизнес-логики от всего, что к ней не относится — базы данных, инфраструктурных сервисов, фреймворка. Слой бизнес-логики должен отражать логику бизнес-процессов, у вас же он отражает то, как устроена база данных и фреймворк, а когда собственно требуется применить бизнес-правило (округлить цену) делегирует это непонятно куда, даже не убедившись, что это правило вообще будет применено. Я могу удалить эту строку из конфига и приложение продолжит себе работать, в то время как бизнес-требование уже не соблюдается — сам слой бизнес-логики никак не следит за этим, он просто «я сделаль», а что сделал — сам не знает. Представьте, что у вас таких бизнес требований сто штук? Получите файл-конфиг набитый никак не связанным кодом, облепленный обьектами бизнес-логики которые сами не при делах. Вы уверены, что работая над сложным проектом в команде, все смогут в этом быстро разобраться? Что вы сами не запутаетесь?
pistol
13.03.2017 19:45Одной из задач слоистой архитектуры является изоляция бизнес-логики от всего, что к ней не относится — базы данных, инфраструктурных сервисов, фреймворка. Слой бизнес-логики должен отражать логику бизнес-процессов, у вас же он отражает то, как устроена база данных и фреймворк
Да, сейчас посмотрел еще раз и вижу, что бизнес-логика размешана с реализацией прикладных задач. Спасибо ;)
а когда собственно требуется применить бизнес-правило (округлить цену) делегирует это непонятно куда, даже не убедившись, что это правило вообще будет применено
А куда должно быть делегировано это бизнес-правило?lair
13.03.2017 20:55А куда должно быть делегировано это бизнес-правило?
Вообще, идея о том, что бизнес-правила должны куда-то делегироваться из слоя бизнес-логики, сама по себе несколько… странная.
pistol
13.03.2017 21:03А если бизнес-правила растут как грибы? При чем у разных представителей одного бизнеса совершенно разные грибы, но все они — грибы со шляпой и ножкой? Куда вынести эту изменчивость?
lair
13.03.2017 21:15А зачем ее куда-то выносить? Бизнес-слой ровно для этого и создан, чтобы описывать бизнес-правила.
pistol
13.03.2017 21:19Чтобы N бизнесов пользовались общим поддерживаемым бизнес-слоем, без необходимости делить его на N веток и поддерживать каждую (отличия там косметические).
lair
13.03.2017 22:25Ну так делаете общее ядро и дальше адаптируете его под каждый бизнес. Все равно это остается в слое бизнес-логики.
Fortop
15.03.2017 08:13-1Это то, что называется конфигурациями в 1С.
То есть сама бизнеслогика делится на некоторую общую часть и индивидуальную.
Поэтому ничего страшного в нахождении второй части в конфигах нет.
Но, естественно, не в одном конфиге с подключением к БДlair
15.03.2017 11:30+1Поэтому ничего страшного в нахождении второй части в конфигах нет.
"Страшное" — вполне себе есть: например, добрая часть разработческих инструментов с конфигами работает не так, как с кодом. Соответственно, применять удобные практики совсем не так… удобно.
Другое дело, что в каких-то случаях достоинства конфигов перевешивают.
mistergonza
14.03.2017 06:30-3Ужас какой-то, и Yii, и Entreprise, и слои. Enterprise это не только сервис контейнеры и внедрение зависимостей, это огромнейший кусок льда под водой, от огромного айсберга. И для Enterprise используйте предназначенные для этого инструменты.
Fortop
15.03.2017 08:26$order = Order::fineOne(1);
yii::$app->order->cancel($order);Вы фреймворк неудачный выбрали для демонстрации.
Т.е. на нем, конечно, можно раскрыть поднятую тему, но сам он не слишком этому способствует. Равно как и его собрат по нише — Laravel.
Смотрите в сторону либо более тяжёлых вещей типа Zend, Symfony.
Либо более лёгких Lumen, Silex, Slim, Expressive.
В первом случае вам будет навязываться фреймворком определённая архитектура. Во втором случае у вас не будет лишнего и все будет зависеть исключительно от вашего опыта и понимания результата.
europanzer
15.03.2017 11:52Первый опыт по разделению приложений на Yii на слои?
Видение проблемы верное, немного не так понимаете способ решения проблемы.
При использовании слоистой архитектуры вы должны отделить слой вашей бизнес-логики от всего, в том числе и от yii. Это не всегда удается сразу. Но статьи пишутся о решениях, близких к идеалу. Бизнес-логика должна быть центром вашего приложения. Сам заголовок статьи выглядит странно. Слоистая архитектура не бывает «на чем то» (на фреймворке X). Она сама по себе — либо есть, либо нет. Фреймворк или просто независимые компоненты, библиотеки, определяют UI к бизнес логике и обеспечивают инфраструктуру (БД, безопасность и проч.).
springimport
16.03.2017 18:03Простите за оффтоп, есть кто, кто уже работал с magento 2?
При многих минусах, я думаю что архитектура там довольно неплохая.
Fesor
16.03.2017 18:13+1довольно неплохая.
относительно. Достаточно глянуть на класс
Order
чтобы ужаснуться насколько раздута эта архитектура только за счет "псевдо универсальности". Многие вещи сделаны дико сложно и при этом сложно кастомизируются. В целом это типично для любых коробочных решений. Мне же хотелось бы увидеть модульную систему где проще выкинуть маленький модуль и заменить на другой под конкретную задачу (например модуль ордеров что бы имплементил сэт контрактов и позволял полностью заменить структуру).
antonksa
У меня от фейспалма на лице отпечаток остался.
pistol
Наверно, вы никогда не программировали промокод с накопительной скидкой для постоянного клиента, действующий только на определенную группу товаров в ночное время по пятницам ;)
antonksa
Если Вы еще добавите:
то я заплачу.
Я Lisp бы выучил за то, что он не php...
Вы написали нормальную статью. Ну для тех кто с этим работает (Yii2).
Просто все это несколько, ну скажем так — банально. Назвать это сверхсложной бизнес логикой? А что тогда не сложная? Добавить страницу через админку?
pistol
Сверхсложной ее назвать трудно. Это просто сложная логика по сравнению с логикой работы с таблицей.