Universal Dependency Injection

Universal Dependency Injection

Dependency injection is a solution in which a system supplies a target 1…n dependencies from external sources; rather than requiring the target to create those dependencies itself.

Dependencies are services, objects, functions, or values that a class (or factory, function) needs to perform its function.

Most developers think DI is only useful for testing with mocks, spys, and stubs. The true value of DI, however, is its ability to decouple origination (configuration, construction, and caching) from destination usages.

No alt text provided for this image

When we say injected, we mean used-by! The best way to provide injected instances is via the constructor (or function arguments). So when we say injected, we mean “passed in as constructor arguments”.

No alt text provided for this image

In our illustration above, we presented a `ContactsFacade`; which could be implemented in two ways:

Traditional Scenario

Consider the following coupling of components:

No alt text provided for this image
No alt text provided for this image

Without DI systems, developers interested in using an instance of ContactsFacade must 1st prepare instances of ContactsService and ContactsStore. Those instances are passed as construction arguments to ContactsFacade.

No alt text provided for this image

After construction, developers interested in sharing the same instance of a service or data model must consider how those instances will be cached, shared, and accessed.

This manual process is not required when a proper DI system is available.

Traditional DI in React

React provides a Context as a way to pass data through the component tree without having to pass props down manually at every level. This allows deep child components to easily lookup services (using Context.Consumer )… services that were previously provided higher in the DOM tree (using Context.Provider).

IMO, such constructs ^ also clutter the HTML markup and the DOM tree.

`useContext()` is another approach that provides programmatic lookups to services registered higher up the view hierarchy.

Lee Warrick recently wrote an article that discusses “Context API’s Dirty Little Secret”. His article emphasizes why the Context should not be used.

No alt text provided for this image

Seasoned developers may even consider Higher-Order Functions (HoF) to encapsulate configuration information (or services) that will be needed for future (deep-tree) use.

Unfortunately, none of these provide a true dependency injection infrastructure nor address the true intent of DI.

  • These approaches do not address the goals of automating construction and decoupling construction dependencies.
  • These solutions do not address the issues of how to test with mocks, etc.

A true dependency injection (DI) system easily solves these issues!


Universal DI: Requirements

A robust DI system allows provides the following features to developers.

No alt text provided for this image

Question: “How can we build a framework-agnostic DI system that can be used in React or raw JavaScript/TypeScript applications?”

A more universal solution will require the following features:

  • Support construction & lookups using tokens
  • Configure the DI system for custom application requirements
  • Provide an easy way to access singleton (shared) instances
  • Provide an easy way to create a non-shared, localized instance
  • Provide an easy way to override/replace existing configurations
  • Provide an easy way to extend existing configurations

Exploring the DI-Flow

Dependency injection uses a lookup process to determine HOW to create an object.

No alt text provided for this image

Using the concept of Provider(s) featured and documented in Angular, we can implement a platform-independent approach:

No alt text provided for this image

The Token (<token>) itself is simply an identifier object used to lookup the cached object instance. In most cases, the token is a specific Class. Sometimes, however, a class reference is not possible. The <token> could be a

  • Class
  • string (not unique)
  • InjectionToken

Developers should use an InjectionToken whenever the type you are injecting is not reified (does not have a runtime representation); such as when injecting an interface, callable type, array or parameterized type.

Above ^ we see that the <token> is used as a lookup for the associated factory methods that will be used to create those instance… and the token is also used as a lookup for an existing instance (which may be internally cached as a singleton).

Developers should note the deps:any[] options that allow each factory to define subsequent dependencies that are needed for proper construction. (soon we will see how this is used)



Custom Configuring DI (for your needs):

If we consider [again] the ContactsFacade dependency tree:

No alt text provided for this image

This construction process is non-trivial and requires a tree-like instantiation process. So how can one easily configure construction relationships to allow programmatic DI that can:

  • easily be used at any view level
  • supports singleton instances
  • supports override (non-singleton) instances
  • supports multiple DI providers

We can easily build a custom injector instance using the makeInjector() factory. Below, we registered a set (1…n) of Contact Provider configurations to create a custom Contact injector.

No alt text provided for this image
The best part of the custom injector is that its on-demand (lazy) feature. When a developer uses `injector.get(<token>)`, the cache registry is first checked to see if the instance exists. If not, only then will the required factories be called to create and cache the instance.


DI with React Components

Using our custom injectors in the View layer is trivial: we import and then use the `injector.get(<token>)` syntax!

No alt text provided for this image

From the perspective of the ContactsList view component ^, the view is not concerned with the’how’, ‘when’, or ‘where’ the ContactsService was constructed or cached.

Instead the view component is only interested in the USE of the ContactsService API.


Using `useInjectorHook()`

Our use of the DI features in the View layer can be simplified even more if we build a custom Injection hook using the useInjectorHook() API (see line 11):

No alt text provided for this image

Now - within the View layer- the DI code is super simple (see line 14):

No alt text provided for this image

Consider an additional requirement to test the view component in isolation. This means testing will need to inject a fake or mock service ContactsService into the ContactsList view component.

With DI, it is super easy to replace a ‘real’ provider with a ‘mock’ provider:

No alt text provided for this image

With this replacement on lines 10–13 above, the existing Provider configurations (defined in ./contacts) will be overridden.

Future requests for an injected ContactsService instance will then deliver only a mock ContactsService.

Considerations

The astute reader will realize that both of the following are required to build custom injectors:

  • `makeInjector()`, and
  • `import {injector} from '….'`

These two (2) manual steps are required since a DI system has not been architected within React like it is in Angular!

Fortunately, this DI solution that can be used in any view component, at any view-depth, or within any (non-view) service! The manual part is a small investment of effort that yields huge ROI with true dependency injection.

Using the Library

Install the DI features into your project using:

npm install @mindspace-io/utils --save

To configure your own custom injector, just just import the DI features:

import { 
   makeInjector, 
   DependendecyInjector, 
   InjectionToken 
} from '@mindspace-io/utils';

Don’t forget to build a custom hook to make DI lookups super easy!


要查看或添加评论,请登录

Thomas Burleson的更多文章

  • Leverage your Git Code Reviews

    Leverage your Git Code Reviews

    During the last few years, I have provided Pull Request code reviews for many FE engineers at Degreed.com.

  • Essence of RxJS

    Essence of RxJS

    RxJS is a great solution and pattern for managing streams of async data emissions. We learn to use operators to…

    1 条评论
  • Push-based Architectures with RxJS

    Push-based Architectures with RxJS

    Before I can show you HOW to implement Push-Based architectures, I need to first describe WHY Pull-Based solutions are…

  • Angular: You may not need NgRx!

    Angular: You may not need NgRx!

    You may not need to use NgRx in your application. But you should use Facades in your application! Let’s explore why…

    2 条评论

社区洞察

其他会员也浏览了