Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2013-01-01) 13. 依赖注入

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

依赖注入

依赖注入(Dependency Injection)是一种软件设计模式,用来处理代码如何获取它的依赖。

如果想对依赖注入进入更深入的讨论,参看Wikipedia上的文章,以及由Martin Fowler写的Inversion of Controller,或者自行找书。

DI起步

一个对象或函数,只有这三种方式可取得依赖:

  1. 使用new操作符创建一个依赖
  2. 使用一个全局变量来寻找一个依赖
  3. 在需要的时候,由外部传入一个依赖

前两种方式都不是最佳的,因为它们都需要在代码中“硬编码”依赖,以致于难以甚至无法修改依赖。在测试时这会是一个大问题,因为测试时通常都会使用一些mock来代替原有的依赖,以方便测试。

第三种方式最可行,因为它把“定位依赖”的责任从组件中移除了。依赖可以简单的传递给组件。

function SomeClass(greeter) {
  this.greeter = greeter
}

SomeClass.prototype.doSomething = function(name) {
  this.greeter.greet(name);
}

在上面的例子中,SomeClass不用关心它的依赖greeter从何而来,直接用就行了。greeter会在运行时直接传入。

这么做虽然可行,但它却把取得依赖的责任踢给了SomeClass的构造函数。

为了管理依赖的创建,每个angular程序都有一个injector。该injector是一个定位服务,专门用于创建和寻找依赖。

下面是使用injector的例子:

// Provide the wiring information in a module
angular.module('myModule', []).

  // Teach the injector how to build a 'greeter'
  // Notice that greeter itself is dependent on '$window'
  factory('greeter', function($window) {
    // This is a factory function, and is responsible for 
    // creating the 'greet' service.
    return {
      greet: function(text) {
        $window.alert(text);
      }
    };
  }).

// New injector is created from the module. 
// (This is usually done automatically by angular bootstrap)
var injector = angular.injector('myModule');

// Request any dependency from the injector
var greeter = injector.get('greeter');

虽然用“请求依赖”解决了“硬编码”的问题,但它也意味着有一个injector需要传到程序中。传入injector破坏了迪米特法则,即“一个软件实体应当与尽可能少的其他实体发生相互作用”。为了解决这个问题,我们把寻找依赖的责任通过声明依赖的方式,转移给injector。

<!-- Given this HTML -->
<div ng-controller="MyController">
  <button ng-click="sayHello()">Hello</button>
</div>
// And this controller definition
function MyController($scope, greeter) {
  $scope.sayHello = function() {
    greeter('Hello World');
  };
}

// The 'ng-controller' directive does this behind the scenes
injector.instantiate(MyController);

注意,当我们使用ng-controller这个directive来实例化这个类时,它会自动向MyController传入它需要的依赖,而controller根本不需要知道injector的存在。这是最好的结果。程序代码不需要与injector交互,就可以请求它所需要的依赖。这也没有违反迪米特法则

依赖注解

injector如何知道哪个service应该被注入进去呢?

程序开发者需要提供注解信息,让injector用来寻找依赖。根据Angular的API文档,Angular的API函数都是由injector调用的。Injector需要知道哪些services应该注入到函数中。下面是将service名称注解到代码中的三种等价方式。你可以根据需要使用:

推断依赖

最简单的方式,就是直接用函数参数名来表示依赖名。

function MyController($scope, greeter) {
    ...
}

Injector可以检查函数定义,找到参数名,并以此去寻找依赖。在上例中,$scopegreeter就是需要注入的两个依赖。

然而,如果你使用一些工具,对JavaScript代码进行了压缩或混淆处理,则这种方式就行不通了,因为参数名会被修改。所以这种方式常用于原型或演示程序。

$inject注解

为了配合压缩工具,可以使用$inject属性。它是一个由service名称组成的数组,用于寻找依赖。如果设置了它,则参数名将不再用来寻找依赖。

var MyController = function(renamed$scope, renamedGreeter) { ... } MyController.$inject = ['$scope', 'greeter']; ```

需要注意的是,$inject中的数据要跟Controller函数中的参数保持一致。

对于controller的声明,这种方法比较有用,因为它把注解信息赋给了函数。

行内注解

使用$inject向directives添加注解信息不太方便。

比如这个例子:

someModule.factory('greeter', function($window) { ...; }); 

如果要使用$inject,需要用到临时变量:

var greeterFactory = function(renamed$window) { ...; }; greeterFactory.$inject = ['$window']; someModule.factory('greeter', greeterFactory); 

所以Angular提供了第三种注解方式:

someModule.factory('greeter', ['$window', function(renamed$window) { ...; }]);

注意上面三种方式是完全等价的,在Angular中任何地方都可以使用它们中任意一种。

我应该在哪儿使用DI?

DI在Angular中使用得非常普遍。最常用在controller和factory方法中。

controllers中的DI

Controllers是用来描述程序行为的类。推荐使用下面的方式来声明controller:

var MyController = function(dep1, dep2) {
  ...
}
MyController.$inject = ['dep1', 'dep2'];

MyController.prototype.aMethod = function() {
  ...
}

工厂方法

工厂方法用来创建Angular中大多数的对象。例如directives, services,和filters。工厂方法注册到模块上,推荐的声明方式为:

angualar.module('myModule', []).
  config(['depProvider', function(depProvider){
    ...
  }]).
  factory('serviceId', ['depService', function(depService) {
    ...
  }]).
  directive('directiveName', ['depService', function(depService) {
    ...
  }]).
  filter('filterName', ['depService', function(depService) {
    ...
  }]).
  run(['depService', function(depService) {
    ...
  }]
);
comments powered by Disqus