要搞清楚 IoC 那就用 TS 手写一个

 

最近把Spring重学一遍,总算是理清IOC,DI,AOP的概念,想整理一下,写篇笔记,又想锻炼自己的ts类型体操 (^_^),所以就用Typescript来篇IOC文章吧。

本文来自林小骏大佬的投稿

为什么要用IOC容器

通常说IOC容器,第一时间会想到Spring,它可以说是IOC最成熟的实践,但IOC不是java或Spring独有,它是一种设计模式,主要是用来解决软件工程里的耦合问题。那什么是耦合? 所谓耦合指的是类之间,或模块之间的依赖,如果两个或以上的类或模块紧密耦合,意味着只要改其中之一,就会影响另一个类,也意味着我们要改另一个的代码,不然会出现错误。强耦合,某方面来说,也可以说是我们日常所说 "屎山代码"的代名词,当我们改了一段代码,要不就某个地方报错,甚至可能整个项目无法运行。所以说,解耦合,是软件开发经常要处理的问题。

我们日常开发中,如果要创建一个类,在js/ts中,我们会直接new一个类,高级些的开发者可能会考虑到耦合问题,会用接口思想来解决,就像以下代码:

// User.ts
export interface User {
  getName: () => string,
  getAge: () => number
}

// UserImpl1.ts
import { User } from './interface_demo'

export class UserImpl implements User {
   private name:string;
   private age:number;

  constructor(name: string,age: number) {
    this.name = name;
    this.age = age;
  }


  public getName():string {
    return "User1的名字是" + this.name;
  }

  public getAge():number {
    return "User1的年龄是" this.age;
  }
}

//demo.ts
import { UserImpl1 } from "./UserImpl1";
import { User } from './interface_demo';

const user: User = new UserImpl1("Tom",45);

console.log(user.getAge()); // "User1的名字是Tom"
console.log(user.getName()); // "User1的年龄是45"

利用工厂模式解耦合

上述代码有问题吗? 功能上一定是没有问题,但如果改需求,不要 UserImpl1,要改 UserImpl2,意味着我要在代码上去改,一个还好,如果是有好几个,就要去好几个地方去改。这意味着什么? 耦合,UserImpl的改动会影响到涉及到它的模块的运行。有什么比较好的解决方法? 加一个中间层,通常软件工程问题,没有什么加中间层解决不了。我们可以用工厂模式去解决。工厂模式指的是指类的创建统一交给一个类来处理:

// BeanFactory.ts
import { UserImpl } from './UserImpl';

type BeanFactoryType = Record<string,Object>;

const beanFactory: BeanFactoryType = {
  'userImpl': new UserImpl("Tom",45)
}

type beanTypes = keyof BeanFactoryType;

export function getBean<T>(key: beanTypes): T {
  const result = beanFactory[key] as T;
  return result;

}

//demo.ts
import { UserImpl1 } from "./UserImpl1";
import { getBean } from "./BeanFactory";

const user: User = getBean<User>('userImpl');

console.log(user.getAge()); // "User1的名字是Tom"
console.log(user.getName()); // "User1的年龄是45"

配置文件解耦参数

这样子如果要改需求,把 UserImpl1 改为 UserImpl2,只需要去 BeanFactory文件改就可以了,减少改动,意味着松耦合。所以现在没有问题了? 看了一下 BeanFactory,发现构造方法传参是硬编码进法,意味着耦合,所以要再加一层中间层,我们利用json文件作为参数的配置文件。(注意: Typescript下,引入json文件,要先在tsconfig.json中把 resolveJsonModule 设为 true):

// beanConfig.json
{
  "user": {
    "name": "Tom",
    "age": 45
  }
}
import { UserImpl } from './UserImpl';
import * as config from './beanConfig.json';

type BeanFactoryType = Record<string,Object>;

const beanFactory: BeanFactoryType = {
  'userImpl': new UserImpl(config.user.name,config.user.age)
}

type beanTypes = keyof BeanFactoryType;

export function getBean<T>(key: beanTypes): T {
  const result = beanFactory[key] as T;
  return result;
}

总算是完成了一个小型的IOC容器了,简单来说就是利用工厂模式和配置文件,做到类(或是函数)和类的构造方法传入的参数解耦合,我们可以把所有的类创建交给 IOC容器处理,一旦需求改变,可以减少代码的改动。

全栈修仙之路

专注分享 TS、Vue3、前端架构和源码解析等技术干货。

142篇原创内容

公众号

利用装饰器构建IOC容器

我们简单做了一个ioc容器,功能已经实现了。但有没有更好的实践方案? 可以利用装饰器来作更好的实现。

反射

说正题前,先了解一些前置知识,先了解一下反射,这是很重要的概念,工业级的IOC框架 (如 Spring) 就是基于反射实现,Java里有句话叫 接口加反射,啥都能干,可想而知,反射有多重要。那什么是反射? google一下反射,看到这句话:

In object-oriented programming languages such as Java,reflection allows inspection of classes,interfaces,fields and methods at runtime without knowing the names of the interfaces,fields,methods at compile time. It also allows instantiation of new objects and invocation of methods.

翻译过来就是: 在面向对象语言中,如java,反射可以在运行期间,做到类型,接口,属性和方法的检查,不需要知道在编译期间知道接口,属性和方法的名称。它也可以实例化对象和调用方法。

如果在其他OOP语言,会经常使用反射,IOC,ORM框架等就是基于反射实现的。但在js/ts里,很少会直接使用反射 (Reflect)的api,为什么? 我的想法是因为js是一门动态语言,它的对象操作就像是操作哈希表 (事实上,在v8中,对象就是类似哈希表的结构),在对象里,属性和方法的赋值增删改没什么太大区别,就是键值对 (也因此在js中很少用Map这个数据结构,因为对象已经能实现它的功能),键值对结构即使在静态语言中,本身就可以做到动态操作。当然这也意味着js的一系列对象操作是不安全的 (例如经常出现的undefined问题),这是另一个话题啦。

既然如此,为什么我们要了解反射? 因为我们要操作装饰器 (decorator)。

装饰器

装饰器是一个在其他常见的oop语言 (如 java,C#)中一个常见的特性,在ioc框架中大量使用,但在js中依然是实验性质的存在。装饰器是什么? 看代码是最容易理解:

@sealed
class BugReport {
  type = "report";
  title: string;
 
  constructor(t: string) {
    this.title = t;
  }
}

就是在代码加了一些装饰器,然后可以利用反射对加上注解的类,属性和方法做一些操作,这样做有什么好处?就是可以做到只需加入一些装饰器,就能加强代码的功能。

写一个demo来看看吧。先创建一个项目,引入依赖:

npm install reflect-metadata

Reflect Metadata 是ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。

然后要在tsconfig.js做些配置:

    /* Language and Environment */
    "target": "es2016",                                 /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "lib": ["ES6","DOM"],                                      /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    "experimentalDecorators": true,                  /* Enable experimental support for TC39 stage 2 draft decorators. */
    "emitDecoratorMetadata": true,                     /* Emit design-type metadata for decorated declarations in source files. */

    /* Modules */
    "module": "commonjs",                               /* Specify what module code is generated. */
     "moduleResolution": "node",                      /* Specify how TypeScript looks up a file from a given module specifier. */
     "types": ["reflect-metadata"],                                  /* Specify type package names to be included without being referenced in a source file. */
import 'reflect-metadata'

@Reflect.metadata('inClass','A')
class Test {
  @Reflect.metadata('inMethod','B')
  public hello(): string {
    return 'hello world';
  }
}

console.log(Reflect.getMetadata('inClass',Test)); // 'A'
console.log(Reflect.getMetadata('inMethod',new Test(),'hello')); // 'B'

可以直接用Reflect.metadata作为装饰器,然后利用Reflect.getMetadata就可以获取相应的值。

也可以自己写一个装饰器:

import 'reflect-metadata';

function classDecorator(): ClassDecorator {
  return target => {
    // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
    Reflect.defineMetadata('classMetaData','a',target);
  };
}

function methodDecorator(): MethodDecorator {
  return (target,key,descriptor) => {
    // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
    Reflect.defineMetadata('methodMetaData','b',target,key);
  };
}

@classDecorator()
class SomeClass {
  @methodDecorator()
  someMethod() {}
}

console.log(Reflect.getMetadata('classMetaData',SomeClass)); // 'a'
console.log(Reflect.getMetadata('methodMetaData',new SomeClass(),'someMethod')); // 'b'

由于本文主要是IOC容器,装饰器只作简单介绍。

写一个装饰器的简单应用

我们希望写一个函数,调用它,可以根据装饰器信息创建一个类,效果如下:

// Person.ts

@Injectable()
export class Person {
  @InjectValue("Tom")
  name: string

  @InjectValue(12)
  age: number

  constructor(name: string,age: number) {
    this.name = name;
    this.age = age;
  }
}


//demo.ts

import { Person } from "./Person";
const person: Person = getBean('Person') as Person;

console.log("名字是",person.name); // 名字是Tom
console.log("年龄是",person.age);  // 年龄是12

首先要先写好装饰器函数,用来给类赋值:

// decorator.ts

import 'reflect-metadata'

/**
 * 标记该类可注入属性
 * @returns
 */
export function Injectable(): ClassDecorator {
  return target => {
    Reflect.defineMetadata("injectable",true,target);
  }
}

/**
 * 为属性赋值
 * @param value 
 * @returns 
 */
export function InjectValue(value: unknown) {
  return Reflect.metadata("injectValue",value);
}

然后我们要有一个函数,用来存放要初始化的类:

import 'reflect-metadata';
import { Person } from './Person';

type beanType = Record<string,Function>;

const beans:beanType = {};

saveBean(Person);

export function saveBean(target: Function) {
  const name = target.name;
  beans[name] = target;
}

然后还得有一个函数,用来获取装饰器 @InjectValue赋的值:

export function getMetaValue(target: Object,propertyKey: string) {
  const metaValue = Reflect.getMetadata("injectValue",target,propertyKey);
  return metaValue;
}

最后我们要创建一个函数,可以用来实例化对象:

export function createBean(beanName: string) {
  // 获取saveBean存放的类
  const Bean = beans[beanName];

  if (!Bean) {
    throw new Error("没有对应的类,无法创建");
  }

  // 判断是否可以注入属性
  const isInjectable = Reflect.getMetadata("injectable",Bean) as boolean;

  if (!isInjectable) {
    return Bean;
  }

  // 利用反射,创建实例
  const result = Reflect.construct(Bean,[]);

  Object.keys(result).forEach(key => {
    // 获取装饰器InjectValue赋的值
    const propertyValue = getMetaValue(result,key);

    if (propertyValue !== undefined || propertyValue !== null) {
      Reflect.defineProperty(result,key,{
        value: propertyValue
      });
    } else {
      const value = Reflect.get(Bean,key);
      Reflect.defineProperty(result,key,{
        value
      });
    }
  })

  return result;
}

写个测试吧:

import { Person } from "./Person";
import { createBean } from "./reflectUtils";

const person: Person = createBean('Person') as Person;

console.log("名字是",person.name); // 名字是Tom
console.log("年龄是",person.age);  // 年龄是12

有点东西,这不就是我们之前做的IOC容器装饰器版吗!?不过现在 @InjectValue的值是硬编码,我们可以再完善一下。

完善装饰器版IOC容器

写代码之前,先整理一下我们想要的需求:

  1. @InjectValue不要硬编码,要可以从配置文件中获取

  2. 可以注入实例对象

  3. 以上需求完成后,可以直接用函数调用,就像之前一样

期望实现效果如下:

// Person.ts
export interface Person {
  name: string,
  age: number,
  introduce: () => string
}

----

// Father.ts

import { Person } from './Person';

@Injectable()
export class Father implements Person {
  @InjectValue("${father.name}")
  name: string;

  @InjectValue("${father.age}")
  age: number;


  constructor(name: string,age: number) {
    this.name = name;
    this.age = age;
  }

  introduce() {
    return `My name is ${this.name}. I am ${this.age} years old`;
  }
}

----

// Student.ts

import { Person } from './Person';

@Injectable()
export class Student implements Person {
  @InjectValue("${student.name}")
  name: string;

  @InjectValue("${student.age}")
  age: number;

  @Inject("father")
  father: Person;

  constructor(name: string,age: number,father: Person) {
    this.name = name;
    this.age = age;
    this.father = father;
  }

  introduce() {
    return `My name is ${this.name}. I am ${this.age} years old. My father's name is ${this.father.name}.`;
  }
}

----

// container.ts

{
  "father": {
    "name": "Thomas",
    "age": 50
  },
  "student": {
    "name": "Tom",
    "age": 16
  }
}

----

// container.ts

import * as beanConfig from './beanConfig.json';
import { Father } from './Father';
import { Student } from './Student';

const beanFactory = beanFactoryConfig(beanConfig);
beanFactory.add(Student);
beanFactory.add(Father,"father");

export const getBean = beanFactory.build();

----

// demo.ts

import { getBean } from "./container";
import { Person } from "./Person";

const student = getBean("Student") as Person;

// My name is Tom. I am 16 years old. My father's name is Thomas.
console.log(student.introduce());

接下来要开始写代码了,也是同样套路,先把装饰器函数完成:

// decorator.ts

import 'reflect-metadata'

/**
 * 标记该类可注入属性
 * @returns
 */
export function Injectable(): ClassDecorator {
  return target => {
    Reflect.defineMetadata("injectable",true,target);
  }
}

/**
 * 为属性赋值
 * @param value 
 * @returns 
 */
export function InjectValue(value: unknown) {
  return Reflect.metadata("injectValue",value);
}

/**
 * 
 * @param value 属性注入实例
 * @returns 
 */
export function Inject(value: unknown) {
  return Reflect.metadata("inject",value);
}

处理容器部分前,先写下实用函数:

import 'reflect-metadata';

/**
 * 获取InjectValue的值
 * @param target 
 * @param valueName 
 * @param config  json配置文件
 */
export function parseInjectValue(target: Object,property: string,config?: unknown) {
  const injectValue = Reflect.getMetadata("injectValue",target,property);

  const regPattern = /\$\{(.*)\}/;
  let result;

  if (regPattern.test(injectValue)) {
    const jsonProperty = RegExp.$1.trim();
    const jsonConfig = JSON.parse(config as string);

    const propertyArr = jsonProperty.split(".");

    propertyArr.forEach(item => {
      result = jsonConfig[item];

      if (!result) {
        throw new Error("无法获取配置中的值,请检查属性名是否正确")
      }
    })
  } else {
    result = injectValue;
  }

  return result as unknown;
}

终于开始要处理beanFactoryConfig,传入json配置创建IOC容器。我们知道beanFactoryConfig只是一个工厂函数,真正要创建的是 BeanFactory实例,先处理 BeanFactory:

class BeanFactory {
  private container: Record<string | symbol,Function>

  private jsonConfig: object

  constructor(config: unknown) {
    this.jsonConfig = config as object;
    this.container = {};
  }

  add(target: Function,name?: string | symbol) {
    const containerName = name ? name : target.name;
    this.container[containerName] = target;
  }
}

export function beanFactoryConfig(jsonConfig: unknown) {
  return new BeanFactory(jsonConfig as object);
}

最后要完成build方法,它要返回getBean,实现原理与之前类似,只是是多加注入类的处理:

import { parseInjectValue,getInjectObj } from './reflectUtils'

class BeanFactory {
  private container: Record<string | symbol,Function>

  private jsonConfig: object

  constructor(config: unknown) {
    this.jsonConfig = config as object;
    this.container = {};
  }

  add(target: Function,name?: string | symbol) {
    const containerName = name ? name : target.name;
    this.container[containerName] = target;
  }

  build() {
    const getBean = (beanName: string | symbol) => {
      const bean = this.container[beanName];
      if (!bean) {
        throw new Error("没有对应的类,无法创建");
      }

      if (!this.isInjectable(bean)) {
        return bean;
      }

      // 利用反射,创建实例
      const result = Reflect.construct(bean,[]);
      Object.keys(result).forEach(key => {
        // 获取装饰器InjectValue赋的值
        const injectValue = parseInjectValue(result,key,this.jsonConfig);
        if (injectValue) {
          Reflect.defineProperty(result,key,{
            value: injectValue
          });
        }

        // 获取Inject的值
        const injectName = getInjectObj(result,key);
        if (injectName) {
          const injectedBean = getBean(injectName);
          Reflect.defineProperty(result,key,{
            value: injectedBean
          });
        }

        // 如果没有装饰器情况
        if (!injectValue && !injectName) {
          const value = Reflect.get(bean,key);
          Reflect.defineProperty(result,key,{
            value
          });
        }
      })

      return result as object;
    };

    return getBean;
  }

  private isInjectable(bean: object) {
    const isInjectable = Reflect.getMetadata("injectable",bean) as boolean;
    return isInjectable;
  }
}

export function beanFactoryConfig(jsonConfig: unknown) {
  return new BeanFactory(jsonConfig as object);
}

测试一下代码:

import { getBean } from "./container";
import { Person } from "./Person";

const student = getBean("Student") as Person;

// My name is Tom. I am 16 years old. My father's name is Thomas.
console.log(student.introduce());

总算是完成基于装饰器的IOC容器,一般如果只是简单讲解ioc原理的,到了这阶段已经完结,但那些讲解给出的demo代码通常有一个致命的bug。

循环依赖

想一想,如果在Father类里添加Student类,然后注入依赖,会发生什么? 如果直接调用getBean("Student"),它会先实例化Student类,然后实例化Father类,由于Father里有Student类,所以上述过程会一直循环,卡死。

所以我们得要有一个变量去记录依赖实例化的过程,避免循环依赖,我们可以用一个Set数据结构来解决:

import { parseInjectValue,getInjectObj } from './reflectUtils'

class BeanFactory {
  private container: Record<string | symbol,Function>

  private jsonConfig: object

  private injectProcess: Set<string | symbol>

  constructor(config: unknown) {
    this.jsonConfig = config as object;
    this.container = {};
    this.injectProcess = new Set();
  }
...
  build() {
    const getBean = (beanName: string | symbol) => {
      const bean = this.container[beanName];
      if (!bean) {
        throw new Error("没有对应的类,无法创建");
      }

      if (!this.isInjectable(bean)) {
        return bean;
      }

      // 利用反射,创建实例
      const result = Reflect.construct(bean,[]);
      this.injectProcess.add(beanName);
      Object.keys(result).forEach(key => {
...
        if (injectName) {
          if (this.injectProcess.has(injectName)) {
            console.log(this.injectProcess.keys())
            throw new Error("该类在依赖注入过程中已被注入,存在循环依赖,请检查依赖注入过程")
          }
...
      this.injectProcess.delete(beanName);

      return result as object;
    };

    return getBean;
  }
}

一路下来,终于完成了,如果有了解过inversify的读者,会发现上述的实现很类似 (我就是看着它的设计来实现)。

源码地址: https://github.com/DominguitoLamo/IOC_Demo

 

转自要搞清楚 IoC ,那就用 TS 手写一个! 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 很高兴回答您的问题。首先,需要定义一个Bean类,然后使用反射机制创建Bean实例并将其存储在Map中。接着,需要解析Bean的依赖关系并进行注入。最后,需要实现Bean的生命周期管理,包括初始化和销毁。具体实现可以参考Spring源码。 ### 回答2: 实现一个简单的自定义Spring IOC容器需要以下步骤: 1. 定义一个bean类,该类包含需要注入的属性和对应的setter方法。 2. 创建一个容器类,用于管理bean的实例。在该容器类中,定义一个Map用来存储bean的名称和对应的实例。 3. 创建一个注入类,该类负责根据配置文件或注解来实例化和注入bean。可以使用反射机制来动态创建对象。 4. 编写一个配置类,该类用于读取配置文件或注解,获取需要创建的bean的信息。 5. 在容器类中,编写一个方法用于注册bean实例,将bean的名称和对应的实例放入Map中。 6. 在容器类中,编写一个方法用于从容器中获取bean的实例。 7. 将以上步骤整合到一个主程序中,测试容器类的功能。 以下是一个简单的实现示例: 1. 创建一个User类,包含name和age属性,并提供相应的setter方法。 ```java public class User { private String name; private int age; // Getter and Setter methods // ... } ``` 2. 创建一个容器类MyIOCContainer,使用Map来存储bean的名称和对应的实例。 ```java public class MyIOCContainer { private Map<String, Object> beans = new HashMap<>(); public void registerBean(String name, Object bean) { beans.put(name, bean); } public Object getBean(String name) { return beans.get(name); } } ``` 3. 创建一个注入类,用于实例化和注入bean。 ```java public class BeanInjector { public static void inject(Object bean) { // 通过反射机制实例化对象 // 通过反射机制注入属性 } } ``` 4. 创建一个配置类,使用注解来获取bean的信息。 ```java public class AppConfig { @Bean public User getUser() { User user = new User(); // 设置属性值 return user; } } ``` 5. 在容器类中实现注册和获取bean的方法。 ```java public class MyIOCContainer { // ... public void registerBean(String name, Object bean) { BeanInjector.inject(bean); // 注入属性 beans.put(name, bean); } public Object getBean(String name) { return beans.get(name); } // ... } ``` 6. 编写一个主程序,测试容器类的功能。 ```java public class Main { public static void main(String[] args) { MyIOCContainer container = new MyIOCContainer(); // 创建配置类实例 AppConfig config = new AppConfig(); // 获取bean的信息 Method[] methods = config.getClass().getMethods(); for (Method method : methods) { if (method.isAnnotationPresent(Bean.class)) { try { // 调用配置类的方法获取bean实例 Object bean = method.invoke(config); // 注册到容器中 container.registerBean(method.getName(), bean); } catch (Exception e) { e.printStackTrace(); } } } // 从容器中获取bean的实例 User user = (User) container.getBean("getUser"); System.out.println("Name: " + user.getName()); System.out.println("Age: " + user.getAge()); } } ``` 这是一个简单的自定义Spring IOC容器的实现示例,可以根据需要进行扩展和优化。 ### 回答3: 自定义一个简单的Spring IOC容器需要包含几个基本的组件和功能。以下是一个简单的实现示例: ```java import java.util.HashMap; import java.util.Map; public class CustomIOCContainer { private Map<String, Object> beanMap; // 存储bean的map public CustomIOCContainer() { beanMap = new HashMap<>(); } public void registerBean(String beanName, Object bean) { beanMap.put(beanName, bean); // 注册bean到map中 } public Object getBean(String beanName) { return beanMap.get(beanName); // 通过beanName获取对应的bean对象 } public static void main(String[] args) { CustomIOCContainer container = new CustomIOCContainer(); // 创建bean对象并注册到容器中 UserService userService = new UserServiceImpl(); container.registerBean("userService", userService); // 从容器中获取bean对象并使用 UserService userServiceFromContainer = (UserService) container.getBean("userService"); userServiceFromContainer.sayHello(); // 调用方法 // 结果输出:Hello, World! } } interface UserService { void sayHello(); } class UserServiceImpl implements UserService { @Override public void sayHello() { System.out.println("Hello, World!"); } } ``` 以上代码实现了一个简单的Spring IOC容器,核心思想是使用一个Map来存储注册的bean,通过bean的名称来获取对应的对象。在main方法中,我们创建了一个UserService对象并注册到容器中,然后通过容器的getBean方法获取UserService对象,并调用其sayHello方法进行输出。 这只是一个简化的实现示例,实际的Spring IOC容器功能还包括依赖注入、生命周期管理、AOP等复杂功能,不过以上代码可作为基本框架的起点。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值