前端也要懂的IOC

餓了麼新餐飲前端團隊發表於2019-11-22

IOC(inversion of control) 是什麼?

望文生義即“反向控制”,它是一種設計思想,大致意思就是把物件控制的所有權交給別人(容器)

有了反轉,那麼什麼是正轉呢?

看以下程式碼,即是自身應用程式主動去獲取依賴物件,並且自己建立物件


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

class C {
  constructor() {
    this.a = new A();
    this.b = new B(this.a);
  }
}複製程式碼

那為什麼要使用IOC呢?

我們看上面的程式碼發現A被B和C依賴,這種依賴關係隨這著應用的增大,越來越複雜,耦合度也越來越高。所以有人提出了IOC理念,解決物件間的解耦。

IOC是如何解決耦合嚴重的問題的呢

提供了一個container容器來管理,它是依賴注入設計模式的體現,以下程式碼就使得C和A、B沒有的強耦合關係,直接通過container容器來管控

// 使用 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 {
    A:B
  constructor() {
    this.a = container.get('a');
    this.b = container.get('b');
  }
}複製程式碼


那麼IOC容器裡主要做了哪些事?

  • 類的例項化
  • 查詢物件的依賴關係

以下是實現IOC容器的最簡虛擬碼:

class Container {
    //存放每個檔案暴露的類和類名
    classObjs = {}
    get(Module) {
        let obj = new Module()
        const properties = Object.getOwnPropertyNames(obj);
        for(const p of properties) {
            if(!obj[p]) {
                if(!this.classObjs[p]) {
                    obj[p] = this.get(this.classObjs[p])
                }
            }
        }
        return obj
    }
}複製程式碼

但是業界實現的方式主要是通過裝飾器 decorator 和 reflect-metadata來實現的,接下來就聊聊這兩者是如何配合實現依賴注入(DI)的。注: DI是IOC的一種實現方式。

裝飾器

裝飾器是一種函式,是在程式碼編譯的時候對類的行為進行修改,比如:


function helloWord(target: any) {
  console.log('hello Word!');
}

@helloWord
class HelloWordClass {
}

//tsc編譯後
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;
};
function helloWord(target) {
    console.log('hello Word!');
}
let HelloWordClass = class HelloWordClass {
};
HelloWordClass = __decorate([
    helloWord
], HelloWordClass);複製程式碼

裝飾器主要有這幾種: 類裝飾器,方法、屬性裝飾器、引數裝飾器。當裝飾器執行的時候,函式會接收三個引數:target, key ,descriptor,  修飾不同的型別 target、key、descriptor 有所不同,詳細請看文件

Reflect-Metadata

Reflect Metadata 是 ES7 的一個提案, 它本質是一個WeakMap物件,資料結構如下:

WeakMap {
  target: Map {
    propertyKey: Map {
      metadataKey: metadataValue
    }
  }
}複製程式碼

所以 Reflect.defineMetadata(metadataKey, metadataValue, target[, propertyKey]) 簡化版實現如下:

const weakMap = new WeakMap()
const defineMetadata = (metadataKey, metadataValue, target, propertyKey) => {
  const metadataMap = new Map();
  metadataMap.set(metadataKey, metadataValue)
  const targetMap = new Map();
  targetMap.set(propertyKey, metadataMap)
  weakMap.set(target, targetMap)
}複製程式碼


裝飾器與Reflect-Metadata結合實現依賴注入

Reflect-Metadata一般結合著decorators一起用,為類和類屬性新增後設資料。

基於Typescript的依賴注入就是通過這兩者結合來實現的。


type Constructor<T = any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => {};

class OtherService {
  a = 1;
}

@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a);
  }
}

const Factory = <T>(target: Constructor<T>): T => {
  // 獲取所有注入的服務
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // 1複製程式碼


通過以下編譯後的程式碼發現,Typescriopt 通過__decorate將OtherService注入到了TestService類裡面,然後通過new target(...args)OtherService賦值到例項屬性上


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);
};
const Injectable = () => target => { };
class OtherService {
    constructor() {
        this.a = 1;
    }
}
let TestService = class TestService {
    constructor(otherService) {
        this.otherService = otherService;
    }
    testMethod() {
        console.log(this.otherService.a);
    }
};

TestService = __decorate([
    Injectable(),
    __metadata("design:paramtypes", [OtherService])
], TestService);

const Factory = (target) => {
    // 獲取所有注入的服務
    const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
    const args = providers.map((provider) => new provider());
    return new target(...args);
};
Factory(TestService).testMethod(); // 1複製程式碼

裝飾器與Reflect-Metadata結合實現@Controller/@Get

我們在後端的框架裡看到很多這種註解的寫法,其實也是這樣實現的

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {}
}複製程式碼

首先我們先利用自定義metaKey生成裝飾器

const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';
const Controller = (path: string): ClassDecorator => {
  return target => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}
const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');複製程式碼

然後在裝飾器裡通過Reflect.getMetadata獲取到剛剛存入(Reflect.defineMetadata)的後設資料,

最後在將這些後設資料重組生成一個map資料結構。

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);
  // 篩選出類的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
                              .filter(item => !isConstructor(item) && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];
    // 取出定義的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    }
  })
};複製程式碼

有了以上的方法,通過以下呼叫,再將生成的Routes繫結到koa上就ok了

Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'
mapRoute(new SomeClass());
/**
 * [{
 *    route: '/a',
 *    method: 'GET',
 *    fn: someGetMethod() { ... },
 *    methodName: 'someGetMethod'
 *  },{
 *    route: '/b',
 *    method: 'POST',
 *    fn: somePostMethod() { ... },
 *    methodName: 'somePostMethod'
 * }]
 *
 */複製程式碼

最後你會發現很多spring後端框架的一些思想,其實都慢慢的被運用到了前端,比如現在比較流行的web框架Midway

參考

深入理解typescript

解讀IOC框架invertisifyJS

Midway


相關文章