Reflect,IOC,DI

Posted coderlin_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Reflect,IOC,DI相关的知识,希望对你有一定的参考价值。

Reflect

  • Reflect对象与proxy对象一样,也是ES6为了操作对象而提供的新的API。
  • JS的装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定this等功能。
  • 但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上,Reflect Metadata就是做这个事情的。

Reflect Metadata
  • 可以通过装饰器给类添加一些自定义的信息
  • 然后通过反射将这些信息提取出来。
import "reflect-metadata";

const target = 

//定义元数据
// 给target添加属性Name,值为test, 对比target.name="test",这样只会在对象上面添加源数据,不会改变对象本身
Reflect.defineMetadata('name', 'test', target);
//给target.props上面添加属性name,值为test
Reflect.defineMetadata('name', 'test123', target, 'props');
console.log(Reflect.getOwnMetadata('name',target)); 		//test
console.log(Reflect.getOwnMetadata('name',target, "props"));  // test1234
console.log('target',target);

Reflect.defineMetadata只会在对象上面添加源数据,不会改变对象本身, 对比target.name=“test”,他会对对象本身进行修改。

declator

装饰器
Reflect.metadata会返回一个装饰器,如

Reflect.metadata 当作 Decorator 装饰器使用,

  • 当修饰类时,在类上添加元数据,
  • 当修饰类属性时,在类原型的属性上添加元数据
import "reflect-metadata";

function classMetadata(key: string, value: string) 
  return function (target: any) 
    Reflect.defineMetadata(key, value, target);
  ;


function methodMetadata(key: string, value: string) 
  //方法装饰器的参数是类的prototype和prototypename
  return function (target: any, propertyName: string) 
    Reflect.defineMetadata(key, value, target, propertyName);
  ;


//修饰类,给Persont添加元数据name,值为person
//@Reflect.metadata("name", "我是装饰Person类的元数据")
@classMetadata("name", "我是装饰Person类的元数据")
class Person 
  //@Reflect.metadata("name", "method")
  @methodMetadata("name", "method")
  method(): string 
    return "work";
  


console.log(Reflect.getMetadata("name", Person));
console.log(Reflect.getMetadata("name", Person.prototype, "method"));

Reflect.Metadata的本质是Reflect.defineMetadata,装过装饰器接收类或者类的实例。

IOC和DI

例子:
//显示器接口
interface Monitor 

// 27寸显示器
class Monitor27inch implements Monitor 

// HOST主机接口
interface Host 

//联想
class LegendHost implements Host 

//电脑类
class Computer 
  monitor: Monitor;
  host: Host;
  constructor() 
    this.monitor = new Monitor27inch();
    this.host = new LegendHost();
  

  startUp() 
    console.log("电脑组装完毕");
  


const computer = new Computer();
computer.startUp();

如上,定义了电脑类和主机显示器类。但是这样的设计有缺点:

  • 无法创建不同的部分组件,如24存显示器
  • 需要在类内容手工创建零件或者组件。

// 解决第一个问题
class Computer1 
  monitor: Monitor;
  host: Host;
  constructor(monitor: Monitor, host: Host) 
    this.monitor = monitor;
    this.host = host;
  

  startUp() 
    console.log("电脑组装完毕");
  


const monitor = new Monitor27inch();
const host = new LegendHost();
const computer1 = new Computer1(monitor, host);
computer1.startUp();

解决了第一个问题。
解决第二个问题,需要手工创建零件或者组件。需要借助IOC

IOC(控制反转)
  • IOC是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)

    比如上面的例子我们需要自己创建实例。但是IOC的概念中,实例由容器控制。
    IOC意味着把对象交给容器,让容器控制,而不是自己控制。
    举个例子(纯属举例子):
  • 结婚需要依赖车子房子,所以需要买车买房,才能构成结婚的条件。
  • 你就是服务的使用者,需要知道结婚本身与其依赖的关系是如何构建的,并且需要你手工维护。

    如上,就是传统的方式,跟我们上面的案例一样。
    使用IOC容器之后:
  • 系统的服务,同一注册到IOC容器中,如果服务有依赖其他的服务的时候,也需要对依赖进行声明
  • 当用户需要使用特定的服务时,IOC容器会负责该服务及其依赖对象的创建与管理工作。

如女朋友注册的时候,需要声明依赖,比如需要房子和车子,就需要进行声明。
当你使用服务的时候,容器会负责创建车子,房子。
但是需要一些代价,就是你会失去控制权。

IOC控制反转

IOC意味着你设计好的对象交给容器恐吓之,而不是使用传统的方式,在对象内部直接控制。

  • 以前我们直接在对象内容通过New的方式创建对象,是程序主动创建依赖对象。而现在是由容器来帮忙创建及注入了依赖对象,我们创建对象只是被动的接收依赖对象。

DI

  • 对于控制反转来说,其中最常见的就是依赖注入,简称DI。
  • 通过依赖注入机制,我们只需要通过简单的配置,而不需要任何代码就可以指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
  • 理解DI关键:
    • 谁依赖了谁,应用程序依赖IOC容器
    • 为什么需要依赖?应用程序需要IOC容器来提供对象需要的外部资源
    • 谁注入了谁?IOC容器向应用程序注入依赖的对象。
    • 注入了什么?注入了某个对象所需要的外部资源。
  • IOC和DI是同一个概念的不同角度描述,依赖注入明确描述了被注入对象依赖。IOC容器配置依赖对象。

以nest.js为案例

nest.js的源码
Module的源码

Controller的源码

可以看到,都是通过Relfec来实现的。

nest.js官网
准备一个module。module是一个容器

// 模块是一个容器
@Module(
  //控制器
  controllers: [AppController],
  
)
class AppModule 
export default AppModule;

控制器是控制路由

// 控制器一般只接受参数,返回相应,不会处理业务,业务由services服务处理
@Controller("/app")
class AppController 
  @Get("/hello")
  hello() 
    return "hello";
  

但是控制器一般只接受参数,返回响应,业务由services服务处理

// 服务需要注册
class AppService 
  getHello() 
    return "heelo";
  


服务需要注册,就跟IOC案例中的,女朋友依赖房子与车子,需要在容器里面注册提供者。

// 模块是一个容器
@Module(
  //控制器
  controllers: [AppController],
  //提供服务的,比如IOC下的案例,车和房子需要在这里注册。
  providers: [AppService],
)

这样当控制器在使用的时候,就可以使用该服务

@Controller("/app")
class AppController 
  constructor(private appService: AppService) 
  @Get("/hello")
  hello() 
    return this.appService.getHello()
  

它会在发现该属性的时候,module会创建好这个Appservice服务,然后注入进来,控制器就可以直接使用。只要在constructor声明我需要谁,容器就会自动帮你注入。

服务里使用其他的服务

比如想在一个服务里面使用其他服务,如

// 日志服务
class UseClassLoggerService 
    log(message: string)
        return message
    

需要在容器里面注册

// 模块是一个容器
@Module(
  //控制器
  controllers: [AppController],
  //提供服务的,比如IOC下的案例,车和房子需要在这里注册。
  providers: [AppService,
      provide: UseClassLoggerService, //token标识
      useClass: UseClassLoggerService,
      
  ],
)

然后在使用的服务

import  Module, Controller, Get, Injectable  from "@nestjs/common";
// 服务需要注册
@Injectable() //injectable表示可注入的
class AppService 
  constructor(private useClassLoggerService: UseClassLoggerService) 
  getHello() 
    return this.useClassLoggerService.log("hello");
  

Injectable表示可注入的。
解析遇到UseClassLoggerService类型的时候,会拿类UseClassLoggerService作为标识去容器也就是module里面的Providers寻找提供者,刚才在module的时候我们通过UseClassLoggerService注册了一个token。找到之后就用useClass提供的类创建一个实例,注入到AppService

// 其他服务
class OtherService 
  test() 
    return "test";
  

// 模块是一个容器
@Module(
  //控制器
  controllers: [AppController],
  //提供服务的,比如IOC下的案例,车和房子需要在这里注册。
  providers: [
    AppService,
    
      provide: UseClassLoggerService, //token标识
      useClass: UseClassLoggerService, //注册的是一个类
    ,
    
      provide: OtherService,
      useValue: new OtherService(), //注册的是一个实例
    ,
  ],
)

useValue注册的就是一个实例。

自定义token
// 模块是一个容器
@Module(
  //控制器
  controllers: [AppController],
  //提供服务的,比如IOC下的案例,车和房子需要在这里注册。
  providers: [
    AppService,
    
      provide: UseClassLoggerService, //token标识
      useClass: UseClassLoggerService, //注册的是一个类
    ,
    
      provide: 'test',
      useValue: new OtherService(), //注册的是一个实例
    ,
  ],
)

如上,otherSerce注册的时候,使用自定义的token,那么使用的时候,如果还是

 constructor(
    private useClassLoggerService: UseClassLoggerService,
    private otherSerivces: OtherService
  ) 

就会报错,因为内部会拿OtherService类作为token去容器里面的提供者找,找不到,因为没有对应的token标识。所以这时候需要属性装饰器Inject

  constructor(
    private useClassLoggerService: UseClassLoggerService,
    @Inject('test') private otherSerivces: OtherService
  ) 

这样内部就会通过这个test作为token去容器里面寻找,找到后拿到useValue的实例注入进去。

实现IOC


三个提供者,类提供者依赖value和factory提供者,注册到IOC容器中,通过Map存储, key是token,值就是provider。当用户获取实例的时候,容器就会获取classProvider和依赖,然后注入依赖,创建实例,返回给用户。

  • 定义ts类型
interface Type<T> 
  new (...args: any[]): T;


type Token<T> = Type<T> | InjectionToken;

// 针对字符串的token,类似于nest的Inject,防止字符串重名
class InjectionToken 
  constructor(public injectionIdentifier: string) 


interface BaseProvider<T> 
  provide: Token<T>;

// 类提供者
interface ClassProvider<T> extends BaseProvider<T> 
  useClass: Type<T>;

//值提供者
interface ValueProvider<T> extends BaseProvider<T> 
  useValue: T;

// 工厂提供者
interface FactoryProvider<T> extends BaseProvider<T> 
  useFactory: () => T;

type Provider<T> = ClassProvider<T> | ValueProvider<T> | FactoryProvider<T>;

提供者有三中,token有两种。

  • 实现注册
// 容器
class Container 
  public providers = new Map<Token<any>, Provider<any>>();

  //注册提供者
  addProvider<T>(provider: Provider<T>) 
    // provide就是token
    this.providers.set(provider.provide, provider);
  

例子:

const container = new Container();

const point =  x: 10, y: 10 ;
class BasicClass 
container.addProvider( provide: BasicClass, useClass: BasicClass );

console.log(container.providers);

值:

Map(1) 
  [Function: BasicClass] =>  provide: [Function: BasicClass], useClass: [Function: BasicClass] 

以IOC上的案例,

容器改造

// 容器
class Container 
  public providers = new Map<Token<any>, Provider<any>>();

  //注册提供者
  addProvider<T>(provider: Provider<T>) 
    // provide就是token
    this.providers.set(provider.provide, provider);
  

  // 注入,根据token创建对应的实例
  inject(token: Token<any>) 
    const provider = this.providers.get(token);
    return this.injectWithProvider(token, provider);
  

  getTokenName<T>(token: Token<T>) 
    // token可能是一个类或者是实例
    return token instanceof InjectionToken
      ? token.injectionIdentifier
      : token.name;
  

  injectWithProvider<T>(type: Token<T>, provider: Provider<T>) 
    if (isClassProvider(provider)) 
      return this.injectClass(provider);
     else if (isValueProvider(provider)) 
      return this.injectValue(provider);
     else if (isFactoryProvider(provider)) 
      return this.injectFactory(provider);
     else 
      throw new TypeError(`No provider for type $this.getTokenName(type)`);
    
  

  injectClass<T>(provider: ClassProvider<T>) 
    // TODO 服务可能依赖于其他服务,比如GirlFriend依赖与House和Car
    //return new provider.useClass();
    const target = provider.useClass
    const params = []
    return Reflect.construct(provider.useClass, params, target); //相当于new provider.useClass()
  
  injectValue<T>(provider: ValueProvider<T>) 
    return provider.useValue;
  
  injectFactory<T>(provider: FactoryProvider<T>) 
    return provider.useFactory();
  

可以取出对应的服务。
然后注册三个类

const container = new Container();
class Car  // ValueProvider
class House  // FactoryProvider
class GirlFrend 
  // ClassProvider 依赖Car和House
  constructor(private car: Car, private house: House) 


container.addProvider( provide: House, useFactory: () => new House() );
container.addProvider( provide: Car, useValue: new Car() );
container.addProvider( provide: GirlFrend, useClass: GirlFrend );

const car = container.inject(Car);
const house = container.inject(House);
const girlFreind = container.inject(GirlFrend)
console.log(car);
console.log(house);
console.log('girlFreind',girlFreind);


如上,GirlFried需要依赖House和Car服务,那么在容器在创建GirlFriend这个实例的时候,就需要先创建House和Car的实例,然后传入。如

injectClass<T>(provider: ClassProvider

以上是关于Reflect,IOC,DI的主要内容,如果未能解决你的问题,请参考以下文章

spring的IOC与DI

深入理解IoC/DI

Spring -- IOC/DI 基础概念的理解

Unity IOC/DI使用

IOC和DI的区别详解

IOC/DI的理解