【TS】736- 用 TS 裝飾器實(shí)現(xiàn)一個(gè)簡(jiǎn)單依賴注入
導(dǎo)語(yǔ):在我們的代碼中,依賴就是兩個(gè)模塊間的一種關(guān)聯(lián)(如兩個(gè)類)——往往是其中一個(gè)模塊使用另外一個(gè)模塊去做些事情。使用依賴注入降低模塊之間的耦合度,使代碼更簡(jiǎn)潔
作者:charryhuang
原文地址:https://mp.weixin.qq.com/s/waBAJgYaOJQu2qdcbgmi1A
什么是依賴(Dependency)
????????有兩個(gè)元素A、B,如果元素A的變化會(huì)引起元素B的變化,則稱元素B依賴(Dependency)于元素A。在類中,依賴關(guān)系有多種表現(xiàn)形式,如:一個(gè)類向另一個(gè)類發(fā)消息;一個(gè)類是另一個(gè)類的成員;一個(gè)類是另一個(gè)類的某個(gè)操作參數(shù),等等。
為什么要依賴注入(DI)?
????????我們先定義四個(gè)Class,車,車身,底盤,輪胎。然后初始化這輛車,最后跑這輛車。
假設(shè)我們需要改動(dòng)一下輪胎(Tire)類,把它的尺寸變成動(dòng)態(tài)的,而不是一直都是30。

由于我們修改了輪胎的定義,為了讓整個(gè)程序正常運(yùn)行,我們需要做以下改動(dòng):
由此我們可以看到,僅僅是為了修改輪胎的構(gòu)造函數(shù),這種設(shè)計(jì)卻需要修改整個(gè)上層所有類的構(gòu)造函數(shù)!在軟件工程中,這樣的設(shè)計(jì)幾乎是不可維護(hù)的。
所以我們需要進(jìn)行?控制反轉(zhuǎn)(IoC),即上層控制下層,而不是下層控制著上層。我們用?依賴注入(Dependency Injection)?這種方式來(lái)實(shí)現(xiàn)控制反轉(zhuǎn)。所謂依賴注入,就是把底層類作為參數(shù)傳入上層類,實(shí)現(xiàn)上層類對(duì)下層類的“控制”。
這里我們用構(gòu)造方法傳遞的依賴注入方式重新寫車類的定義:
????這里我只需要修改輪胎類就行了,不用修改其他任何上層類。這顯然是更容易維護(hù)的代碼。不僅如此,在實(shí)際的工程中,這種設(shè)計(jì)模式還有利于不同組的協(xié)同合作和單元測(cè)試。
2
1.安裝?typescript?環(huán)境以及重要的 polyfill?reflect-metadata。
2.在?tsconfig.json?中配置?compilerOptions
{
? ?"experimentalDecorators": true, // 開(kāi)啟裝飾器
? ?"emitDecoratorMetadata": true, // 開(kāi)啟元編程}
同時(shí)在入口文件引入?reflect-metadata。
3
Reflect
簡(jiǎn)介
Proxy?與?Reflect?是?ES6?為了操作對(duì)象引入的?API,Reflect?的?API?和?Proxy?的?API?一一對(duì)應(yīng),并且可以函數(shù)式的實(shí)現(xiàn)一些對(duì)象操作。另外,使用?reflect-metadata?可以讓?Reflect?支持元編程。
參考資料
Reflect - JavaScript | MDN
Metadata Proposal - ECMAScript
類型獲取
類型元數(shù)據(jù):design:type
參數(shù)類型元數(shù)據(jù):design:paramtypes
返回類型元數(shù)據(jù):design:returntype
使用方法
/**
* target: Object* propertyKey?: string | symbol*/Reflect.getMetadata('design:type', target, propertyKey); // 獲取被裝飾屬性的類型
Reflect.getMetadata("design:paramtypes", target, propertyKey); // 獲取被裝飾的參數(shù)類型Reflect.getMetadata("design:returntype", target, propertyKey); // 獲取被裝飾函數(shù)的返回值類型
4
編寫模塊
首先寫一個(gè)服務(wù)提供者,作為被依賴模塊:
// @services/log.tsclass LogService {?public debug(...args: any[]): void {
? ?console.debug('[DEB]', new Date(), ...args);
?}
?public info(...args: any[]): void {
? ?console.info('[INF]', new Date(), ...args);
?}
?public error(...args: any[]): void {
? ?console.error('[ERR]', new Date(), ...args);
?}}
然后我們?cè)賹懸粋€(gè)消費(fèi)者:
// @controllers/custom.tsimport LogService from '@services/log';
class CustomerController {?private log!: LogService;
?private token = "29993b9f-de22-44b5-87c3-e209f4174e39";
?constructor() {
? ?this.log = new LogService();
?}
?public main(): void {
? ?this.log.info('Its running.');
?}}
現(xiàn)在我們看到,消費(fèi)者在?constructor?構(gòu)造函數(shù)內(nèi)對(duì)?LogService?進(jìn)行了實(shí)例化,并在?main?函數(shù)內(nèi)進(jìn)行了調(diào)用,這是傳統(tǒng)的調(diào)用方式。
當(dāng)?LogService?變更,修改了構(gòu)造函數(shù),而這個(gè)模塊被依賴數(shù)很多,我們就得挨個(gè)找到引用此模塊的部分,并一一修改實(shí)例化代碼。
架構(gòu)設(shè)計(jì)
我們需要用一個(gè)?
Map?來(lái)存儲(chǔ)注冊(cè)的依賴,并且它的?key?必須唯一。所以我們首先設(shè)計(jì)一個(gè)容器。注冊(cè)依賴的時(shí)候盡可能簡(jiǎn)單,甚至不需要用戶自己定義?
key,所以這里使用?Symbol?和唯一字符串來(lái)確定一個(gè)依賴。我們注冊(cè)的依賴不一定是類,也可能是一個(gè)函數(shù)、字符串、單例,所以要考慮不能使用裝飾器的情況。
Container
先來(lái)設(shè)計(jì)一個(gè)?Container?類,它包括?ContainerMap、set、get、has?屬性或方法。ContainerMap?用來(lái)存儲(chǔ)注冊(cè)的模塊,set?和?get?用來(lái)注冊(cè)和讀取模塊,has?用來(lái)判斷模塊是否已經(jīng)注冊(cè)。
set?形參?id?表示模塊?id,?value?表示模塊。get?用于返回指定模塊?id?對(duì)應(yīng)的模塊。has?用于判斷模塊是否注冊(cè)。
// @libs/di/Container.tsclass Container {?private ContainerMap = new Map<string | symbol, any>();
?public set = (id: string | symbol, value: any): void => {
? ?this.ContainerMap.set(id, value);
?}
?
?public get = <T extends any>(id: string | symbol): T => {
? ?return this.ContainerMap.get(id) as T;
?}
?public has = (id: string | symbol): Boolean => {
? ?return this.ContainerMap.has(id);
?}}const ContainerInstance = new Container();export default ContainerInstance;
Service
現(xiàn)在實(shí)現(xiàn)?Service?裝飾器來(lái)注冊(cè)類依賴。
// @libs/di/Service.tsimport Container from './Container';interface ConstructableFunction extends Function {
?new (): any;}// 自定義 id 初始化export function Service (id: string): Function;// 作為單例初始化export function Service (singleton: boolean): Function;// 自定義 id 并作為單例初始化export function Service (id: string, singleton: boolean): Function;export function Service (idOrSingleton?: string | boolean, singleton?: boolean): Function {?return (target: ConstructableFunction) => {
? ?let _id;
? ?let _singleton;
? ?let _singleInstance;
? ?if (typeof idOrSingleton === 'boolean') {
? ? ?_singleton = true;
? ? ?_id = Symbol(target.name);
? ?} else {
? ? ?// 判斷如果設(shè)置 id,id 是否唯一
? ? ?if (idOrSingleton && Container.has(idOrSingleton)) {
? ? ? ?throw new Error(`Service:此標(biāo)識(shí)符(${idOrSingleton})已被注冊(cè).`);
? ? ?}
? ? ?_id = idOrSingleton || Symbol(target.name);
? ? ?_singleton = singleton;
? ?}
? ?Reflect.defineMetadata('cus:id', _id, target);
? ?if (_singleton) {
? ? ?_singleInstance = new target();
? ?}
? ?Container.set(_id, _singleInstance || target);
?};};
Service?作為一個(gè)類裝飾器,id?是可選的一個(gè)標(biāo)記模塊的變量,singleton?是一個(gè)可選的標(biāo)記是否單例的變量,target?表示當(dāng)前要注冊(cè)的類,拿到這個(gè)類之后,給它添加?metadata,方便日后使用。
Inject
接下來(lái)實(shí)現(xiàn)?Inject?裝飾器用來(lái)注入依賴。
// @libs/di/Inject.tsimport Container from './Container';// 使用 id 定義模塊后,需要使用 id 來(lái)注入模塊export function Inject(id?: string): PropertyDecorator {
?return (target: Object, propertyKey: string | symbol) => {
? ?const Dependency = Reflect.getMetadata("design:type", target, propertyKey);
? ?const _id = id || Reflect.getMetadata("cus:id", Dependency);
? ?const _dependency = Container.get(_id);
? ?// 給屬性注入依賴
? ?Reflect.defineProperty(target, propertyKey, {
? ? ?value: _dependency,
? ?});
?};}
5
服務(wù)提供者
log 模塊:
// @services/log.tsimport { Service } from '@libs/di';@Service(true)class LogService {
?...}
config 模塊:
// @services/config.tsimport { Container } from '@libs/di';export const token = '29993b9f-de22-44b5-87c3-e209f4174e39';// 可在入口文件處調(diào)用以載入export default () => {
?Container.set('token', token);};
消費(fèi)者
// @controllers/custom.tsimport LogService from '@services/log';class CustomerController {?// 使用 Inject 注入
?@Inject()
?private log!: LogService;
?// 使用 Container.get 注入
?private token = Container.get('token');
?public main(): void {
? ?this.log.info(this.token);
?}}
運(yùn)行結(jié)果
[INF] 2020-08-07T11:56:48.775Z 29993b9f-de22-44b5-87c3-e209f4174e39注意事項(xiàng)
decorator?有可能會(huì)在正式調(diào)用之前初始化,因此?Inject?步驟可能會(huì)在使用?Container.set?注冊(cè)之前執(zhí)行(如上文的?config?模塊注冊(cè)和?token?的注入),此時(shí)可以使用?Container.get?替代。
我們甚至可以讓參數(shù)注入在?constructor?形參里面,使用?Inject?直接在構(gòu)造函數(shù)里注入依賴。當(dāng)然這就需要自己下去思考了:
constructor(@Inject() log: LogService) {
?this.log = log;}

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 80+ 篇原創(chuàng)文章
