IoC: Inversion of Control. It is an implementation that relies on the Dependence Inversion Principle, which is interface oriented programming. The implementation of IoC relies on third-party containers to decouple dependent objects and reduce development and maintenance costs.

Let’s take a closer look at these concepts through a complete example.

A business module to be expanded

First, let’s look at an example:

class Order{
    constructor(){}
    getInfo(){
        console.log('This is the order information')}}let order = new Order('New Order');
order.getInfo()
Copy the code

The above code for the order management module of a system, the current function is to output order information.

Add the evaluation function to the order module

With the development of the business, it is necessary to add the evaluation function to the order: allow users to evaluate the order to improve the service quality.

Very simple requirements, right? Modify the original code slightly and add the evaluation module:

class Rate{
    star(stars){
        console.log('Your rating on the order is %s star',stars); }}class Order{
    constructor(){
        this.rate = new Rate();
    }
    // Leave out the rest of the module...
}

let order = new Order('New Order');
order.getInfo();
order.rate.star(5);
Copy the code

A small change that was easily implemented: add a rating module and introduce it as a dependency into the orders module. Soon QA passed and now a cup of coffee to celebrate ☕️

Add sharing to the module

As SOON as I picked up the cup, I found the head picture of the product on IM lit up:

PM: If orders and comments can be shared in moments and other scenes, it will improve a lot

RD: OK, I’ll look into it

Just added the scoring module, the sharing module is no big deal:

class Rate(a){ /** Evaluation module implementation */}

class Share(a){
    shareTo(platform){
        switch (platform) {
            case 'wxfriend':
                console.log('Share with your wechat friends');
                break;
            case 'wxposts':
                console.log('Share to wechat moments');
                break;
            case 'weibo':
                console.log('Share to Weibo');
                break;
            default:
                console.error('Sharing failed, please check platform');
                break; }}}class Order{
    constructor(){
        this.rate = new Rate();
        this.share = new Share();
    }
    // Leave out the rest of the module...
}

const order = new Order();
order.share.shareTo('wxposts');
Copy the code

Add a share module again this time, and then introduce it in the orders module. After the rewrite runs the single test, QA then needs to test the Share module and regression test the Order module.

Does something seem wrong? Predictably, the order module is still in the early stages of our product life cycle, and it will be extended/upgraded or maintained frequently in the future. If we changed the main module and dependency module every time, it would meet the requirements, but not be development and test friendly enough: double single tests (if you have one), smoke, regression… Moreover, the business logic and dependencies in the production environment are far more complex than in the example, and an approach that does not fully adhere to the open-closed principle can easily lead to additional bugs.

Remodel modules using IoC ideas

As the name implies, the IoC’s main act is to reverse control of modules. In the example above, we call Order a high-level module and Rate and Share a low-level module; A high-level module relies on a low-level module. IoC inverts this dependency: high-level modules define interfaces, low-level modules implement interfaces; This way we don’t break the open close principle when we modify or add low-level modules. This is usually done by dependency injection: injecting dependent low-level modules into higher-level modules.

Define static properties in high-level modules to maintain dependencies:

class Order {
    // Map used to maintain dependencies
    static modules = new Map(a);constructor(){
        for (let module of Order.modules.values()) {
            // Call the module init method
            module.init(this); }}// Inject modules into the dependency Map
    static inject(module) {
        Order.modules.set(module.constructor.name, module);
    }
    /** ** /
}

class Rate{
    init(order) {
        order.rate = this;
    }
    star(stars){
        console.log('Your rating on the order is %s star',stars); }}const rate = new Rate();
// Inject dependencies
Order.inject(rate);
const order = new Order();
order.rate.star(4); 
Copy the code

In the example above, you maintain your own dependency module in the Order class, while implementing the init method in the module for Order to call when the constructor initializes. The Order can then be called the container, and it takes the dependency into its bag.

Understanding IoC again

Having completed the transformation of the order module, let’s go back to IoC:

Dependency injection is to inject the lower-level dependencies of higher-level modules as parameters, which can modify the lower-level dependencies without affecting the higher-level dependencies.

However, be careful about how you inject, because it is impossible to know all the dependent low-level modules in advance in a high-level module, nor should you rely on the implementation of low-level modules in a high-level module.

Therefore, injection needs to be divided into two parts: high-level modules decoupled from low-level modules by loader mechanism, and instead relied on low-level module abstraction; Low-level modules are implemented abstractly by convention, and dependencies are injected into high-level modules through injectors.

In this way, the high-level module becomes a container for the low-level module, which is oriented to interface programming: it satisfies the conventions of the interfaces initialized by the high-level module. This is inversion of control: giving control to the dependent lower-level modules by injecting dependencies.

More concise and efficient IoC implementation

The implementation of IoC in the above example is still a bit cumbersome: the module needs to declare the init method explicitly, and the container needs explicit injection dependencies and initialization. These business-independent things can be optimized by encapsulation into base classes, inherited by subclasses, or simplified by decorator methods.

Decorators provide a way to add annotations to class declarations and members through metaprogramming syntax. Modifiers in Javascript are currently in the second phase of the call for proposals, but are supported as an experimental feature in TypeScript.

Let’s focus on how to implement IoC through decorators.

Injected through a class decorator

The following example code is TypeScript

First we implement low-level modules that handle only their own business logic and care nothing else:


class Aftermarket {
    repair() {
        console.log('We have received your after-sales request'); }}class Rate {
    star(stars: string) {
        console.log(` score for${stars}Star `); }}class Share {
    shareTo(platform: string) {
        switch (platform) {
            case 'wxfriend':
                console.log('Share with your wechat friends');
                break;
            case 'wxposts':
                console.log('Share to wechat moments');
                break;
            case 'weibo':
                console.log('Share to Weibo');
                break;
            default:
                console.error('Sharing failed, please check platform');
                break; }}}Copy the code

Next we implement a class decorator that instantiates the dependent low-level modules and injects them into the container:

function Inject(modules: any) {
    return function(target: any) {
        modules.forEach((module:any) = > {
            target.prototype[module.name] = new module(a); }); }; }Copy the code

Finally, use this modifier on the container class:

@Inject([Aftermarket,Share,Rate])
class Order {
    constructor() {}
    /** Other implementations are omitted */
}

const order:any = new Order();
order.Share.shareTo('facebook');
Copy the code

Implemented using property modifiers

Use property modifiers in Ursajs to implement injection dependencies.

Ursajs provides the @Resource decorator and @Inject decorator.

Where @resource is the class decorator, the instance of the class it decorates will be injected into the IoC container of Ursajs:

@Resource(a)class Share{}
Copy the code

Inject @inject is an attribute decorator. Use it in classes to Inject instances of the class decorated by @Resource into the specified variable:

class Order{
    @Inject('share')
    share:Share;
    /** Other implementations are omitted */
}
Copy the code

In addition, as a simple and elegant framework, Ursajs also has built-in addressing optimization, which can be more efficient to obtain resources.

There is no silver bullet

Powerful as it is, IoC is still just a design idea, a refinement of a solution for certain scenarios. It cannot and cannot solve all the problems of high coupling. As developers, we need to identify which scenarios work for which solutions.

summary

  • High coupling degree in complex system will lead to high development and maintenance cost
  • IoC uses containers to decouple and reduce system complexity
  • Decorators make IoC simpler and more efficient
  • There is no silver bullet

reference

  • tslang-decorators
  • Ursajs- Dependency injection

🕯 ️ R.I.P.