Enseñarle cómo crear su propia herramienta de inyección de dependencia.

Preparación antes de leer

Antes de leer este documento, primero puede comprender estos conocimientos para poder mantenerse al día con el contenido del documento:

  • Conocimientos conceptuales: Inversión de Control, Inyección de Dependencia, Inversión de Dependencia;

  • Conocimientos técnicos: decorador Decorador, reflexión Reflexionar;

  • Definición de TSyringe: token, proveedor https://github.com/microsoft/tsyringe#injection-token .

Todas las prácticas de implementación en este artículo están escritas en codesandbox. Si está interesado, puede hacer clic para ver el código fuente https://codesandbox.io/s/di-playground-oz2j9.

¿Qué es la inyección de dependencia?

ejemplo sencillo

Aquí implementamos un ejemplo simple para explicar qué es la inyección de dependencia: un estudiante conduce un vehículo de casa a la escuela.

class Transportation {
  drive() {
    console.log('driving by transportation')
  }
}

class Student {
  transportation = new Transportation()
  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

Luego, en la vida real, los estudiantes que están lejos elegirán conducir a la escuela y los estudiantes que están cerca elegirán ir a la escuela en bicicleta. Entonces podemos continuar abstrayendo el código anterior y escribirlo de la siguiente manera:

class Car extends Transportation {
  drive() {
    console.log('driving by car')
  }
}

class FarStudent extends Student {
  transportation = new Car()
}

Esto satisface las necesidades de los estudiantes que están lejos de ir a la escuela en coche, pero aquí hay un problema: los estudiantes también tienen sus propias opciones y preferencias específicas: a algunas personas les gusta BMW y a otras les gusta Tesla. Para resolver este tipo de problemas, podemos seguir utilizando la herencia y seguir abstrayendo, para conseguir estudiantes ricos a los que les gusten BMW y Tesla.

Probablemente todo el mundo piense que escribir código de esta manera es completamente inviable y que el grado de acoplamiento es demasiado alto: cada tipo de estudiante está directamente vinculado a un medio de transporte específico cuando se abstrae. Los medios de transporte que posee un estudiante no los crea el estudiante, sino que los determina según su situación familiar y sus preferencias, qué tipo de medio de transporte utiliza para ir a la escuela, incluso puede haber muchos autos en casa, y conduce a la escuela todos los días dependiendo de su estado de ánimo.

Luego, para reducir el acoplamiento y crear dependencias de acuerdo con estados y condiciones específicos, debemos hablar de los siguientes patrones.

Inversión de control

La inversión de control (Inversión de control, abreviada como IoC ) es un principio de diseño que reduce el acoplamiento entre códigos al invertir la lógica del programa.

Un contenedor de inversión de control (contenedor IoC) es una herramienta o marco específico que se utiliza para ejecutar la lógica del código invertida desde un programa interno, mejorando así la reutilización y legibilidad del código. La herramienta DI que utilizamos a menudo desempeña el papel de contenedor de IoC, conectando todos los objetos y sus dependencias.

e29967fd9ce06208ccbaabb3f939ff71.png

Consulte el artículo de Martin Fowler sobre inversión de control e inyección de dependencia https://martinfowler.com/articles/injection.html

inyección de dependencia

La inyección de dependencia es una implementación específica de inversión de control: al ceder el control de la creación de vida de los objetos dentro del programa, los objetos dependientes se crean e inyectan desde el exterior.

Hay cuatro métodos principales de inyección de dependencia:

  • Basado en interfaz. Implemente una interfaz específica para contenedores externos para inyectar objetos del tipo dependiente.

  • Basado en el método establecido. Implemente el método de conjunto público de una propiedad específica para permitir que el contenedor externo llame al objeto del tipo dependiente.

  • Basado en el constructor. Implemente un constructor con parámetros específicos y pase un objeto del tipo dependiente al crear un nuevo objeto.

  • Según las anotaciones, agregue anotaciones como "@Inject" antes de las variables privadas, para que las herramientas o marcos puedan analizar dependencias e inyectar dependencias automáticamente.

Los dos primeros métodos no se utilizarán en las herramientas DI comúnmente utilizadas en el front-end, aquí presentamos principalmente los dos últimos.

Si se pasa desde el constructor, se puede escribir así:

class Student {
  constructor(transportation: Transportation) {
    this.transportation = transportation
  }

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

class Car extends Transportation {
  drive() {
    console.log('driving by car')
  }
}

const car = new Car()
const student = new Student(car)
student.gotoSchool()

En ausencia de herramientas, el método de definición basado en el constructor se puede escribir a mano, pero aunque el método de escritura aquí pertenece a la inyección de dependencia, demasiadas instancias manuales engorrosas serán una pesadilla para los desarrolladores, especialmente Car El objeto en sí puede depender de diferentes neumáticos, diferentes instancias de motores.

Herramientas para la inyección de dependencia

La herramienta para la inyección de dependencias es una especie de contenedor de IoC. Al analizar automáticamente las dependencias, el proceso de creación de instancias de objetos que originalmente se realizaba manualmente se completa en la herramienta.

@Injectable()
class Student {
  constructor(transportation: Transportation) {
    this.transportation = transportation
  }

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const injector = new Injector()
const student = injector.create(Student)
student.gotoSchool()

Si usa anotaciones, puede escribirlo así:

@Injectable()
class Student {
  @Inject()
  private transportation: Transportation

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const injector = new Injector()
const student = injector.create(Student)
student.gotoSchool()

La diferencia entre los dos radica en la dependencia de la herramienta: la clase definida desde el constructor aún puede ejecutarse normalmente incluso si se crea manualmente, pero la clase definida mediante anotación solo puede ser creada por la herramienta y no puede ser creado manualmente.

Inversión de dependencia

Uno de los seis principios de los patrones de diseño de software, Principio de inversión de dependencia, abreviatura en inglés DIP , nombre completo Principio de inversión de dependencia.

Los módulos de alto nivel no deberían depender de los módulos de bajo nivel, ambos deberían depender de sus abstracciones; las abstracciones no deberían depender de los detalles, los detalles deberían depender de las abstracciones.

En el escenario con un contenedor loC, el control de la creación de objetos no está en nuestras manos, sino que se crea dentro de la herramienta o marco. Si un estudiante conduce un BMW o un Tesla cuando está en la escuela está determinado por el entorno operativo. En el entorno operativo real de JS, se sigue el modelo de pato, ya sea un vehículo o no, siempre que se pueda conducir, se puede utilizar cualquier automóvil.

Entonces podemos cambiar el código a lo siguiente, confiando en una abstracción en lugar de una implementación específica.

// src/transportations/car.ts
class Car {
   drive() {
     console.log('driving by car')
   }
}

// src/students/student.ts
interface Drivable {
   drive(): void
}

class Student {
  constructor(transportation: Drivable) {
    this.transportation = transportation
  }

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

¿Por qué es tan importante confiar en abstracciones en lugar de implementaciones? En una arquitectura compleja, una abstracción razonable puede ayudarnos a mantener la simplicidad, mejorar la cohesión dentro de los límites del dominio, reducir el acoplamiento entre diferentes límites y guiar los proyectos para dividirlos en estructuras de directorios razonables. Al implementar la capacidad compuesta de SSR y CSR, cuando el cliente se está ejecutando, necesita solicitar datos a través de HTTP, mientras que en el servidor, solo necesitamos llamar directamente a DB o RPC para obtener los datos.

Podemos abstraer el objeto que solicita datos, definir un Servicio abstracto e implementar la misma función en el cliente y el servidor respectivamente para solicitar datos:

interface IUserService {
  getUserInfo(): Promise<{ name: string }>
}

class Page {
  constructor(userService: IUserService) {
    this. userService = userService
  }

  async render() {
    const user = await this.userService. getUserInfo()
    return `<h1> My name is ${user.name}. </h1>`
  }
}

Utilice esto en una página web:

class WebUserService implements IUserService {
  async getUserInfo() {
    return fetch('/api/users/me')
  }
}

const userService = new WebUserService()
const page = new Page(userService)

page.render().then((html) => {
  document.body.innerHTML = html
})

Utilice esto en el lado del servidor:

class ServerUserService implements IUserService {
  async getUserInfo() {
    return db.query('...')
  }
}

class HomeController {
  async renderHome() {
    const userService = new ServerUserService()
    const page = new Page(userService)
    ctx.body = await page.render()
  }
}

Capacidad de prueba

Además de lograr la alta cohesión y el bajo acoplamiento más importantes en la ingeniería de software, la inyección de dependencia también puede mejorar la capacidad de prueba del código.

Una prueba general podríamos escribirla así:

class Car extends Transportation {
  drive() {
    console.log('driving by car')
  }
}

class Student {
  this.transportation = new Car()

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

it('goto school successfully', async () => {
  const driveStub = sinon.sub(Car.prototype, 'drive').resolve()
  const student = new Student()
  student. gotoSchool()
  expect(driveStub.callCount).toBe(1)
})

Aunque dicha prueba unitaria se puede ejecutar normalmente, debido a que la función del Stub está invadida en el prototipo, este es un efecto secundario global que afectará otras pruebas unitarias si se ejecutan aquí. Si los efectos secundarios de Sinon desaparecen al final de la prueba, no afectará la prueba unitaria en serie, pero no será posible realizar pruebas en paralelo. Con el método de inyección de dependencia, no habrá tales problemas.

class Student {
  constructor(transportation: Transportation) {
    this.transportation = transportation
  }

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

it('goto school successfully', async () => {
  const driveFn = sinon.fake()
  const student = new Student({
    { drive: driveFn },
  })
  student.gotoSchool()
  expect(driveFn.callCount).toBe(1)
})

dependencia circular

En el caso de dependencias circulares, generalmente no podemos crear objetos, como las siguientes dos definiciones de clase. Aunque es lógico evitar tal situación, es difícil decir que dicha situación no se utilizará por completo en el proceso de escritura de código.

export class Foo {
  constructor(public bar: Bar) {}
}

export class Bar {
  constructor(public foo: Foo) {}
}

Con DI, a través de la inversión del control de la creación de IoC, cuando el objeto se crea por primera vez, la instancia no se creará realmente, pero se proporcionará un objeto proxy, y la instancia se creará cuando el objeto se use realmente, y luego La dependencia circular se resolverá. En cuanto a por qué debe existir el decorador Lazy aquí, lo explicaremos más adelante cuando lo implementemos más adelante.

@Injectable()
export class Foo {
  constructor(public @Lazy(() => Bar) bar: Bar) {}
}

@Injectable()
export class Bar {
  constructor(public @Lazy(() => Foo) foo: Foo) {}
}

algunas desventajas

Por supuesto, el uso de herramientas DI no está completamente exento de desventajas, las más obvias incluyen:

  • Ciclo de vida incontrolable, porque la creación de instancias del objeto está en IoC, por lo que el programa actual no determina completamente el momento en que se crea el objeto. Por lo tanto, esto requiere que comprendamos bien los principios antes de usar herramientas o marcos, y es mejor leer el código fuente que contiene.

  • Cuando las dependencias van mal, es más difícil localizar cuál va mal. Debido a que las dependencias se inyectan, cuando las dependencias salen mal, solo puede analizarlas a través de la experiencia o de la depuración en vivo en el sitio y realizar una depuración un poco en profundidad para saber dónde está mal el contenido. Esto requiere mucha capacidad de depuración o capacidad de control general del proyecto.

  • El código no se puede leer de forma coherente. Si depende de la implementación, puede ver todo el árbol de ejecución del código desde la entrada hacia abajo; si depende de la abstracción, la relación de conexión entre la implementación específica y la implementación está separada, y generalmente se requieren documentos para ver el panorama general del proyecto.

herramientas comunitarias

Desde la categoría DI de github, puede ver algunas herramientas DI populares https://github.com/topics/dependency-injection?l=typescript.

InversifyJS (https://github.com/inversify/InversifyJS): una poderosa herramienta de inyección de dependencia con una implementación estricta de la abstracción de dependencia; aunque la declaración estricta es buena, es muy repetitiva y detallada de escribir.

TSyringe (https://github.com/microsoft/tsyringe): fácil de usar, heredado de la definición abstracta de angular, vale la pena aprenderlo.

alcanzar competencias básicas

Para implementar las capacidades de las herramientas DI básicas, hacerse cargo de la creación de objetos, realizar la inversión de dependencia y la inyección de dependencia, implementamos principalmente tres capacidades:

  • Análisis de dependencias: Para poder crear un objeto, la herramienta necesita saber qué dependencias existen.

  • Creador de registro: para admitir diferentes tipos de métodos de creación de instancias, admite dependencias directas, dependencias abstractas y creación de fábrica; diferentes contextos registran diferentes implementaciones.

  • Cree una instancia: utilice el creador para crear una instancia, que admita el modo singleton y el modo de instancias múltiples.

Suponiendo que nuestra forma final es un código de ejecución como este, si desea el resultado final, puede hacer clic en el enlace de codificación en línea https://codesandbox.io/s/di-playground-oz2j9.

@Injectable()
class Transportation {
  drive() {
    console.log('driving by transportation')
  }
}

@Injectable()
class Student {
  constructor(
    private transportation: Transportation,
  ) {}

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const container = new Container()
const student = container.resolve(Student)
student.gotoSchool()

Análisis de dependencia

Para permitir que las herramientas DI realicen análisis de dependencia, es necesario habilitar la función decoradora de TS y la función de metadatos del decorador.

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Decorador

Primero, echemos un vistazo a cómo se analiza la dependencia del constructor. Después de habilitar las funciones de decorador y metadatos, intente compilar el código anterior en el patio de juegos de TS y podrá ver que el código JS en ejecución se ve así.

bc8f0160bd6789904a4479ba8260040d.png

Se puede observar que las definiciones de código más críticas son las siguientes:

Student = __decorate([
  Injectable(),
  __metadata("design:paramtypes", [Transportation])
], Student);

Si lee atentamente la lógica de la función __decorate, en realidad es una función de orden superior. Para ejecutar ClassDecorator y Metadata Decorator en orden inverso, traduzca el código anterior, que es equivalente a:

Student = __metadata("design:paramtypes", [Transportation])(Student)
Student = Injectable()(Student)

Luego leemos atentamente la lógica de la función __metadata, que ejecuta la función de Reflect, que es equivalente al código:

Student = Reflect.metadata("design:paramtypes", [Transportation])(Student)
Student = Injectable()(Student)

reflexión

Para los resultados del código anterior, podemos ignorar temporalmente la primera línea y leer el significado de la segunda línea, que es exactamente la capacidad de análisis de dependencia que necesitamos. Reflect.metadata es una función de alto nivel que devuelve una función decoradora. Después de la ejecución, los datos se definen en el constructor. Puede encontrar los datos definidos de este constructor o sus sucesores a través de getMetadata.

eeea816ea2b2e41be0eaef3daef553f0.png

Por ejemplo, en la reflexión anterior, podemos obtener los datos definidos de la siguiente manera:

const args = Reflect.getMetadata("design:paramtypes", Student)
expect(args).toEqual([Transportation])

Propuesta de metadatos reflexivos: https://rbuckton.github.io/reflect-metadata/#syntax

Después de habilitar emitDecoratorMetadata, TS completará automáticamente tres tipos de metadatos al compilar el lugar decorado:

  • design:type El tipo de metadatos de la propiedad actual, que aparece en PropertyDecorator y MethodDecorator;

  • Los metadatos de los parámetros de entrada design:paramtypes aparecen en ClassDecorator y MethodDecorator;

  • design:returntype devuelve metadatos de tipo, que aparecen en MethodDecorator.

f8f7687eb577ee4fcd53d0adb001e7db.png

dependencias de etiquetas

Para que las herramientas DI recopilen y almacenen dependencias, debemos analizar los constructores dependientes en Injectable y luego definir los constructores mediante la reflexión y registrar la descripción de los datos en la reflexión mediante un valor de símbolo.

const DESIGN_TYPE_NAME = {
  DesignType: "design:type",
  ParamType: "design:paramtypes",
  ReturnType: "design:returntype"
};

const DECORATOR_KEY = {
  Injectable: Symbol.for("Injectable"),
};

export function Injectable<T>() {
  return (target: new (...args: any[]) => T) => {
    const deps = Reflect.getMetadata(DESIGN_TYPE_NAME.ParamType, target) || [];
    const injectableOpts = { deps };
    Reflect.defineMetadata(DECORATOR_KEY.Injectable, injectableOpts, target);
  };
}

Esto tiene dos propósitos:

  • Los datos de configuración están marcados por el símbolo interno y el constructor superficial está decorado con Injectable y puede ser creado por IoC.

  • Recopile y ensamble datos de configuración, definidos en el constructor, incluidos datos dependientes, datos de configuración como instancias únicas y instancias múltiples que pueden usarse más adelante.

definir contenedor

Con los datos definidos por el decorador en reflexión se puede crear la parte más importante del Contenedor en IoC. Implementaremos una función de resolución para crear automáticamente la instancia:

const DECORATOR_KEY = {
  Injectable: Symbol.for("Injectable"),
};

const ERROR_MSG = {
  NO_INJECTABLE: "Constructor should be wrapped with decorator Injectable.",
}

export class ContainerV1 {
  resolve<T>(target: ConstructorOf<T>): T {
    const injectableOpts = this.parseInjectableOpts(target);
    const args = injectableOpts.deps.map((dep) => this.resolve(dep));
    return new target(...args);
  }

  private parseInjectableOpts(target: ConstructorOf<any>): InjectableOpts {
    const ret = Reflect.getOwnMetadata(DECORATOR_KEY.Injectable, target);
    if (!ret) {
      throw new Error(ERROR_MSG.NO_INJECTABLE);
    }
    return ret;
  }
}

La lógica central principal es la siguiente:

  • Analice los datos definidos por el decorador Injectable a través de la reflexión del constructor. Si no hay datos, se generará un error; se debe prestar un poco de atención al hecho de que debido a que los datos de reflexión se heredan, solo se puede usar getOwnMetadata para obtener los datos de reflexión del objetivo actual. Asegúrese de que el objetivo actual debe estar decorado.

  • Luego cree recursivamente instancias dependientes a través de dependencias y obtenga la lista de parámetros de entrada del constructor.

  • Finalmente, al crear una instancia del constructor, obtenemos el resultado que queremos.

crear instancia

En este punto, se ha realizado la función más básica de crear objetos y el siguiente código finalmente puede ejecutarse normalmente.

@Injectable()
class Transportation {
  drive() {
    console.log('driving by transportation')
  }
}

@Injectable()
class Student {
  constructor(
    private transportation: Transportation,
  ) {}

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const container = new Container()
const student = container.resolve(Student)
student.gotoSchool()

También puede visitar codesandbox y seleccionar el modo ContainerV1 a la izquierda para ver dicho resultado.

1012a51979f1f826653d53f03fbeeb89.png

Abstracción de dependencia

Luego completamos el IoC básico, pero luego necesitamos cambiar los requisitos, con la esperanza de reemplazar el vehículo con cualquier herramienta que queramos en tiempo de ejecución, y la dependencia de Student aún debe ser un vehículo que se pueda conducir.

A continuación, lo implementamos en dos pasos:

  • Reemplazo de instancia: Reemplace Transporte con Bicicleta en tiempo de ejecución.

  • Abstracción de dependencia: cambie el transporte de clase a interfaz.

Antes de darnos cuenta de la capacidad de reemplazo de instancias y abstracción de dependencias, primero debemos definir la relación entre dependencias e implementación de dependencias, de modo que IoC pueda saber qué instancia crear para inyectar dependencias, por lo que primero debemos hablar sobre Token y Proveedor.

Simbólico

Como token único de dependencia, puede ser String, Symbol, Constructor o TokenFactory. En ausencia de una abstracción dependiente, en realidad es una dependencia directa entre diferentes constructores; String y Symbol son los ID de dependencia que usaremos después de confiar en la abstracción; y TokenFactory se usa cuando realmente queremos realizar referencias circulares de archivos. esquemas.

Podemos ignorar TokenFactory. La otra parte de la definición de Token no necesita implementarse por separado, es solo una definición de tipo:

export type Token<T = any> = string | symbol | ConstructorOf<T>;

Proveedor

La definición de creación de instancia correspondiente al Token registrado en el contenedor, y luego IoC puede crear el objeto de instancia correcto a través del Proveedor después de obtener el Token. Subdividiéndolo aún más, los proveedores se pueden dividir en tres tipos:

  • Proveedor de clase

  • Proveedor de valor

  • Proveedor de fábrica

Proveedor de clase

Utilice constructores para definir la creación de instancias. Generalmente, la versión simple que implementamos anteriormente es en realidad una versión simplificada de este modelo; con una pequeña modificación, es fácil implementar esta versión y, después de implementar ClassProvider, podemos pasar La forma de registrar el Proveedor es para sustituir el medio de transporte del ejemplo anterior.

interface ClassProvider<T = any> {
  token: Token<T>
  useClass: ConstructorOf<T>
}

Proveedor de valor

ValueProvider es muy útil cuando ya existe una implementación única a nivel global, pero las dependencias abstractas se definen internamente. Para dar un ejemplo simple, en el modo de arquitectura concisa, requerimos que la lógica del código central sea independiente del contexto, por lo que si el front-end quiere usar objetos globales en el entorno del navegador, debe definirlos de manera abstracta y luego colocarlos. Estos objetos se pasan a través de ValueProvider.

interface ClassProvider<T = any> {
  token: Token<T>
  useValue: T
}

Proveedor de fábrica

Este proveedor tendrá una función de fábrica y luego creará una instancia, lo cual es muy útil cuando necesitamos usar el patrón de fábrica.

interface FactoryProvider<T = any> {
  token: Token<T>;
  useFactory(c: ContainerInterface): T;
}

Implementar registro y creación.

Después de definir Token y Proveedor, podemos implementar una función de registro a través de ellos y conectar al Proveedor con la creación. La lógica también es relativamente simple, con dos puntos clave:

  • Utilice Map para formar la relación de mapeo entre Token y Proveedor, y al mismo tiempo deduplicar la implementación del Proveedor, y los registrados cubrirán los anteriores. TSyringe se puede registrar varias veces. Si el constructor depende de una matriz de ejemplo, se creará una instancia para cada proveedor por turno; esta situación en realidad rara vez se usa y aumentará la complejidad de la implementación del proveedor. Muy alto, los estudiantes interesados ​​pueden Estudiar esta parte de su implementación y definición.

  • Al analizar diferentes tipos de proveedores, cree diferentes dependencias.

export class ContainerV2 implements ContainerInterface {
  private providerMap = new Map<Token, Provider>();

  resolve<T>(token: Token<T>): T {
    const provider = this.providerMap.get(token);
    if (provider) {
      if (ProviderAssertion.isClassProvider(provider)) {
        return this.resolveClassProvider(provider);
      } else if (ProviderAssertion.isValueProvider(provider)) {
        return this.resolveValueProvider(provider);
      } else {
        return this.resolveFactoryProvider(provider);
      }
    }

    return this.resolveClassProvider({
      token,
      useClass: token
    });
  }

  register(...providers: Provider[]) {
    providers.forEach((p) => {
      this.providerMap.set(p.token, p);
    });
  }
 }

reemplazo de instancia

Luego de implementar la función que soporta el registro de Proveedores, podemos reemplazar el medio de transporte que usan los estudiantes cuando van a la escuela definiendo el Proveedor de Transporte.

const container = new ContainerV2();
container.register({
  token: Transportation,
  useClass: Bicycle
});

const student = container.resolve(Student);
return student.gotoSchool();

Entonces podemos ver el siguiente efecto en codesandbox y finalmente podemos ir a la escuela en bicicleta.

976e7af51b5ebb713a6dd442c9d67910.png

patrón de fábrica

Nos dimos cuenta del reemplazo de dependencias. Antes de implementar la abstracción de dependencias, primero insertamos un nuevo requisito, porque es muy difícil ir a la escuela en bicicleta, por lo que las condiciones de la carretera son mejores los fines de semana y esperamos poder conducir. a la escuela. A través del patrón de fábrica, podemos implementarlo de la siguiente manera:

const container = new ContainerV2();
container.register({
  token: Transportation,
  useFactory: (c) => {
    if (weekday > 5) {
      return c.resolve(Car);
    } else {
      return c.resolve(Bicycle);
    }
  }
});

const student = container.resolve(Student);
return student.gotoSchool();

Aquí hay una introducción simple al modo de fábrica. Tanto TSyringe como InversifyJS tienen funciones de creación del modo de fábrica, lo cual es una forma recomendada; al mismo tiempo, también puede diseñar otras herramientas DI. Algunas herramientas pondrán el juicio de la función de fábrica en donde se declara la clase.

Esto no es imposible. Será más fácil escribir cuando una sola función se implemente individualmente, pero aquí hablaremos sobre el propósito de introducir DI para desacoplar. El juicio lógico de la función de fábrica es en realidad parte de la lógica de negocios y no pertenece al campo de implementación específica; y cuando la implementación se usa en múltiples lógicas de fábrica, la lógica en este lugar se volverá muy extraña.

definir la abstracción

Entonces, después del reemplazo de la instancia, veamos cómo hacer que Transporte sea una abstracción en lugar de un objeto de implementación concreto. Entonces, el primer paso es cambiar la dependencia de Student de una lógica de implementación concreta a una lógica abstracta.

Lo que necesitamos es una abstracción del transporte, un vehículo que se pueda conducir, bicicletas, motocicletas y automóviles; mientras se pueda conducir, se puede usar cualquier automóvil. Luego cree una nueva clase de estudiante para heredar el objeto anterior para distinguirlo y compararlo.

interface ITransportation {
  drive(): string
}

@Injectable({ muiltple: true })
export class StudentWithAbstraction extends Student {
  constructor(protected transportation: ITransportation) {
    super(transportation);
  }
}

Si lo escribe así, encontrará que el análisis de dependencia será incorrecto, porque cuando se compila TS, la interfaz es un tipo y se convertirá en el objeto de construcción correspondiente del tipo en tiempo de ejecución, y la dependencia no se puede resolver correctamente. .

5039719890e82237db246a09ae977ed4.png

Entonces, además de definir un tipo abstracto aquí, también necesitamos definir una etiqueta única para este tipo abstracto, que es la cadena o símbolo en el Token. Generalmente elegimos el símbolo, que es un valor único a nivel mundial. Aquí puede utilizar las múltiples definiciones de TS con el mismo nombre en valor y tipo, y dejar que TS las analice por sí mismo como valor y tipo.

const ITransportation = Symbol.for('ITransportation')
interface ITransportation {
  drive(): string
}

@Injectable({ muiltple: true })
export class StudentWithAbstraction extends Student {
  constructor(
    protected @Inject(ITransportation) transportation: ITransportation,
  ) {
    super(transportation);
  }
}

reemplazar dependencias abstractas

Tenga en cuenta que, además de definir el valor del token de la dependencia abstracta, también necesitamos agregar un decorador adicional para crear la dependencia del parámetro del constructor del marcador y darle una marca de token.

function Inject(token: Token) {
  return (target: ConstructorOf<any>, key: string | symbol, index: number) => {
    if (!Reflect.hasOwnMetadata(DECORATOR_KEY.Inject, target)) {
      const tokenMap = new Map([[key, token]]);
      Reflect.defineMetadata(DECORATOR_KEY.Inject, tokenMap, target);
    } else {
      const tokenMap: Map<number, Token> = Reflect.getOwnMetadata(
        DECORATOR_KEY.Inject,
        target
      );
      tokenMap.set(index, token);
    }
  };
}

Al mismo tiempo, también es necesario cambiar la lógica en Injectable para reemplazar las dependencias en las posiciones correspondientes.

export function Injectable<T>(opts: InjectableDecoratorOpts = {}) {
  return (target: new (...args: any[]) => T) => {
    const deps = Reflect.getMetadata(DESIGN_TYPE_NAME.ParamType, target) || [];
    const tokenMap: Map<number, Token> = Reflect.getOwnMetadata(
      DECORATOR_KEY.Inject,
      target
    );
    if (tokenMap) {
      for (const [index, token] of tokenMap.entries()) {
        deps[index] = token;
      }
    }

    const injectableOpts = {
      ...opts,
      deps
    };
    Reflect.defineMetadata(DECORATOR_KEY.Injectable, injectableOpts, target);
  };
}

Registrar proveedor de resumen

Todavía queda el último paso aquí, se puede inyectar el Proveedor correspondiente del Token, solo necesitamos cambiar la definición del Token del FactoryProvider anterior, y luego habremos alcanzado nuestro objetivo.

const ITransportation = Symbol.for('ITransportation')
interface ITransportation {
  drive(): string
}

const container = new ContainerV2();
container.register({
  token: ITransportation,
  useFactory: (c) => {
    if (weekday > 5) {
      return c.resolve(Car);
    } else {
      return c.resolve(Bicycle);
    }
  }
});
const student = container.resolve(StudentWithAbstraction);
return student.gotoSchool();

Implementar la creación diferida

Anteriormente, implementamos el método de inyección de dependencia basado en constructor, que es muy bueno y no afecta el uso normal del constructor. Pero un problema con esto es que todas las instancias de objetos en el árbol de dependencia se crearán cuando se cree el objeto raíz. De esta manera habrá algo de desperdicio y es posible que aquellas instancias que no se utilicen no se creen originalmente.

Para garantizar que se utilicen las instancias creadas, elegimos crear instancias cuando las usamos en lugar de inicializar el objeto raíz.

Definir uso

Aquí necesitamos cambiar la función Inject para que pueda soportar tanto la decoración del constructor como la decoración de la Propiedad.

const ITransportation = Symbol.for('ITransportation')
interface ITransportation {
  drive(): string
}

@Injectable()
class Student {
  @Inject(ITransportation)
  private transportation: ITransportation

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const container = new Container()
const student = container.resolve(Student)
student.gotoSchool()

decorador de propiedades

Echemos un vistazo a las características de ParameterDecorator y PropertyDecorator combinando los resultados de la compilación de TS y las definiciones de tipos.

A continuación se muestra la descripción en .d.ts.

acc6faa471f279101c66b456b5469b15.png

El siguiente es el resultado de la compilación.

1c8bb607855bc7c2732659ef69631080.png

Puedes ver las siguientes diferencias:

  • La cantidad de parámetros de entrada es diferente, porque ParameterDecorator tendrá los datos de la cantidad de parámetros.

  • La descripción del objeto es diferente: el ParameterDecorator del constructor describe el constructor y el PropertyDecorator describe el prototipo del constructor.

Entonces, al identificar la marca y luego devolver el archivo de descripción de la propiedad, la función getter de la propiedad correspondiente se agrega al Prototipo y se realiza la lógica de creación de objetos durante el uso.

function decorateProperty(_1: object, _2: string | symbol, token: Token) {
  const valueKey = Symbol.for("PropertyValue");
  const ret: PropertyDescriptor = {
    get(this: any) {
      if (!this.hasOwnProperty(valueKey)) {
        const container: IContainer = this[REFLECT_KEY.Container];
        const instance = container.resolve(token);
        this[valueKey] = instance;
      }

      return this[valueKey];
    }
  };
  return ret;
}

export function Inject(token: Token): any {
  return (
    target: ConstructorOf<any> | object,
    key: string | symbol,
    index?: number
  ) => {
    if (typeof index !== "number" || typeof target === "object") {
      return decorateProperty(target, key, token);
    } else {
      return decorateConstructorParameter(target, index, token);
    }
  };
}

Una cosa a tener en cuenta aquí es que en el diseño de la propia descripción de TS, no se recomienda volver a PropertyDescriptor para cambiar la definición de propiedades, pero de hecho, en la implementación de estándares y TS, en realidad hizo esto, por lo que aquí puede ser en el futuro cambiará.

dependencia circular

Después de terminar la creación diferida, hablemos de un problema con una pequeña relación, la dependencia circular. En general, debemos evitar las dependencias circulares de nuestra lógica, pero si tenemos que usarlas, aún debemos proporcionar soluciones para resolver las dependencias circulares.

Por ejemplo, tal ejemplo:

@Injectable()
class Son {
  @Inject()
  father: Father

  name = 'Thrall'

  getDescription() {
    return `I am ${this.name}, son of ${this.father.name}.`
  }
}

@Injectable()
class Father {
 @Inject()
  son: Son

  name = 'Durotan'

  getDescription() {
    return `I am ${this.name}, my son is ${this.son.name}.`
  }
}

const container = new Container()
const father = container.resolve(Father)
console.log(father. getDescription())

Por que hay un problema

El problema se debe al tiempo de ejecución del decorador. El propósito del decorador del constructor es describir el constructor, es decir, cuando se declara el constructor, la lógica del decorador se ejecutará inmediatamente, pero en este momento sus dependencias no han sido declaradas y el valor obtenido aún no está definido.

a19e858fbad32dddffdf8dd630193292.png

bucle de archivos

Además de los bucles dentro de los archivos, también hay bucles entre archivos, como el siguiente ejemplo.

ae37a45d872fd31b857290498a5afb9f.png

Sucederá lo siguiente:

  • Node lee el archivo padre;

  • El archivo padre inicializará un módulo en Node y lo registrará en el total de módulos, pero exports sigue siendo un objeto vacío, esperando asignación;

  • El archivo Padre comienza a ejecutar la primera línea, haciendo referencia al resultado del Hijo;

  • Comience a leer el archivo Son;

  • El archivo Son inicializará un módulo en Node y lo registrará en el total de módulos, pero exports sigue siendo un objeto vacío, esperando ser asignado;

  • El archivo Hijo ejecuta la primera línea, cita el resultado del Padre y luego lee el módulo vacío registrado por el Padre;

  • El hijo comienza a declarar el constructor, luego lee el constructor del padre, pero en este momento no está definido y ejecuta la lógica del decorador;

  • El Módulo de Son asigna exportaciones y finaliza la ejecución;

  • Después de que el Padre lee el constructor del Hijo, comienza a declarar el constructor; lee el constructor del Hijo correctamente y ejecuta la lógica decoradora.

Rompe el ciclo

Cuando ocurre una dependencia circular, la primera idea debería ser romper el ciclo; dejar que la dependencia se convierta en una lógica abstracta sin ciclo y romper la secuencia de ejecución.

export const IFather = Symbol.for("IFather");
export const ISon = Symbol.for("ISon");

export interface IPerson {
  name: string;
}

@Injectable()
export class FatherWithAbstraction {
  @Inject(ISon)
  son!: IPerson;

  name = "Durotan";
  getDescription() {
    return `I am ${this.name}, my son is ${this.son.name}.`;
  }
}

@Injectable()
export class SonWithAbstraction {
  @Inject(IFather)
  father!: IPerson;

  name = "Thrall";
  getDescription() {
    return `I am ${this.name}, son of ${this.father.name}.`;
  }
}

const container = new ContainerV2(
 { token: IFather, useClass: FatherWithAbstraction },
 { token: ISon, useClass: SonWithAbstraction }
);
const father = container.resolve(FatherWithAbstraction);
const son = container.resolve(SonWithAbstraction);
console.log(father.getDescription())
console.log(son.getDescription())

Al definir la abstracción de Persona pública, la función getDescription se puede ejecutar normalmente; al proporcionar los Proveedores de ISON e IFather, se proporcionan las implementaciones específicas de sus respectivas dependencias y el código lógico se puede ejecutar normalmente.

dependencia perezosa

Además de confiar en la abstracción, si realmente se necesitan dependencias circulares, aún podemos resolver este problema a través de medios técnicos, es decir, permitir que la resolución de dependencias se ejecute después de que se define el constructor, en lugar de cuando se declara con el constructor. En este momento, solo se necesita un método simple que utilice la ejecución de funciones, que es la lógica perezosa que mencionamos anteriormente.

Debido a que se promueven variables dentro del alcance de JS, las referencias de variables se pueden mantener en funciones. Siempre que a las variables se les hayan asignado valores cuando se ejecuta la función, las dependencias se pueden resolver correctamente.

@Injectable()
class Son {
  @LazyInject(() => Father)
  father: Father

  name = 'Thrall'

  getDescription() {
    return `I am ${this.name}, son of ${this.father.name}.`
  }
}

@Injectable()
class Father {
  @LazyInject(() => Son)
  son: Son

  name = 'Durotan'

  getDescription() {
    return `I am ${this.name}, my son is ${this.son.name}.`
  }
}

const container = new Container()
const father = container.resolve(Father)
console.log(father. getDescription())

fábrica de tokens

Lo que debemos hacer es agregar un nuevo método de análisis de tokens, que puede usar funciones para obtener dependencias dinámicamente.

interface TokenFactory<T = any> {
  getToken(): Token<T>;
}

Luego agregue un decorador LazyInject y sea compatible con esta lógica.

export function LazyInject(tokenFn: () => Token): any {
  return (
    target: ConstructorOf<any> | object,
    key: string | symbol,
    index?: number
  ) => {
    if (typeof index !== "number" || typeof target === "object") {
      return decorateProperty(target, key, { getToken: tokenFn });
    } else {
      return decorateConstructorParameter(target, index, { getToken: tokenFn });
    }
  };
}

Finalmente, haga que esta lógica sea compatible en Container y escriba una versión V3 de Container.

export class ContainerV3 extends ContainerV2 implements IContainer {
  resolve<T>(tokenOrFactory: Token<T> | TokenFactory<T>): T {
    const token =
      typeof tokenOrFactory === "object"
        ? tokenOrFactory.getToken()
        : tokenOrFactory;

    return super.resolve(token);
  }
}

Finalmente, observe el efecto del uso:

const container = new ContainerV3();
const father = container.resolve(FatherWithLazy);
const son = container.resolve(SonWithLazy);
father.getDescription();
son.getDescription();
84b973cbc08fabf26b69f10648f35119.png

por fin

Hasta ahora básicamente hemos implementado una herramienta DI básica y utilizable, repasemos un poco nuestro contenido:

  • Utilizando la reflexión y la lógica decoradora de TS, implementamos la resolución de dependencias y la creación de objetos;

  • A través de la definición del Proveedor, se realizan el reemplazo de instancias, la abstracción de dependencias y el patrón de fábrica;

  • Al utilizar PropertyDecorator para definir la función getter, logramos una creación diferida;

  • Al obtener dependencias dinámicamente a través de TokenFactory, hemos resuelto las dependencias circulares.

- FIN -

Acerca de la compañía Qi Wu

Qi Wu Troupe es el equipo front-end más grande de 360 ​​Group y participa en el trabajo de los miembros del W3C y ECMA (TC39) en nombre del grupo. Qi Wu Troupe concede gran importancia a la formación de talentos. Hay ingenieros, profesores, traductores, personas de interfaz empresarial, líderes de equipo y otras direcciones de desarrollo para que los empleados elijan, y se complementan con la capacitación correspondiente en habilidades técnicas, habilidades profesionales, habilidades generales, curso de habilidades de liderazgo, etc. Qi Dance Troupe da la bienvenida a todo tipo de talentos destacados a quienes prestar atención y unirse a Qi Dance Troupe con una actitud abierta y de búsqueda de talentos.

bd69e53b4b655e3ab8d79d03ed94843c.png

Supongo que te gusta

Origin blog.csdn.net/qiwoo_weekly/article/details/132288468
Recomendado
Clasificación