Полгода назад я написал пост о придуманной мною технологии программирования ( habrahabr.ru/post/163881/ ), которая сильно мне помогла ускориться (и не только мне) и делать своё дело более качественно.
Тогда как прошлый раз был посвящён практике и сравнению с привычной моделью разработки, в этот раз я хочу рассказать о теоретических основах технологии.
Для упрощения объяснения из системы 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}); }, ... };
Теперь, когда код написан в чётком соответствии с логикой, работать с кодом легко.
Вместо заключения
Работа в такой парадигме сильно упрощает проектирование, и содержание проекта. Мы можем перемоделировать сколько угодно раз, но логика задачи останется той же. Почему бы и не создавать код от неё? А если немного потренироваться, работать в такой парадигме — проще простого, потому как мы, фактически, просто должны описать задачу в тех словах, в которых мы о ней думаем, и всё.
В моём опыте, это сильно упрощает проектирование интерфейсов, и даже серверных приложений. Содержать код в том уровне абстракции, которого требует задача — можно и нужно.
Автор: okneigres