淺談IoC容器

1. 概述

咱們在開發node應用時,一般會遇到裝飾器來實現依賴注入,那麼用裝飾器實現依賴注入背後實現的原理是什麼?node

提到依賴注入就必須提到Ioc容器,提到Reflect Metadata,接下來咱們來詳細理解一下這些概念git

2. Ioc容器

控制反轉(Inversion of Control,縮寫爲IoC),是面向對象程序設計中一種核心的設計原則,通常狀況下,大多數應用程序中都會依賴其餘對象,原來的實現過程都是靠自身程序調用實現,高耦合度,調試和開發成本很高,好比,傳統的實現方式:github

// 常見的依賴
import {A} from './A';
import {B} from './B';

class C {
  consturctor() {
    this.a = new A();
    this.b = new B(this.a);
  }
}
複製代碼

爲了下降耦合度,社區提出一個方案,創建一個對象池來維護這些依賴,而且在應用初始化的時候自動處理相關依賴,並將類進行實例化,這個對象池咱們稱之爲Ioc容器,容器提供一些基本功能,好比註冊,刪除,獲取等基本操做,midwayjs團隊在社區方案的基礎上進行了封裝,推出了injection包,injection中相關源碼以下:express

export class ObjectDefinitionRegistry extends Map implements IObjectDefinitionRegistry {
  private singletonIds = [];

  get identifiers() {
    const ids = [];
    for (const key of this.keys()) {
      if (key.indexOf(PREFIX) === -1) {
        ids.push(key);
      }
    }
    return ids;
  }

  get count() {
    return this.size;
  }

  getSingletonDefinitionIds(): ObjectIdentifier[] {
    return this.singletonIds;
  }

  getDefinitionByName(name: string): IObjectDefinition[] {
    const definitions = [];
    for (const v of this.values()) {
      const definition = <IObjectDefinition> v;
      if (definition.name === name) {
        definitions.push(definition);
      }
    }
    return definitions;
  }

  registerDefinition(identifier: ObjectIdentifier, definition: IObjectDefinition) {
    if (definition.isSingletonScope()) {
      this.singletonIds.push(identifier);
    }
    this.set(identifier, definition);
  }

  getDefinition(identifier: ObjectIdentifier): IObjectDefinition {
    return this.get(identifier);
  }

  getDefinitionByPath(path: string): IObjectDefinition {
    for (const v of this.values()) {
      const definition = <IObjectDefinition> v;
      if (definition.path === path) {
        return definition;
      }
    }
    return null;
  }

  removeDefinition(identifier: ObjectIdentifier): void {
    this.delete(identifier);
  }

  hasDefinition(identifier: ObjectIdentifier): boolean {
    return this.has(identifier);
  }

  clearAll(): void {
    this.clear();
  }

  hasObject(identifier: ObjectIdentifier): boolean {
    return this.has(PREFIX + identifier);
  }

  registerObject(identifier: ObjectIdentifier, target: any) {
    this.set(PREFIX + identifier, target);
  }

  getObject(identifier: ObjectIdentifier): any {
    return this.get(PREFIX + identifier);
  }
}
複製代碼

而在對象中實際應用Ioc容器時,一般有兩種應用形式:依賴注入及依賴查找,npm

3. Ioc應用方式

實現控制反轉主要有兩種方式:依賴注入和依賴查找。二者的區別在於,前者是被動的接收對象,在類A的實例建立過程當中即建立了依賴的B對象,經過類型或名稱來判斷將不一樣的對象注入到不一樣的屬性中,然後者是主動索取相應類型的對象,得到依賴對象的時間也能夠在代碼中自由控制。json

  1. 依賴注入有如下方法:
  • 基於接口。實現特定接口以供外部容器注入所依賴類型的對象。markdown

  • 基於 set 方法。實現特定屬性的public set方法,來讓外部容器調用傳入所依賴類型的對象。app

  • 基於構造函數。實現特定參數的構造函數,在新建對象時傳入所依賴類型的對象。框架

  • 基於註解。基於Java的註解功能,在私有變量前加「@Autowired」等註解,不須要顯式的定義以上三種代碼,即可以讓外部容器傳入對應的對象。該方案至關於定義了public的set方法,可是由於沒有真正的set方法,從而不會爲了實現依賴注入致使暴露了不應暴露的接口(由於set方法只想讓容器訪問來注入而並不但願其餘依賴此類的對象訪問)。ide

    // 使用 IoC import {Container} from 'injection'; import {A} from './A'; import {B} from './B'; const container = new Container(); container.bind(A); container.bind(B);

    class C { consturctor() { this.a = container.get('A'); this.b = container.get('B'); } }

  1. 依賴查找的方法
  • 依賴查找更加主動,在須要的時候經過調用框架提供的方法來獲取對象,獲取時須要提供相關的配置文件路徑、key等信息來肯定獲取對象的狀態

4. 裝飾器的引入

若是每次代碼都手動寫入依賴,實在太繁瑣,並且不必,因此通常的框架語言,好比angular,midway等使用了裝飾器,裝飾器的使用離不開Reflect Metadata,Reflect Metadat是 ES7 的一個提案,它主要用來在聲明的時候添加和讀取元數據。

裝飾器是一種特殊類型的聲明,裝飾器自己是一個函數,它可以被附加到類聲明,方法,訪問符(getter, setter),屬性或參數上。裝飾器採用@expression這種形式進行使用。分爲類裝飾器,參數裝飾器,方法、屬性、訪問器的裝飾器。不一樣的裝飾器使用的場景和做用不一樣。

4.1 類裝飾器,聲明在類定義以前,接收的參數是類自己,用來監控,修改或者替換類定義,使用方法以下:

provide代碼地址

function provide(identifier?: ObjectIdentifier) {
  // 這裏的target默認是緊鄰着的DemoClass
  return function (target: any) {
    // .....省略操做
    return target;
  };
}

@provide()
class DemoClass {}
複製代碼

4.2 參數裝飾器,聲明在一個參數聲明以前。運行時當作函數被調用,這個函數接收下面三個參數:

  • target:靜態屬性是構造函數,實例屬性是指類的原型對象
  • key:調用的屬性(函數)名字
  • index:參數的序列號

使用方法以下:

function paramsDecorator(target: string, key: string, index: number) {
  // 這裏的target,DemoClass.propertype
  // key: getList
  // index: 0
}

class DemoClass {
    getList(@paramsDecorator pageId: string){// ...}
}
複製代碼

4.3 方法、屬性、訪問器裝飾器,聲明在一個方法、屬性、訪問器以前,運行時看成函數執行,接收的參數以下:

  • target:靜態屬性是構造函數,實例屬性是指類的原型對象
  • key: 方法、屬性、訪問器名字
  • description: 屬性描述

injection代碼地址

function inject(identifier?: ObjectIdentifier) {
  return function (target: any, targetKey: string, index?: number): void {

    if (typeof index === 'number') {
      if (!identifier) {
        const args = getParamNames(target);
        if (target.length === args.length && index < target.length) {
          identifier = args[index];
        }
      }
      const metadata = new Metadata(INJECT_TAG, identifier);
      tagParameter(target, targetKey, index, metadata);
    } else {
      if (!identifier) {
        identifier = targetKey;
      }
      const metadata = new Metadata(INJECT_TAG, identifier);
      tagProperty(target, targetKey, metadata);
    }
  };
}

class Parent {
  // target: parent.propertype
  // key: ctx
  // description: PropertyDescriptor類型
  @inject()
  ctx: FaaSContext;
}
複製代碼

5. Reflect MetaData

Reflect Metadata 是 ES7 的一個提案,它主要用來在聲明的時候添加和讀取元數據。TypeScript 在 1.5+ 的版本已經支持它,你只須要:

  • npm i reflect-metadata --save
  • tsconfig.json 裏配置 emitDecoratorMetadata 選項。

因爲reflect-meta庫封裝了一些API,能夠訪問類或者類屬性,因此在裝飾器使用時,能夠操做類或者類屬性添加或獲取元數據,midway中的一些裝飾器是基於Reflect Metadata實現的,以provide爲例

import 'reflect-metadata';
import {DUPLICATED_INJECTABLE_DECORATOR} from '../utils/errMsg';
import { initOrGetObjectDefProps, TAGGED_CLS } from '..';
import {ObjectIdentifier, TagClsMetadata} from '../interfaces';

const camelCase = require('camelcase');

function provide(identifier?: ObjectIdentifier) {
  return function (target: any) {
    // 檢測是否已有此元數據,即此類是否註冊到Ioc容器中,是否有重名
    if (Reflect.hasOwnMetadata(TAGGED_CLS, target)) {
      throw new Error(DUPLICATED_INJECTABLE_DECORATOR);
    }

    if (!identifier) {
      identifier = camelCase(target.name);
    }

    // 若是沒有註冊,則將此target
    Reflect.defineMetadata(TAGGED_CLS, {
      id: identifier,
      originName: target.name,
    } as TagClsMetadata, target);

    // init property here
    initOrGetObjectDefProps(target);

    return target;
  };
}
複製代碼

當調用provide裝飾器時,此類會被Ioc容器自動掃描,並在容器上綁定定義,對應的是container.bind,同理調用@inject時,將容器中的定義實例化成一個對象,而且綁定到屬性中,調用的時候就能夠訪問到該屬性,詳情可參考inject實現源碼

6. 依賴注入(DI)

有了上述的基礎知識,就不難理解依賴注入了,咱們來看一個例子以及編譯以後的代碼

import { provide, inject, FaaSContext } from '@ali/midway-faas';
import { InjectEntityModel } from '@midwayjs/orm';
import { Repository, DeepPartial } from 'typeorm';
import { CommonFieldStatus } from '../common/enum';
import { ERROR, throwError } from '../common/error';
import { Node } from '../model/node';
import { Operation, TargetType, OperationActionType } from '../model/operation';
import { StructureService } from '../service/structureService';
import { OperationService } from '../service/operationService';

@provide()
export class NodeService {
  @inject()
  ctx: FaaSContext;

  @inject()
  operationService: OperationService;

  @inject()
  structureService: StructureService;

  @InjectEntityModel(Node)
  repository: Repository<Node>;
    // .....
}
複製代碼

編譯以後:

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeService = void 0;
const midway_faas_1 = require("@ali/midway-faas");
const orm_1 = require("@midwayjs/orm");
const typeorm_1 = require("typeorm");
const enum_1 = require("../common/enum");
const error_1 = require("../common/error");
const node_1 = require("../model/node");
const operation_1 = require("../model/operation");
const structureService_1 = require("./structureService");
const operationService_1 = require("./operationService");
let NodeService = class NodeService {
    // 邏輯代碼
};
__decorate([
    midway_faas_1.inject(),
    __metadata("design:type", Object)
], NodeService.prototype, "ctx", void 0);
__decorate([
    midway_faas_1.inject(),
    __metadata("design:type", operationService_1.OperationService)
], NodeService.prototype, "operationService", void 0);
__decorate([
    midway_faas_1.inject(),
    __metadata("design:type", structureService_1.StructureService)
], NodeService.prototype, "structureService", void 0);
__decorate([
    orm_1.InjectEntityModel(node_1.Node),
    __metadata("design:type", typeorm_1.Repository)
], NodeService.prototype, "repository", void 0);
NodeService = __decorate([
    midway_faas_1.provide()
], NodeService);
exports.NodeService = NodeService;
//# sourceMappingURL=nodeService.js.map
複製代碼

從編譯後的代碼能夠看出,Ioc容器將依賴(operationService等)經過元數據存儲了起來。因此在依賴注入的時候,咱們就能夠經過Reflect.getMetadata('design:paramtypes', target)取出了這個參數,並將其實例化後賦值到this.operationService中,這樣一個依賴注入就完成了。

midway框架會對編譯後的語言進行解析處理,最終獲得node引擎可以執行的代碼

相關文章
相關標籤/搜索