Фундамент масштабируемости javascript приложения
«Если хочешь идти быстро — иди один. Если хочешь идти далеко — идите вместе.» ©
С этой лирической строки в данной статье я буду рассуждать о том, как правильно организовать код в вашем приложении, чтобы оно могло расти в высоту и в ширь. Если вы хотите, чтобы продукт вашей мозговой активности был мощнее, чем у ваших конкурентов, то вам неизбежно придется приглашать новых программистов в вашу команду. А если не положить вектор масштабируемости, то порывы энтузиазма через год превратятся в лапшу-код и командная работа превратит каждого сотрудника от злости в маленького сатану.
Так вот… Для того, чтобы ваши бойцы чувствовали себя комфортно вместе в одном проекте, надо чтобы они не мешали друг другу и писали свои буквы в разных не пересекающихся участках кода. Для этого им нужно будет писать «Самостоятельные» компоненты.
«Самостоятельные» — это такие компоненты, которые сами управляют своим поведением, ориентируясь на события из внешней среды. При знании о том, как работает ваше приложение и какие события в нем протекают, можно легко писать такие «самостоятельные» компоненты, не затрагивая старые и чужие участки кода.
«Несамостоятельные» — компоненты, которые ничего не знают о внешней среде, но у них есть очень развернутое api. Этому компоненту нужно объяснить, как себя вести в вашем приложении. Такие компоненты, в отличие от «самостоятельных» пишутся ради многоразового использования в вашем или публичном проектах, как например открытые библиотеки на github и др.
Как определить какие компоненты нужны в вашем приложении? Очень просто. Если компонент применим только к одной задаче и не многоразовый, то его нужно писать так, чтобы он был «самостоятельным».
Вот например, рассмотрим компонент олицетворяющий поля ввода сообщения в ленте чата. Скорее всего такое поле ввода в вашем приложении вы будете использовать только по прямому назначению и не будете его использовать, скажем, в форме ввода никнейма или пароля при авторизации, ибо там у компонентов будет своя специфика.
Не будем тянуть кота за то, что не стоит оттягивать и разберем конкретный пример. Пускай это будет имитация чатика.
Представим, что у вас два программиста в команде. Петька и Толик. И у них есть ядро масштабируемого приложения. Простое, как два пальца у двупалого человека. В ядре есть сетевой транспорт, хранилище ленты сообщений в виде массива (в этом примере не будем выделять его в отдельный файл с методами) и event emitter, который в этом случае является залогом масштабируемости.
В качестве event emitter в этом примере я взял Backbone.Events, хотя этим и ограничимся в использовании Backbone, чтобы показать пример как можно проще.
//app.js
var App = function(){
var app = _.extend({
init: function(){
this.connection.connect();
}
}, Backbone.Events);
app.connection = new Connection(),
app.messages = [];
app.connection.on('connected', function(){
console.info('App connected!');
});
app.connection.on('incoming_message', function(text){
app.messages.push(text);
app.trigger('new_message', text)
});
return app;
}
//connection.js
var Connection = function(){
return _.extend({
connect: function(){
/*просто имитируем то, что наш сетевой транспорт принимает сообщения от сервера и отдает какие то сигналы каждую секунду во внешнюю среду*/
var i=0;
setInterval(_.bind(function(){
i++;
var text = 'message_' + i;
this.trigger('incoming_message', text);
},this),1000);
this.trigger('connected');
},
}, Backbone.Events);
}
Ну вот, у нас есть приложение, в котором пока ни одной вьюхи и работу которого можно протестировать через консоль браузера. Кстати, если из вашего приложения удалить все вспомогательные компоненты и вьюхи, и оно сможет работать через консоль, то это очень хорошо. Значит у вас достигнута в какой-то мере слабая связанность между компонентами и код можно покрыть автоматизированными тестами. Погнали дальше.
Теперь сведущий в стратегических планах человек ставит задачу Петьке и Толику, мол, надо, чтобы приложение показывало ленту сообщений, а в шапке был счетчик всех сообщений в из ленты. У вас мог возникнуть вопрос… кому вообще нужен этот, блин, счетчик сообщений в шапке в реальной жизни? Это просто для примера.
Ок, думают Петька и Толик, ок. Они решают одновременно написать два разных компонента для приложения.
Петька взял на себя задачу по ленте сообщений
Но он не слышал о том, как программировать масштабируемое приложение и начал писать код:
//list-view.js - "несамостоятельный" компонент
var ListView = function(container){
var el = document.createElement('div');
container.appendChild(el);
return {
addMessage: function(text){
var row = document.createElement('div');
row.innerHTML = 'message: ' + text;
el.appendChild(row);
}
}
}
//app.js изменение кода
var App = function(){
var app = _.extend({
init: function(){
connection.connect();
}
}, Backbone.Events);
app.connection = new Connection(),
app.messages = [];
//добавил код
app.listView = ListView(document.getElementById('container'));
app.connection.on('connected', function(){
console.info('App connected!');
});
app.connection.on('incoming_message', function(text){
app.messages.push(text);
app.trigger('new_message', text);
app.listView.addMessage(text); //добавил код
});
return app;
}
Петя создал компонент, которым приходится управлять посредством методов на более высоком уровне и, как следствие, помимо простого объявления компонента, пришлось копаться в коде app.js и добавить строки в обработчик incoming_message. Теперь вы не сможете просто закомментировать строку «app.listView = …» так, чтобы ваше приложение не сломалось. Ибо app.listView.addMessage (text); выдаст Exception. Приложение начало обрастать связанностью. Ядро начало зависеть от view.
Посмотрим, как справился Толик с задачей по счетчику сообщений в шапке
Он знает, как писать код так, чтобы не мешать другим:
//header-view.js
var HeaderView = function(container) {
var el = document.createElement('div'),
span = document.createElement('span'),
view = {
setCounter: function(num){
span.innerHTML = num;
}
}
el.innerHTML = 'Кол-во сообщений: ';
el.appendChild(span);
container.appendChild(el);
view.setCounter(0);
app.on('new_message', function(){
view.setCounter(app.messages.length);
});
return view;
}
//app.js изменение кода
...
app.connection = new Connection(),
app.messages = [];
//добавил код
app.headerView = HeaderView(document.getElementById('head'));
...
Что сделал Толик за пределами своего компонента — это только объявил компонент в области переменных app и все. Компонент остается также доступным для ручного тестирования через консоль или для модульного тестирования, так как он все же возвращает api.
Зона ответственности за код Толика ограничивается по сути всего одним файлом header-view.js и эти правки легче ревьюить, ведь надо смотреть всего в один файл.
Писать «самостоятельные» компоненты выгодно
Если бы Толик тоже написал несамостоятельный компонент, то в app.js он затронул бы те же куски кода, что и Петя. Сложно мержить, связанность между компонентами увеличивается. На таком маленьком примере может этого не сильно будет заметно, но если у вас суммарно многотысячный код и пишутся большие сложные фичи, то это можно будет хорошо почувствовать.
В процессе написания кода всегда будет выбор, либо управлять компонентом на более высоком уровне иерархии, либо дать компоненту управляться самостоятельно.
Разделяйте и властвуйте господа, пишите для своих приложений «самостоятельные» компоненты.
p.s. Хотя примеры кода в данной статье были написаны на голом JS без использования фреймворков, данная философия слабой связанности справедлива и при их использовании, будь то Backbone или React с хитрыми методологиями изоморфных приложений типа Flux и Redux, или еще каких других фреймворков.
Всегда стремитесь ограничивать зону ответственности в коде ваших программистов, когда они пилят новые фичи. Если вам дали такой гаечный ключ, как React, то им нужно закручивать гайки, а не бить себе им по пальцам.
Команда разработчиков JivoSite.ru желает вам чистого и понятного кода.