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

由于我們修改了輪胎的定義,為了讓整個程序正常運行,我們需要做以下改動:
由此我們可以看到,僅僅是為了修改輪胎的構造函數(shù),這種設計卻需要修改整個上層所有類的構造函數(shù)!在軟件工程中,這樣的設計幾乎是不可維護的。
所以我們需要進行?控制反轉(IoC),即上層控制下層,而不是下層控制著上層。我們用?依賴注入(Dependency Injection)?這種方式來實現(xiàn)控制反轉。所謂依賴注入,就是把底層類作為參數(shù)傳入上層類,實現(xiàn)上層類對下層類的“控制”。
這里我們用構造方法傳遞的依賴注入方式重新寫車類的定義:
????這里我只需要修改輪胎類就行了,不用修改其他任何上層類。這顯然是更容易維護的代碼。不僅如此,在實際的工程中,這種設計模式還有利于不同組的協(xié)同合作和單元測試。
2
1.安裝?typescript?環(huán)境以及重要的 polyfill?reflect-metadata。
2.在?tsconfig.json?中配置?compilerOptions
{
? ?"experimentalDecorators": true, // 開啟裝飾器
? ?"emitDecoratorMetadata": true, // 開啟元編程}
同時在入口文件引入?reflect-metadata。
3
Reflect
簡介
Proxy?與?Reflect?是?ES6?為了操作對象引入的?API,Reflect?的?API?和?Proxy?的?API?一一對應,并且可以函數(shù)式的實現(xiàn)一些對象操作。另外,使用?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
編寫模塊
首先寫一個服務提供者,作為被依賴模塊:
// @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);
?}}
然后我們再寫一個消費者:
// @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)在我們看到,消費者在?constructor?構造函數(shù)內對?LogService?進行了實例化,并在?main?函數(shù)內進行了調用,這是傳統(tǒng)的調用方式。
當?LogService?變更,修改了構造函數(shù),而這個模塊被依賴數(shù)很多,我們就得挨個找到引用此模塊的部分,并一一修改實例化代碼。
架構設計
我們需要用一個?
Map?來存儲注冊的依賴,并且它的?key?必須唯一。所以我們首先設計一個容器。注冊依賴的時候盡可能簡單,甚至不需要用戶自己定義?
key,所以這里使用?Symbol?和唯一字符串來確定一個依賴。我們注冊的依賴不一定是類,也可能是一個函數(shù)、字符串、單例,所以要考慮不能使用裝飾器的情況。
Container
先來設計一個?Container?類,它包括?ContainerMap、set、get、has?屬性或方法。ContainerMap?用來存儲注冊的模塊,set?和?get?用來注冊和讀取模塊,has?用來判斷模塊是否已經(jīng)注冊。
set?形參?id?表示模塊?id,?value?表示模塊。get?用于返回指定模塊?id?對應的模塊。has?用于判斷模塊是否注冊。
// @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)在實現(xiàn)?Service?裝飾器來注冊類依賴。
// @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 {
? ? ?// 判斷如果設置 id,id 是否唯一
? ? ?if (idOrSingleton && Container.has(idOrSingleton)) {
? ? ? ?throw new Error(`Service:此標識符(${idOrSingleton})已被注冊.`);
? ? ?}
? ? ?_id = idOrSingleton || Symbol(target.name);
? ? ?_singleton = singleton;
? ?}
? ?Reflect.defineMetadata('cus:id', _id, target);
? ?if (_singleton) {
? ? ?_singleInstance = new target();
? ?}
? ?Container.set(_id, _singleInstance || target);
?};};
Service?作為一個類裝飾器,id?是可選的一個標記模塊的變量,singleton?是一個可選的標記是否單例的變量,target?表示當前要注冊的類,拿到這個類之后,給它添加?metadata,方便日后使用。
Inject
接下來實現(xiàn)?Inject?裝飾器用來注入依賴。
// @libs/di/Inject.tsimport Container from './Container';// 使用 id 定義模塊后,需要使用 id 來注入模塊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
服務提供者
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';// 可在入口文件處調用以載入export default () => {
?Container.set('token', token);};
消費者
// @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);
?}}
運行結果
[INF] 2020-08-07T11:56:48.775Z 29993b9f-de22-44b5-87c3-e209f4174e39注意事項
decorator?有可能會在正式調用之前初始化,因此?Inject?步驟可能會在使用?Container.set?注冊之前執(zhí)行(如上文的?config?模塊注冊和?token?的注入),此時可以使用?Container.get?替代。
我們甚至可以讓參數(shù)注入在?constructor?形參里面,使用?Inject?直接在構造函數(shù)里注入依賴。當然這就需要自己下去思考了:
constructor(@Inject() log: LogService) {
?this.log = log;}
分享前端好文,點亮?在看
