Понимание MVC и MVP (для разработчиков JavaScript и Backbone)
- понедельник, 19 мая 2025 г. в 00:00:04
Прежде чем изучать какие-либо JavaScript-фреймворки, помогающие в структурировании приложений, может быть полезно получить базовое представление об архитектурных шаблонах проектирования. Шаблоны проектирования являются проверенными решениями распространенных проблем разработки и могут предложить структурные парадигмы, которые помогут нам организовать наше приложение.
Я думаю, паттерны очень интересны, поскольку они фактически представляют собой массовые усилия, опирающиеся на коллективный опыт опытных разработчиков, которые ранее сталкивались с теми же проблемами, с которыми сталкиваемся мы сейчас. Хотя разработчики 10 или 20 лет назад, возможно, не использовали те же языки программирования для реализации паттернов, мы можем извлечь из их усилий много уроков.
В этом разделе мы рассмотрим два популярных шаблона – MVC и MVP. Контекст нашего исследования будет заключаться в том, как эти шаблоны связаны с популярным фреймворком JavaScript Backbone.js, который будет рассмотрен более детально позже.
MVC (Модель-Представление-Контроллер, Model-View-Controller) – это архитектурный шаблон проектирования, который способствует улучшению организации приложения за счет разделения задач. Это обеспечивает изоляцию бизнес-данных (Models) от пользовательских интерфейсов (Views), при этом третий компонент (Controllers) (традиционно) управляет логикой, пользовательским вводом и координирует как модели (Models), так и представления (Views). Первоначально шаблон был разработан Трюгве Ренскаугом во время его работы над Smalltalk-80 (1979), где он изначально назывался Model-View-Controller-Editor. Затем MVC был подробно описан в 1994году в книге «Шаблоны проектирования: элементы переиспользуемого объектно-ориентированного программного обеспечения» («Design patterns: elements of reusable object-oriented software») (книга GoF, или «Банда четырех»), которая сыграла свою роль в популяризации его использования.
Важно понимать, на решение каких проблем был направлен исходный шаблон MVC, поскольку он довольно сильно видоизменился со времен своего появления. В 70-е годы графические пользовательские интерфейсы были еще редки, и концепция, известная как «разделенное представление», начала использоваться как средство для четкого разделения между объектами предметной области, которые моделировали концепции реального мира (например, фотография, человек), и объектами представления, которые отображались на экране пользователя.
Реализация MVC в Smalltalk-80 развила эту концепцию и преследовала цель отделить логику приложения от пользовательского интерфейса. Идея заключалась в том, что разделение этих частей приложения также позволит переиспользовать модели для других интерфейсов в приложении. Есть несколько интересных моментов, которые стоит отметить в архитектуре MVC в Smalltalk-80:
Элемент предметной области (Domain element) был известен как Модель и не имел представления о пользовательском интерфейсе (Представлениях и Контроллерах).
Отображение было организовано Представлением и Контроллером, но не было просто одного представления и контроллера. Пара Представление-Контроллер требовалась для каждого элемента, отображаемого на экране, и поэтому между ними не было настоящего разделения.
Роль Контроллера в этой паре заключалась в обработке пользовательского ввода (такого, как нажатия клавиш и клики мышью), делая с ним что-то полезное.
Шаблон «Наблюдатель» (Observer pattern) использовался для обновления представления всякий раз, когда изменялась Модель.
Разработчики иногда удивляются, когда узнают, что шаблон «Наблюдатель» (в настоящее время обычно реализуемый как система «Издатель»/«Подписчик» (Publish/Subscribe)) был включен как часть архитектуры MVC много десятилетий назад. В MVC Smalltalk-80 Представление и Контроллер наблюдают за Моделью. Как упоминалось выше, каждый раз, когда изменяется Модель, реагируют Представления. Простым примером этого является приложение, основанное на данных фондового рынка: чтобы приложение было полезным, любое изменение данных в наших Моделях должно приводить к мгновенному обновлению Представления.
Мартин Фаулер проделал превосходную работу, описывая историю становления MVC, и если вам интересна дополнительная историческая информация о MVC Smalltalk-80, я рекомендую прочитать его работу.
Мы рассмотрели 70-е, но давайте теперь вернемся к настоящему. В наше время шаблон MVC применяется к широкому диапазону языков программирования, включая наиболее актуальный для нас: JavaScript. Теперь у JavaScript есть ряд фреймворков, которые могут похвастаться поддержкой MVC (или его вариаций, которые мы называем семейством MV*), позволяющего разработчикам легко добавлять структуру в свои приложения без особых усилий. Вы, вероятно, сталкивались по крайней мере с одним из этих фреймворков, но они включают в себя такие, как Backbone, Ember.js и JavaScriptMVC. Учитывая важность избегания «спагетти»-кода (термин, описывающий код, который очень сложно читать или поддерживать из-за отсутствия структуры), важно, чтобы современный JavaScript-разработчик понимал, чем полезен этот шаблон. Мы можем эффективно оценить, что эти фреймворки позволяют нам делать по-другому.
Мы знаем, что MVC состоит из трех основных компонентов:
Модели управляют данными для приложения. Они не зависят от слоев пользовательского интерфейса или представления, но, вместо этого, представляют уникальные формы данных, которые могут потребоваться приложению. Когда модель изменяется (например, обновляется), она, как правило, уведомляет своих наблюдателей (например, представления, концепцию которых мы вскоре рассмотрим), что произошло изменение, чтобы они могли отреагировать соответствующим образом.
Чтобы понять модели лучше, представим, что у нас есть JavaScript-приложение фотогалереи. В фотогалерее концепция фотографии заслуживает собственную модель, поскольку она представляет собой уникальный вид данных конкретной предметной области. Такая модель может содержать связанные атрибуты, такие как подпись, источник изображения и дополнительные метаданные. Конкретная фотография будет храниться в экземпляре модели, а модель также может быть повторно использована. Ниже мы можем видеть пример очень упрощенной модели, реализованной с помощью Backbone.
var Photo = Backbone.Model.extend({
// Default attributes for the photo
defaults: {
src: "placeholder.jpg",
caption: "A default image",
viewed: false
},
// Ensure that each photo created has an `src`.
initialize: function() {
this.set({"src": this.defaults.src});
}
});
Встроенные возможности моделей различаются в зависимости от фреймворка, однако они довольно часто поддерживают проверку атрибутов, где атрибуты представляют свойства модели, такие как идентификатор модели. При использовании моделей в реальных приложениях мы обычно также хотим сохранения модели. Сохранение позволяет нам редактировать и обновлять модели, зная, что их последнее состояние будет сохранено либо в памяти, либо в хранилище данных пользователя localStorage, либо синхронизировано с базой данных.
Кроме того, у модели может также быть несколько представлений, наблюдающих за ней. Если, скажем, наша модель фото содержит метаданные, такие как геометка (долгота и широта), друзья, запечатленные на фотографии (список идентификаторов) и список тегов, разработчик может решить предоставить единое представление, чтобы отобразить каждый из этих трех аспектов.
Нередко современные MVC/MV*-фреймворки предоставляют средства для группировки моделей (например, в Backbone эти группы называются «коллекциями»). Управление моделями в группах позволяет нам писать логику приложения, основанную на уведомлениях от группы, если какая-либо модель в ней будет изменена. Это избавляет от необходимости вручную отслеживать отдельные экземпляры модели.
Пример группировки моделей в упрощенную коллекцию Backbone можно увидеть ниже.
var PhotoGallery = Backbone.Collection.extend({
// Reference to this collection's model.
model: Photo,
// Filter down the list of all photos
// that have been viewed
viewed: function() {
return this.filter(function(photo){
return photo.get('viewed');
});
},
// Filter down the list to only photos that
// have not yet been viewed
unviewed: function() {
return this.without.apply(this, this.viewed());
}
});
Представления – это визуальное отображение моделей, которые обеспечивают настроенное представление их текущего состояния. Представление обычно наблюдает за моделью и получает уведомление когда модель изменяется, что позволяет представлению обновляться соответствующим образом. В литературе по шаблонам проектирования представления обычно называют «глупыми», поскольку их знания о моделях и контроллерах в приложении ограничены.
Пользователи могут взаимодействовать с представлениями, и это включает возможность читать и редактировать (т. е. получать или задавать значения атрибутов) модели. Поскольку представление является уровнем отображения, мы обычно предоставляем возможность редактирования и обновления в удобном для пользователя виде. Например в предыдущем приложении фотогалереи, которое мы обсуждали ранее, редактирование модели можно было бы упростить с помощью представления «редактирования», где пользователь, выбравший определенную фотографию, мог бы редактировать ее метаданные.
Настоящая задача обновления модели ложится на контроллеры (о которых мы вскоре поговорим).
Давайте рассмотрим представления немного подробнее, с помощью реализацию примера на «ванильном» JavaScript. Ниже мы можем видеть функцию, которая создает единое представление Photo, используя экземпляр модели и экземпляр контроллера.
Мы определяем в нашем представлении утилиту render()
, которая отвечает за рендер содержимого photoModel
, используя шаблонизатор JavaScript (шаблоны Underscore), и обновление содержимого нашего представления, на которое ссылается photoEl
.
Затем photoModel
добавляет наш обратный вызов render()
в качестве одного из своих подписчиков, чтобы с помощью шаблона «Наблюдатель» мы могли инициировать обновление представления при изменении модели.
Вы можете задаться вопросом, где здесь взаимодействие с пользователем. Когда пользователи нажимают на какие-либо элементы в представлении, представление не обязано знать, что делать дальше. Оно полагается на контроллер, который принимает это решение. В нашей реализации примера это достигается путем добавления слушателя событий к photoEl
, который делегирует обработку поведения клика обратно контроллеру, передавая вместе с ней информацию о модели, если она необходима.
Преимущество этой архитектуры в том, что каждый компонент играет свою отдельную роль, обеспечивая функционирование приложения по мере необходимости.
var buildPhotoView = function( photoModel, photoController ){
var base = document.createElement('div'),
photoEl = document.createElement('div');
base.appendChild(photoEl);
var render= function(){
// We use a templating library such as Underscore
// templating which generates the HTML for our
// photo entry
photoEl.innerHTML = _.template('photoTemplate', {src: photoModel.getSrc()});
}
photoModel.addSubscriber( render );
photoEl.addEventListener('click', function(){
photoController.handleEvent('click', photoModel );
});
var show = function(){
photoEl.style.display = '';
}
var hide = function(){
photoEl.style.display = 'none';
}
return{
showView: show,
hideView: hide
}
}
Templating
В контексте фреймворков JavaScript, поддерживающих MVC/MV*, стоит кратко обсудить шаблонизацию JavaScript и ее связь с представлениями, так как мы затронули ее в предыдущем разделе.
Долгое время считалось (и было доказано), что создание вручную больших блоков HTML-разметки в памяти с помощью конкатенации строк приводит к снижению производительности. Разработчики, которые так поступали, стали жертвами неэффективной итерации из-за данных, обернутых во вложенные div'ы, и из-за использования устаревших методов, таких как document.write
, для добавления «шаблона» в DOM. Поскольку это обычно означает приведение скриптовой разметки в соответствие со стандартной разметкой, она может быстро стать трудночитаемой и, что более важно, ее станет сложно поддерживать в рабочем состоянии, особенно при создании приложений нетривиального размера.
Решения для шаблонизации JavaScript (такие как Handlebars.js и Mustache) часто используются чтобы определить шаблоны для представлений как разметку (хранящейся либо внешне, либо внутри тегов скрипта с пользовательским типом – например, text/template), содержащую переменные шаблона. Переменные могут быть разделены с помощью синтаксиса переменных (например), а фреймворки обычно достаточно умны, чтобы принимать данные в формате JSON (в который можно преобразовать экземпляры моделей), так что нам нужно беспокоиться только о поддержании чистых моделей и чистых шаблонов. Большую часть рутинной работы по заполнению берет на себя сам фреймворк. У этого подхода множество преимуществ, особенно при выборе внешнего хранения шаблонов, поскольку позволяет динамически загружать шаблоны по мере необходимости при создании более крупных приложений.
Ниже мы видим два примера HTML-шаблонов. Один реализован с использованием популярного фреймворка Handlebars.js, а другой – с использованием шаблонов Underscore.
Handlebars.js:
<li class="photo">
<h2></h2>
<img class="source" src=""/>
<div class="meta-data">
</div>
</li>
Underscore.js Microtemplates:
<li class="photo">
<h2><%= caption %></h2>
<img class="source" src="<%= src %>"/>
<div class="meta-data">
<%= metadata %>
</div>
</li>
Также стоит отметить, что в классической веб-разработке навигация между независимыми представлениями требовала обновления страницы. Однако в одностраничных приложениях JavaScript после того, как данные передаются с сервера через Ajax, их можно просто динамически отобразить в новом представлении на той же странице без необходимости в таком обновлении. Таким образом, роль навигации отводится «маршрутизатору» (Router), который помогает управлять состоянием приложения (например, позволяя пользователям добавлять в закладки определенное представление, к которому они перешли). Однако, поскольку маршрутизаторы не являются частью MVC и не присутствуют ни в одном MVC-подобном фреймворке, я не буду рассматривать их более подробно в этом разделе.
Итак, представления – это визуальное отображение данных нашего приложения.
Контроллеры являются посредниками между моделями и представлениями, классически отвечают за две задачи: обновляют представление при изменении модели и обновляют модель, когда пользователь взаимодействует с представлением.
В нашем приложении фотогалереи контроллер будет отвечать за обработку изменений, которые внесет пользователь в представление при редактировании конкретной фотографии, и обновление модели этой фотографии после того, как пользователь закончит редактирование.
Однако большинство фреймворков JavaScript отходят от традиционного понимания контроллеров, принятого в MVC. Есть разные причины этого, но, по моему мнению, авторы фреймворков изначально смотрят на «серверную» сторону MVC, понимают, что она не транслируется 1:1 на сторону клиента и переосмысливают «C» в MVC, чтобы обозначить то, что, по их мнению, имеет больше смысла. Однако проблема в том, что это субъективно и усложняет как понимание классического шаблона MVC, так и, конечно, роли контроллеров в современных фреймворках.
В качестве примера давайте кратко рассмотрим архитектуру популярного архитектурного фреймворка Backbone.js. В Backbone есть модели и представления (несколько похожие на те, что мы рассматривали ранее), однако на самом деле у него нет настоящих контроллеров. Его представления и маршрутизаторы действуют немного похоже на контроллер, но ни один из них не является контроллером сам по себе.
В этом отношении, вопреки тому, что может быть упомянуто в официальной документации или в сообщениях блога, Backbone не является ни настоящим MVC/MVP, ни MVVM-фреймворком. На самом деле лучше считать его членом семейства MV*, которое подходит к архитектуре по-своему. Конечно, в этом нет ничего плохого, но важно различать классический MVC и MV*, если вы полагаетесь на советы из классической литературы по первому, чтобы использовать его во втором.
Spine.js
Теперь мы знаем, что контроллеры традиционно отвечают за обновление представления при изменении модели (и аналогично за модель, когда пользователь обновляет представление). Поскольку фреймворк, который мы будем обсуждать в этой книге (Backbone), не имеет собственных явных контроллеров, нам может быть полезно рассмотреть контроллер из другого фреймворка MVC, чтобы оценить разницу в реализациях. Для этого давайте рассмотрим пример контроллера из Spine.js.
В этом примере у нас будет контроллер под названием PhotosController
, который будет отвечать за отдельные фотографии в приложении. Это гарантирует, что при обновлении представления (например, когда пользователь редактирует метаданные фотографии) соответствующая модель также обновится.
Примечание: мы не будем углубляться в Spine.js, а просто кратко рассмотрим, что могут делать его контроллеры:
// Controllers in Spine are created by inheriting from Spine.Controller
var PhotosController = Spine.Controller.sub({
init: function(){
this.item.bind("update", this.proxy(this.render));
this.item.bind("destroy", this.proxy(this.remove));
},
render: function(){
// Handle templating
this.replace($("#photoTemplate").tmpl(this.item));
return this;
},
remove: function(){
this.el.remove();
this.release();
}
});
В Spine контроллеры считаются связующим звеном для приложения, добавляя события DOM и реагируя на них, отображая шаблоны и обеспечивая синхронизацию представлений и моделей (что имеет смысл в контексте того, что мы называем контроллером).
В приведенном выше примере мы настраиваем слушателей в событиях update
и remove
с помощью render()
и remove()
. Когда запись о фото обновлена, мы повторно рендерим представление, чтобы отразить изменения в метаданных. Аналогично, если фотография удаляется из галереи, мы удаляем ее из представления. Если вам интересно, что такое функция tmpl()
во фрагменте кода: в функции render()
мы используем ее для рендеринга шаблона JavaScript с именем #photoTemplate, который просто возвращает строку HTML, используемую для замены текущего элемента контроллера.
Это дает нам очень легкий и простой способ управления изменениями между моделью и представлением.
Backbone.js
Далее в этом разделе мы вернемся к различиям между Backbone и традиционным MVC, но сейчас давайте сосредоточимся на контроллерах.
В Backbone ответственность контроллера делится между Backbone.View и Backbone.Router. Некоторое время назад Backbone поставлялся со своим собственным Backbone.Controller, но поскольку название этого компонента не имело смысла в контексте, в котором он использовался, позже он был переименован в Router.
Маршрутизаторы берут на себя немного больше ответственности контроллера, поскольку можно привязать события к моделям и заставить представление реагировать на события DOM и рендеринг. Как ранее отметил Тим Браньен (еще один участник Backbone на базе Bocoup), для этого можно обойтись вообще без Backbone.Router, поэтому, вероятно, можно представить это с помощью парадигмы Router так:
var PhotoRouter = Backbone.Router.extend({
routes: { "photos/:id": "route" },
route: function(id) {
var item = photoCollection.get(id);
var view = new PhotoView({ model: item });
something.html( view.render().el );
}
}):
Подводя итог этого раздела, можно сделать вывод, что контроллеры управляют логикой и координацией между моделями и представлениями в приложении.
Такое разделение задач в MVC упрощает модуляризацию функциональности приложения и позволяет:
Более простое общее обслуживание. Когда необходимо внести обновления в приложение, становится совершенно ясно, касаются ли изменения данных, то есть изменения моделей и, возможно, контроллеров, или же они чисто визуальные, то есть имеем дело с измененим представлений.
Разделение моделей и представлений означает, что писать модульные тесты для бизнес-логики становится значительно проще.
Дублирование кода низкоуровневой модели и контроллера (то есть того, что вы могли использовать вместо этого) устраняется во всем приложении.
В зависимости от размера приложения и разделения ролей эта модульность позволяет разработчикам, отвечающим за основную логику, и разработчикам, работающим над пользовательскими интерфейсами, работать одновременно.
На данный момент у вас, скорее всего, есть базовое представление о том, что обеспечивает шаблон MVC, но для любознательных мы можем рассмотреть его немного подробнее.
В GoF (Gang of Four) авторы не называют MVC шаблоном проектирования, а скорее считают его «набором классов для построения пользовательского интерфейса». По их мнению, это на самом деле вариация трех других классических шаблонов проектирования: шаблонов «Наблюдатель» («Издатель»/«Подписчик»), «Стратегия» и «Компоновщик» (Observer (Pub/Sub), Strategy, Composite). В зависимости от того, как MVC был реализован в фреймворке, он также может использовать шаблоны «Фабрика» (Factory) и «Декоратор» (Decorator). Я рассмотрел некоторые из этих шаблонов в моей другой бесплатной книге, «JavaScript Design Patterns For Beginners», если вы хотите почитать о них подробнее.
Как мы уже обсуждали, модели представляют данные приложения, в то время как представления – то, что пользователь видит на экране. Таким образом, MVC опирается на «Издатель»/«Подписчик» для некоторых своих основных коммуникаций (что, как ни странно, не рассматривается во многих статьях о шаблоне MVC). Когда модель изменяется, она уведомляет остальную часть приложения о том, что она была обновлена. Затем контроллер соответствующим образом обновляет представление. Задача наблюдателя в этих отношениях – облегчить присоединение нескольких представлений к одной и той же модели.
Для разработчиков, желающих узнать больше о несвязанной природе MVC (опять же, в зависимости от реализации), одна из целей шаблона – помочь определить отношения «один ко многим» между темой и ее наблюдателями. Когда тема изменяется, ее наблюдатели обновляются. У представлений и контроллеров немного иные отношения. Контроллеры облегчают отклик представления на различные действия пользователя и являются примером шаблона «Стратегия».
Рассмотрев классический шаблон MVC, мы теперь должны понять, как он позволяет нам четко разделять задачи в приложении. Также теперь мы должны понимать, как фреймворки JavaScript MVC могут различаться в своей интерпретации шаблона MVC, который, хотя и довольно открыт для вариаций, по-прежнему разделяет некоторые фундаментальные концепции, предлагаемые исходным шаблоном.
При рассмотрении нового JavaScript-фреймворка MVC/MV* помните: может быть полезно сделать шаг назад и проанализировать выбранный подход к архитектуре (в частности, как он поддерживает реализацию моделей, представлений, контроллеров или других альтернатив), поскольку это может помочь вам лучше понять, как предполагается использовать фреймворк.
Модель-представление-презентер (Model-view-presenter, MVP) – это производная от шаблона проектирования MVC, которая фокусируется на улучшении логики представления. Она возникла в компании Taligent в начале 1990-х годов, когда они работали над моделью для среды C++ CommonPoint. Хотя и MVC, и MVP нацелены на разделение задач между несколькими компонентами, между ними есть некоторые фундаментальные различия.
В рамках данного обзора мы сосредоточимся на версии MVP, наиболее подходящей для веб-архитектур.
«P» в MVP означает презентера (presenter). Это компонент, который содержит бизнес-логику пользовательского интерфейса для представления. В отличие от MVC, вызовы из представления делегируются презентеру, который отделен от представления и вместо этого общается с ним через интерфейс. Это открывает нам доступ к множеству полезных вещей, таких, как имитация представления в модульных тестах.
Наиболее распространенной реализацией MVP является реализация, использующая пассивное представление (Passive View) (представление, которое по всем намерениям и целям «глупое»), содержащее немного или совсем не содержащее логики. Модели MVP почти идентичны моделям MVC и обрабатывают данные приложения. Презентер выступает в качестве посредника, который общается как с представлением, так и с моделью, однако оба они изолированы друг от друга. Презентеры эффективно связывают модели с представлениями, ответственность за которые ранее лежала на контроллерах в MVC. Они находятся в центре шаблона MVP и, как вы можете догадаться, включают в себя логику отображения (вслед за представлениями).
Запрошенные представлением, презентеры выполняют любую работу, связанную с пользовательскими запросами, и передают данные им обратно. Они извлекают данные, манипулируют ими и определяют, как данные должны отображаться в представлении. В некоторых реализациях презентер также взаимодействует с сервисным слоем для сохранения данных (моделей). Модели могут вызывать события, но роль презентера заключается в подписке на них, чтобы они могли обновлять представление. В этой пассивной архитектуре у нас нет концепции прямой привязки данных. Представления предлагают сеттеры, которые презентеры могут использовать для записи данных.
Преимущество такого изменения перед MVC заключается в том, что оно повышает тестируемость вашего приложения и обеспечивает более четкое разделение между представлением и моделью. Однако такой подход имеет свою цену: отсутствие поддержки привязки данных в шаблоне часто может означать необходимость решения этой задачи отдельно.
Хотя распространенная реализация пассивного представления (Passive View) заключается в том, что представление реализует интерфейс, существуют его вариации, включая использование событий, которые могут немного отделить представление от презентера. Поскольку в JavaScript нет конструкции интерфейса, мы используем здесь скорее протокол, чем настоящий интерфейс. Технически это все еще API, и с этой точки зрения, вероятно, справедливо называть его интерфейсом.
Есть также вариант MVP Надзирающий контроллер (Supervising Controller), который ближе к шаблонам MVC и MVVM, поскольку он обеспечивает привязку данных Модели непосредственно из Представления. Плагины наблюдения за ключами и значениями (Key-value observing, KVO) (например, плагин Дерика Бейли Backbone.ModelBinding) как правило, выводят Backbone из Пассивного Представления и др. и превращают его в Надзирающий контроллер или варианты MVVM.
Обычно MVP чаще всего используется в корпоративных приложениях, где необходимо повторно использовать как можно больше логики представления. Для приложений с очень сложными представлениями и большим количеством взаимодействий с пользователем MVC может не совсем подходить, поскольку решение этой задачи может означать большую зависимость от нескольких контроллеров. В MVP вся эта сложная логика может быть инкапсулирована в презентере, что может значительно упростить обслуживание.
Поскольку представления MVP определяются через интерфейс, а интерфейс технически является единственной точкой пересечения системы и представления (кроме презентера), этот шаблон также позволяет разработчикам писать логику представления не дожидаясь, пока дизайнеры создадут макеты и графику для приложения.
В зависимости от реализации, MVP может быть проще для автоматического модульного тестирования, чем MVC. Причина, которую часто приводят в защиту этого, заключается в том, что презентер может использоваться как полная имитация пользовательского интерфейса, и поэтому его можно тестировать независимо от других компонентов. По моему опыту, все зависит от языков, на которых вы реализуете MVP (существует большая разница между выбором MVP для проекта JavaScript и, скажем, для ASP.net).
В конце концов, основные трудности, которые у вас могут быть с MVC, скорее всего, будут актуальны и для MVP, учитывая, что различия между ними в основном семантические. Пока вы четко разделяете задачи на модели, представления и контроллеры (или презентеры), вы получаете много одних и тех же преимуществ, независимо от выбранного вами шаблона.
Существует очень мало, если вообще существует, архитектурных фреймворков JavaScript, которые заявляют о реализации шаблонов MVC или MVP в их классической форме, поскольку многие разработчики JavaScript не рассматривают MVC и MVP как взаимоисключающие (на самом деле мы с большей вероятностью увидим строго реализованный MVP, если посмотрим на веб-фреймворки, такие как ASP.net или GWT). Это связано с возможностью реализации в вашем приложении дополнительной логики презентера/представления, при этом оно будет считаться разновидностью MVC.
Один из азработчиков Backbone Ирен Рос (Irene Ros) (из бостонской компании Bocoup) придерживается такого подхода, поскольку, когда она разделяет представления на отдельные компоненты, ей нужно что-то, что могло бы их объединить. Это может быть как маршрут контроллера (например, Backbone.Router, который будет рассмотрен далее в книге), так и обратный вызов в ответе (response) на получение данных (fetch data).
Тем не менее, некоторые разработчики считают, что Backbone.js больше соответствует описанию MVP, чем MVC. Вот почему:
Презентер в MVP лучше описывает Backbone.View (слой между шаблонами представления и привязанными к нему данными), чем контроллер.
Модель соответствует Backbone.Model (она ничем не отличается от моделей в MVC).
Представления лучше всего представляют шаблоны (например, шаблоны разметки Handlebars/Mustache).
Ответить на это можно тем, что представление может быть просто Представлением (согласно MVC), поскольку Backbone достаточно гибок для его использования в различных целей. «V» в MVC и «P» в MVP могут быть реализованы с помощью Backbone.View, поскольку они способны решать две задачи: как рендеринг атомарных компонентов, так и сборку этих компонентов, рендеринг которых выполнен другими представлениями.
Мы также увидели, что в Backbone ответственность контроллера делится между Backbone.View и Backbone.Router, и в следующем примере мы можем фактически увидеть, что некоторые аспекты этого, безусловно, верны.
Наш PhotoView
в Backbone использует шаблон Наблюдатель для «подписки» на изменения модели Представления в строке this.model.bind('change',...)
. Он также обрабатывает шаблоны в методе render()
, но, в отличие от некоторых других реализаций, взаимодействие с пользователем также обрабатывается в Представлении (смотри events
).
var PhotoView = Backbone.View.extend({
//... is a list tag.
tagName: "li",
// Pass the contents of the photo template through a templating
// function, cache it for a single photo
template: _.template($('#photo-template').html()),
// The DOM events specific to an item.
events: {
"click img" : "toggleViewed"
},
// The PhotoView listens for changes to
// its model, re-rendering. Since there's
// a one-to-one correspondence between a
// **Photo** and a **PhotoView** in this
// app, we set a direct reference on the model for convenience.
initialize: function() {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.model.bind('destroy', this.remove);
},
// Re-render the photo entry
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
// Toggle the `"viewed"` state of the model.
toggleViewed: function() {
this.model.viewed();
}
});
Другое (совсем иное) мнение состоит в том, что Backbone больше напоминает MVC Smalltalk-80, который мы рассматривали ранее.
Как ранее выразился постоянный пользователь Backbone Дерик Бейли, в конечном счете лучше не заставлять Backbone соответствовать каким-либо конкретным шаблонам проектирования. Шаблоны проектирования следует рассматривать как гибкие руководства по тому, как могут быть структурированы приложения, и в этом отношении Backbone не соответствует ни MVC, ни MVP. Вместо этого он заимствует некоторые лучшие концепции из множества архитектурных шаблонов и создает гибкую структуру, которая просто хорошо работает.
Однако стоит понять откуда и почему возникли эти концепции, поэтому я надеюсь, что мои объяснения MVC и MVP были полезны. Называйте это путь Backbone, MV* или как угодно, что поможет отразить его особенность архитектуры приложений. Большинство структурных JavaScript-фреймворков намеренно или случайно будут использовать свой собственный подход к классическим шаблонам, но важно то, что они помогают нам разрабатывать приложения, которые хорошо организованы, чисты и могут быть легко обслужены.
Основные компоненты: Модель, Вид, Коллекция, Маршрутизатор. Реализует свой собственный вариант MV*.
Более полная документация, чем у некоторых фреймворков (на момент написания статьи, например, Ember.js), а также множество улучшений в разработке.
Используется крупными компаниями, такими как SoundCloud и Foursquare, для создания нетривиальных приложений.
Событийно-управляемая связь между представлениями и моделями означает более полный контроль над тем, что происходит. Относительно просто добавить слушатели событий к любому атрибуту в модели, что означает больший контроль над тем, что изменяется в представлении.
Поддерживает привязку данных через ручные события или отдельную библиотеку наблюдения за ключами и значениями (KVO).
Отличная поддержка интерфейсов RESTful «из коробки», благодаря чему модели можно легко «привязать» к бэкэнду.
Обширная система событий. Очень просто добавить поддержку «Издатель»/«Подписчик» в Backbone.
Прототипы создаются с использованием ключевого слова «new», которое некоторым нравится.
Нет шаблонизатора по умолчанию, однако для этого часто используется микрошаблон Underscore. Также хорошо работает с решениями типа Handlebars.
Не поддерживает глубоко вложенные модели «из коробки», но, как и для многих распространенных проблем, существуют плагины Backbone, которые могут помочь. Например этот.
Понятные и гибкие соглашения для структурирования приложений. Backbone не навязывает использование всех своих компонентов и может работать только с теми, которые необходимы.
Чтобы продолжить, см. раздел «Основы Backbone».
Ссылки содержат Сравнение архитектурных шаблонов и полностью будут задокументированы в «Основах Backbone».