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
參考