И опять про MVC
Я нашел в сети очень мало реализаций MVC для JavaScript, поэтому хотел бы раскрыть преимущества данного подхода именно в реализации JavaScript.
Этот пост является противоположной точкой зрения на статью Прочь от MVC
Немного истории: MVC (Model View Controller) описан в 1979 году, еще для языка SmallTalk.
С тех пор ничего нового в интерфейсах так и не изобрели. Шаблон улучшался, дополнялся и расширялся. Появлялись последователи в зависимости от возможностей платформы и языка на котором пишется приложение, такие как HMVC (Hierarhical Model View Controller), MVP (Model View Presenter), MVVM (Model View ViewModel), но суть осталась прежняя - отделение источника данных от изменящих и командующих функций и их слабое связывание.
Заранее скажу что с самого появления шаблона в 1979 году первый отзыв был примерно такой: "Зачем это нужно? И так всё работает хорошо.", второй отзыв: "Ба! Да тут же данные дуплицируются в Виде и в Модели. Всё будет тормозить! Зачем это нужно?" и, наконец, третий :"При каждом изменении модели вид обновляется. Всё будет тормозить. Зачем это нужно?!". Поэтому не удивляйтесь что при прочтенни данной статьи у вас возникнут похожие вопросы. Несмотря на всё, MVC и его последователи очень стал очень удачным и проверенным временем решением не для всех, но для большого круга задач. Надеюсь вы тоже в этом убедитесь.
Задача: создать простейшую интерактивную страничку - список значений, и две кнопки - для добавления и удаления значений. Традиционный подход довольно простой - создаём html файл. Для списка используется select, для манипуляций - button. В атрибуте onClick у кнопок прописываем вызов JavaScript функции. Для удаления значения из select обращаемся к DOM объекту select, спрашиваем свойство selectedIndex и удаляем соответствующий объект .
<html><head><script>
// <![CDATA[
function addItem (value) {
if (!value) {
return;
}
var myselect = document.getElementById('myselect');
var newoption = myselect.appendChild(document.createElement('option'));
newoption.value = value;
newoption.innerHTML=value;
}
function removeCurrentItem () {
var myselect = document.getElementById('myselect');
if (myselect.selectedIndex === -1) {
return;
}
var selectedOption = myselect.options[myselect.selectedIndex];
selectedOption.parentNode.removeChild(selectedOption);
}
// ]]>
</script>
</head>
<body>
<select id="myselect" size="4"></select>
<button onClick="addItem(prompt('enter value'))">+</button>
<button onClick="removeCurrentItem()" />-</button>
</body>
</html>
Всё просто и отлично работает. Зачем же нужно что-то еще? Для данного приложения проблем не ожидается, однако, по статистике, собственно, написание кода занимает около 25% времени, а остальные 75% программист занимается развитием, поддержкой проекта, добавление новых функций. И по мере усложнения приложения и увеличения количества поддерживать код становится всё сложнее и сложнее, количество багов увеличивается.
"Я сделаю через Jquery/Dojo/MooTools/Моя_любимая_аякс_библиотека" скажете вы. Например так:
<html><head>
<script src="jquery.js"></script>
<script>
// <![CDATA[
$(document).ready(function(){
$('#plus').bind('click', function(){
var value = prompt('add value');
if (!value) {
return;
}
$('<option>').html(value).appendTo($('#myselect'));
});
$('#minus').bind('click', function(){
var myselect = $('#myselect');
if (myselect.attr('selectedIndex') === -1) {
return;
}
myselect.children().remove(':selected');
});
});
// ]]>
</script>
</head>
<body>
<select id="myselect" size="4"></select>
<button id="plus">+</button>
<button id="minus"/>-</button>
</body>
</html>
Стало еще лучше, код стал более компактный и он выглядит очень круто. Однако это не решило основную проблему: данные (список) хранится внутри пользовательского интерфейса, внутри html элемента select. Второй недостаток - у нас мешанина из html и javascript кода. Несмотря на простоту, он плохо развивается. К примеру к нам подходит начальник и говорит простейшую просьбу: "хочу чтобы кнопка 'минус' появлялась только когда что-то выделено в списке.". Сразу у вас, как у программиста, возникает вопрос: где делать это изменение - в html или в JavaScript. "Хм, пожалуй выключу кнопку в html" - скажете вы. А ваш коллега, пока вы в отпуске, сделает похожую операцию внутри JavaScript.
Получается что, казалось бы, простое изменение начинает очень сильно запутывать код, этого хотелось бы избежать.
В классической реализации MVC основные принципы такие:
- Слабое связывание
- Модель ничего не знает ни о ком. Модель рассылает оповещения, которые может слушать, например, Вид, если хочет.
- Вид знает о модели но не может её менять, вид может манипулировать контроллером
- Контроллер знает о Модели и может её менять, а также знает о Виде (или Видах) и может его (их) менять.
В данном случае отличия следующие:
- Модель ничего не знает ни о ком. Может рассылать оповещения
- Вид знает только о модели
- Контроллер знает и о Виде и о Модели
Как видим, в отличие от классического шаблона, данный шаблон более жёсткий, т.к. вид ничего не знает о контроллере, мне не нравится идея перекрестной линковки объектов (Вида и Контроллера), и пока можно я попытаюсь без такой линковки обойтись. Назовем его, к примеру, Ортодоксальный MVC (Orthodox MVC)
Модель в нашем случае хранит просто массив объектов (строк) которые мы ввели. Также хранится порядковый номер "текущего" объекта. Также здесь есть стандартные методы добавления и удаления объектов: addItem, removeCurrentItem, без них не обойтись, т.к. модели необходимо рассылать оповещения о своём изменении.
Для рассылки оповещения используется объект типа сабджект (modelChangedSubject), можно сделать несколько таких объектов и рассылать подписчикам не просто извещения "модель изменилась" а "свойство X у модели изменилось". Это поможет Виду обновляться не целиком а только тем участкам которые реально изменились в модели.
Я не нашёл в jquery стандартную функцию создания сабджектов, поэтому взял готовую makeObservableSubject из отличной статьи по mvc одного Канадского программиста.
Как видим, у нашего html тело (body) "пустое", все элементы рисуются "вручную" т.е. с помощью javascript. Посмотрите Gmail - там то же самое. Все элементы: кнопочки, списки рисуются яваскриптом.
Вид знает о модели, но в идеале Вид должен иметь доступ к модели в режиме read-only, т.е. нельзя разрешить виду вызывать removeCurrentItem даже если это очень хочется.
К сожалению я пока не придумал как можно в JavaScript одним функциям разрешить изменения другого объекта, а другим функциям оставить только доступ в режиме чтения. Если у вас есть идеи как этого добиться, поделитесь пожалуйста.
В данном контроллере мы просто вешаем эвенты на объекты вида, связывая модель и вид.
Полный текст страницы:
<!doctype html>
<html>
<body>
<script src="jquery.js"></script>
<script>
// <![CDATA[
var OMVC = {};
OMVC.makeObservableSubject = function () {
var observers = [];
var addObserver = function (o) {
if (typeof o !== 'function') {
throw new Error('observer must be a function');
}
for (var i = 0, ilen = observers.length; i < ilen; i += 1) {
var observer = observers[i];
if (observer === o) {
throw new Error('observer already in the list');
}
}
observers.push(o);
};
var removeObserver = function (o) {
for (var i = 0, ilen = observers.length; i < ilen; i += 1) {
var observer = observers[i];
if (observer === o) {
observers.splice(i, 1);
return;
}
}
throw new Error('could not find observer in list of observers');
};
var notifyObservers = function (data) {
// Make a copy of observer list in case the list
// is mutated during the notifications.
var observersSnapshot = observers.slice(0);
for (var i = 0, ilen = observersSnapshot.length; i < ilen; i += 1) {
observersSnapshot[i](data);
}
};
return {
addObserver: addObserver,
removeObserver: removeObserver,
notifyObservers: notifyObservers,
notify: notifyObservers
};
};
OMVC.Model = function () {
var that = this;
var items = [];
this.modelChangedSubject = OMVC.makeObservableSubject();
this.addItem = function (value) {
if (!value) {
return;
}
items.push(value);
that.modelChangedSubject.notifyObservers();
};
this.removeCurrentItem = function () {
if (that.selectedIndex === -1) {
return;
}
items.splice(that.selectedIndex, 1);
that.modelChangedSubject.notifyObservers();
};
this.getItems = function () {
return items;
};
this.selectedIndex = -1;
this.getSelectedIndex = function () {
return that.selectedIndex;
}
this.setSelectedIndex = function (value) {
that.selectedIndex = value;
}
};
OMVC.View = function (model, rootObject) {
var that = this;
that.select = $('<select/>').appendTo(rootObject);
that.select.attr('size', '4');
that.buttonAdd = $('<button>+</button>').appendTo(rootObject).height(20);
that.buttonRemove = $('<button>-</button>').appendTo(rootObject).height(20);
model.modelChangedSubject.addObserver(function () {
var items = model.getItems();
var innerHTML = '';
for (var i = 0; i<items.length; i += 1) {
innerHTML += "<option>"+items[i]+"</option>";
}
that.select.html(innerHTML);
});
};
OMVC.Controller = function (model, view) {
view.buttonAdd.bind('click', function () {
model.addItem(prompt('addvalue'));
});
view.buttonRemove.bind('click', function () {
model.removeCurrentItem();
});
view.select.bind('click', function () {
model.setSelectedIndex(view.select[0].selectedIndex);
});
};
$(document).ready(function () {
var model = new OMVC.Model();
var view = new OMVC.View(model, $('<div/>').appendTo($("body")));
var controller = new OMVC.Controller(model, view);
});
// ]]>
</script>
</body>
</html>
Напоследок, вопрос: какие же преимущества нам даёт реализация в виде MVC перед первой, стандартной или улучшенной jQuery-евской? Мы написали в три раза больше кода и ради чего?
Ответ: Даже на таком простом примере, потихоньку начинают становиться заметными преимущества шаблона MVC:
- Нет нужды присваивать элементам интерфейса id, чтобы получать их потом по document.getElementById и им подобным функциям, ведь ссылки на все элементы уже хранятся в Виде, их искать по html документу не нужно.
- Все объекты рисуются внутри элемента rootObject, Вид можно легко использовать много раз для любого rootObject.
- В html body ничего не хранится, т.е. конечно что-то там есть, но нас это совершенно не интересует - раньше изменения нам нужно было искать и делать в двух местах: в html и в функциях JavaScript. Теперь всё делается только в JavaScript и более того, дизайн хранится только в объекте Вид.
- Изменения очень легки. К примеру, упомянутая выше просьба "начальника" выполняется следующим образом:
- создаём сабджект по изменению selectedIndex
this.selectedIndexChangedSubject = OMVC.makeObservableSubject();
- при изменении оповещаем that.selectedIndexChangedSubject.notifyObservers();
- и в Виде подписываемся на оповещенеие selectedIndexChangedSubject
Полная, финальная версия с добавлением фичи с подсветкой:
<!doctype html>
<html>
<body>
<script src="jquery.js"></script>
<script>
// <![CDATA[
var OMVC = {};
OMVC.makeObservableSubject = function () {
var observers = [];
var addObserver = function (o) {
if (typeof o !== 'function') {
throw new Error('observer must be a function');
}
for (var i = 0, ilen = observers.length; i < ilen; i += 1) {
var observer = observers[i];
if (observer === o) {
throw new Error('observer already in the list');
}
}
observers.push(o);
};
var removeObserver = function (o) {
for (var i = 0, ilen = observers.length; i < ilen; i += 1) {
var observer = observers[i];
if (observer === o) {
observers.splice(i, 1);
return;
}
}
throw new Error('could not find observer in list of observers');
};
var notifyObservers = function (data) {
// Make a copy of observer list in case the list
// is mutated during the notifications.
var observersSnapshot = observers.slice(0);
for (var i = 0, ilen = observersSnapshot.length; i < ilen; i += 1) {
observersSnapshot[i](data);
}
};
return {
addObserver: addObserver,
removeObserver: removeObserver,
notifyObservers: notifyObservers,
notify: notifyObservers
};
};
OMVC.Model = function () {
var that = this;
var items = [];
this.modelChangedSubject = OMVC.makeObservableSubject();
this.addItem = function (value) {
if (!value) {
return;
}
items.push(value);
that.modelChangedSubject.notifyObservers();
};
this.removeCurrentItem = function () {
if (that.selectedIndex === -1) {
return;
}
items.splice(that.selectedIndex, 1);
if (items.length === 0) {
that.setSelectedIndex(-1);
}
that.modelChangedSubject.notifyObservers();
};
this.getItems = function () {
return items;
};
this.selectedIndex = -1;
this.getSelectedIndex = function () {
return that.selectedIndex;
}
this.selectedIndexChangedSubject = OMVC.makeObservableSubject();
this.setSelectedIndex = function (value) {
that.selectedIndex = value;
that.selectedIndexChangedSubject.notifyObservers();
}
};
OMVC.View = function (model, rootObject) {
var that = this;
that.select = $('<select/>').appendTo(rootObject);
that.select.attr('size', '4');
that.buttonAdd = $('<button>+</button>').appendTo(rootObject).height(20);
that.buttonRemove = $('<button>-</button>').appendTo(rootObject).height(20).fadeOut();
model.modelChangedSubject.addObserver(function () {
var items = model.getItems();
var innerHTML = '';
for (var i = 0; i<items.length; i += 1) {
innerHTML += "<option>"+items[i]+"</option>";
}
that.select.html(innerHTML);
});
model.selectedIndexChangedSubject.addObserver(function () {
if(model.getSelectedIndex() === -1) {
that.buttonRemove.fadeOut();
} else {
that.buttonRemove.fadeIn();
}
});
};
OMVC.Controller = function (model, view) {
view.buttonAdd.bind('click', function () {
model.addItem(prompt('addvalue'));
});
view.buttonRemove.bind('click', function () {
model.removeCurrentItem();
});
view.select.bind('click', function () {
model.setSelectedIndex(view.select[0].selectedIndex);
});
};
$(document).ready(function () {
var model = new OMVC.Model();
var view = new OMVC.View(model, $('<div/>').appendTo($("body")));
var controller = new OMVC.Controller(model, view);
});
// ]]>
</script>
</body>
</html>
Раньше у нас была интерактивная html страничка - а теперь стало приложение на языке javascript с графической оболочкой на HTML. Почувствуйте разницу.
P.S.: Напоминаю, что использование MVC больше подходит к бизнес приложениям, интерфейсам типа админки или что-то подобное Gmail/Google docs. Если у вас промо-сайт с тысячей баннеров и летающими снежинками по экрану, использование MVC бессмысленно и даже вредно.
|
Да какая разница, мвц, цвм…
Главное — уменьшить связанность. MVC в частности и n-звенная архитектура вообще — это только один из способов добиться меньшей связанности.
Как известно, паттерны нужны только тем, кто до них сам додумался.
Полезная хрень. Я бы еще воспользовался бы шаблонное библиотекой из комплекта ajax от мелкомягких.
Отправить комментарий
Приветствуются комментарии:Для остальных вопросов и обсуждений есть форум.