Auto dependency injection в Javascript

Вступление


Как все мы знаем javascript это язык в котором очень просто выстрелить себе в ногу. Работая с этим языком уже почти пять лет, я не раз сталкивался с тем, что javascript предоставляет очень скудные инструменты для создания абстракций высокого уровня. А, создавая полноценные MVVM/MVP приложения, сталкиваешься с тем что, основной проблемой является трудность сохранить код и абстракцию в чистоте, не говоря уж о полноценном следовании SOLID принципам.

Со временем я пришел к пониманию, что один из основных паттернов который смог бы мне помочь -это Dependency Injection. И я решил поэкспериментировать с ним в JS.
Конечно, JS не предоставляет инструментов для полноценного следования этому паттерну (элементарное отсутствие тех же рефлекшенов), поэтому я решил поставить для себя несколько Acceptance Criteria, которых я хотел бы достигнуть адаптировав этот паттерн к такой уникальной среде как JS.

1. Избавиться от всех возможных глобальных переменных. (за исключением common библиотек)
2. Возможность модернизировать или изменять поведение приложения не меняя его кода.
3. Иметь полную карту зависимостей.
4. Убрать все «неявности» в структуре приложения.
5. Сделать код который возможно покрыть тестами на 100%

После нескольких дней раздумий о том, каким я хочу видеть DI manager, я написал его буквально за один вечер. Потом, на выходных, написал небольшое приложение (WYSIWYG template editor), чтобы посмотреть на узкие места в этом подходе создания приложений. В итоге я пришел к небольшому менеджеру, предоставляющему доступ ко всем компонентам приложения, а так-же способному собирать компоненты по JSON конфигу.

Прошу внимания. Сразу прудпреждаю — что это не классический Dependency Injection паттерн, а очень адаптированный под JS среду и под мои нужды, поэтому не нужно меня пинками отправлять читать спецификацию. Критике буду очень рад.

Примеры использования


Случай 1

Класс GreeterClass, который приветствует пользователя, метод и текст приветствия задается инъекцией:
var GreeterClass = function(){
    this.say = function(){
        var method = this._getGreetMethod(); 
        var greet = this._getTextMsg();
        method(greet);
    };
};
SERVICES['constructor']['greet-class'] = GreeterClass; //записываем класс в пул сервисов доступных DI

Описываем зависимости класса:
SERVICES['dependency']['greet-class'] = {
    'greetMethod' : {'object' : 'alert'},
    'textMsg' : {'value' : 'Hello world'}
};

Запрашиваем instance GreeterClass класса и вызываем метод say:
DI.get('greet-class').say();

Результат:


UPD

Это статья не о коде, а о подходе к организации кода, но думаю, стоит объяснить что тут произошло. После вызова:
DI.get('greet-class').say();

В DI происходят такие процессы:
1. Ищется 'greet-class' в списке сервисов, после он инстанцируется.
2. Подгружаются зависимости.
3. Идет проверка – существуют ли методы в 'greet-class' c именем совпадающим с именами зависимостей.
4. Если таких методов не наблюдется – они создаются, с именем совпадающем с именем зависимости и своеобразной приставкой _get. Такой метод при вызове возвращает инъецированную зависимость.
5. Если такие методы существуют – они вызываются, а зависимость передается в качестве аргумента.

То есть методы ._getGreetMethod() и. _getTextMsg() икусственные, создаются динамически в DI менеджере.
Чтобы было яснее я сделал пример c предопределенным методом:
SERVICES['constructor']['stack'] = function(){
	var stack = [];
	
	this.flush = function(){
		console.log(stack);
	};
	
	this.push = function(el){
		/*** some actions ***/
		stack.push(el);
		return this;
	};
}

SERVICES['dependency']['stack'] = {
	'push' : [
		{'value' : 1},
		{'value' : 2},
		{'value' : 3}
	]
};
DI.get('stack').flush(); // [1,2,3]

Тут DI вызвал родной метод push для каждой зависимости.

Случай 2

Допустим перед нами встала задача изменить метод вывода:
SERVICES['dependency']['greet-class'] = {
    'greetMethod' : {'object' : 'console.log'},
    'textMsg' : {'value' : 'Hello world'}
};


Результат:


Я изменили реализацию не меняя абстракции, то чего и добивался.

Случай 3

Сейчас в greetMethod инъекцируется простой объект, но это так-же может быть другой сервис со своими зависимостями.
Так-же DI имеет несколько других обязанностей. Например, он может являться чем то вроде «мультиона»

Пример:
SERVICES['config']['greet-class'] = {
    'singleton' : true
}
DI.get('greet-class') === DI.get('greet-class'); // true


Случай 4

Подмена зависимостей находу:
DI.get('greet-class').say(); // Hello world
DI.get('greet-class', {'textMsg' : {'value' : 'Bye world'}}).say(); //Bye world


Случай 5

Возможность создавать «хаки» не вписывающиеся в концепция DI (иногда нужно);
SERVICES['dependency']['greet-class'] = {
    'greetMethod' : {'value' : function(txt){document.body.innerHTML = txt}},
    'textMsg' : {'value' : 'Hello world'}
};
DI.get('greet-class').say(); 


Результат:


Итог


А вот так выглядит мой DI config для тестового приложения:
/*пока не без хаков*/
DEPENDENCY['application'] = {
    'template-manager' : {
        'addWidgetModel' : [
            {
                'service' : 'widget-model',
                'dependency' : { 'domainObject' : {'instance' : function(){return WidgetDO(incomingWidget);}}} /*TODO: remove this hack*/
            },
            {
                'service' : 'widget-model',
                'dependency' : { 'domainObject' : {'instance' : function(){return WidgetDO(incomingWidget2);}}} /*TODO: remove this hack*/
            }
        ],
        'toolsManager' : {
            'service' : 'widget-manager',
            'dependency' :{
                'addRenderer' : {
                    'service' : 'text-tools-renderer',
                    'dependency' : {
                        'richView' : {
                            'service-constructor' : 'rich-view',
                            'dependency': {
                                'setEventManager' : {
                                    'service' : 'event-manager',
                                    'dependency' : {
                                        'setContext' : {'poll' : 'rich-view'}
                                    }
                                },
                                'template' : {'value' : 'code/template/tools.html'}
                            }
                        }
                    }
                },
                'addHandler' : {'instance' : 'TextToolsHandler'},
                'containerRenderer' : {
                    'service' : 'rich-view',
                    'dependency': {
                        'setEventManager' : {
                            'service' : 'event-manager',
                            'dependency' : {
                                'setContext' : {'poll' : 'rich-view'}
                            }
                        },
                        'template' : {'value' : 'code/template/tools-container.html'}
                    }
                }
            }
        },
        'editorManager' : {
            'service' : 'widget-manager',
            'dependency' :{
                'addRenderer' : {
                    'service' : 'text-editor-renderer',
                    'dependency' : {
                        'globalEventManager' : {'service' : 'global-event-manager'},
                        'richView' : {
                            'service-constructor' : 'rich-view',
                            'dependency': {
                                'setEventManager' : {
                                    'service' : 'event-manager',
                                    'dependency' : {
                                        'setContext' : {'poll' : 'rich-view'}
                                    }
                                },
                                'template' : {'value' : 'code/template/editor.html'}
                            }
                        }
                    }
                },
                'addHandler' : {'instance' : 'TextEditorHandler'},
                'containerRenderer' : {
                    'service' : 'rich-view',
                    'dependency': {
                        'setEventManager' : {
                            'service' : 'event-manager',
                            'dependency' : {
                                'setContext' : {'poll' : 'rich-view'}
                            }
                        },
                        'template' : {'value' : 'code/template/editor-container.html'}
                    }
                }
            }
        },
        'applicationRenderer' : {
            'service' : 'rich-view',
            'dependency': {
                'setEventManager' : {
                    'service' : 'event-manager',
                    'dependency' : {
                        'setContext' : {'poll' : 'rich-view'}
                    }
                },
                'template' : {'value' : 'code/template/application.html'}
            }}
    },
    'widget-manager' : {},
    'widget-model' : {
        'eventManager' : {
            'service' : 'event-manager',
            'dependency' : {
                'setContext' : {'poll' : 'widget-model'}
            }
        }
    },
    'global-event-manager' : {
        'context' : {'object' : 'window'}
    }
};
SERVICES['config'] = {
    'global-event-manager' : {
        'singleton' : true
    }
};

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

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

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

Код DI на GIThub Должен сказать что многие моменты «могут быть проще» но в данный момент я работаю над приложениями для Samsung SmartTV, поэтому он местами «адаптирован». Так же старался придерживаться KISS принципа. Естественно если DI себя оправдает я допишу два driver'a для считывания конфига с JSON и XML.

Демо приложение о котором писалось выше — писал непосредственно под webkit, в остальных браузерах не тестировал. Увы.

PS: уже пользуюсь данным подходом на работе, доволен как слон. Для полного счастья осталось только какой-то contract менеджер подключить.

*Случай 1 обновлен
Share post

Comments 20

    +2
    Бросайте уже пытаться сделать из яваскрипта яву или сишарп с их мощными IDE, compile-time проверками и статической типизацией.
    Если тут у вас возникает сильное желание использовать DI, то имхо проблема в ваших программистских скилах/неправильном подходе, а не в сложности или объёмности проекта.
    Интересно, кстати, утверждение про тесты с учётом того, что в проекте на гитхабе они напрочь отсутствуют.
    DI и вездесущие фабрики(об этом речь не велась, но близкая по духу вещь), распространённые в других языках, во многих случаях являются вынужденным овеэнженирингом, обусловленным либо изначально неправильной разработкой не в TDD/BDD стиле, с итоговой получающейся кривой и труднотестируемой архитектурой, либо негибкостью самого языка(статическая типизация). Даже крупные, сложные и сильно покрытые тестами проекты отлично могут разрабатываться без монстрообразного DI. В качестве примера… ну например Ruby on Rails.
    А в яваскрипте, где в основном мы обеспечивам рендеринг html, работу интерфейса и проброску событий туда-сюда, переключайте ваш или процедурный(лапша jQuery колбеков), или ООП подходы на MVC головного мозга с backbone или его анлогом в связке с jasmine/mocha.
      +4
      в яваскрипте, где в основном мы обеспечивам рендеринг html, работу интерфейса и проброску событий туда-сюда

      Если ваши задачи на Javascript не выходили за эти рамки, мне нечего вам возразить. 90% людей пришедших в Javascript — это бывшие Front-end Guy, действительно, для них эта статья будет бесполезна. Но люди которые занимаются профессионально программированием на JS, часто сталкиваются со сложными задачами, когда нужно написать большое приложение не имея возможности построить нормальную абстракцию inbox инструментами.
      Сейчас для меня web-браузер – это не основная, а одна из платформ для JS. А созданием GUI для обычных сайтов я вообще никогда не занимался.
      Интересно, кстати, утверждение про тесты с учётом того, что в проекте на гитхабе они напрочь отсутствуют.

      Я не утверждал, что моё приложение покрыто тестами. Эта демка, для людей которые после прочтения статьи задаются вопросом – «А что дальше, как это будет выглядеть на практике в рабочем приложении?». Увы, в этот раз я был сильно ограничен по времени для покрытия демо приложения тестами. Тем более у меня была в мыслях идея написать статью о TDD в JS.
        0
        Бросайте уже пытаться сделать из яваскрипта яву или сишарп

        Написано же:
        в данный момент я работаю над приложениями для Samsung SmartTV
        Я работал на IPTV 5 лет назад. Телевизионная приставка — это микрокомп (типа Raspberry PI только хуже — с 64 или 128 М памяти) на ARM с обрезанным Firefox с яваскриптуемым мультимедиа-компонентом (типа плугина), HTML- и JS-ресурсы либо зашиты локально либо грузятся с сервера. Не думаю, что SmartTV сильно отличается, разве что андроид вместо линукса. Так что на каком языке писать — выбора практически нет. И JS-фреймворки тоже не впихнуть — из-за ограничений по доступной памяти. Приходится извращаться и изобретать велосипеды.
        –3
        Если ваши задачи на Javascript не выходили за эти рамки, мне нечего вам возразить. 90% людей пришедших в Javascript — это бывшие Front-end Guy, действительно, для них эта статья будет бесполезна. Но люди которые занимаются профессионально программированием на JS, часто сталкиваются со сложными задачами, когда нужно написать большое приложение не имея возможности построить нормальную абстракцию inbox инструментами.
        Сейчас для меня web-браузер – это не основная, а одна из платформ для JS. А созданием GUI для обычных сайтов я вообще никогда не занимался.

        Угу, наслышан. Мы серьёзные разработчики, занимающиеся серьёзными вещами и пишущие только серьёзный код.
        Пожалуй, стоит тогда указывать в статье, что она предназначена для занимающихся «профессионально программированием на JS», для которых веб не основная платформа.
          +1
          Какое-то программирование на конфиг-файлах получается.
          Похоже ноги растут из
          2. Возможность модернизировать или изменять поведение приложения не меняя его кода.
          Тем что у вас код, например console.log записан в текстовой переменной он не перестает быть кодом.
            +3
            Тем что у вас код, например console.log записан в текстовой переменной он не перестает быть кодом.

            Извините, я говорил об Open/closed principle. А Вы о чем?
            Какое-то программирование на конфиг-файлах получается.

            А TDD это вообще программирование тестами. Но многим нравится.

            Извините, за оффтоп. Мне всегда не нравилось что на хабре мало статей о такой замечательной, и быстроразвивающийся, технологии как Javascript. Я говорю о статьях с приемами программирования, или организации кода (как эта), а не статьи вроде — «Вышла новая либа нa JS» или «Пишем слайдер картинок вместе». И вот моя первая статья на хабре, но меня никто не понял.
            Я понимаю, что это очень узкое направление в JS, но оно имеет право жить. Может кому-то будет интересно, а может даже и полезно.
              0
              Извините, я говорил об Open/closed principle. А Вы о чем?

              Не пойму как связан код в string переменных и open/close principle? Или наследование на прототипах ему не удовлетворяет. Ну даже если отбросить console.log и те строки которые у вас помечены как TODO remove, у вас все равно логика реализована в конфиге.
              И это в то время как космические корабли бороздят везде отделяют мух от котлет путем MVC, MVVM.

              А TDD это вообще программирование тестами. Но многим нравится.

              Не путайте методологию разработки с формированием архитектуры. Эта аналогия уровня «многим нравится программировать на клавиатуре mitsumi» — связи с DI столько-же.

              Я понимаю, что это очень узкое направление в JS, но оно имеет право жить. Может кому-то будет интересно, а может даже и полезно.

              Так напишите что сподвигло вас на это. DI ради DI никому не нужно.
                +2
                Не пойму как связан код в string переменных

                Относитесь к этому как к имени сущности. Вы, наверное, заметили eval в коде, я его использую только потому, что сейчас пишу под SmartTV и планирую перенести на Node.js. Где разные объекты отвечают за глобальную область видимости.
                В изначальном варианте eval’a нет, я извлекал непосредственно сущность с глобальной области видимости в JS так:
                var SomeGlobalClass = function(){
                    this.say = function(){
                        console.log('I\'m here');
                    };
                };
                var str = 'SomeGlobalClass';
                new window[str]().say(); // I'm here
                

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

                Можете указать конкретный момент? Вы, кажется, не поняли сути DI.
                Понимаете, что когда вы пишете подобный код:
                var Menu = function(){
                	var ajax, domBouilder;
                    var constructor = function(){
                		ajax = new AJAXclass();
                		domBouilder = new SomeDOMboilder();
                	};
                	...
                };
                new Menu();
                

                То это уже не правильный код, вы создали класс, в котором зависимости прописаны хардкодом. И это в любом языке будет не правильно. Даже в теории программирования желательно придерживаться чистых функций, об этом даже где-то тут писали. И да, этот момент можно решить фабриками для всех типов сущностей. Но, по-моему, это куда более тяжеловесный и трудоёмкий процесс и в принципе не полностью соответствует Open/closed principle.
                Это же решение полностью решает такую проблему, и довольно элегантно. Я не выношу логику за приложение, я выношу определение компонентов (в данном случае AJAXclass и SomeDOMboilder) используемых в «классе», за его пределы.
                Не путайте методологию разработки с формированием архитектуры. Эта аналогия уровня «многим нравится программировать на клавиатуре mitsumi» — связи с DI столько-же.

                Безусловно. Имелось в виду, что использование такого подхода сразу подразумевает использование как минимум половины SOLID принципов разработки. Я это упомянул в статье, такой подход заставляет писать правильно.

                Так напишите что сподвигло вас на это. DI ради DI никому не нужно.

                Из этого состоит «Вступление», спасибо что внимательно прочли.
              –2
              Видимо вы дальше прочитать не осилили.
                –1
                Говорите пожалуйста за себя. Я прочитал, и пытаюсь прояснить неясные для меня моменты.
                  –2
                  Тогда перечитайте ещё раз, так как дальше того пункта у вас явно не пошло.
              0
              Мне статья понравилась. Интересно, оригинально и на мой взгляд — имеет право на существование. Гораздо интереснее и познавательнее топиков «новостей» или кривых переводов. Автору спасибо.
                0
                Спасибо!
                Но проблема в том что это не оригинальный подход, а уже довольно обыденный, и успешно используемый в других языках, например на такую реализацию меня частично подтолкнул DI в ZendFramework 2.0. Я же просто адаптировал и применил этот подход в Javascript, и для моих целей он себя оправдал.
                А меня бьют в карму за то что показал какие есть подходы в разработке ПО, в том числе для JS. Так будто это я придумал этот паттерн.
                0
                Под оригинальным я имел ввиду реализацию DI в javascript. Сам я работаю с Symfony 2, где активно используется DI. Мне очень нравится такой подход.
                  0
                  хотите DI в JS? пожалуйста:
                  функцию приводите к строке через toString, разбираете объявление функции и делаете инъекции, всё просто
                  подсмотренно у angular.js
                    0
                    JS не предоставляет инструментов для полноценного следования этому паттерну (элементарное отсутствие тех же рефлекшенов)


                    for(var i in {f:1, g:'r', m:0, h:null, l:undefined})
                    {
                        alert(i);
                    }


                    Чем не reflection? Есть интроспекция, даже проще чем в Java.
                      +1
                      Рефлекшен, это по сути это возможность читать и обрабатывать не объекты (как в вашем случае), а их конструкторы (классы). И таким способом вы не прочтете локальный скоуп объекта и сигнатуру методов.

                      Например, не инстанцируя объект от этого конструктора вы не прочтете его публичный метод, а до локальных вы вообще никак не достучитесь.
                      var Class = function(){
                      	var privateMethod1 = function(a, b){
                      		return a + b;
                      	};
                      	var privateMethod2 = function(c, d){
                      		return c + d;
                      	};
                      	this.publicMethod1 = function(e, f){
                      		return e + f;
                      	};
                      }
                      

                      tzlom выше написал вполне неплохое решение для рефлекшенов в JS.
                      0
                      Спасибо. Интересная статья. Наверняка в ближайшее время появятся не мало подобных решений (кроме уже упомянутого angular.js). Все таки js слишком гибок для больших приложений, и его нужно как-то дробить на независимые модули.

                      По поводу подхода насколько я знаю Java-программисты как раз наоборот стараются уйти от xml-конфигурирования проекта — т.к. как минимум:

                      * IDE плохо поддерживает такие нотации;
                      * при рефакторингах приходиться менять в 2х местах (код + конфигурация);

                      Очень выручили бы аннотация, но в js их нет :(
                        0
                        Java-программисты как раз наоборот стараются уйти от xml-конфигурирования проекта

                        А в какую сторону они уходят? Я, по большей части, lamp разработчик, так что немного далек от того что происходит в Java.
                        * IDE плохо поддерживает такие нотации;

                        Если конфиг находится в xml формате, тут не поспоришь.
                        * при рефакторингах приходиться менять в 2х местах (код + конфигурация);

                        По-моему это надуманная проблема. Вроде — «Мы не будем писать тесты т.к. уходит много времени на поддержку их в актуальном состоянии» :)
                        Тем более, в конфиге прописываешь только внешние компоненты используемые классом, а не какую-либо логику. А если уж рефакторинг зашел так далеко, что класс поменял компоненты которые он использует, нужно готовится к тому что переписывать нужно будет многое.
                        [blah-blah mode]
                        Но в итоге, кода писать нужно будет меньше. Например часто вместо того чтобы создавать отдельный подкласс, можно просто запросить существующий класс, только другой сборки. Например, у нас была проблема, в приложении было два контроллера для старого и нового API, так получилось, что в новое API не успели перенести все методы из старого, и для определенного списка команд мы использовали старое API. Причем у одного API ответ был в XML, у другого в JSON, да и их структура была сильно отличной. Нам пришлось написать производный DAO, который парсил JSON и новый формат данных. И в разных модулях пришлось использовать DAO в зависимости от используемого API. Когда же всё перевели в новый API нам пришлось облазить весь код в поисках мест где использовался старый DAO и заменить его на новый.
                        Используя DI подход, единственное что мы бы меняли в коде это — написали бы новый парсер. А в DI сделали бы основную сборку DAO, и ее алиас который бы использовал другой парсер. И по ходу обновления api мы бы меняли только конфиг зависимостей, не трогая код.
                        А один раз встала необходимость вывести один виджет на всех поддерживаемых языках, вроде простая задача, и пришлось переписывать существующие компоненты. «Случай 4
                        » примерно об этом.
                        [/blah-blah mode]
                        Очень выручили бы аннотация, но в js их нет :(

                        Ну, это уже другая песня и на мотив метапрограммирования. Использовать аннотации для внедрения зависимостей, по-моему, это как микроскопом забивать гвозди :). А вообще, правда, их в JS не хватает. Я даже придумал механизм для создания, своего рода, «хуков» в js объектах, даже собираюсь написать о нем в новой статье. Но она у меня уже вышла больше этой, так что опубликую ее наверное в октябре, к тому же хочу взять за привычку написание демо приложений, чтобы показать подход на практике, как в этот раз.
                          0
                          спасибо за статью.
                          даже собираюсь написать о нем в новой статье.

                          заинтриговали, жду, даже подписался на вас

                      Only users with full accounts can post comments. Log in, please.