Angular 2 DI - IoC vs DI && Angular 1 DI - part - 1

What is IoC

Ioc - Inversion of Control, or "Control Inversion". In development, IoC means that the objects you design are handed over to the container control, rather than the traditional way of directly controlling within the objects.   

How to understand IoC? The key to understand IoC well is to clarify "who controls whom, what controls, why is reversal (if there is reversal, there should be a positive reversal), and what aspects are reversed". Let's analyze them in depth.   

  • Who controls whom and what: In traditional programming, we create objects directly in the object through the new way, which is the initiative of the program to create dependent objects; while IoC has a special container to create these objects, that is, the creation of objects controlled by the IoC container; who controls whom? Of course, the IoC container controls the object; what does it control? It mainly controls the acquisition of external resources.

  • Why is it reversed and what is it reversed: if there is reversal, there will be positive reversal. Traditional applications are controlled by ourselves to get dependent objects, that is, positive reversal. Reversal is created and injected with the help of containers. Why is reversal? Because containers help us find and inject dependency objects, objects are only passive acceptance of dependency objects, so they are reversed; which aspects are reversed? Dependent object acquisition is reversed.

What Can IoC Do

Ioc is not a technology, but an idea, an important object-oriented programming principle, which can guide us how to design loosely coupled, better system. Traditional applications are created by us actively within the class, which results in high coupling between the class and the class and is difficult to test. With the IoC container, the control of creating and searching dependent objects is given to the container, and the container injects composite objects, so the objects are loosely coupled, which is also easy to test, facilitate functional reuse and, more importantly, make the program more convenient. The overall architecture has become very flexible.   

In fact, the biggest change IoC brings to programming is not from the code, but from the thought, there has been a "master-slave transposition" change. The application is the boss, and it takes the initiative to get any resources, but in the IoC idea, the application becomes passive, waiting passively for the IoC container to create and inject the resources it needs.     

IoC and DI

DI - Dependency Injection, or "Dependency Injection": Dependency relationships between components are determined by the container at runtime. In other words, the container dynamically injects a dependency relationship into components. The purpose of dependency injection is not to bring more functions to software systems, but to increase the frequency of component reuse and build a flexible and scalable platform for the system. Through the dependency injection mechanism, we only need to specify the resources needed by the target and complete our business logic through simple configuration without any code, and we don't need to care about where the specific resources come from and by whom they are implemented.   

The key to understanding DI is: "Who depends on whom, why needs to rely on, who injects who, what injects". Let's go deep into the analysis.

  • Who depends on whom: Of course, applications depend on IoC containers

  • Why you need dependencies: Applications need IoC containers to provide external resources needed by objects

  • Who injects whom: It's clear that the IoC container injects application-dependent objects

  • What is injected: External resources needed to inject an object (including objects, resources, constant data)

What does IoC have to do with DI? In fact, they are different descriptions of the same concept. Because the concept of inversion of control is vague (it may only be understood as the level of container control object, it is difficult to think of who can maintain dependency), Martin Fowler, a master in 2004, gave a new name: "dependency injection", which clearly describes being compared with IoC. The injection object relies on the IoC container to configure the dependent object.   

Generally speaking, Inversion of Control means that the control of creating objects is transferred. In the past, the initiative of creating objects and the time of creating objects are controlled by applications. Now this right is transferred to the IoC container, which is a factory specially used to create objects. It gives you whatever objects you need. With IoC containers, dependencies change, and the original dependencies disappear. They all rely on IoC containers to establish their relationships through IoC containers.   

Application of DI in Angular 1

There are three ways to declare dependencies in angular 1, which are as follows:

// Mode 1: Use $inject annotation
var fn = function (a, b) {};
fn.$inject = ['a', 'b'];

// Mode 2: Use array-style annotations
var fn = ['a', 'b', function (a, b) {}];

// Mode 3: Implicit declaration
var fn = function (a, b) {}; // Not recommended

In order to support the above declarations, angular 1 uses annotate function to resolve dependencies. The implementation of this function is as follows:

var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; // Matching parameter list
var FN_ARG_SPLIT = /,/; // Parameter separator
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; // Matching parameter term
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; // Remove //or /**/comments

function extractArgs(fn) { // Extract parameter list
  var fnText = fn.toString().replace(STRIP_COMMENTS, ''), // Remove notes
      args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
  return args;
}

function anonFn(fn) {
  var args = extractArgs(fn);
  if (args) {
    return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
  }
  return 'fn';
}

function annotate(fn, strictDi, name) {
  var $inject,
      argDecl,
      last;
      
  if (typeof fn === 'function') {
    if (!($inject = fn.$inject)) { // Determine whether to declare dependencies in the $inject ion manner
      $inject = [];
      if (fn.length) {
        if (strictDi) { // Use strict injection mode, i.e. you can't use implicit declaration
         // The function name is either a non-string or a false value (such as undefined, null), and the default value is undefined when not set 
          if (!isString(name) || !name) { 
            name = fn.name || anonFn(fn);
          }
          throw $injectorMinErr('strictdi',
            '{0} is not using explicit annotation and cannot be 
                 invoked in strict mode', name);
        }
        argDecl = extractArgs(fn); // Handling implicit declarations
        forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
          arg.replace(FN_ARG, function(all, underscore, name) {
              $inject.push(name);
          });
        });
      }
      fn.$inject = $inject;
    }
  } else if (isArray(fn)) { // Using array-style annotations
    last = fn.length - 1; // Getting the fn function
    assertArgFn(fn[last], 'fn');
    $inject = fn.slice(0, last); // Getting dependencies
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject; // Return dependency arrays
}

How can we get the dependency list (i.e. dependency array) of the function by calling annotate function in angular 1? Let's take a closer look at the following questions:uuuuuuuuuuu

Suppose we use array-style annotations to declare fn functions:

var fn = ['a', 'b', function (a, b) {}]

After calling the annotate function, we get the dependency list of fn, which returns ['a','b'].

After getting the dependency list, we can get the corresponding dependency object according to the name of the dependency. Therefore, the storage of dependent names and dependent objects should be in the form of Key - Value (in ES5, we can use object literals, such as var cache = {} to achieve K-V storage). Within angular1, a getService method is provided to retrieve dependent objects. Its concrete realization is as follows:

var INSTANTIATING = {}, // Is it instantiated?
    providerSuffix = 'Provider', // provider suffix
    path = []; // Dependent path

var factory = function(serviceName, caller) { // Example factory
   var provider = providerInjector.get(serviceName + providerSuffix, caller);
   return instanceInjector.invoke(provider.$get, provider, undefined, 
        serviceName);
});

function getService(serviceName, caller) {
      if (cache.hasOwnProperty(serviceName)) { // Dependent objects have been created
        if (cache[serviceName] === INSTANTIATING) {// Judging whether there is cyclic dependency
          throw $injectorMinErr('cdep', 'Circular dependency found:   
              {0}',serviceName + ' <- ' + path.join(' <- '));
        }
        return cache[serviceName];
      } else { // Dependent object not created
        try {
          path.unshift(serviceName); // Used to track dependency paths
          cache[serviceName] = INSTANTIATING;
          // Instantiate and store dependent objects corresponding to serviceName
          return cache[serviceName] = factory(serviceName, caller);
        } catch (err) {
          if (cache[serviceName] === INSTANTIATING) {
            delete cache[serviceName]; // Failed instantiation, removed from cache
          }
          throw err;
        } finally {
          path.shift();
        }
      }
    }

Through the implementation of getService, we can know that if the dependent object already exists, we get it directly from the cache. If the dependent object does not exist, we create the dependent object by calling the provider of the serviceName object, and then save it in the object instance cache. In this way, it indirectly illustrates that in angular 1, all dependent objects are singletons.   

Here we will first explain the Provider a little, and then we will list some problems existing in the angular 1 DI system.   

What is provider? In angular 1, provider is a common JS object with the $get attribute. There are two ways to create providers:

// Way 1: Use Object Approach
module.provider('a',{
  $get: function () {
     return 42;
   }
});

// Mode 2: Use the constructor approach
module.provider('a', function AProvider() {
   this.$get = function() { return 42; };
});

Both of the above methods use the provider method provided by module object to register the provider. The specific implementation of provider in angular 1 is as follows:uuuuuuuuuuu

function provider(name, provider_) {
    // provider cannot be named hasOwnProperty
    assertNotHasOwnProperty(name, 'service');
    // Constructor mode, first instantiate
    if (isFunction(provider_) || isArray(provider_)) {
      provider_ = providerInjector.instantiate(provider_);
    }
    if (!provider_.$get) { // Determine whether the provider_object has the $get attribute
      throw $injectorMinErr('pget', "Provider '{0}' must define $get factory 
          method.", name);
    }
 // Use name + Provider as the Key value and save it in providerCache for instance creation
    return providerCache[name + providerSuffix] = provider_;
  }

Problems in angular 1 DI system

  • Internal caching: All dependencies in an angular 1 application are singletons, and we have no control over whether to use new instances or not.

  • Namespace conflict: In the system, we use strings to identify the name of service, assuming that we already have a CarService in the project, but the same service is introduced in third-party libraries, which can easily lead to confusion.

  • DI coupling is too high: DI functionality in angular 1 has been integrated by the framework, and we can't use its DI features alone.

  • Failure to integrate with the module loader: In browser environment, many scenarios are asynchronous processes. The dependency modules we need are not loaded at the beginning. Maybe we will load the dependency modules when we create them, and then create dependencies. angualr's IoC container can't do this.   

Sum up

This paper first introduces the concepts and functions of IoC and DI, and then describes the practical application of DI in angular 1. In addition, a brief introduction of the implementation of angular 1 DI, but not in-depth introduction of the injector in angular 1, interested students can learn about it by themselves. Finally, we introduce the problems existing in the angular 1DI system, which paves the way for us to learn the angular 2DI system later, and we can better understand its design intention.

Tags: Javascript angular Programming Attribute master-slave

Posted on Fri, 11 Jan 2019 11:36:11 -0500 by daijames