前言
这一篇,我们谈谈TypeScript的设计模式,也是对我自己Demo的一个总结。如果说前面几篇所讲的基本都是新手入门,那么这里开始算是进阶的内容。
依赖注入(Dependency Injection)
依赖注入,简称DI。近年非常火,相信如果有订阅一些技术文章推送的朋友,隔三差五会收到一些文章介绍DI的原理然后附带各种IoC框架的推介。不少朋友尽管早已耳熟能详,甚至对其原理和用途随口都能讲出来,可是,一旦动手实践起来总会觉得比较以难入手。最后,只能将探索行动抛在一边去,直接堆框架,开启无脑调用模式。
其实,我想说,DI的实现并没有那么玄乎。也许,在你自己的项目你也曾经不经意地用过,但是没有意识到这就是DI而已。实现DI有很多种方法,我并不打算面面俱到,从概念到原理到编码这样讲。相信这些文章随便一搜就一大把。我主要分享一下我自己在TypeScript项目中,所实践出来的DI套路。
1、模块
要实现依赖注入,首先,得有模块。模块可以作为注入的零件,也快是被注入的操控者。先看看我的模块定义:
src/
├── app.ts
├── libs/
│ └── router.ts
├── modules/
│ ├── product/
│ │ ├── list.ts
│ │ └── detail.ts
│ └── user/
│ ├── login.ts
│ └── info.ts
└── utils/
├── http.ts
├── template.ts
└── cache.ts
这个其实在之前也有分享过的。我的模块主要分两大类
- 工具类(Utils)
例如:router(路由)、http、template(模板)、cache(缓存)
- 业务类(Modules)
例如:user/login(用户登录)、user/info(用户中心)、product/list(产品列表)、product/detail(产品详情)
很容易你就可以判断出来,在我的应用场景下,基本上都是业务类的模块依赖于工具类的模块,来实现相关的业务功能的。拿产品列表来举个栗子:
- GetProductList - 请求服务端产品数据,需要用到http
- RenderData - 将产品列表数据在页面渲染出来,需要用到template
- ShowProductDetail - 打开产品详情页,需要先将产品数据放到cache里,然后product/detail从cache获取数据,无需再多走一次http请求;而模块页面的跳转,需要用到路由router的方法
那么按照,那么传统的做法。就要这么写:
import { Router } from "../../libs/router";
import { Http } from "../../utils/http";
import { DataCache } from "../../utils/cache";
import { Template } from "../../utils/template";
export class ProductList {
GetProductList(){
var http = new Http();
http.Post(...);
}
RenderData(){
var tpl = new Template();
tpl.Parse(...);
}
ShowProductDetail(){
var cache = new DataCache();
cache.Set(...);
var router = new Router();
router.Go(...);
}
}
这样的代码看着,似乎还好吧?只能说没有对比就没有伤害:)
想象一下,如果有几十个业务模块都需要类似这样引用,单单复制粘贴就要几十次。万一要改一下构建函数加个参数呢?都是纯体力活呐~
厌恶了这样的体力活之后,你会首先想到的是DI。
2、容器
有了模块之后,最好有一个容器(Container)把所有工具模组装起来,然后作为一个对象参数注入到业务模块当中。个人认为这是最省时最省力的方法!当然,容器并不是必选项,你可以选择单个单个的注入。那么,同样面临着如果需要扩增注入模块要大面积的改代码的问题。
在TS里面DI容器可以简单定义成一个接口,然后把里面的对象实都例化,然后就可以用于注入了。
interface DIContainer {
http? : Http;
router? : Router;
cache? : DataCache;;
tpl? : Template;
}
const container : DIContainer = {
http: new Http() ;
router: new Router() ;
cache: new DataCache() ;
tpl: new Template() ;
}
其实,也可以弄的稍微复杂一些,可以让容器中的模块先不初始化,而是建立一个工厂模型,需要用到的时候再去初始化。那么容器的弹性会强一些,至少不会一开始占用那么多资源。
interface DIContainer {
http?: Http;
router?: Router;
cache?: DataCache;
tpl?: Template;
Build(key: string): void;
}
class Container implements DIContainer {
http: Http;
router: Router;
cache: DataCache;
tpl: Template;
Build(key: string) {
switch (key) {
case "http":
if (!this.http) this.http = new Http();
break;
case "router":
if (!this.router) this.router = new Router();
break;
case "cache":
if (!this.cache) this.cache = new DataCache();
break;
case "tpl":
if (!this.tpl) this.tpl = new Template();
break;
}
}
}
var container :Container();
container.Build("http");
container.Build("tpl");
container.Build("cache");
container.Build("router");
3、注入
好了,说了半天,总算“万事俱备只欠注入”了!直接上代码:
export class ProductList {
constructor(private di : DIContainer) {}
GetProductList(){
this.di.http.Post(...);
}
RenderData(){
this.di.tpl.Parse(...);
}
ShowProductDetail(){
this.di.cache.Set(...);
this.di.router.Go(...);
}
}
var proList = new Product(container); //把上面的容器作为构建参数注入
就是这样,把容器作为模块构建参数注入。这样容器里面的模块就能随便用了~哈哈,看懂了之后,是不是应该拍手称快呢?
4、控制
反转控制(Inversion of Control),这算是属于纯概念的东西。真要讲的话可以扯上一千多字,我就懒得再去绕了。
反正,依赖注入已经搞定,然后注入的容器中的对象也都用起来了,耦合度大大降低了,代码越看越帅了,心情舒爽不少,胃口也好了。那么,就已经达到我的目的了。至于到底谁控制了谁,自己体会吧。跟我半毛钱关系都没有~~