Première DI: Première DI sur les interfaces pour les applications dactylographiées

Je partage une de mes bibliothèques appelée First DI. Cela m'a aidé pendant de nombreuses années à résoudre le problème de l'injection de dépendances dans les applications de navigateur pour les bibliothèques telles que React, Preact, Mithril et autres. Lors de l'écriture de First DI, l'idéologie des bibliothèques DI des langages C # et Java, telles que autofac, java spring autowired, ninject et autres, a été prise comme base. Et tout comme les bibliothèques de ces langages, First DI fonctionne sur la base de la réflexion et des interfaces Typescript.

À quoi sert DI?


En bref, Dependecy Injection (ci-après DI) est l'une des principales parties de la construction d'une architecture propre. Ce qui vous permet de remplacer n'importe quelle partie du programme par une autre implémentation de cette partie du programme. Et DI est un outil pour mettre en œuvre et remplacer des parties du programme.

En particulier, il vous permet de:

  • Remplacez les implémentations logiques individuellement pour chaque plate-forme. Par exemple, dans le navigateur, affichez les notifications via l'API Notifications et lors de l'assemblage des applications mobiles, remplacez-les par des notifications via l'API de la plateforme mobile.
  • Remplacer l'implémentation de la logique dans divers environnements. Par exemple, une passerelle de paiement ou un système de distribution de notification inaccessible depuis la machine locale du développeur peut être utilisé dans le circuit du produit. Ainsi, au lieu de paiements réels ou d'envois postaux au stade du développement, la logique peut être remplacée par un talon.
  • Remplacez la source de données. Par exemple, au lieu d'une véritable demande au serveur, réduisez les données pré-préparées pour tester la logique métier ou la mise en page pour la conformité avec les mises en page.
  • et bien d'autres utilisations utiles, mais pas à ce sujet maintenant ...

Pour commencer, préparez les outils pour le travail.

Préparation à l'utilisation


Pour l'utiliser, vous devez faire seulement 3 choses simples:

  1. Connectez le polyfill de réflexion-métadonnées .
  2. Dans le fichier tsconfig, activez les options emitDecoratorMetadata et experimentalDecorators. Le premier permet de générer de la réflexion, le second inclut le support des décorateurs
  3. Créez n'importe quel décorateur vide, par exemple const réflexion = (..._ params: Object []): void => {} ou utilisez celui prêt à l'emploi de la bibliothèque.

Je vais maintenant démontrer l'utilisation sous sa forme la plus simple.

Utilisation simple


Par exemple, prenez un programme écrit en utilisant Pure Architecture:

import { autowired, override, reflection } from "first-di";

@reflection // typescript  
class ProdRepository { //   

    public async getData(): Promise<string> {
        return await Promise.resolve("production");
    }

}

@reflection
class MockRepository { //       

    public async getData(): Promise<string> {
        return await Promise.resolve("mock");
    }

}

@reflection
class ProdService {

    constructor(private readonly prodRepository: ProdRepository) { }

    public async getData(): Promise<string> {
        return await this.prodRepository.getData();
    }

}

class ProdController { //   React, Preact, Mithril  .

    @autowired() //  
    private readonly prodService!: ProdService;

    // constructor  ,   React, Preact, Mithril
    //      

    public async getData(): Promise<string> {
        return await this.prodService.getData();
    }

}

if (process.env.NODE_ENV === "test") { //    
    override(ProdRepository, MockRepository);
}

const controllerInstance = new ProdController(); //   
const data = await controllerInstance.getData();

if (process.env.NODE_ENV === "test") {
    assert.strictEqual(data, "mock"); //  
} else {
    assert.strictEqual(data, "production"); //  
}

Dans Pure Architecture, vous devez ajouter seulement 2 lignes de code, câblées automatiquement et remplacer pour que DI commence à fonctionner. Et maintenant, le programme a déjà une implémentation pour la production et une implémentation pour les tests.

Étant donné que First DI a été écrit principalement pour les applications clientes, par défaut, les dépendances seront implémentées en tant que Singleton. Afin de modifier globalement ce comportement, il existe des options par défaut, ainsi que des paramètres pour le décorateur câblé automatiquement et la méthode override ().

Vous pouvez modifier comme suit:

//  1 
import { DI, AutowiredLifetimes } from "first-di";
DI.defaultOptions.lifeTime = AutowiredLifetimes.PER_INSTANCE;

//  2  
import { autowired, AutowiredLifetimes } from "first-di";
@autowired({lifeTime: AutowiredLifetimes.PER_INSTANCE})
private readonly prodService!: ProdService;

//  3  
import { override, AutowiredLifetimes } from "first-di";
override(ProdRepository, MockRepository, {lifeTime: AutowiredLifetimes.PER_INSTANCE});

C'est la façon la plus simple d'utiliser DI. Mais dans l'entreprise, au lieu d'implémentations spécifiques, des interfaces sont utilisées, qui sont un contrat pour les implémentations qui seront implémentées. Il existe un deuxième mode de fonctionnement pour cela.

Usage professionnel


Pour un usage professionnel, nous remplaçons les implémentations spécifiques par des interfaces. Mais dactylographié ne génère pas de code pour exécuter des interfaces en runtime. Heureusement, il existe une solution simple, pour comprendre il faut rappeler la théorie ... qu'est-ce qu'une interface? Ceci est une classe complètement abstraite!

Heureusement, typescript prend en charge les classes entièrement abstraites, génère du code d'exécution et permet l'utilisation de classes abstraites pour décrire les types.

Nous utiliserons ces informations et rédigerons un programme d'entreprise:

import { autowired, override, reflection } from "first-di";

abstract class AbstractRepository { //    

    abstract getData(): Promise<string>;

}

@reflection
class ProdRepository implements AbstractRepository {

    public async getData(): Promise<string> {
        return await Promise.resolve("production");
    }

}

@reflection
class MockRepository implements AbstractRepository {

    public async getData(): Promise<string> {
        return await Promise.resolve("mock");
    }

}

abstract class AbstractService { //    

    abstract getData(): Promise<string>;

}

@reflection
class ProdService implements AbstractService {

    private readonly prodRepository: AbstractRepository;

    constructor(prodRepository: AbstractRepository) {
        this.prodRepository = prodRepository;
    }

    public async getData(): Promise<string> {
        return await this.prodRepository.getData();
    }

}

class ProdController { //   React, Preact, Mithril  .

    @autowired()
    private readonly prodService!: AbstractService;

    // constructor  ,   React, Preact, Mithril
    //      

    public async getData(): Promise<string> {
        return await this.prodService.getData();
    }

}

override(AbstractService, ProdService);

if (process.env.NODE_ENV === "test") {
    override(AbstractRepository, MockRepository);
} else {
    override(AbstractRepository, ProdRepository);
}

const controllerInstance = new ProdController();
const data = await controllerInstance.getData();

if (process.env.NODE_ENV === "test") {
    assert.strictEqual(data, "mock");
} else {
    assert.strictEqual(data, "production");
}

Nous avons maintenant un programme professionnel prêt à l'emploi qui n'a pas d'implémentation par défaut, et tous les contrats sont remplacés par des implémentations déjà au stade de l'assemblage de l'application.

Il est aussi simple d'intégrer DI dans votre application. Et ceux qui ont écrit en C # et Java pourront utiliser l'expérience existante en développement web.

Autres caractéristiques


Utilisation de plusieurs copies de DI:

import { DI } from "first-di";
import { ProductionService } from "../services/ProductionService";

const scopeA = new DI();
const scopeB = new DI();

export class Controller {

    @scopeA.autowired()
    private readonly serviceScopeA!: ProductionService;

    @scopeB.autowired()
    private readonly serviceScopeB!: ProductionService;

    // constructor  ,   React, Preact, Mithril
    //      

    public async getDataScopeA(): Promise<string> {
        return await this.serviceScopeA.getData();
    }

    public async getDataScopeB(): Promise<string> {
        return await this.serviceScopeB.getData();
    }

}

Extensibilité, vous pouvez écrire votre propre DI:

import { DI } from "first-di";

class MyDI extends DI {
    // extended method
    public getAllSingletons(): IterableIterator<object> {
        return this.singletonsList.values();
    }
}

Et de nombreuses fonctionnalités plus intéressantes sont décrites dans le référentiel sur GitHub .

Pourquoi le vôtre est-il écrit et non utilisé?


La raison est simple - lorsque cette DI a été écrite, il n'y avait pas d'alternative. C'était un moment où angulaire 1 n'était plus pertinent, et angulaire 2 n'allait pas sortir. Les cours viennent d'apparaître en Javascript, et la réflexion en tapuscrit. Soit dit en passant, l'apparition de la réflexion est devenue le principal moteur de l'action.

InversifyJS - même dans sa forme actuelle ne me convient pas. Trop de passe-partout. De plus, l'enregistrement par ligne ou caractère de l'autre côté tue la possibilité de refactoriser les dépendances.

Aimé?


Si vous avez aimé cette aide DI, rendez-la plus populaire. Expérimentez-le, envoyez des demandes, mettez des étoiles. Dépôt GitHub .

All Articles