Auto dependency injection manager

в 16:08, , рубрики: 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();

Результат:
Auto dependency injection manager

Случай 2

Допустим перед нами встала задача изменить метод вывода:

SERVICES['dependency']['greet-class'] = {
    'greetMethod' : {'object' : 'console.log'},
    'textMsg' : {'value' : 'Hello world'}
};

Результат:
Auto dependency injection manager

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

Случай 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(); 

Результат:
Auto dependency injection manager

Итог

А вот так выглядит мой DI config для тестового приложения:
/*пока не без хаков*/

SERVICES['dependency'] = {
    'template-manager' : {
        'addWidgetModel' : [
            {
                'service' : 'widget-model',
                'dependency' : { 'domainObject' : {'object' : 'WidgetDO(incomingWidget)'}} /*TODO: remove this hack*/
            },
            {
                'service' : 'widget-model',
                'dependency' : { 'domainObject' : {'object' : 'WidgetDO(incomingWidget2)'} } /*TODO: remove this hack*/
            }
        ],
        'toolsManager' : {
            'service' : 'widget-manager',
            'dependency' :{
                'addRenderer' : {'instance' : 'TextToolsRenderer'},
                'addHandler' : {'instance' : 'TextToolsHandler'},
                'containerRenderer' : {'instance' : 'ToolsContainerRenderer'}
            }
        },
        'editorManager' : {
            'service' : 'widget-manager',
            'dependency' :{
                'addRenderer' : {'instance' : 'TextEditorRenderer'},
                'addHandler' : {'instance' : 'TextEditorHandler'},
                'containerRenderer' : {'instance' : 'EditorContainerRenderer'}
            }
        },
        'applicationRenderer' : {'instance' : 'ApplicationContainerRenderer'}
    },
    'widget-manager' : {},
    'widget-model' : {
        'eventManager' : {
            'service' : 'event-manager',
            'dependency' : {
                'context' : {'poll' : 'widget-model'},
                'registerEvents' : {'value' : [
                    'widget.tools.input',
                    'widget.editor.overflow',
                    'widget.tools.text-style',
                    'widget.tools.align-y',
                    'widget.tools.align-x',
                    'widget.tools.input.blur',
                    'widget.model.set',
                    'widget.editor.start-editing',
                    'widget.editor.input',
                    'widget.editor.blur',
                    'widget.tools.input.focus'
                ]}
            }
        }
    },
    'global-event-manager' : {
        'context' : {'object' : 'window'},
        'registerEvent' : {'value' : 'application.start'}
    }
};
SERVICES['config'] = {
    'global-event-manager' : {
        'singleton' : true
    }
};

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

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

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

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

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

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

Автор: idoroshenko


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js