Технология CORE

    Полгода назад я написал пост о придуманной мною технологии программирования ( 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});
        },
        ...
    };
    


    Теперь, когда код написан в чётком соответствии с логикой, работать с кодом легко.

    Вместо заключения



    Работа в такой парадигме сильно упрощает проектирование, и содержание проекта. Мы можем перемоделировать сколько угодно раз, но логика задачи останется той же. Почему бы и не создавать код от неё? А если немного потренироваться, работать в такой парадигме — проще простого, потому как мы, фактически, просто должны описать задачу в тех словах, в которых мы о ней думаем, и всё.

    В моём опыте, это сильно упрощает проектирование интерфейсов, и даже серверных приложений. Содержать код в том уровне абстракции, которого требует задача — можно и нужно.
    Поделиться публикацией

    Комментарии 16

      +4
      Пусть заклюют, не привыкать, но… всё это не CORE, всё это Фаулер…
        0
        конечно, фаулер )
        однако, я тут скорее об удобном инструменте
        +2
        Поздравляю! Вы описали паттерн MVVM. Две модели — Balance и WinHandler — подписываются на события модели вида UI, и с помощью IoC дергают ее методы. Модель вида, в свою очередь, использует вид (jQuery) для отображения данных пользователю и получения от него обратной связи. Рекомендую посмотреть на AngularJS.
          0
          Я описал принцип — сводить описание любой программерской задачи к двум паттернам, после чего их программировать будет очень легко. что получился MVVM, я не виноват )
          Точно так же можно перегруппировать функциональность в соответствии с BDD, и получится такой же удобно структурированный код, но это уже не будет MVVM.
            0
            Эм, я, похоже, что-то не понимаю, но вроде BDD и MVVM вообще из разных областей, BDD — подход к процессу разработки, а MVVM — к архитектуре, и одно другому не мешает совершенно. А про подходы и паттерны я ниже отписал — не надо придумывать новые термины для одной и той же сущности.
              0
              этот паттерн получился из подхода. если с таким же подходом проектировать другие вещи — будут получаться другие паттерны. так что, одно другому не мешает.
              в общем, наверное я непонятно объяснил и в следующий раз объясню по-другому.
                0
                ну и, я говорю о подходе к процессу разработки как раз, а не об архитектуре. то есть ваше же сравнение с MVVM некорректно.
                  0
                  Статья у вас об архитектуре, про подход там ничего не сказано, вернее, сказано, что надо «просто описать то, что было в задаче» — и это прекрасно реализуется в примере, который «еще хуже», опровергая ваш же тезис, что «языки программирования не похожи на язык описания задач, которым мы пользуемся при постановке». А далее следует (безо всякого обяснения, почему так надо) описание реализации той же задачи с использованием переименованных MVVM + IoC. Я молчу про спорные моменты конкретной реализации, где обработчик событий знает об источнике, и сам подписывается на соответствующие события (что перечеркивает идею событий как таковую), request (реализация IoC?), не привносящие ничего по сравнению с вызовом метода конкретного объекта и пр.
                    0
                    Ну и про реализацию. Описанный диспетчер зависимостей легко реализуется парой функций subscribe и trigger:

                    var handlers = {};
                    
                    function subscribe(evt, handler) {
                        if (!handlers[evt]) handlers[evt] = [];
                        handlers[evt].push(handler);
                    }
                    
                    function trigger(evt, data) {
                        if (handlers[evt]) {
                            handlers[evt].forEach(function(handler) { handler(data); });
                        }
                    }
                    


                    Вот пример реализации той же задачи про монетки:

                    function Balance() {
                        this.balance = 0;
                        subscribe('UI.coinClicked', this.addCoins.bind(this));
                    }
                    
                    Balance.prototype.addCoins = function()  { 
                        this.balance += 15;
                        trigger('Balance.changed', this.balance);
                    };
                    
                    function WinWatcher() {
                        subscribe('Balance.changed', this.checkWin.bind(this));
                    }
                    
                    WinWatcher.prototype.checkWin = function(balance) {
                        if (balance > 20) {
                            trigger('WinWatcher.userWins');
                        }
                    };
                    
                    function UI() {
                        $('.coin').click(function() {
                            trigger('UI.coinClicked');
                        });
                    
                        subscribe('WinWatcher.userWins', this.showWin.bind(this));
                    }
                    
                    UI.prototype.showWin = function() {
                        $('.you_win').animate({opacity: 0});
                    };
                    
                    subscribe('DOM.init', function () {
                        var ui = new UI(),
                            balance = new Balance(),
                            winWatcher = new WinWatcher();
                    });
                    


                    Тут также несложно добавить новый функционал:

                    function GatherStat() {
                        subscribe('UI.coinClicked', this.sendClick.bind(this));
                        subscribe('WinWatcher.userWins', this.sendWin.bind(this));
                    }
                    
                    GatherStat.prototype.sendClick = function() {
                        $.post('/stat/gather/ajax', {click: 1, date: new Date});
                    };
                    
                    GatherStat.prototype.sendWin = function() {
                        $.post('/stat/gather/ajax', {win: 1, date: new Date});
                    };
                    


                    или разделить UI на два объекта:

                    function UI() {
                        $('.coin').click(function() {
                            trigger('UI.coinClicked');
                        });
                    }
                    
                    function UIWin() {
                        subscribe('WinWatcher.userWins', this.showWin.bind(this));
                    }
                    
                    UI.prototype.showWin = function() {
                        $('.you_win').animate({opacity: 0});
                    };
                    
                    


                    Можете объяснить, почему ваше решение лучше, и зачем нужен ваш фреймворк?
                      0
                      объясню:
                      — посмотрите на ваши конструкторы. при усложнении задачи они станут монструозными и в одном месте будет слишком много знаний о разных задачах. это плохо
                      — рефакторинг в моей версии сводится к копи-пасте методов, в вашей — ещё и к переорганизации конструкторов. Это дополнительные минуты. зачем эта лишняя трата времени?
                      — в вашем случае код, относящийся к задаче (подписка на событие и обработка этого события), лежит в двух разных местах, что опять ведёт к трате времени при изучении кода. Зачем? Одна задача — один метод, зачем усложнять?
                      — в вашем случае события — это просто строки. об автодополнении средствами IDE можно забыть. в моём автодополнение есть.
                      — в вашем случае нужно сперва догадаться, что в задаче будет правильнее использовать события. в моём случае фреймворк и сама технология подталкивает к правильному решению.
                      — ваши конструкции вроде «this.showWin.bind» — это ещё одна зависимость от какого-то фреймворка. зачем? в моём случае решение полно и не требует внешних зависимостей, предоставляя полный и удобный стек.

                      И я не очень понимаю, почему, по-вашему, «спорные моменты конкретной реализации, где обработчик событий знает об источнике, и сам подписывается на соответствующие события (что перечеркивает идею событий как таковую)». В чём, по-вашему, идея событий и почему этот подход её перечёркивает?

                        0
                        — посмотрите на ваши конструкторы. при усложнении задачи они станут монструозными и в одном месте будет слишком много знаний о разных задачах. это плохо

                        Не понял, почему при усложнении задачи мое решение должно стать монструознее вашей реализации? Пока 0:0.

                        — рефакторинг в моей версии сводится к копи-пасте методов, в вашей — ещё и к переорганизации конструкторов. Это дополнительные минуты. зачем эта лишняя трата времени?

                        Из реорганизации — перенос подписки на событие. В вашем случае, например, при выносе handleCoinClick надо будет перенести и EventPoint, а в моем случае только cut&paste метода, так что паритет, 1:1

                        — в вашем случае код, относящийся к задаче (подписка на событие и обработка этого события), лежит в двух разных местах, что опять ведёт к трате времени при изучении кода. Зачем? Одна задача — один метод, зачем усложнять?

                        В моем случае можно повесить один обработчик на несколько событий, и несколько обработчиков на одно, и это описание будет в одном месте, а не размазано по коду. Да и изучение такого кода обычно начинается с вопроса, кто обрабатывает то или иное событие, и в моем случае ответ очевиден после взгляда на конструктор, в вашем случае — надо пробежаться по всему коду, 2:1 в мою пользу.

                        — в вашем случае события — это просто строки. об автодополнении средствами IDE можно забыть. в моём автодополнение есть.

                        Про IDE — ну, разве что да. Но подобные архитектуры очень завязаны на схему событий, так что в любом случае одной IDE не обойтись, придется вести документацию и с ней сверяться, но ладно, 2:2.

                        — в вашем случае нужно сперва догадаться, что в задаче будет правильнее использовать события. в моём случае фреймворк и сама технология подталкивает к правильному решению.

                        В вашем тоже. В этой задаче самое главное — понять, что отображением данных должен заниматься один объект, обработкой баланса другой, а определением выигрыша — третий. Ваш подход показывает только их способ взаимодействия, а найти такой способ не так уж и сложно, что и было показано в моем примере. 2:2

                        — ваши конструкции вроде «this.showWin.bind» — это ещё одна зависимость от какого-то фреймворка. зачем? в моём случае решение полно и не требует внешних зависимостей, предоставляя полный и удобный стек.

                        Function.prototype.bind — стандартная функция ECMAScript5, частичное применение и указание контекста. Тут полностью работающее решение, из зависимостей только jQuery, и то не принципиально и легко вырезаемо, т. к. нужно только в UI (к тому же у вас jQuery тоже есть в зависимостях). Так что в моем решении на одну зависимость меньше, что явно лучше, 3:2

                        Итого, мое наколенное решение ничуть не хуже, а то даже и лучше (как минимум, отсутствием дополнительных зависимостей).
                          0
                          подтасовка оценок налицо :)

                          Не понял, почему при усложнении задачи мое решение должно стать монструознее вашей реализации?

                          Речь шла о конструкторах,
                          function AnyConstr(){
                              this.prop1 = 0;
                              this.prop2 = '';
                              ......
                              subscribe('event1', this.method1.bind(this));
                              subscribe('event2', this.method2.bind(this));
                              subscribe('event3', this.method3.bind(this));
                              subscribe('event4', this.method4.bind(this));
                              subscribe('event4', this.method5.bind(this));
                              subscribe('event4', this.method6.bind(this));
                              .......
                          }
                          

                          Вот так будут выглядеть ваши конструкторы при мало-мальском усложнении задачи. Я не считаю это хорошим кодом, и уже пару раз объяснил, почему — в одном месте будет сосредоточено. После этого нам захочется подписку по условию — и всё, мы погрязли в лапше, нам нужен рефакториниг. Так что, 1:0.

                          Из реорганизации — перенос подписки на событие. В вашем случае, например, при выносе handleCoinClick надо будет перенести и EventPoint, а в моем случае только cut&paste метода, так что паритет, 1:1

                          надо будет перенести и EventPoint

                          угу, тот же copy-paste, что займёт не более пяти секунд у программиста в здравом уме и с незагипсованной рукой ))

                          В моем случае можно повесить один обработчик на несколько событий, и несколько обработчиков на одно

                          Несколько событий на один обработчик можно и в моём случае, достаточно написать их через запятую
                          Core.CatchEvent(Event1, Event2, Event3, ...)

                          Несколько обработчиков на одно событие, описанных в одном месте — вот это как раз и противоречит концепции event-driven. Кстати, вы так и не объяснили, в чём, по-вашему, она заключается?
                          Несколько обработчиков на одно событие также противоречит концепции разделения обязанностей (ещё раз смотрим на конструкторы и убеждаемся, что конструктор «знает» слишком много).

                          Что же, ещё один балл в мою пользу

                          Так что в моем решении на одну зависимость меньше, что явно лучше, 3:2


                          функции trigger и subscribe — это тоже зависимости, только наколенные. вы наверняка захотите их перенести в отдельный файл. так что это вообще не аргумент.

                          в итоге, моё решение-таки лучше, по вашим же аргументам и методике оценки =)
                            0
                            Вот так будут выглядеть ваши конструкторы при мало-мальском усложнении задачи.

                            Это ни капли не хуже вашего кода. У вас будет ровно то же самое, но только еще с кодом метода-обработчика рядом.

                            var AnyConstr = {};
                            
                            AnyConstr.Event1 = function () {
                              Core.CatchEvent(Some.Event1);
                              // ...
                            };
                            AnyConstr.Event2 = function () {
                              Core.CatchEvent(Some.Event2);
                              // ...
                            };
                            AnyConstr.Event3 = function () {
                              Core.CatchEvent(Some.Event3);
                              // ...
                            };
                            AnyConstr.Event4 = function () {
                              Core.CatchEvent(Some.Event4);
                              // ...
                            };
                            AnyConstr.Event5 = function () {
                              Core.CatchEvent(Some.Event5);
                              // ...
                            };
                            AnyConstr.Event6 = function () {
                              Core.CatchEvent(Some.Event6);
                              // ...
                            };
                            


                            При этом значительно сложнее понять без детального изучения кода, какие события у нас обрабатываются и чем. С упомянутой вами подпиской по условию это вообще будет ад и израиль. Так что именно такой вариант я бы не назвал хорошим кодом.

                            угу, тот же copy-paste, что займёт не более пяти секунд у программиста в здравом уме и с незагипсованной рукой ))


                            В моем варианте это займет не больше.

                            Несколько обработчиков на одно событие, описанных в одном месте — вот это как раз и противоречит концепции event-driven.


                            Лолшто? В событиях как раз и ценно то, что обработчиков у одного события может быть сколько угодно. Иначе можно обойтись просто коллбэками. Про противоречие нескольких обработчиков одного события концепции разделения обязанностей — это вообще пушка, там как раз наоборот все, несколько событий один обработчик не должен обрабатывать.

                            функции trigger и subscribe — это тоже зависимости,


                            Только раз в двадцать меньше, без парсинга исходников и понятные любому программисту.

                            В итоге имеем отличие от моего решения максимум во внешнем виде кода (и то спорном), и это ценой дополнительных 7кб.
                        0
                        ну и то, что вы называете минусом («обработчик событий знает об источнике») — это, наоборот, плюс — это его обязанность знать об источнике и о том, что он из источника получит. иначе придется где-то ещё описывать связь с источником — лишнее усложнение (что у вас закономерно в конструкторах и получилось)
                0
                Чем больше в интернете разных подходов описать одну и ту же проблему — тем проще новичкам будет учиться.
                Задача опытных девелоперов тут — не допустить в статьях откровенных ошибок в решении проблемы.

                и да, хорошо если в решении будут линки на хрестоматийную литературу

                p.s. сори чет не прикрепилось куда надо, это ответ для maxatwork
                  0
                  Разные подходы — хорошо, но и плодить термины не за чем.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое