Целью данного исследования является уточнение методологии проектирования программных систем, основанных на MVC-архитектуре. В настоящее время при разработке веб-приложений с использованием современных фреймворков (Yii, Laravel и др.) существует проблема корректного использования моделей в представлениях. Задача исследования заключается в том, чтобы выработать стратегии решения этой проблемы и сформулировать методические рекомендации.
MVC – Model View Controller (модель представление контроллер) – архитектурный паттерн проектирования, который используется для организации программного кода. Его основная идея в том, чтобы отделить модели данных, их отображение и операции взаимодействия с пользователем. Выигрыш от использования такой архитектуры заключается в том, что она позволяет упорядочить код, распределив его по уровням, каждый из которых определяет сферу ответственности. Это минимизирует взаимозависимость программных компонент, что в свою очередь облегчает их последующую модификацию. Доработка и развитие такой системы становится проще.
Логика работы приложения построенного на основе MVC-архитектуры представлена на рисунке 1.
Рис. 1.
Пользователь направляет запрос в контроллер (в случае веб-приложений – это обращение по адресу), контроллер (Controller) обрабатывает запрос, запрашивает данные от соответствующих моделей (Model), получает данные, может быть, выполняет какую-то дополнительную их обработку, например, агрегирует их с другими данными и затем передает данные в представление (View). Представление формирует данные в соответствии с заданным шаблоном отображения и возвращает результат пользователю. Штрих-пунктирной линией показано опосредованное взаимодействие представления с контроллером через AJAX-сценарии и POST-запросы.
Это стандартная схема, по которой работает MVC-приложение. Теперь давайте рассмотрим MVC c точки зрения организации архитектуры. MVC – это вариант нестрогой многоуровневой системы, где декомпозиция реализована за счет расслоения.
Рис. 2.
«Многоуровневая структура, наилучшим образом подходящая для УРОВНЕЙ РАЗДЕЛЕНИЯ ОБЯЗАННОСТЕЙ (RESPONSIBILITY LAYERS), называется НЕСТРОГОЙ МНОГОУРОВНЕВОЙ СИСТЕМОЙ (RELAXED LAYERED SYSTEM). В ней компонентам того или иного уровня разрешается обращаться к любым нижним уровням, а не только к тому, который расположен сразу под ними» [1].
Верхние уровни могут использовать программные компоненты, расположенные ниже, нижние не могут обращаться к верхним, они вообще не должны ничего о них знать. Таким образом, базовым элементом такой архитектуры является модель или точнее модели, что отображено на рисунке (уровень Models).
Здесь нужно сделать ряд важных уточнений. Хотя вышестоящие уровни могут в полной мере управлять нижестоящими программными компонентами, в некоторых случаях на эти взаимосвязи накладываются ограничения. В современных MVC-фреймворках модели связаны с базой данных с помощью ORM (object-relational mapping) решений, которые основаны на Active Record и Data Mapper шаблонах.
Active Record. «Объект, выполняющий роль оболочки для строки таблицы или представления базы данных. Он инкапсулирует доступ к базе данных и добавляет к данным логику домена. Как правило, типовое решение активная запись включает в себя методы, предназначенные для выполнения следующих операций: создание экземпляра активной записи на основе строки, полученной в результате выполнения SQL-запроса; создание нового экземпляра активной записи для последующей вставки в таблицу; статические методы поиска, выполняющие стандартные SQL-запросы и возвращающие активные записи; обновление базы данных и вставка в нее данных из активной записи; реализация некоторых фрагментов бизнес-логики» [2].
Data Mapper. «Типовое решение преобразователь данных представляет собой слой программного обеспечения, которое отделяет объекты, расположенные в оперативной памяти, от базы данных. В функции преобразователя данных входит передача данных между объектами и базой данных и изоляция их друг от друга [2].
Обычно в фреймворке реализовано какое-то одно решение, например, в Yii и Laravel используется Active Record, в ASP.NET MVC – Data Mapper. Использование того или иного решения определяет специфику работы с источником данных. В случае Active Record мы можем обращаться к БД напрямую (через ORM-прослойку), а при использовании Data Mapper через объекты-посредники. В рамках данной статьи мы ограничимся рассмотрением специфики использования Active Record.
В Yii2 доступ к базе может выглядеть следующим образом:
$post = Post::findOne(15);
Хотя мы абстрагируемся от таблицы и SQL-запроса, но все равно суть операции не меняется – мы обращаемся к базе данных, а метод findOne – это обертка для запроса:
SELECT * FROM post WHERE id = 15
С точки зрения разделения ответственности уровней такие операции в представлении не совсем уместны. Объектно-ориентированное проектирование декларирует, что объект, который умеет или знает слишком много – плохой объект. Смысл существования представлений в том, чтобы отображать данные, а не заниматься их поиском или извлечением из БД. Таким образом, запрос на получение данных предпочтительнее инициировать в контроллере, а в представление передавать уже полученные данные, например:
public function actionView($id) {
$post = Post::findOne($id);
return $this->render('view', ['post' => $post]);
}
Однако смысл Active Record заключается в том, что в модели инкапсулирована бизнес-логика и методы доступа к источнику данных. Было бы странно игнорировать преимущества данного шаблона, которые, впрочем, некоторые специалисты относят к недостаткам, т.к. нарушается принцип единичной ответственности (модель отвечает не только за бизнес-логику, но и за работу с БД).
В том же Yii2 легко реализовать отображение табличной агрегации на объектную, например:
public function getUser() {
return $this->hasOne(User::className(), ['id' => 'responsible_id']);
}
Этот метод реализован в классе Project, responsible_id – атрибут (а также поле в таблице), который указывает идентификатор пользователя отвечающего за проект. Здесь получается связь многие к одному (один пользователь может отвечать за несколько проектов). Обращение $project->user дает объект User, связанный с данным проектом (происходит вызов getUser):
$user = $project->user;
Данный код эквивалентен вызову запроса:
SELECT * FROM user WHERE id = ?
Если в представлении мы используем что-то вроде:
<p>$project->user->name</p>
То есть, по сути, мы обращаемся к базе данных. Более того, даже если мы передали объект user в представление, то совсем не обязательно, что данные из базы уже получены. Если ORM реализует Lazy Load [2], а чаще всего он ее реализует, то лишь непосредственное обращение к полям объекта будет генерировать SQL-запрос.
Таким образом, получается двоякая ситуация: с одной стороны желательно ограничить доступ представления к БД, а с другой – ORM предоставляет гибкие и эффективные надстройки над БД, которыми можно и нужно пользоваться.
При решении этой дилеммы стоит учитывать следующие соображения. Как минимум, стоит ограничить доступ к базе чтением. Манипуляция данными модели и их запись в базу данных непосредственно в представлении – грубейшее нарушение принципа разделения ответственности, что ведет к запутанному и трудно модифицируемому коду.
Чтение данных из базы также стоит ограничить, рассмотрим две стратегии, которыми здесь можно руководствоваться.
1. Семантическая стратегия – свести к минимуму или полностью исключить все операции, явно отражающие в своей сигнатуре специфику обращения к БД.
С точки зрения данной стратегии такой запрос в представлении неприемлем:
$postponements = Postponement::find()
->andWhere(['type_id' => Postponement::TYPE_TASK])
->andWhere(['object_id' => $task_id])
->asArray()
->all();
Здесь явная зависимость от БД, от специфики обращения к ней (пусть даже и средствами ORM). Однако тот же самый запрос, оформленный как метод модели, вполне приемлем:
class Task extends \yii\db\ActiveRecord
{
public function getPostponements() {
return Postponement::find()
->andWhere(['type_id' => Postponement::TYPE_TASK])
->andWhere(['object_id' => $this->id])
->all();
}
…
}
Использование метода в представлении:
$postponements = $task->postponements;
Общая идея данной стратегии заключается в том, чтобы абстрагировать методы обращения к БД, скрыть их от представления. Представление оперирует моделью предметной области, ее концептуальным смыслом, а не техническими аспектами реализации. В вышеприведенном примере $task->postponements предоставляет переносы (записи о том, когда и кем были осуществлены переносы сроков выполнения задачи). Представление будет отображать информацию, но каким образом эта информация сформирована, представлению знать не нужно.
2. Стратегия запросов – свести к минимуму или полностью исключить все SQL-запросы к БД из представления. В рамках этой стратегии мы инициируем все обращения к БД в контроллере и передаем полученные данные в представление, т.е. представление оперирует данными из оперативной памяти.
Если в представление передается коллекция объектов модели имеющей агрегированные данные, отражающие реляционную связь с другими моделями, то необходимо осуществить жадную загрузку (eager loading). Возможность такой загрузки реализована в современных ORM-решениях (и в Yii и в Laravel она есть).
Исключение всех операций из представления, инициирующих SQL-запросы – это более строгий по сравнению с первой стратегией подход. Здесь мы следуем принципу единичной ответственности в представлении буквально: только отображение имеющихся данных и ничего сверх этого. Методы модели могут быть вызваны только в том случае, если они не генерируют SQL-запросы.
Помимо преимуществ чисто методологического плана (соблюдение принципа единичной ответственности) такой подход привлекателен с точки зрения управления кэшированием данных. В рамках этой стратегии все данные проходит через контроллер, что существенно облегчает проверку их наличия в кэше и сохранения, если данные отсутствуют. Альтернатива – перекладывание этой работы на модель, что, естественно, не самая удачная идея, т.к. ответственность модели неоправданно расширяется. Кэширование же на уровне контроллера вполне естественно и органично.
Выбор той или иной стратегии (или их сочетание) – личное дело программиста. Каждый разработчик имеет свои предпочтения и говорить о том, что есть единственно правильный способ – не корректно. Есть решения, которые могут затруднить дальнейшее развитие проекта, но это в большей степени зависит от совокупности уже принятых решений, от сроков завершения проекта, его текущего прогресса и перспектив дальнейшего сопровождения. Немаловажную роль играет и общий командный стиль, выбранную стратегию должны разделять все члены команды. Усилия, направленные на поддержание чистоты архитектуры отдельным программистом могут быть легко нивелированы другими членами команды, не столько искушенными или щепетильными в методологическом плане.
Литература:
1. Эванс, Э., Предметно-ориентированное проектирование (DDD): структуризация сложных программных систем [Текст]: пер. с англ. / Э. Эванс – М.: Вильямс, 2011.
2. Фаулер, М., Архитектура корпоративных программных приложений [Текст]: пер. с англ. / М. Фаулер – М.: Вильямс, 2006.