Полгода назад я написал пост о придуманной мною технологии программирования ( habrahabr.ru/post/163881 ), которая сильно мне помогла ускориться (и не только мне) и делать своё дело более качественно.
Тогда как прошлый раз был посвящён практике и сравнению с привычной моделью разработки, в этот раз я хочу рассказать о теоретических основах технологии.
Для упрощения объяснения из системы Context-Object-Request-Event я выкину контексты, и мы поговорим о постановке задач и как они связаны с объектами, событиями и запросами.
В программировании нам часто приходится сталкиваться с протекающими абстракциями. Когда, например, наш код не учитывает, что соединение с базой данных может отвалиться, это приводит к проблемам.
Есть и более фундаментальная проблема протекающих абстракций, и она касается моделирования сложного поведения. Часто бывает, что при добавлении к задаче дополнительных требований код, который только что был написан, становится непригодным, и всё приходится переделывать. Это тоже протекающая абстракция: мы смоделировали решение задачи не в терминах задачи, а в терминах языка программирования — переменных, списков, ссылок, объектов, шаблонов проектирования. При небольшом изменении требований весь код оказался негодным.
Это происходит потому, что языки программирования не похожи на язык описания задач, которым мы пользуемся при постановке.
Я утверждаю, что большинство практических задач сводится к двух паттернам: «когда A сделать B» и «нужно, чтобы было сделано C. Давайте мы сделаем это способом D».
Введём обозначения: A — это событие, B — реакция на событие, C — запрос действия, D — способ исполнения.
Примеры для первого случая:
— когда пользователь нажмёт на эту кнопку, давайте покажем ему вот такую анимацию (хм, прямо как на собрании)
— когда дизайнер Аня отрисует прототип интерфейса, я хочу на него посмотреть и высказать свои идеи по поводу улучшений
— давайте собирать статистику по каждому клику
— каждый пятый заход показываем баннер
Для второго:
— Нужно отрисовать дизайн для этой идеи. Пусть это сделает Аня. Ну, или, Вова.
— сохраним значение текущего счёта в базу данных
Заметьте вот что: суть задачи в первом случае в том, чтобы было сделано B, когда произошло A. Неважно конкретно, что за A, к каком конкретно контексте оно возникло, и так далее. И эта задача совершенно отдельна от задачи, где происходит событие A, она посвящена другим целям. во втором случае — наоборот. принципиально не важно как будет сделана задача C. Важно чтобы она хоть как-то была сделана, любым подходящим способом.
Почему это важно понять? Рассмотрим следующий код абстрактной игры (или чего угодно другого) на js:
Этот код очень плох. почему? потому что в нём логика денег смешана с логикой показа. А вот ещё хуже:
В прототипах такой пример очень част. Затронь любой аспект задачи — дизайн, подсчёт денег, анимацию — всему каюк, это быстро превратится в кашу и будет глючить. Как было бы сделать по нормальному? Просто описать то, что было в задаче:
— когда пользователь кликнул на монетку, добавить её на баланс
— когда баланс стал больше 250, показать баннер, что мы выиграли
Делим задачу на три объекта, один из которых будет отвечать за отображение и UI, второй за состояние счёта, третий за определение выигрыша или проигрыша пользователя:
UI здесь отвечает только за отображение и клики — взаимодействие с пользователем.
Теперь эти компоненты нужно как-то связать. Нехорошо будет, если мы будем их вызывать из друг друга, поэтому свяжем их событиями и запросами
Теперь нужно связать кусочки кода там, где комментарии «вызвать, когда ...» и «здесь бросить/запросить». Но тут мы сталкиваемся с теми самыми протекающими абстракциями. Если вызывать из UI методы Balance и WinHandler напрямую, нам потом может понадобиться сбор статистики, или ещё какое-нибудь усложнение, и в метод UI добавятся ещё вызовы, связанные с другими задачами. Метод перестанет быть чистым.
Поэтому постараемся сделать метод простым. Предоставим разруливание зависимости диспетчеру событий
В прошлый раз я обещал сделать open-source реализацию. На данный момент есть реализация для javascript github.com/okneigres/corejs
Библиотека работает как в браузере, так и под Node.js
Теперь с этим кодом легко делать всё, что угодно: добавлять новый функционал:
Рефакторим UI (разделяем на два объекта — UI и UIWin):
Теперь, когда код написан в чётком соответствии с логикой, работать с кодом легко.
Работа в такой парадигме сильно упрощает проектирование, и содержание проекта. Мы можем перемоделировать сколько угодно раз, но логика задачи останется той же. Почему бы и не создавать код от неё? А если немного потренироваться, работать в такой парадигме — проще простого, потому как мы, фактически, просто должны описать задачу в тех словах, в которых мы о ней думаем, и всё.
В моём опыте, это сильно упрощает проектирование интерфейсов, и даже серверных приложений. Содержать код в том уровне абстракции, которого требует задача — можно и нужно.
Тогда как прошлый раз был посвящён практике и сравнению с привычной моделью разработки, в этот раз я хочу рассказать о теоретических основах технологии.
Для упрощения объяснения из системы Context-Object-Request-Event я выкину контексты, и мы поговорим о постановке задач и как они связаны с объектами, событиями и запросами.
Постановка задач и протекающие абстракции
В программировании нам часто приходится сталкиваться с протекающими абстракциями. Когда, например, наш код не учитывает, что соединение с базой данных может отвалиться, это приводит к проблемам.
Есть и более фундаментальная проблема протекающих абстракций, и она касается моделирования сложного поведения. Часто бывает, что при добавлении к задаче дополнительных требований код, который только что был написан, становится непригодным, и всё приходится переделывать. Это тоже протекающая абстракция: мы смоделировали решение задачи не в терминах задачи, а в терминах языка программирования — переменных, списков, ссылок, объектов, шаблонов проектирования. При небольшом изменении требований весь код оказался негодным.
Это происходит потому, что языки программирования не похожи на язык описания задач, которым мы пользуемся при постановке.
Я утверждаю, что большинство практических задач сводится к двух паттернам: «когда A сделать B» и «нужно, чтобы было сделано C. Давайте мы сделаем это способом D».
Введём обозначения: A — это событие, B — реакция на событие, C — запрос действия, D — способ исполнения.
Примеры для первого случая:
— когда пользователь нажмёт на эту кнопку, давайте покажем ему вот такую анимацию (хм, прямо как на собрании)
— когда дизайнер Аня отрисует прототип интерфейса, я хочу на него посмотреть и высказать свои идеи по поводу улучшений
— давайте собирать статистику по каждому клику
— каждый пятый заход показываем баннер
Для второго:
— Нужно отрисовать дизайн для этой идеи. Пусть это сделает Аня. Ну, или, Вова.
— сохраним значение текущего счёта в базу данных
Заметьте вот что: суть задачи в первом случае в том, чтобы было сделано B, когда произошло A. Неважно конкретно, что за A, к каком конкретно контексте оно возникло, и так далее. И эта задача совершенно отдельна от задачи, где происходит событие A, она посвящена другим целям. во втором случае — наоборот. принципиально не важно как будет сделана задача C. Важно чтобы она хоть как-то была сделана, любым подходящим способом.
Почему это важно понять? Рассмотрим следующий код абстрактной игры (или чего угодно другого) на js:
addMoney: function(amount) {
this.balance+=amount;
if(this.balance > 250) {
$('.you_win!').animate({css:{opacity: 1}})
}
}
Этот код очень плох. почему? потому что в нём логика денег смешана с логикой показа. А вот ещё хуже:
$('.coin').click(function(){
this.balance+=15;
if(this.balance > 250) {
$('.you_win").animate({css:{opacity: 1}})
}
})
В прототипах такой пример очень част. Затронь любой аспект задачи — дизайн, подсчёт денег, анимацию — всему каюк, это быстро превратится в кашу и будет глючить. Как было бы сделать по нормальному? Просто описать то, что было в задаче:
— когда пользователь кликнул на монетку, добавить её на баланс
— когда баланс стал больше 250, показать баннер, что мы выиграли
Делим задачу на три объекта, один из которых будет отвечать за отображение и UI, второй за состояние счёта, третий за определение выигрыша или проигрыша пользователя:
var UI = {
handleCoinClick: function() {
....
},
showWinAnimation: function() {
....
},
...
}
var Balance = {
addCoins: function() {
...
},
...
}
var WinWatcher = {
watchForWin: function() {
....
}
...
}
UI здесь отвечает только за отображение и клики — взаимодействие с пользователем.
Теперь эти компоненты нужно как-то связать. Нехорошо будет, если мы будем их вызывать из друг друга, поэтому свяжем их событиями и запросами
var UI = {
handleCoinClick: function() {
// вызвать, когда происходит DOM Init, или другое событие, которое оповещает о генерации карты
....
$('.coin').click(function(){
// здесь бросить событие клик на монетку Event_CoinClick
.....
});
},
showWinAnimation: function() {
// вызвать, когда потребуется показать пользователю что он выиграл Request_ShowUserWin
$('.you_win').animate({opacity: 0});
},
...
}
var Balance = {
addCoins: function() {
// вызвать, когда будет событие «клик на монетку» Event_CoinClick
this.balance+=15;
// здесь бросить событие, что баланс счёта изменён Event_BalanceChanged
},
...
}
var WinHandler = {
watchForWin: function(balance) {
// вызвать, когда произошло событие, что баланс изменён Event_BalanceChanged
if(balance > 250) {
// запросить показ пользователю, что он выиграл Request_ShowUserWin
}
}
...
}
Теперь нужно связать кусочки кода там, где комментарии «вызвать, когда ...» и «здесь бросить/запросить». Но тут мы сталкиваемся с теми самыми протекающими абстракциями. Если вызывать из UI методы Balance и WinHandler напрямую, нам потом может понадобиться сбор статистики, или ещё какое-нибудь усложнение, и в метод UI добавятся ещё вызовы, связанные с другими задачами. Метод перестанет быть чистым.
Поэтому постараемся сделать метод простым. Предоставим разруливание зависимости диспетчеру событий
Core.js
В прошлый раз я обещал сделать open-source реализацию. На данный момент есть реализация для javascript github.com/okneigres/corejs
Библиотека работает как в браузере, так и под Node.js
<script src="core.js"></script>
var Game = { }; //определяем неймспейс
Game.UI = {
CoinClickEvent: new Core.EventPoint,
handleCoinClick: function() {
Core.CatchEvent(Event.DOM.Init);
$('.coin').click(function(){
new Game.UI.CoinClickEvent();
});
},
showWinAnimation: function() {
Core.CatchRequest(Game.WinHandler.ShowUserWinRequest);
$('.you_win').animate({opacity: 0});
},
...
}
Game.Balance = {
ChangedEvent: new Core.EventPoint,
addCoinsOnClick: function() {
Core.CatchEvent(Game.UI.CoinClickEvent)
this.balance+=15;
new Game.Balance.ChangedEvent;
}
...
}
Game.WinHandler = {
ShowUserWinEvent: new Core.EventPoint,
ShowUserWinRequest: new Core.RequestPoint,
watchForWin: function(balance) {
Core.CatchEvent(Game.Balance.ChangedEvent)
if(balance > 250) {
new Game.WinHandler.ShowWinRequest;
}
}
...
}
Core.processNamespace(Game);
Теперь с этим кодом легко делать всё, что угодно: добавлять новый функционал:
// отправляем клики и выигрыши на сервер
Game.GatherStat = {
sendClick: function() {
Core.CatchEvent(Game.UI.CoinClickEvent);
$.post('/stat/gather/ajax', {click: 1, date: new Date});
},
sendWin: function() {
Core.CatchEvent(Game.WinHandler.ShowUserWinEvent);
$.post('/stat/gather/ajax', {win: 1, date: new Date});
}
}
Рефакторим UI (разделяем на два объекта — UI и UIWin):
Game.UI = {
CoinClickEvent: new Core.EventPoint,
handleCoinClick: function() {
Core.CatchEvent(Event.DOM.Init);
$('.coin').click(function(){
new Game.UI.CoinClickEvent();
});
}
};
Game.UIWin = {
showWinAnimation: function() {
Core.CatchRequest(Game.WinHandler.ShowUserWinRequest);
$('.you_win').animate({opacity: 0});
},
...
};
Теперь, когда код написан в чётком соответствии с логикой, работать с кодом легко.
Вместо заключения
Работа в такой парадигме сильно упрощает проектирование, и содержание проекта. Мы можем перемоделировать сколько угодно раз, но логика задачи останется той же. Почему бы и не создавать код от неё? А если немного потренироваться, работать в такой парадигме — проще простого, потому как мы, фактически, просто должны описать задачу в тех словах, в которых мы о ней думаем, и всё.
В моём опыте, это сильно упрощает проектирование интерфейсов, и даже серверных приложений. Содержать код в том уровне абстракции, которого требует задача — можно и нужно.