By: Doodle -Bayek

Come doodle work: job.tuya.com/


🤔️ : Aren’t wheels round? Why is it square?

😣 : Because circles work.

DI (Dependency Injection) is an Inversion of Control (IoC of Control). It is an Inversion of Dependency Injection.

Many articles will confuse the two, but in my understanding, IoC is a conceptual design idea. Apart from Spring and Angular, frameworks like React and Vue, we write code according to the rules of the framework, and the framework takes our code and performs a series of transformations, which is also called inversion of control. In contrast, with libraries such as jQuery and Lodash, we call library methods for purposes that are not inversion of control. Going back to DI, DI is a concrete pattern of the IoC idea used to reduce coupling between classes. Dependency injection is a basic concept in frameworks like Spring, Nest.js, Angular, etc.

This paper will implement a basic dependency injection process based on TS, mainly reflects the basic principle of dependency injection.

Let’s start with the following code.

import { AuthService } from "./AuthService";

class AuthController {
  constructor() {
    this.authService = new AuthService(); 
  }
  
  login() {
    this.authService.xxx()
  }
}
Copy the code

We introduced the Service class in Controller and instantiated it directly, with strong dependencies between classes.

In Java Spring, Nest.js and the like, it would look something like this.

/** AuthService.ts **/
@Service(a)// This class goes to the IoC container
class AuthService {
	xxx() {
    // xxx}}Copy the code
/** AuthController.ts **/
import { AuthService } from "./AuthService";

class AuthController {
  @AutoWired(a)// The container helps create and inject AuthService
  authService: AuthService;
  
  login() {
    this.authService.xxx()
  }
}
Copy the code

Instead of a manual new object, an annotation is made to the property, and the container instantiates the corresponding class and assigns a value to the property. The benefits of this are:

  1. For easy collaboration, if two classes are written by two students together, you only need to define interface first and introduce interface as the type. There will be no error if the other side does not develop well, and the Class dependency is completely gone. (However, TS cannot implement automatic injection based on interface. Abstract class-based injection will be implemented as an alternative.)
  2. Easy to replace, if we have a refactored V2 version of service, just change the implementation class.
  3. Easy to test, just give the IoC container a simple Mock instance to test, without worrying about the side effects of the internal new instance.

Front knowledge

A decorator

To be brief, decorators are essentially syntactically sugar for higher-order functions. They can be applied to classes, methods, accessors (getters, setters), and method parameters, and they can get and modify the meta information of the element being applied. Instead of me bringing the document here, I’d better go to the official document

The main ones used here are class decorators and attribute decorators.

  • Component is a class decorator on a class to be handed over to container management.

  • AutoWired is a property decorator that hits properties that require the container to help inject instances. (Implement property injection only for now)

Meta information

In the decorator, we can get some information about the element being applied. For example, in the class decorator, we can get the constructor of the class, the constructor name (that is, the class name), which is the default, and we can define some new information for it, which is called meta information. We can define and read the meta-information to get the dependencies of each component.

Decomposition of demand

  1. An IoC container class is required to create and inject instances.
  2. Class that requires the @Component decorator standard to be container-managed.
  3. The @AutoWired decorator is required to annotate properties that need to be injected automatically.

Suppose we want to implement a function to obtain user information, using the Controller — Service — Repository architecture most commonly used on the Web.

Container implementation:

We maintain two lists, a registration method and a GET method. And then we can take the container new out first.

class Container {
  ** * Components List of maintenance components * instances List of maintenance instances */
  components = new Map<string.any> ();// key -> Constructor
  instances = new Map<string.object> ();// key -> Instance

  /** * Register component *@param Constructor The constructor of the decorated class@param Alias Specifies the name of the component. The default is the class name */
  regist(constructor: Function, alias? :string) {
    let name = alias;
    if(! name) { name =constructor.name;
    }
    if (this.components.has(name)) {
      console.warn("Re-register Component:" + name);
    }
    this.components.set(name, constructor);
		console.log(this);
  }

  /** * get instance, instance is lazily loaded singleton, created on first fetch *@param Alias Component name */
  get(alias: string) {
    if (this.instances.has(alias)) {
      return this.instances.get(alias);
    }
    const component = this.components.get(alias);
    if(! component) {throw "Unregistered: + alias;
    }
    const ins = new component();
    this.instances.set(alias, ins);
    console.log(this);
    returnins; }}const iocContainer = new Container();

Copy the code

@ Component implementation

Target is the constructor of UserRepo, take the constructor target and alias, register it in the container and return the constructor as is.

function Component(alias? :string) {
  return function (target: any) {
    iocContainer.regist(target, alias || target.name);
    return target;
  };
}
Copy the code
@Component(a)class UserRepo {
  getUserById(id: number) {
    return { user: "jj.zhang", id }; }}Copy the code

The components of the container will then have the UserRepo constructor.

The @autowired implementation

function AutoWired(alias? :string) {
  return function (target: any, propertyName: string) {
    let name = alias;
    if(! name) {const classConstructor = Reflect.getMetadata(
        "design:type",
        target,
        propertyName
      );
      console.log(99, classConstructor, target, propertyName);
      name = classConstructor.name;
      if (name === "Object") {
        // If there is no write type, try capitalizing the attribute name to find the instance
        name = camelcase(propertyName, { pascalCase: true}); }}const instance = iocContainer.get(name || "");
    target[propertyName] = instance;
    return instance;
  };
}
Copy the code
@Component(a)class UserService {
  @AutoWired() userRepo! : UserRepo;getUserById(id: number) {
    return this.userRepo.getUserById(id); }}Copy the code

GetMetadata (” Design :type”, target, propertyName)

  • Target is the prototype object for UserService

  • PropertyName is the name of the property, userRepo

  • Design: Type is the built-in metadata key

Get the type of target[“userRepo”], the constructor of the decorated property, whose name property is the string of the property type we want, namely userRepo, and then ask the container for an instance of userRepo to assign to target[“userRepo”].

If the attribute does not declare a type, for example:

  @AutoWired()
  userRepo;
Copy the code

The name property gets “Object”, in which case we convert the propertyName propertyName to pascalCase as an alias to fetch the instance from the container.

Finally, the instances of the container will have an additional instance of UserRepo.

Abstract class injection

Java Spring’s interface injection is one of the things THAT I find so quintessential, and it makes clever use of polymorphism.

But in TypeScript, there is no interface information in the decorator, so interface injection is not possible.

function Component(target: any) {
  // There is no information about TestInterface
}

interface TestInterface {
	test(): string;
}

@Component
class A implements TestInterface {
	test() {
    return 'hello tuya'; }}Copy the code

But you can use abstract classes to achieve a similar effect:

  1. Add an Abstracrs list to the container that maintains all the implementation subclasses of each abstract class.

  2. Add an @impl decorator that collects each pair of abstract classes & subclasses, registers them with the container, and writes meta information indicating that this is an abstract class.

  3. Modify @AutoWired injection logic. According to the meta-information written in step 2, if the attribute type is an abstract class, find the class instance matching the attribute name in all the implementation classes of the abstract class and inject.

Finally, the container is this structure, which leads to the implementation class.

Interestingly, neither Nest.js nor Midway’s Injection library provides a similar solution. Is this a bogus requirement?

After all, even in Java projects, most interfaces go from life to death with only one Implements😅

  • Effect (compact version, complete code is longer will not paste, want to see the point here)
abstract class IUserService {}@Impl(IUserService)
@Component(a)class UserService extends IUserService {}@Impl(IUserService)
@Component(a)class UserServiceV2 extends IUserService {}@Component(a)class UserController {
  // // injects UserService
  // @AutoWired()
  // userService! : IUserService;

  // // inject UserServiceV2
  // @AutoWired("UserServiceV2")
  // userService! : IUserService;

  // Inject UserService, since the name is changed to UserService
  @AutoWired()
  userService;
}
Copy the code

conclusion

The above uses the most basic approach to achieve the basic principle of dependency injection. There are a lot of features that are not implemented, such as constructor injection, asynchronous initialization, etc.

The actual Component can appear anywhere in any file, and in the example above, if you move AutoWired to the front of the Component, you won’t find the dependencies.

There are two reasons:

  1. No pre-scan of all Components.
  2. You should not create instances directly in decorators.

The decorator of the actual mature framework is only responsible for writing meta information, not instantiating it. When the ApplicationContext is started, the global package scan is performed to get all meta information, build dependency diagrams, and ensure that all components are registered with the container. This is an Midway package with injection source code.


Come doodle work: job.tuya.com/