Углубляемся во внедрение зависимостей в AngularJS

Углубленно рассматриваем систему внедрения зависимостей в AngularJS

Введение

В AngularJS существует один недооцененный, но очень важный компонент. Именно он отвечает за целостность и скорость работы фреймворка. Внедрение зависимостей лежит за всеми способностями AngularJS.

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

Так что же такое внедрение зависимостей?

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

Почти каждая строчка нашего кода, независимо от того, на каком языке мы пишем, наверняка, зависит от какого-то другого кода. Именно такие библиотеки и называются зависимостями (так как наш код зависит от них).

К примеру, нам не придёт в голову писать свой метод printf в с. Это уже сделано до нас в библиотеке libc, поэтому мы и указываем библиотеку libc в качестве зависимости. Во время работы приложения, ему необходимо знать где искать зависимости. Существует несколько методов указать их расположение:

  • указать на глобальном уровне где искать зависимости
  • передать сами библиотеки во время исполнения

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

При использовании, например, jQuery библиотеки, браузер всегда держит в памяти ссылку на объект jQuery, доступ к которому, как правило, происходит через символ $. Хотя при таком подходе обращение к библиотеке не явное, браузер делает всю работу по поиску за нас.

Например, поиск службы в JavaScript вполне может выглядеть следующим образом:

var ServiceLocator = function() {
 this.services = {};
 this.singletons = {};

 this.registerService = function(name, serviceConstructor) {
   this.services[name] = serviceConstructor;
 };

 this.getService = function(name) {
   if (this.services[name]) {
     if (!this.singletons[name]) 
       this.singletons[name] = this.services[name]();
     return this.singletons[name];
   }
 };
};

Чтобы начать использовать ServiceLocator, нашему приложению необходимо дать знать о его существовании. Мы можем обратиться к ServiceLocator через глобальную область видимости (как в случае с jQuery) и код вполне отработает.

// Теоретически, это может выглядеть как:
app.service(function() {
 var sl = new ServiceLocator();
 var http = sl.getService('$http');
});

Но такой подход может вызвать ряд проблем, особенно при тестировании. Более того, таким образом мы создаем жесткую связку между самой службой и ServiceLocator, а также нам необходимо использовать глобальную область видимости.

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

В AngularJS внедрение зависимостей выглядит следующим образом:

angular.module('myApp', [])
.service(function($http) {
 this.getName = function() {
   // Используйте тут $http 
  }
});

При инициализации нашей службы AngularJS автоматически передает объект $http. На нас, как на разработчиков, падает ответственность на создание службы таким образом, чтобы $injector AngularJS смог подключить эту службу (на $injector мы еще посмотрим немного позднее).

Использование зависимостей дает нам определенную свободу. В зависимости от контекста мы можем подключать необходимые компоненты.

Например, служба $http уже встроена в AngularJS. По сути это абстракция XMLRequestObject, которая работает при помощи другой службы AngularJS - $httpBackend.

При запуске приложения в браузере, AngularJS подключает эти абстракции, которые в свою очередь создают сам XMLRequestObject. В тоже время при тестировании, AngularJS создает $httpBackend, который не производит настоящих запросов.

Очередной плюс использования зависимостей - AngularJS внедряет необходимый объект по необходимости. Например, когда $injector создает контроллер, он передает уникальный объект $scope дочернему контроллеру. Что не получится при первоначальном подходе. Сама схема работает только при помощи внедрения зависимостей.

$injector

Вся система внедрения зависимостей в AngularJS базируется на службе $injector. Она отвечает за создание объектов, типов, методов и загрузку модулей.

При создании объекта происходит следующее:

  • $injector просматривает аннотации и находит все необходимые зависимости
  • создает сам объект и передает ему все необходимые зависимости

При создании, следующая служба автоматически получит службы $http, $q и $rootScope:

angular.module('myApp', [])
.service('GithubService', function($http, $q, $rootScope) {
 // $injector находит объекты $http, $q, and $rootScope
  // перед тем как GithubService будет инициализирован
  // и вставляет их автогматически.
});

Как создавать аннотации

Так каким же образом AngularJS распознает нужные зависимости?

Службе $injector необходимо сообщить, что подключать. Так сложилось, что зависимости указываются при помощи аннотаций. Во время компиляции, $injector находит требуемые зависимости и то как их подключать во время работы приложения.

В EcmaScript 5|JavaScript не существует официального способа использования аннотаций. Хотя AngularJS использует свой подход к этому вопросу.

В JavaScript мы можем обратиться к методу toString() любой функции и получим строковое представление аргументов функции. Эту строку мы можем разбить на аргументы (по именам) и использовать их в качестве аннотаций.

Например, взяв конструктор GithubService, мы сможем выделить аннотации следующим образом:

// angular.module('myApp', [])
// .service('GithubService', function($http, $q, $rootScope) {});
var f = function($http, $q, $rootScope) {}
var annotations = [];
f.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
.split(',').forEach(function(match) {
 annotations.push(match.replace(/\s+/, ''));
});
// аннотации теперь содержат аргументы аннотаций
// для функции конструктора GithubService
// ["$http", "$q", "$rootScope"]

Обратите внимание на то, что порядок аргументов не имеет значения, так как AngularJS просто переводит их в строковое представление.

Следующие два примера - полные функциональные аналоги:

angular.module('myApp', [])
.service('GithubService', function($http, $q, $rootScope) {})
.service('GithubService', function($rootScope, $http, $q) {});

Минификация

Опытный читатель, наверняка заметит, что могут возникнуть проблемы с определением аннотаций при минификации кода.

При минификации удаляется всё лишнее из кода, чтобы уменьшить размер файла передаваемого по сети. То есть мы лишаемся всех пробелов и комментариев. Длинные имена переменных будут изменены на короткие. Например, минифицированная версия конструктора GithubService будет выглядеть следующим образом:

angular.module("myApp",[]).service("GithubService",function(e,t,n){});

$injector попытается найти зависимости по именам e, t, и n. Что конечно же вызовет ошибку.

Существует и другой способ указания аннотаций. Вместо создания конструктора, AngularJS позволяет нам указать аннотации в качестве массива, последний элемент которого - функция конструктора. Так как при минификации строковые данные не изменяются, то зависимости следует указывать именно в виде строк.

Вернемся к GithubService, укажем зависимости по новому:

angular.module('myApp', [])
.service('GithubService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) {
}]);

После минификации получим:

angular.module("myApp",[]).service("GithubService",["$http","$q","$rootScope",function(e,t,n){}])

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

Перспективы

На данный момент команда AngularJS активно работает над Angular 2.0, который строится в соответствии с EcmaScript 6 Harmoni API. Так как EcmaScript 6 пока довольно слабо распространен, описания процесса внедрения зависимостей в Angular 2.0 пока не существует.

Обратите внимание на то, что JavaScript 2.0/ES6 в процессе изменения, поэтому синтаксис может видоизменится ко времени релиза ES6.

Так как Angular 2.0 основан на ES6 мы получим доступ к настоящим классам. Например, допустим у нас есть три класса: Electricity, Switch и LightBulb. Класс Switch зависит от LighBulb и Electricity. При включении лампы, нам понадобится удостовериться, что электричество доступно. Наши классы можно реализовать следующим способом:

// ES6
class Electricity 
{
    constructor(state) 
    {
        this.on = state;
    }

    isOn() 
    {
        return this.on;
    }
}

class LightBulb 
{
    constructor() {}
    turnOn() {}
    turnOff() {}
}

class Switch 
{
    constructor(electricity, lightbulb) {
        this.electricity = electricity;
        this.lightbulb = lightbulb;
    }

    flip() {
        if (this.electricity.isOn()) {
            this.lightbulb.turnOn();
        }
    }
}

За счет использования ES6 мы можем напрямую импортировать необходимые модули из классов. Этот функционал имеет большое значение, мы больше не используем общее пространство имен. Вместо этого мы сразу импортируем необходимые файлы.

import {Electricity} from './electricity'; 

Обратите внимание на то, что функция import не заменяет внедрение зависимостей, она только дает нам доступ к методам импортируемого объекта. Но нам нужен полноценный способ использования аннотаций для указания зависимостей на уровне объекта.

Хотя следующий способ не реализован в ES6, сообщество предлагает такой подход:

import {Electricity} from './electricity';
import {LightBulb} from './lightbulb';

@Inject(Electricity, LightBulb)
export class Switch 
{
    constructor(electricity, lightbulb) 
    {
        this.electricity = electricity;
        this.lightbulb = lightbulb;
    }
    // ...
}

Теперь, при загрузки приложения нам не придется жестко связывать весь функционал, мы просто используем injector как обычно:

import {Injector} from 'di/injector';
import {Switch} from './switch';

function main() 
{
    var injector = new Injector();
    // Загрузка всех зависимостей
    var switch = injector.get([Switch]);

    switch.flip();
}

Заметьте в методе main() мы указываем только те зависимости, которые планируем использовать. В данном примере нам нужен только класс Switch. В случае приложения Angular, этот класс выступит в роли главного модуля.

Angular автоматически обрабатывает метод main() и у нас нет необходимости лишний раз в нем что-то изменять. Angular соберёт все элементы приложения при помощи этого метода, а $injector позаботится о загрузке требуемых классов, так же как и в предыдущей версии Angular.

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

Наконец, мы сможем использовать любой модуль ES6 в качестве зависимости. У вас есть любимый модуль в ES6? Теперь его можно просто загрузить как любой компонент Angular.

На этом закончим рассмотрение внедрения зависимостей. Надеюсь, статья вам понравилась. В книге ng-book вы найдете еще больше информации на тему AngularJS, а так же не забудьте подписаться на нашу новостную рассылку.

Комментарии

0
Войти
Комментариев нет.
Войдите чтобы оставлять комментарии.