Pull to refresh

Auto dependency injection в Javascript

Reading time 5 min
Views 10K

Вступление


Как все мы знаем 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 обновлен
Tags:
Hubs:
+2
Comments 20
Comments Comments 20

Articles