Большинство сайтов в вебе работают исключительно с простой информацией: страница, статья, категория статей. При генерации HTML, на стороне сервера происходят некоторые простые процессы: подключение к базе, получение статьи по ID, привязка к статье комментариев и т.д.

Однако, с развитием Интернета и бизнеса в нем, на сайте нередко начинают происходить сложные бизнес-процессы, для которых никакие CMS не предназначаны.

Пример бизнес-процессов:

  • Применить промокод
  • Отменить заказ
  • Рассчитать размер вознаграждения продавцу

Разработчики сайтов, как правило, не видят никаких таких процессов более высокого уровня и продолжают работать на низком уровне как знают: с таблицами БД и прочими примитивами. Все это размазано тонким слоем по всей системе: в контроллере, в модели, в футере сайта. Рано или поздно, система становится такой большой, что уже не помещается в разум одного разработчика-создателя и проект начинает рассыпаться. Создатель уже не помнит все места в коде, где нужно прописать небольшие изменения в бизнес-логике, чтобы везде все работало единообразно. Система перестает быть консолидированной, вызывая одно и то же действие в разных местах, можно получить разный результат.

Как быть? Разрабатывать E-commerce сайты в стиле Enterprise: делить все на слои, хранить бизнес-логику в отдельном слое приложения, инкапсулировать изменчивость. И вообще, следовать принципам SOLID при написании кода.

На PHP в 2017 тоже можно писать качественный Enterprise, это уже не просто шаблонизатор. В статье рассказывается про некоторые вещи, которые обязательно применять при разработке Enterprise на примере PHP и Yii2 фреймворка.


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:
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:

Filling.php
<?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)


  1. antonksa
    12.03.2017 22:48
    +1

    Однако, с развитием Интернета и бизнеса в нем, на сайте нередко начинают происходить сложные бизнес-процессы, для которых никакие CMS не предназначаны.

    Пример бизнес-процессов:

    Применить промокод
    Отменить заказ
    Рассчитать размер вознаграждения продавцу

    У меня от фейспалма на лице отпечаток остался.


    1. pistol
      12.03.2017 22:51

      Наверно, вы никогда не программировали промокод с накопительной скидкой для постоянного клиента, действующий только на определенную группу товаров в ночное время по пятницам ;)


      1. antonksa
        12.03.2017 23:54
        -2

        Если Вы еще добавите:


        • и за еду

        то я заплачу.


        $cost = $cartEvent->cost;
        $this->cost = $cost;
        return $this->cost;

        Я Lisp бы выучил за то, что он не php...


        Вы написали нормальную статью. Ну для тех кто с этим работает (Yii2).
        Просто все это несколько, ну скажем так — банально. Назвать это сверхсложной бизнес логикой? А что тогда не сложная? Добавить страницу через админку?


        1. pistol
          12.03.2017 23:59
          +1

          Сверхсложной ее назвать трудно. Это просто сложная логика по сравнению с логикой работы с таблицей.


  1. 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 завоевал мир, хуки закрыли большинство возражений «а что если...», «а можно ли...». Да, можно всё, если в месте, о котором идет речь, есть хук. Это просто идеальный способ быстро изменить поведение стороннего модуля или отдельного участка системы, не трогая код ядра.

    … и прекрасный способ сделать систему непонятной и неанализируемой без отладки. Что произойдет, если я вызову вот этот метод? А хрен знает, какой хук вызовется, то и произойдет.


    1. pistol
      13.03.2017 00:39

      Спасибо за развернутую критику!

      Про crosscutting concerns почитаю. А в каком порядке вы видите порядок слоев?

      имплементируемый yii2-cart — это не зависимость, да?

      yii2-cart его не иммплементирует. Его имплементирует другой Cart, расположенный в приложении и наследующий yii2-cart.

      и прекрасный способ сделать систему непонятной и неанализируемой без отладки

      Согласен, WP в этом плане ужасен.


      1. lair
        13.03.2017 00:47

        А в каком порядке вы видите порядок слоев?

        Ну есть же типовая картинка: UI и API (в смысле, публично предоставляемые) зависят от бизнеса (и да, UI и API — это два разных, не связанных друг с другом слоя), бизнес зависит от БД и инфраструктурных сервисов (типа email, и снова БД и сервисы друг с другом никак не связаны). В принципе, на этом пауке уже видно, что идея слоев для архитектуры в целом плоха.


        yii2-cart его не иммплементирует. Его имплементирует другой Cart, расположенный в приложении и наследующий yii2-cart.

        А вы говорите, просто и понятно. Расскажите тогда, как именно связаны эти два модуля, и что обеспечивает выполнение контракта между ними.


        Согласен, WP в этом плане ужасен.

        Это не WP ужасен, это общее место модели хуков/событий в целом.


        1. pistol
          13.03.2017 01:05

          Расскажите тогда, как именно связаны эти два модуля

          Через контейнер в приложении и интерфейс.

          Слой вью действительно забыл наверх поставить.


          1. lair
            13.03.2017 01:06

            Через контейнер в приложении и интерфейс.

            Прекрасно, и где этот интерфейс определен?


            1. pistol
              13.03.2017 01:16

              В приложении.


              1. lair
                13.03.2017 01:17

                Эмм. У вас приложение использует компоненты (которые, поскольку компоненты, ничего не знают о приложении), которые реализуют/используют интерфейс, определенный в приложении? У вас большая проблема с зависимостями.


                1. pistol
                  13.03.2017 01:19

                  Сам интерфейс поставляется модуле, а его реализация с Cart — в приложении.


                  1. lair
                    13.03.2017 01:22

                    Итак, у вас есть интерфейс Cart, который поставляется в модуле orders, правильно? И реализация которого нужна orders для функционирования?


                    1. pistol
                      13.03.2017 01:28

                      Да, зависимость на абстракцию получается. А как передать элементы корзины Ордеру, не имея зависимости вообще?


                      1. lair
                        13.03.2017 01:31

                        А как передать элементы корзины Ордеру, не имея зависимости вообще?

                        Динамическое связывание, вот это всё.


                        Но я повторюсь, DIP — он не про то, чтобы не было зависимостей, он про то, куда зависимости направлены. Дословно: "High-level modules should not depend on low-level modules. Both should depend on abstractions." (это первая половина, там еще вторая есть). У вас это нарушено (только у вас модули одного уровня, но не суть).


                        1. pistol
                          13.03.2017 01:41

                          Все равно не понимаю, что конкретно нарушено?


                          1. lair
                            13.03.2017 01:44

                            yii2-order напрямую зависит от yii2-cart через наследование: pistol88\order\drivers\Pistol88Cart extends \pistol88\cart\Cart.


                        1. pistol
                          13.03.2017 01:42

                          Буду благдарен за примеры динамического связывания.


                          1. lair
                            13.03.2017 01:43
                            -1

                            Smalltalk.


                            1. pistol
                              13.03.2017 01:45

                              )))) Но статья-то про PHP.


                              1. lair
                                13.03.2017 01:48

                                В PHP можно сделать по аналогии, мне просто лень искать примеры.


                                (собственно, PHP — динамический язык, там изначально было динамическое связывание; если я ничего не путаю, конечно)


                  1. 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 — т.е., что ваш код вообще работает?


                    1. pistol
                      13.03.2017 01:31

                      Ну… Это скорее некий examples, сборник готовых связей с разными модулями. Его лучше перенести в другое место, он просто для удобства. В сообществе yii много новичков программирования, они хотят только копипастить по инструкции и чтобы все сразу работало. В readme четко написано по это.

                      этом мне интересно: а что же гарантирует, что \pistol88\cart\Cart реализует \pistol88\order\interfaces\Cart — т.е., что ваш код вообще работает?


                      Проверка на тип в конструкторе LoadData.


                      1. lair
                        13.03.2017 01:34

                        Это скорее некий examples, сборник готовых связей с разными модулями. Его лучше перенести в другое место, он просто для удобства.

                        После чего ваши модули станут не связанными вообще, и DIP к ним не будет применим.


                        Проверка на тип в конструкторе LoadData.

                        Покажите код, я не смог найти.


                        1. pistol
                          13.03.2017 01:36

                          LoadElements, туплю в 5 ночи.

                          https://github.com/pistol88/yii2-order/blob/master/logic/LoadElements.php


                          1. lair
                            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, и все сломать.


                            1. pistol
                              13.03.2017 01:49

                              Теперь понял про что вы, спасибо. Если создать такой же интерфейс в yii2-cart, это решит проблему?


                              1. lair
                                13.03.2017 01:50

                                Если создать такой же интерфейс в yii2-cart, это решит проблему?

                                Что значит "такой же"? С таким же контрактом? Нет, не решит, потому что между интерфейсами в yii2-cart и yii2-order не будет никакой связи, у вас все равно не будет проверок.


                                1. pistol
                                  13.03.2017 01:59

                                  Понял. Спасибо большое. Подумаю, как решить проблему.


                                  1. lair
                                    13.03.2017 02:00
                                    +2

                                    … вообще-то, ее решение и есть Dependency Inversion Principle.


                        1. pistol
                          13.03.2017 01:38

                          После чего ваши модули станут не связанными вообще, и DIP к ним не будет применим.

                          Они и так не связаны вообще. Только потенциально, через интерфейс. Папка drivers создана только для того, чтобы в Ридми понятно описать принцип связи.


                          1. lair
                            13.03.2017 01:40
                            +1

                            Они и так не связаны вообще.

                            Значит, DIP к ним не применим.


                            Только потенциально, через интерфейс.

                            До тех пор, пока это интерфейс, определенный в одном из модулей (как это у вас), DIP у вас нарушен.


                            1. pistol
                              13.03.2017 01:44

                              Если убрать из drivers связь в отдельный репозиторий — нарушения уже не будет?


                              1. lair
                                13.03.2017 01:45

                                Не будет, но поскольку модули больше не будут связаны вообще, к ним перестанет быть применим DIP (не выполнено "they both should depend on abstractions").


                                1. pistol
                                  13.03.2017 01:47

                                  То есть проблема в том, что у нас односторонняя зависимость на абстракцию? Не сомвсем понимаю до сих пор.


                                  1. lair
                                    13.03.2017 01:49

                                    Проблема в том, что у вас зависимость не "на абстрацию", а "на модуль".


    1. pistol
      13.03.2017 00:45

      И про перескакивание через слой поясните :) Где мы перескочили через слой?


      1. lair
        13.03.2017 00:49

        Вот здесь и перескочили:


        $model = new Order; //Это слой моделей
        $db = Order::find(); //А это уже слой БД
        $db = $db->where(['id' => '1'])->one(); //Снова слой моделей

        Один и тот же код, находящийся в одном и том же слое, вызывает два разных слоя, которые, при этом, знают друг о друге.


    1. Fesor
      14.03.2017 00:06

      (а) кто вам это сказал и (б) что вы понимаете под абстракцией?

      Ну я по пункту (а) пройдусь.


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


      1. 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."


        Поэтому вопрос "кто сказал" — он как раз очень важный.


        1. Fesor
          14.03.2017 02:19

          Поэтому вопрос "кто сказал" — он как раз очень важный.

          Кто сказал — вопрос совершенно не важный. Важно что имелось ввиду.


          По сути приведенная вами цитата описывает как раз таки actor model. Late binding и hinding of state-process лишь говорят что акторы представляют собой абстракции (снаружи мы знаем и хотим знать только о поведении, а все остальное инкапсулировано внутри). То есть без грамотной декомпозиции и построения хороших абстракций не выйдет построить хорошую систему.


          1. lair
            14.03.2017 11:24

            Late binding и hinding of state-process лишь говорят что акторы представляют собой абстракции (снаружи мы знаем и хотим знать только о поведении, а все остальное инкапсулировано внутри).

            И вот в этот момент мы и приходим к вопросу (б) ("что вы понимаете под абстракцией?"). Потому что "снаружи мы знаем только о поведении" — это в моем понимании инкапсуляция, а не абстракция.


            1. Fesor
              14.03.2017 11:49

              это в моем понимании инкапсуляция, а не абстракция.

              Ну как бы суть инкапсуляции в том что мы детали реализации прячем. Что как бы является синонимом абстракции. Так что "идеальную инкапсуляция" можно трактовать как "качественную абстракцию". Хотя вашу точку зрения по этому вопросу я бы очень хотел услышать.


              1. lair
                14.03.2017 12:01

                Ну как бы суть инкапсуляции в том что мы детали реализации прячем. Что как бы является синонимом абстракции.

                А является ли?


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


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


                Но это сугубо терминологический спор, и именно поэтому я всегда начинаю такие разговоры с вопроса "что вы понимаете под абстракцией".


    1. europanzer
      15.03.2017 09:39

      Под cross-cutting concerns имеются ввиду в том числе транзакции БД?
      Подскажите статью/эксперта раскритиковавшего слоистую архитектуру.


      1. 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/


        http://alistair.cockburn.us/Hexagonal+architecture


        1. europanzer
          15.03.2017 11:55

          Спасибо большое. Почитаю.Вместо слоистой рекомендуете гексагональную? Или в гексагональной та же проблема?


          1. lair
            15.03.2017 11:57
            +1

            Я рекомендую думать и выбирать то, что подходит под задачу.


          1. Fesor
            15.03.2017 19:51

            Давайте думать...


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


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


            Потому перед тем как выбирать архитектуру, нужно прекрасно понимать какие характеристики системы вам нужны для каждого конкретного случая. И уже потом смотреть какие виды архитектур чего решают. Можно вообще комбинировать идеи например чтобы максимально эффективно решать проблемы.


  1. funca
    13.03.2017 02:11
    +2

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

    Подход «кодом вперед» скорее из области, хм… кодинга. Когда нет возможности отдельно заниматься проектированием, попробуйте TDD. Тесты реально помогают снижать количество ошибок, указывая в том числе и на некоторые архитектурные просчеты.


  1. sspat
    13.03.2017 14:25
    +5

    Иронично, но единственный кусок кода в статье, который действительно является бизнес-логикой, вы вынесли в…

    'on cart_models_rounding' => function($event) {
        $event->cost = ceil($event->cost/10)*10;
    }
    


    … конфигурационный файл приложения, где он будет сидеть рядом с настройками подключения к бд и DI-контейнера.


    1. pistol
      13.03.2017 15:33

      Это же конфиг. Оттуда функция берется в компоненте сервисного слоя, так что фактически эта логика сидит в компоненте.


      1. sspat
        13.03.2017 17:18
        +4

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


        1. pistol
          13.03.2017 19:45

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

          Да, сейчас посмотрел еще раз и вижу, что бизнес-логика размешана с реализацией прикладных задач. Спасибо ;)

          а когда собственно требуется применить бизнес-правило (округлить цену) делегирует это непонятно куда, даже не убедившись, что это правило вообще будет применено

          А куда должно быть делегировано это бизнес-правило?


          1. lair
            13.03.2017 20:55

            А куда должно быть делегировано это бизнес-правило?

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


            1. pistol
              13.03.2017 21:03

              А если бизнес-правила растут как грибы? При чем у разных представителей одного бизнеса совершенно разные грибы, но все они — грибы со шляпой и ножкой? Куда вынести эту изменчивость?


              1. lair
                13.03.2017 21:15

                А зачем ее куда-то выносить? Бизнес-слой ровно для этого и создан, чтобы описывать бизнес-правила.


                1. pistol
                  13.03.2017 21:19

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


                  1. lair
                    13.03.2017 22:25

                    Ну так делаете общее ядро и дальше адаптируете его под каждый бизнес. Все равно это остается в слое бизнес-логики.


                    1. Fortop
                      15.03.2017 08:13
                      -1

                      Это то, что называется конфигурациями в 1С.


                      То есть сама бизнеслогика делится на некоторую общую часть и индивидуальную.


                      Поэтому ничего страшного в нахождении второй части в конфигах нет.
                      Но, естественно, не в одном конфиге с подключением к БД


                      1. lair
                        15.03.2017 11:30
                        +1

                        Поэтому ничего страшного в нахождении второй части в конфигах нет.

                        "Страшное" — вполне себе есть: например, добрая часть разработческих инструментов с конфигами работает не так, как с кодом. Соответственно, применять удобные практики совсем не так… удобно.


                        Другое дело, что в каких-то случаях достоинства конфигов перевешивают.


                  1. sspat
                    14.03.2017 00:51
                    +1

                    Как вариант, выносите эти отличия применив шаблон Стратегия, и в разных приложениях через DI-контейнер подсовываете разную реализацию.


  1. mistergonza
    14.03.2017 06:30
    -3

    Ужас какой-то, и Yii, и Entreprise, и слои. Enterprise это не только сервис контейнеры и внедрение зависимостей, это огромнейший кусок льда под водой, от огромного айсберга. И для Enterprise используйте предназначенные для этого инструменты.


  1. Knase
    14.03.2017 18:07
    -1

    Зачем изобретать велосипед если можно взять уже разработанную Enterprise Magento. Там уже все слои реализованы тока надо правильно уметь расширять.


    1. pistol
      14.03.2017 20:06
      +1

      Что-то с виду этот Энтерпрайз похож на Битриксовский, но для приличия использующий паттерны)))


      1. mistergonza
        14.03.2017 23:53

        Зачем так сразу Битриксом обзывать. Никто такого не заслуживает.


  1. Fortop
    15.03.2017 08:26

    $order = Order::fineOne(1);
    yii::$app->order->cancel($order);

    Вы фреймворк неудачный выбрали для демонстрации.


    Т.е. на нем, конечно, можно раскрыть поднятую тему, но сам он не слишком этому способствует. Равно как и его собрат по нише — Laravel.


    Смотрите в сторону либо более тяжёлых вещей типа Zend, Symfony.
    Либо более лёгких Lumen, Silex, Slim, Expressive.


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


  1. europanzer
    15.03.2017 11:52

    Первый опыт по разделению приложений на Yii на слои?
    Видение проблемы верное, немного не так понимаете способ решения проблемы.

    При использовании слоистой архитектуры вы должны отделить слой вашей бизнес-логики от всего, в том числе и от yii. Это не всегда удается сразу. Но статьи пишутся о решениях, близких к идеалу. Бизнес-логика должна быть центром вашего приложения. Сам заголовок статьи выглядит странно. Слоистая архитектура не бывает «на чем то» (на фреймворке X). Она сама по себе — либо есть, либо нет. Фреймворк или просто независимые компоненты, библиотеки, определяют UI к бизнес логике и обеспечивают инфраструктуру (БД, безопасность и проч.).


  1. springimport
    16.03.2017 18:03

    Простите за оффтоп, есть кто, кто уже работал с magento 2?


    При многих минусах, я думаю что архитектура там довольно неплохая.


    1. Fesor
      16.03.2017 18:13
      +1

      довольно неплохая.

      относительно. Достаточно глянуть на класс Order чтобы ужаснуться насколько раздута эта архитектура только за счет "псевдо универсальности". Многие вещи сделаны дико сложно и при этом сложно кастомизируются. В целом это типично для любых коробочных решений. Мне же хотелось бы увидеть модульную систему где проще выкинуть маленький модуль и заменить на другой под конкретную задачу (например модуль ордеров что бы имплементил сэт контрактов и позволял полностью заменить структуру).