Architecture nette pour les applications Web

Je veux partager avec vous une approche que j'utilise depuis de nombreuses années dans le développement d'applications, y compris des applications Web. De nombreux développeurs d'applications de bureau, de serveur et mobiles connaissent bien cette approche. est fondamental lors de la création de telles applications, cependant, il est très mal représenté sur le Web, bien qu'il y ait certainement des gens qui souhaitent utiliser cette approche. De plus, l' éditeur de code VS est écrit sur cette approche .

Architecture pure

En appliquant cette approche, vous vous débarrasserez d'un cadre spécifique. Vous pouvez facilement basculer la bibliothèque de vues dans votre application, par exemple React, Preact, Vue, Mithril sans réécrire la logique métier, et dans la plupart des cas même des vues. Si vous avez une application sur Angular 1, vous pouvez facilement la traduire en Angular 2+, React, Svelte, WebComponents ou même votre bibliothèque de présentation. Si vous avez une application sur Angular 2+, mais qu'il n'y a pas de spécialistes, vous pouvez facilement transférer l'application vers une bibliothèque plus populaire sans réécrire la logique métier. Mais au final, oubliez complètement le problème de la migration du framework vers le framework. Quel genre de magie est-ce?

Qu'est-ce qu'une architecture propre


Pour comprendre cela, il est préférable de lire le livre de Martin Robert «Architecture propre» ( par Robert C.Martin «Architecture propre» ). Un bref extrait est donné dans l' article par référence .

Les principales idées ancrées dans l'architecture:

  1. Indépendance par rapport au cadre. L'architecture ne dépend pas de l'existence d'une bibliothèque. Cela vous permet d'utiliser le framework comme un outil, au lieu de presser votre système dans ses limites.
  2. Testabilité. Les règles métier peuvent être testées sans interface utilisateur, base de données, serveur Web ou tout autre composant externe.
  3. Indépendance de l'interface utilisateur. L'interface utilisateur peut être facilement modifiée sans changer le reste du système. Par exemple, l'interface Web peut être remplacée par la console, sans changer les règles métier.
  4. Indépendance par rapport à la base de données. Vous pouvez échanger Oracle ou SQL Server contre MongoDB, BigTable, CouchDB ou autre chose. Vos règles métier ne sont pas liées à la base de données.
  5. Indépendance vis-à-vis de tout service externe. En fait, les règles de votre entreprise ne savent tout simplement rien du monde extérieur.

Les idées décrites dans ce livre depuis de nombreuses années ont été à la base de la construction d'applications complexes dans divers domaines.

Cette flexibilité est obtenue en divisant l'application en couches de service, de référentiel et de modèle. J'ai ajouté l'approche MVC à Clean Architecture et j'ai obtenu les couches suivantes:

  • Vue - affiche les donnĂ©es pour le client, visualise rĂ©ellement l'Ă©tat de la logique pour le client.
  • ContrĂ´leur - est responsable de l'interaction avec l'utilisateur via IO (entrĂ©e-sortie).
  • Service - est responsable de la logique mĂ©tier et de sa rĂ©utilisation entre les composants.
  • RĂ©fĂ©rentiel - chargĂ© de recevoir des donnĂ©es de sources externes, telles qu'une base de donnĂ©es, une API, un stockage local, etc.
  • Modèles - est responsable du transfert des donnĂ©es entre les couches et les systèmes, ainsi que de la logique de traitement de ces donnĂ©es.

Le but de chaque couche est discuté ci-dessous.

Qui est l'architecture pure


Le développement Web a parcouru un long chemin, du simple script jquery au développement de grandes applications SPA. Et maintenant, les applications Web sont devenues si importantes que la quantité de logique métier est devenue comparable, voire supérieure, aux applications serveur, de bureau et mobiles.

Pour les développeurs qui écrivent des applications complexes et volumineuses, ainsi que transfèrent la logique métier du serveur vers des applications Web pour économiser sur le coût des serveurs, Clean Architecture aidera à organiser le code et à le faire évoluer sans problème à grande échelle.

Dans le même temps, si votre tâche consiste simplement à mettre en page et à animer des pages de destination, alors Clean Architecture n'a simplement nulle part où insérer. Si votre logique métier est en arrière-plan et que votre tâche est d'obtenir les données, de les afficher au client et de traiter le clic sur le bouton, vous ne ressentirez pas la flexibilité de Clean Architecture, mais cela peut être un excellent tremplin pour la croissance explosive de l'application.

Où déjà appliqué?


L'architecture pure n'est liée à aucun cadre, plateforme ou langage de programmation particulier. Pendant des décennies, il a été utilisé pour écrire des applications de bureau. Son implémentation de référence se trouve dans les frameworks pour les applications serveur Asp.Net Core, Java Spring et NestJS. Il est également très populaire lors de l'écriture d'applications iOS et Android. Mais dans le développement web, il est apparu sous une forme extrêmement infructueuse dans les frameworks Angular.

Étant donné que je ne suis pas seulement Typescript, mais aussi un développeur C #, je prendrai par exemple l'implémentation de référence de cette architecture pour Asp.Net Core.

Voici un exemple d'application simplifié:

Exemple d'application sur Asp.Net Core
    /**
     * View
     */

    @model WebApplication1.Controllers.Profile

    <div class="text-center">
        <h1>  @Model.FirstName</h1>
    </div>

    /**
     * Controller
     */

    public class IndexController : Controller
    {
        private static int _counter = 0;
        private readonly IUserProfileService _userProfileService;

        public IndexController(IUserProfileService userProfileService)
        {
            _userProfileService = userProfileService;
        }

        public async Task<IActionResult> Index()
        {
            var profile = await this._userProfileService.GetProfile(_counter);
            return View("Index", profile);
        }

        public async Task<IActionResult> AddCounter()
        {
            _counter += 1;
            var profile = await this._userProfileService.GetProfile(_counter);
            return View("Index", profile);
        }
    }

    /**
     * Service
     */

    public interface IUserProfileService
    {
        Task<Profile> GetProfile(long id);
    }

    public class UserProfileService : IUserProfileService
    {
        private readonly IUserProfileRepository _userProfileRepository;

        public UserProfileService(IUserProfileRepository userProfileRepository)
        {
            this._userProfileRepository = userProfileRepository;
        }

        public async Task<Profile> GetProfile(long id)
        {
            return await this._userProfileRepository.GetProfile(id);
        }
    }

    /**
     * Repository
     */

    public interface IUserProfileRepository
    {
        Task<Profile> GetProfile(long id);
    }

    public class UserProfileRepository : IUserProfileRepository
    {
        private readonly DBContext _dbContext;
        public UserProfileRepository(DBContext dbContext)
        {
            this._dbContext = dbContext;
        }

        public async Task<Profile> GetProfile(long id)
        {
            return await this._dbContext
                .Set<Profile>()
                .FirstOrDefaultAsync((entity) => entity.Id.Equals(id));
        }
    }

    /**
     * Model
     */

    public class Profile
    {
        public long Id { get; set; }
        public string FirstName { get; set; }
        public string Birthdate { get; set; }
    }


Si vous ne comprenez pas qu'il ne dit rien de mal, alors nous l'analyserons en partie chaque fragment.

Un exemple est donné pour une application Asp.Net Core, mais pour Java Spring, WinForms, Android, React, l'architecture et le code seront les mêmes, seuls le langage et le travail avec la vue (le cas échéant) changeront.

Application Web


Le seul framework qui a essayé d'utiliser l'architecture propre était Angular. Mais il s'est avéré horrible, qu'en 1, qu'en 2+.

Et il y a plusieurs raisons Ă  cela:

  1. Cadre monolithique angulaire. Et c'est son principal problème. Si vous n’aimez pas quelque chose, vous devez vous étouffer quotidiennement et vous ne pouvez rien y faire. Non seulement il y a beaucoup de problèmes, mais cela contredit également l'idéologie de l'architecture pure.
  2. DI. , Javascript.
  3. . JSX. , 6, . .
  4. . Rollup ES2015 2 4 , angular 9.
  5. Et bien d'autres problèmes. En général, jusqu'à la technologie angulaire moderne roule avec un retard de 5 ans par rapport à React.

Mais qu'en est-il des autres cadres? React, Vue, Preact, Mithril et autres sont exclusivement des bibliothèques de présentation et ne fournissent aucune architecture ... mais nous avons déjà l'architecture ... il reste à tout assembler en un seul ensemble!

Nous commençons à créer une application


Nous considérerons Pure Architecture par l'exemple d'une application fictive la plus proche possible d'une vraie application web. Il s'agit d'un bureau de la compagnie d'assurance qui affiche le profil de l'utilisateur, les événements assurés, les tarifs d'assurance proposés et les outils pour travailler avec ces données.

Prototype d'application

Dans l'exemple, seule une petite partie de la fonctionnalité sera implémentée, mais à partir de là, vous pouvez comprendre où et comment positionner le reste de la fonctionnalité. Commençons par créer l'application à partir de la couche Controller et connectons la couche View à la toute fin. Et au cours de la création, nous considérons chaque couche plus en détail.

Modèle de contrôleur


Contrôleur - est responsable de l'interaction de l'utilisateur avec l'application. Il peut s'agir d'un clic sur un bouton d'une page Web, d'une application de bureau, d'une application mobile ou de la saisie d'une commande dans une console Linux, ou d'une demande réseau, ou de tout autre événement d'E / S entrant dans l'application.

Le contrĂ´leur le plus simple dans une architecture propre est le suivant:

export class SimpleController { // extends React.Component<object, object>

    public todos: string[] = []; //  

    public addTodo(todo: string): void { //    
        this.todos.push(todo);
    }

    public removeTodo(index: number): void { //    
        this.todos.splice(index, 1);
    }

    // public render(): JSX.Element {...} // view injection

}

Sa tâche est de recevoir un événement de l'utilisateur et de démarrer les processus métier. Dans le cas idéal, le contrôleur ne sait rien de View, puis il peut être réutilisé entre des plates-formes, telles que Web, React-Native ou Electron.

Écrivons maintenant un contrôleur pour notre application. Sa tâche est d'obtenir un profil utilisateur, les tarifs disponibles et d'offrir le meilleur tarif à l'utilisateur:

UserPageController. Contrôleur avec logique métier
export class UserPageControlle {

    public userProfile: any = {};
    public insuranceCases: any[] = [];
    public tariffs: any[] = [];
    public bestTariff: any = {};

    constructor() {
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> { //  
        try {
            const response = await fetch("./api/user-profile");
            this.userProfile = await response.json();
            this.findBestTariff();
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> { //  
        try {
            const response = await fetch("./api/tariffs");
            this.tariffs = await response.json();
            this.findBestTariff();
        } catch (e) {
            console.error(e);
        }
    }

    public findBestTariff(): void { //    
        if (this.userProfile && this.tariffs) {
            this.bestTariff = this.tariffs.find((tarif: any) => {
                return tarif.ageFrom <= this.userProfile.age && this.userProfile.age < tarif.ageTo;
            });
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}


Nous avons un contrôleur régulier sans architecture propre, si nous l'héritons de React.Component, nous obtenons un composant fonctionnel avec la logique. De nombreux développeurs d'applications Web écrivent, mais cette approche présente de nombreux inconvénients importants. Le principal est l'incapacité de réutiliser la logique entre les composants. Après tout, le tarif recommandé peut être affiché non seulement dans votre compte personnel, mais également sur la page de destination et de nombreux autres endroits pour attirer un client au service.

Afin de pouvoir réutiliser la logique entre les composants, il est nécessaire de la placer dans une couche spéciale appelée Service.

Modèle de service


Service - responsable de toute la logique métier de l'application. Si le contrôleur devait recevoir, traiter et envoyer des données, il le fait via le service. Si plusieurs contrôleurs ont besoin de la même logique, ils fonctionnent avec Service. Mais la couche Service elle-même ne doit rien savoir de la couche Controller and View et de l'environnement dans lequel elle fonctionne.

Déplaçons la logique du contrôleur vers le service et implémentons le service dans le contrôleur:

UserPageController. Contrôleur sans logique métier
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";

export class UserPageController {

    public userProfile: any = {};
    public insuranceCases: any[] = [];
    public tariffs: any[] = [];
    public bestTariff: any = {};

    //    
    private readonly userProfilService: UserProfilService = new UserProfilService();
    private readonly tarifService: TariffService = new TariffService();

    constructor() {
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            //     
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            //     
            this.tariffs = await this.tarifService.getTariffs();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

UserProfilService. Service pour travailler avec le profil utilisateur
export class UserProfilService {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

TariffService. Service pour travailler avec les tarifs
export class TariffService {
    //  
    public async getTariffs(): Promise<any> {
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    //    
    public async findBestTariff(userProfile: any): Promise<any> {
        const tariffs = await this.getTariffs();
        return tariffs.find((tarif: any) => {
            return tarif.ageFrom <= userProfile.age &&
                userProfile.age < tarif.ageTo;
        });
    }
    
    /**
     * ...       
     */
}


Désormais, si plusieurs contrôleurs ont besoin d'obtenir un profil utilisateur ou des tarifs, ils peuvent réutiliser la même logique à partir des services. Dans les services, l'essentiel est de ne pas oublier les principes SOLID et que chaque service est responsable de son domaine de responsabilité. Dans ce cas, un service est responsable de travailler avec le profil utilisateur et un autre service est responsable de travailler avec les tarifs.

Mais que se passe-t-il si la source de données change, par exemple, l'extraction peut changer pour websocket ou grps ou la base de données, et les données réelles doivent être remplacées par des données de test? Et en général, pourquoi la logique métier doit-elle savoir quelque chose sur une source de données? Pour résoudre ces problèmes, il existe une couche de référentiel.

Modèle de référentiel


Référentiel - responsable de la communication avec l'entrepôt de données. Le stockage peut être un serveur, une base de données, de la mémoire, un stockage local, un stockage de session ou tout autre stockage. Sa tâche est d'abstraire la couche Service de l'implémentation de stockage spécifique.

Faisons des requêtes réseau à partir des services dans le référentiel, tandis que le contrôleur ne change pas:
UserProfilService. Service pour travailler avec le profil utilisateur
import { UserProfilRepository } from "./UserProfilRepository";

export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository =
        new UserProfilRepository();

    public async getUserProfile(): Promise<any> {
        return await this.userProfilRepository.getUserProfile();
    }
    
    /**
     * ...        
     */
}

UserProfilRepository. Service pour travailler avec le stockage de profil utilisateur
export class UserProfilRepository {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

TariffService. Service pour travailler avec les tarifs
import { TariffRepository } from "./TariffRepository";

export class TariffService {
    
    private readonly tarifRepository: TariffRepository = new TariffRepository();

    public async getTariffs(): Promise<any> {
        return await this.tarifRepository.getTariffs();
    }

    //    
    public async findBestTariff(userProfile: any): Promise<any> {
        //    
        const tariffs = await this.tarifRepository.getTariffs();
        return tariffs.find((tarif: any) => {
            return tarif.ageFrom <= userProfile.age &&
                userProfile.age < tarif.ageTo;
        });
    }
    
    /**
     * ...       
     */
}

TarifRepository. Référentiel pour travailler avec le stockage tarifaire
export class TariffRepository {
    //  
    public async getTariffs(): Promise<any> {
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    /**
     * ...        
     */
}


Il suffit maintenant d'écrire une demande de données une fois et n'importe quel service pourra réutiliser cette demande. Plus tard, nous examinerons un exemple de la façon de redéfinir le référentiel sans toucher au code de service et d'implémenter le référentiel mocha pour les tests.

Dans le service UserProfilService, il peut sembler que ce n'est pas nécessaire et le contrôleur peut accéder directement au référentiel de données, mais ce n'est pas le cas. À tout moment, des exigences peuvent apparaître ou changer dans la couche métier, une demande supplémentaire peut être requise ou des données peuvent être enrichies. Par conséquent, même en l'absence de logique dans la couche de service, la chaîne Contrôleur - Service - Référentiel doit être préservée. C'est une contribution à votre demain.

Il est temps de déterminer quel type de référentiel est défini, s'il est correct du tout. La couche Modèles en est responsable.

Modèles: DTO, Entités, ViewModels


Modèles - est responsable de la description des structures avec lesquelles l'application fonctionne. Une telle description aide grandement les nouveaux développeurs de projets à comprendre avec quoi l'application fonctionne. De plus, il est très pratique de l'utiliser pour créer des bases de données ou valider des données stockées dans le modèle.

Les modèles sont divisés en différents modèles en fonction du type d'utilisation:
  • EntitĂ©s - sont responsables de travailler avec la base de donnĂ©es et sont une structure rĂ©pĂ©tant un tableau ou un document dans la base de donnĂ©es.
  • DTO (Data Transfer Object) - sont utilisĂ©s pour transfĂ©rer des donnĂ©es entre diffĂ©rentes couches de l'application.
  • ViewModel - contient les informations prĂ©dĂ©finies nĂ©cessaires Ă  l'affichage dans la vue.


Ajoutez le modèle de profil utilisateur et d'autres modèles à l'application et faites savoir aux autres couches que nous ne travaillons pas maintenant avec un objet abstrait, mais avec un profil très spécifique:
UserPageController. Au lieu de tout, les modèles décrits sont utilisés.
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";

export class UserPageController {

    /**
     *          
     *          .
     */
    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    private readonly userProfilService: UserProfilService = new UserProfilService();
    private readonly tarifService: TariffService = new TariffService();

    constructor() {
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

UserProfilService. Au lieu de tout, spécifiez le modèle renvoyé
import { UserProfilRepository } from "./UserProfilRepository";
import { UserProfileDto } from "./UserProfileDto";

export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository =
        new UserProfilRepository();

    public async getUserProfile(): Promise<UserProfileDto> { //  
        return await this.userProfilRepository.getUserProfile();
    }
    
    /**
     * ...        
     */
}

TariffService. Au lieu de tout, spécifiez le modèle renvoyé
import { TariffRepository } from "./TariffRepository";
import { TariffDto } from "./TariffDto";
import { UserProfileDto } from "./UserProfileDto";

export class TariffService {
    
    private readonly tarifRepository: TariffRepository = new TariffRepository();

    public async getTariffs(): Promise<TariffDto[]> { //  
        return await this.tarifRepository.requestTariffs();
    }

    //  
    public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
        const tariffs = await this.tarifRepository.requestTariffs();
        return tariffs.find((tarif: TariffDto) => {
            //  userProfile.age  userProfile.getAge()
            const age = userProfile.getAge();
            return age &&
                tarif.ageFrom <= age &&
                age < tarif.ageTo;
        });
    }
    
    /**
     * ...       
     */
}

UserProfilRepository. Au lieu de tout, spécifiez le modèle renvoyé
import { UserProfileDto } from "./UserProfileDto";

export class UserProfilRepository {
    public async getUserProfile(): Promise<UserProfileDto> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

TarifRepository. Au lieu de tout, spécifiez le modèle renvoyé
import { TariffDto } from "./TariffDto";

export class TariffRepository {
    public async requestTariffs(): Promise<TariffDto[]> { //  
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    /**
     * ...        
     */
}

UserProfileDto. Un modèle avec une description des données avec lesquelles nous travaillons
export class UserProfileDto { // <--      
    public firstName: string | null = null;
    public lastName: string | null = null;
    public birthdate: Date | null = null;

    public getAge(): number | null {
        if (this.birthdate) {
            const ageDifMs = Date.now() - this.birthdate.getTime();
            const ageDate = new Date(ageDifMs);
            return Math.abs(ageDate.getUTCFullYear() - 1970);
        }
        return null;
    }

    public getFullname(): string | null {
        return [
            this.firstName ?? "",
            this.lastName ?? ""
        ]
            .join(" ")
            .trim() || null;
    }

}

TarifDto. Un modèle avec une description des données avec lesquelles nous travaillons
export class TariffDto {
    public ageFrom: number = 0;
    public ageTo: number = 0;
    public price: number = 0;
}


Quelle que soit la couche de l'application dans laquelle nous nous trouvons, nous savons exactement avec quelles données nous travaillons. De plus, en raison de la description du modèle, nous avons trouvé une erreur dans notre service. Dans la logique de service, la propriété userProfile.age a été utilisée, qui n'existe pas réellement, mais a une date de naissance. Et pour calculer l'âge, vous devez appeler la méthode du modèle userProfile.getAge ().

Mais il y a un problème. Si nous essayons d'utiliser les méthodes du modèle fourni par le référentiel actuel, nous obtiendrons une exception. Le fait est que les méthodes response.json () et JSON.parse ()Il ne renvoie pas notre modèle, mais un objet JSON, qui n'est en aucun cas associé à notre modèle. Vous pouvez le vérifier si vous exécutez la commande userProfile instanceof UserProfileDto, vous obtenez une fausse déclaration. Afin de convertir les données reçues d'une source externe vers le modèle décrit, il existe un processus de désérialisation des données.

Désérialisation des données


Désérialisation - processus de restauration de la structure nécessaire à partir d'une séquence d'octets. Si les données contiennent des informations non spécifiées dans les modèles, elles seront ignorées. S'il y a des informations dans les données qui contredisent la description du modèle, une erreur de désérialisation se produira.

Et la chose la plus intéressante ici est que lors de la conception de l'ES2015 et de l'ajout du mot - clé class , ils ont oublié d'ajouter la désérialisation ... Le fait que dans toutes les langues est sorti de la boîte, dans ES2015, ils ont simplement oublié ...

Pour résoudre ce problème, j'ai écrit une bibliothèque TS-Serializable pour la désérialisation , un article sur lequel peut être lu sur ce lien . Le but est de retourner la fonctionnalité perdue.

Ajoutez la prise en charge de la désérialisation dans le modèle et la désérialisation elle-même dans le référentiel:
TarifRepository. Ajouter un processus de désérialisation
import { UserProfileDto } from "./UserProfileDto";

export class UserProfilRepository {
    public async getUserProfile(): Promise<UserProfileDto> {
        const response = await fetch("./api/user-profile");
        const object = await response.json();
        return new UserProfileDto().fromJSON(object); //  
    }
    
    /**
     * ...        
     */
}

TarifRepository. Ajouter un processus de désérialisation
import { TariffDto } from "./TariffDto";

export class TariffRepository {
    public async requestTariffs(): Promise<TariffDto[]> { //  
        const response = await fetch("./api/tariffs");
        const objects: object[] = await response.json();
        return objects.map((object: object) => {
            return new TariffDto().fromJSON(object); //  
        });
    }

    /**
     * ...        
     */
}

ProfileDto. Ajout de la prise en charge de la désérialisation
import { Serializable, jsonProperty } from "ts-serializable";

export class UserProfileDto extends Serializable { // <--    

    @jsonProperty(String, null) // <--  
    public firstName: string | null = null;

    @jsonProperty(String, null) // <--  
    public lastName: string | null = null;

    @jsonProperty(Date, null) // <--  
    public birthdate: Date | null = null;

    public getAge(): number | null {
        if (this.birthdate) {
            const ageDifMs = Date.now() - this.birthdate.getTime();
            const ageDate = new Date(ageDifMs);
            return Math.abs(ageDate.getUTCFullYear() - 1970);
        }
        return null;
    }

    public getFullname(): string | null {
        return [
            this.firstName ?? "",
            this.lastName ?? ""
        ]
            .join(" ")
            .trim() || null;
    }

}

TarifDto. Ajout de la prise en charge de la désérialisation
import { Serializable, jsonProperty } from "ts-serializable";

export class TariffDto extends Serializable { // <--    

    @jsonProperty(Number, null) // <--  
    public ageFrom: number = 0;

    @jsonProperty(Number, null) // <--  
    public ageTo: number = 0;

    @jsonProperty(Number, null) // <--  
    public price: number = 0;

}


Maintenant, dans toutes les couches de l'application, vous pouvez être absolument sûr que nous travaillons avec les modèles que nous attendons. Dans la vue, le contrôleur et d'autres couches, vous pouvez appeler les méthodes du modèle décrit.

Ă€ quoi servent Serializable et jsonProperty?
Serializable — Ecmascript Typescript. jsonProperty , Typescript uniontypes. .

Nous avons maintenant une application presque terminée. Il est temps de tester la logique écrite dans les couches Contrôleur, Service et Modèles. Pour ce faire, nous devons renvoyer des données de test spécialement préparées dans la couche Référentiel au lieu d'une véritable demande au serveur. Mais comment remplacer le référentiel sans toucher au code qui entre en production. Il existe un modèle d'injection de dépendance pour cela.

Injection de dépendance - Injection de dépendance


Injection de dépendances - injecte des dépendances dans les couches Contrôleur, Service, Référentiel et vous permet de remplacer ces dépendances en dehors de ces couches.

Dans le programme, la couche Controller dépend de la couche Service et dépend de la couche Repository. Dans la forme actuelle, les couches elles-mêmes provoquent leurs dépendances par instanciation. Et pour redéfinir la dépendance, la couche doit définir cette dépendance de l'extérieur. Il existe plusieurs façons de le faire, mais la plus populaire consiste à transmettre la dépendance en tant que paramètre dans le constructeur.

Ensuite, la création d'un programme avec toutes les dépendances ressemblera à ceci:
var programm = new IndexPageController(new ProfileService(new ProfileRepository()));

D'accord - ça a l'air horrible. Même en tenant compte du fait qu'il n'y a que deux dépendances dans le programme, cela a déjà l'air horrible. Que dire des programmes dans lesquels des centaines et des milliers de dépendances.

Pour résoudre le problème, vous avez besoin d'un outil spécial, mais pour cela, vous devez le trouver. Si nous nous tournons vers l'expérience d'autres plates-formes, par exemple, Asp.Net Core, alors là, l'enregistrement des dépendances se produit au stade de l'initialisation du programme et ressemble à ceci:
DI.register(IProfileService,ProfileService);

puis, lors de la création du contrôleur, le framework lui-même créera et implémentera cette dépendance.

Mais il y a trois problèmes importants:
  1. Lors du transpilage de Typescript en Javascript, il ne reste aucune trace des interfaces.
  2. Tout ce qui est tombé dans la DI classique y reste pour toujours. Il est très difficile de le nettoyer lors du refactoring. Et dans une application Web, vous devez enregistrer chaque octet.
  3. Presque toutes les bibliothèques de vues n'utilisent pas DI, et les concepteurs de contrôleurs sont occupés par les paramètres.


Dans les applications Web, DI est utilisé uniquement dans Angular 2+. Dans Angular 1, lors de l'enregistrement des dépendances, au lieu d'une interface, une chaîne a été utilisée; dans InversifyJS, Symbol est utilisé à la place de l'interface. Et tout cela est mis en œuvre si terriblement qu'il est préférable d'avoir beaucoup de nouveautés comme dans le premier exemple de cette section que ces solutions.

Pour résoudre les trois problèmes, ma propre DI a été inventée, et la solution m'a aidée à trouver le framework Java Spring et son décorateur auto-câblé. La description du fonctionnement de cette DI peut être trouvée dans l'article sur le lien et le référentiel GitHub .

Il est temps d'appliquer le DI résultant dans notre application.

Mettre tous ensemble


Pour implémenter DI sur toutes les couches, nous ajouterons un décorateur de réflexion, ce qui fera que le script typographique générera des méta-informations supplémentaires sur les types de dépendance. Dans le contrôleur où vous devez appeler les dépendances, nous accrocherons le décorateur câblé automatiquement. Et à l'endroit où le programme est initialisé, nous déterminons dans quel environnement quelle dépendance sera implémentée.

Pour le référentiel UserProfilRepository, créez le même référentiel, mais avec des données de test au lieu de la demande réelle. En conséquence, nous obtenons le code suivant:
Main.ts. Emplacement d'initialisation du programme
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";

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

UserPageController. Dépendance à travers le décorateur câblé
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";

export class UserPageController {

    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    @autowired() //  
    private readonly userProfilService!: UserProfilService;

    @autowired() //  
    private readonly tarifService!: TariffService;

    constructor() {
        //     , ..  
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

UserProfilService. Présentation de la réflexion et de la génération de dépendances
import { UserProfilRepository } from "./UserProfilRepository";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository;

    constructor(userProfilRepository: UserProfilRepository) {
        //    
        this.userProfilRepository = userProfilRepository;
    }

    public async getUserProfile(): Promise<UserProfileDto> {
        return await this.userProfilRepository.getUserProfile();
    }

    /**
     * ...        
     */
}

TariffService. Présentation de la réflexion et de la génération de dépendances
import { TariffRepository } from "./TariffRepository";
import { TariffDto } from "./TariffDto";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class TariffService {

    private readonly tarifRepository: TariffRepository;

    constructor(tarifRepository: TariffRepository) {
        //    
        this.tarifRepository = tarifRepository;
    }

    public async getTariffs(): Promise<TariffDto[]> {
        return await this.tarifRepository.requestTariffs();
    }

    public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
        const tariffs = await this.tarifRepository.requestTariffs();
        return tariffs.find((tarif: TariffDto) => {
            const age = userProfile.getAge();
            return age &&
                tarif.ageFrom <= age &&
                age < tarif.ageTo;
        });
    }

    /**
     * ...       
     */
}


UserProfilRepository. Présentation de la génération de réflexion
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class UserProfilRepository {
    public async getUserProfile(): Promise<UserProfileDto> {
        const response = await fetch("./api/user-profile");
        const object = await response.json();
        return new UserProfileDto().fromJSON(object);
    }

    /**
     * ...        
     */
}

MockUserProfilRepository. Nouveau référentiel pour les tests
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class MockUserProfilRepository { //   
    public async getUserProfile(): Promise<UserProfileDto> {
        const profile = new UserProfileDto();
        profile.firstName = "";
        profile.lastName = "";
        profile.birthdate = new Date(Date.now() - 1.5e12);
        return Promise.resolve(profile); //   
    }

    /**
     * ...        
     */
}

TarifRepository. Présentation de la génération de réflexion
import { TariffDto } from "./TariffDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class TariffRepository {
    public async requestTariffs(): Promise<TariffDto[]> {
        const response = await fetch("./api/tariffs");
        const objects: object[] = await response.json();
        return objects.map((object: object) => {
            return new TariffDto().fromJSON(object);
        });
    }

    /**
     * ...        
     */
}


Maintenant, n'importe où dans le programme, il est possible de modifier la mise en œuvre de toute logique. Dans notre exemple, au lieu d'une véritable demande de profil utilisateur au serveur dans l'environnement de test, les données de test seront utilisées.

Dans la vie réelle, les remplacements peuvent être trouvés n'importe où, par exemple, vous pouvez changer la logique d'un service, implémenter l'ancien service en production, et dans le refactoring, il est déjà nouveau. Effectuez des tests A / B avec la logique métier, changez la base de données basée sur des documents en relationnelle et changez généralement la demande réseau en sockets Web. Et tout cela sans arrêter le développement pour réécrire la solution.

Il est temps de voir le résultat du programme. Il existe un calque View pour cela.

Implémentation de la vue


La couche View est chargée de présenter à l'utilisateur les données contenues dans la couche Controller. Dans l'exemple, je vais utiliser React pour cela, mais à sa place peut être n'importe quel autre, par exemple Preact, Svelte, Vue, Mithril, WebComponent ou tout autre.

Pour ce faire, héritez simplement de notre contrôleur de React.Component et ajoutez-y une méthode de rendu avec la représentation de la vue:

Main.ts. Commence Ă  dessiner un composant React
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";
import { UserPageController } from "./UserPageController";
import React from "react";
import { render } from "react-dom";

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

render(React.createElement(UserPageController), document.body);

UserPageController. Hérite de React.Component et ajoute une méthode de rendu
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
import React from "react";

//    React.Component
export class UserPageController extends React.Component<object, object> {

    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    @autowired()
    private readonly userProfilService!: UserProfilService;

    @autowired()
    private readonly tarifService!: TariffService;

    //   
    constructor(props: object, context: object) {
        super(props, context);
    }

    //     
    public componentDidMount(): void {
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
            this.forceUpdate(); //  view   
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
            this.forceUpdate(); //  view   
        } catch (e) {
            console.error(e);
        }
    }

    //   view
    public render(): JSX.Element {
        return (
            <>
                <div className="user">
                    <div className="user-name">
                         : {this.userProfile.getFullname()}
                    </div>
                    <div className="user-age">
                        : {this.userProfile.getAge()}
                    </div>
                </div>
                <div className="tarifs">
                    {/*    */}
                </div>
            </>
        );
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}


En ajoutant seulement deux lignes et un modèle de présentation, notre contrôleur est devenu un composant React avec une logique de travail.

Pourquoi forceUpdate est-il appelé au lieu de setState?
forceUpdate , setState. setState, Redux, mobX ., . React Preact forceUpdate, ChangeDetectorRef.detectChanges(), Mithril redraw . Observable , MobX Angular, . .

Mais même dans une telle implémentation, il s'est avéré que nous étions liés à la bibliothèque React avec son cycle de vie, l'implémentation de la vue et le principe d'invalidation de la vue, ce qui contredit le concept d'architecture propre. Et la logique et la mise en page sont dans le même fichier, ce qui complique le travail parallèle du typographe et du développeur.

SĂ©paration du contrĂ´leur et de la vue


Pour résoudre ces deux problèmes, nous plaçons la couche de vue dans un fichier séparé, et au lieu du composant React, nous créons le composant de base qui fera abstraction de notre contrôleur d'une bibliothèque de présentation spécifique. Dans le même temps, nous décrivons les attributs que le composant peut prendre.

Nous obtenons les modifications suivantes:
UserPageView. Ils ont pris connaissance d'un dossier séparé
import { UserPageOptions, UserPageController } from "./UserPageController";
import React from "react";

export const userPageView = <P extends UserPageOptions, S>(
    ctrl: UserPageController<P, S>,
    props: P
): JSX.Element => (
    <>
        <div className="user">
            <div className="user-name">
                 : {ctrl.userProfile.getFullname()}
            </div>
            <div className="user-age">
                : {ctrl.userProfile.getAge()}
            </div>
        </div>
        <div className="tarifs">
            {/*    */}
        </div>
    </>
);

UserPageOptions. Prendre connaissance et réagir à un fichier séparé
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
import { BaseComponent } from "./BaseComponent";
import { userPageView } from "./UserPageview";

export interface UserPageOptions {
    param1?: number;
    param2?: string;
}

//    BaseComponent
export class UserPageController<P extends UserPageOptions, S> extends BaseComponent<P, S> {

    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    //  
    public readonly view = userPageView;

    @autowired()
    private readonly userProfilService!: UserProfilService;

    @autowired()
    private readonly tarifService!: TariffService;

    //  
    constructor(props: P, context: S) {
        super(props, context);
    }

    //   componentDidMount, . BaseComponent
    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
            this.forceUpdate();
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
            this.forceUpdate();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

BaseComponent Un composant qui nous Ă©loigne d'un cadre particulier
import React from "react";

export class BaseComponent<P, S> extends React.Component<P, S> {

    //  view
    public view?: (ctrl: this, props: P) => JSX.Element;

    constructor(props: P, context: S) {
        super(props, context);
        //    
    }

    //      
    public componentDidMount(): void {
        this.activate && this.activate();
    }

    //      
    public shouldComponentUpdate(
        nextProps: Readonly<P>,
        nextState: Readonly<S>,
        nextContext: any
    ): boolean {
        return this.update(nextProps, nextState, nextContext);
    }

    public componentWillUnmount(): void {
        this.dispose();
    }

    public activate(): void {
        //   
    }

    public update(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean {
        //   
        return false;
    }

    public dispose(): void {
        //   
    }

    //   view
    public render(): React.ReactElement<object> {
        if (this.view) {
            return this.view(this, this.props);
        } else {
            return React.createElement("div", {}, "  ");
        }
    }

}


Maintenant, notre vue est dans un fichier séparé, et le contrôleur n'en sait rien, sauf que c'est le cas. Vous ne devriez peut-être pas injecter la vue via la propriété du contrôleur, mais le faire via le décorateur comme le fait Angular, mais c'est un sujet de réflexion.

Le composant de base contient également une abstraction du cycle de vie du cadre. Ils sont différents dans tous les cadres, mais ils le sont dans tous les cadres. Angulaire est ngOnInit, ngOnChanges, ngOnDestroy. Dans React et Preact, il s'agit de componentDidMount, shouldComponentUpdate, componentWillUnmount. Dans Vue, cela est créé, mis à jour, détruit. En Mithril, il est créé, mis à jour, supprimé. Dans WebComponents, il s'agit de connectedCallback, attributeChangedCallback, disconnectedCallback. Et donc dans chaque bibliothèque. La plupart ont même la même interface ou une interface similaire.

De plus, les composants de la bibliothèque peuvent désormais être développés avec leur propre logique pour une réutilisation ultérieure entre tous les composants. Par exemple, introduire des outils d'analyse, de surveillance, de journalisation, etc.

Nous regardons le résultat


Il ne reste plus qu'à évaluer ce qui s'est passé. L'ensemble du programme a la forme finale suivante:
Main.ts. Le fichier à partir duquel le programme est lancé
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";
import { UserPageController } from "./UserPageController";
import React from "react";
import { render } from "react-dom";

if (process.env.NODE_ENV === "test") {
    override(UserProfilRepository, MockUserProfilRepository);
}

render(React.createElement(UserPageController), document.body);

UserPageView. Représentation d'une des composantes du programme.
import { UserPageOptions, UserPageController } from "./UserPageController";
import React from "react";

export const userPageView = <P extends UserPageOptions, S>(
    ctrl: UserPageController<P, S>,
    props: P
): JSX.Element => (
    <>
        <div className="user">
            <div className="user-name">
                 : {ctrl.userProfile.getFullname()}
            </div>
            <div className="user-age">
                : {ctrl.userProfile.getAge()}
            </div>
        </div>
        <div className="tarifs">
            {/*    */}
        </div>
    </>
);

UserPageController. La logique de l'un des composants de l'interaction utilisateur
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
import { BaseComponent } from "./BaseComponent";
import { userPageView } from "./UserPageview";

export interface UserPageOptions {
    param1?: number;
    param2?: string;
}

export class UserPageController<P extends UserPageOptions, S> extends BaseComponent<P, S> {

    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    public readonly view = userPageView;

    @autowired()
    private readonly userProfilService!: UserProfilService;

    @autowired()
    private readonly tarifService!: TariffService;

    //   componentDidMount, . BaseComponent
    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
            this.forceUpdate();
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
            this.forceUpdate();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

BaseComponent Classe de base pour tous les composants du programme
import React from "react";

export class BaseComponent<P, S> extends React.Component<P, S> {

    public view?: (ctrl: this, props: P) => JSX.Element;

    constructor(props: P, context: S) {
        super(props, context);
    }

    public componentDidMount(): void {
        this.activate && this.activate();
    }

    public shouldComponentUpdate(
        nextProps: Readonly<P>,
        nextState: Readonly<S>,
        nextContext: any
    ): boolean {
        return this.update(nextProps, nextState, nextContext);
    }

    public componentWillUnmount(): void {
        this.dispose();
    }

    public activate(): void {
        //   
    }

    public update(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean {
        //   
        return false;
    }

    public dispose(): void {
        //   
    }

    public render(): React.ReactElement<object> {
        if (this.view) {
            return this.view(this, this.props);
        } else {
            return React.createElement("div", {}, "  ");
        }
    }

}

UserProfilService. Service de réutilisation de la logique entre les composants pour travailler avec un profil utilisateur
import { UserProfilRepository } from "./UserProfilRepository";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection
export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository;

    constructor(userProfilRepository: UserProfilRepository) {
        this.userProfilRepository = userProfilRepository;
    }

    public async getUserProfile(): Promise<UserProfileDto> {
        return await this.userProfilRepository.getUserProfile();
    }

    /**
     * ...        
     */
}

TariffService. Service de réutilisation de la logique entre les composants pour travailler avec les tarifs
import { TariffRepository } from "./TariffRepository";
import { TariffDto } from "./TariffDto";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection
export class TariffService {

    private readonly tarifRepository: TariffRepository;

    constructor(tarifRepository: TariffRepository) {
        this.tarifRepository = tarifRepository;
    }

    public async getTariffs(): Promise<TariffDto[]> {
        return await this.tarifRepository.requestTariffs();
    }

    public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
        const tariffs = await this.tarifRepository.requestTariffs();
        return tariffs.find((tarif: TariffDto) => {
            const age = userProfile.getAge();
            return age &&
                tarif.ageFrom <= age &&
                age < tarif.ageTo;
        });
    }

    /**
     * ...       
     */
}

UserProfilRepository. Référentiel pour recevoir un profil du serveur, le vérifier et le valider
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection
export class UserProfilRepository {
    public async getUserProfile(): Promise<UserProfileDto> {
        const response = await fetch("./api/user-profile");
        const object = await response.json();
        return new UserProfileDto().fromJSON(object);
    }

    /**
     * ...        
     */
}

MockUserProfilRepository.
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class MockUserProfilRepository { //   
    public async getUserProfile(): Promise<UserProfileDto> {
        const profile = new UserProfileDto();
        profile.firstName = "";
        profile.lastName = "";
        profile.birthdate = new Date(Date.now() - 1.5e12);
        return Promise.resolve(profile); //   
    }

    /**
     * ...        
     */
}

TariffRepository. ,
import { TariffDto } from "./TariffDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class TariffRepository {
    public async requestTariffs(): Promise<TariffDto[]> {
        const response = await fetch("./api/tariffs");
        const objects: object[] = await response.json();
        return objects.map((object: object) => {
            return new TariffDto().fromJSON(object);
        });
    }

    /**
     * ...        
     */
}

UserProfileDto.
import { Serializable, jsonProperty } from "ts-serializable";

export class UserProfileDto extends Serializable {

    @jsonProperty(String, null)
    public firstName: string | null = null;

    @jsonProperty(String, null)
    public lastName: string | null = null;

    @jsonProperty(Date, null)
    public birthdate: Date | null = null;

    public getAge(): number | null {
        if (this.birthdate) {
            const ageDifMs = Date.now() - this.birthdate.getTime();
            const ageDate = new Date(ageDifMs);
            return Math.abs(ageDate.getUTCFullYear() - 1970);
        }
        return null;
    }

    public getFullname(): string | null {
        return [
            this.firstName ?? "",
            this.lastName ?? ""
        ]
            .join(" ")
            .trim() || null;
    }

}

TariffDto.
import { Serializable, jsonProperty } from "ts-serializable";

export class TariffDto extends Serializable {

    @jsonProperty(Number, null)
    public ageFrom: number = 0;

    @jsonProperty(Number, null)
    public ageTo: number = 0;

    @jsonProperty(Number, null)
    public price: number = 0;

}


En conséquence, nous avons obtenu une application modulaire évolutive avec une très petite quantité de passe-partout (1 composant de base, 3 lignes pour implémenter les dépendances par classe) et une surcharge très faible (en fait uniquement pour implémenter les dépendances, tout le reste est logique). De plus, nous ne sommes liés à aucune bibliothèque de présentation. À la mort d'Angular 1, beaucoup ont commencé à réécrire des applications dans React. Lorsque les développeurs d'Angular 2 se sont épuisés, de nombreuses entreprises ont commencé à souffrir en raison de la vitesse de développement. Lorsque React meurt à nouveau, vous devrez réécrire les solutions liées à son cadre et à son écosystème. Mais avec Chita Architecture, vous pouvez oublier de lier le cadre.

Quel est l'avantage sur Redux?


Afin de comprendre la différence, voyons comment Redux se comporte lorsque l'application se développe.
Redux

Comme vous pouvez le voir sur le diagramme, avec la croissance des applications Redux à l'échelle verticale, le magasin et le nombre de réducteurs augmentent et se transforment en goulot d'étranglement. Et le montant des frais généraux pour reconstruire le magasin et trouver le bon réducteur commence à dépasser la charge utile.

Vous pouvez vérifier le rapport entre les frais généraux et la charge utile sur une application de taille moyenne avec un simple test.
let create = new Function([
"return {",
...new Array(100).fill(1).map((val, ind) => `param${ind}:${ind},`),
"}"
].join(""));

let obj1 = create();

console.time("store recreation time");
let obj2 = {
    ...obj1,
    param100: 100 ** 2
}
console.timeEnd("store recreation time");

console.time("clear logic");
let val = 100 ** 2;
console.timeEnd("clear logic");

console.log(obj2, val);

// store recreation time: 0.041015625ms
// clear logic: 0.0048828125ms

Recréer le magasin dans 100 propriétés a pris 8 fois plus de temps que la logique elle-même. Avec 1000 éléments, c'est déjà 50 fois plus. De plus, une action utilisateur peut engendrer toute une chaîne d'actions, dont l'appel est difficile à détecter et à déboguer. Vous pouvez certainement affirmer que 0,04 ms pour recréer le magasin est très petit et ne ralentira pas. Mais 0,04 ms est sur le processeur Core i7 et pour une seule action. Étant donné les processeurs mobiles plus faibles et le fait qu'une action d'un seul utilisateur peut engendrer des dizaines d'actions, tout cela conduit au fait que les calculs ne tiennent pas en 16 ms et un sentiment est créé que l'application ralentit.

Comparons la croissance de l'application Clean Architecture:
Architecture pure

Comme on peut le voir en raison de la séparation de la logique et des responsabilités entre les couches, l'application évolue horizontalement. Si un composant doit traiter les données, il se tournera vers le service correspondant sans toucher aux services non liés à sa tâche. Après avoir reçu les données, un seul composant sera redessiné. La chaîne de traitement des données est très courte et évidente, et pour un maximum de 4 sauts de code, vous pouvez trouver la logique nécessaire et la déboguer. De plus, si nous établissons une analogie avec un mur de briques, nous pouvons retirer toute brique de ce mur et la remplacer par une autre sans affecter la stabilité de ce mur.

Bonus 1: fonctionnalité améliorée des composants du cadre


En prime, ils ont eu la possibilité de compléter ou de modifier le comportement des composants de la bibliothèque de présentation. Par exemple, une réaction en cas d'erreur dans la vue ne rend pas l'intégralité de l'application, si une petite révision est effectuée:
export class BaseComponent<P, S> extends React.Component<P, S> {

    ...

    public render(): React.ReactElement<object> {
        try {
            if (this.view) {
                return this.view(this, this.props);
            } else {
                return React.createElement("div", {}, "  ");
            }
        } catch (e) {
            return React.createElement(
                "div",
                { style: { color: "red" } },
                `    : ${e}`
            );
        }
    }

}

Désormais, la réaction ne dessinera pas uniquement le composant dans lequel l'erreur s'est produite. De la même manière simple, vous pouvez ajouter une surveillance, des analyses et d'autres nishtyaki.

Bonus 2: validation des données


En plus de décrire les données et de transférer des données entre les couches, les modèles ont une grande marge d'opportunité. Par exemple, si vous connectez la bibliothèque de validateurs de classe , puis en suspendant simplement les décorateurs, vous pouvez valider les données de ces modèles, y compris avec un peu de raffinement, vous pouvez valider des formulaires web.

Bonus 3: Création d'entités


De plus, si vous aviez besoin de travailler avec une base de données locale, vous pouvez connecter la bibliothèque de types et vos modèles se transformeront en entités par lesquelles la base de données sera générée et exécutée.

Merci pour l'attention


Si vous avez aimé l'article ou l'approche, aimez et n'ayez pas peur d'expérimenter. Si vous êtes un adhérent Redux et que vous n'aimez pas la dissidence, veuillez expliquer dans les commentaires comment vous mettez à l'échelle, testez et validez les données dans votre application.

All Articles