Arquitectura de red para aplicaciones web

Quiero compartir con ustedes un enfoque que he estado utilizando durante muchos años en el desarrollo de aplicaciones, incluidas las aplicaciones web. Muchos desarrolladores de aplicaciones de escritorio, servidores y dispositivos móviles están familiarizados con este enfoque. es fundamental cuando se compilan tales aplicaciones, sin embargo, está muy mal representado en la web, aunque definitivamente hay personas que desean utilizar este enfoque. Además, el editor de VS Code está escrito sobre este enfoque .

Arquitectura pura

Como resultado de aplicar este enfoque, se deshará de un marco específico. Puede cambiar fácilmente la biblioteca de vistas dentro de su aplicación, por ejemplo React, Preact, Vue, Mithril sin tener que volver a escribir la lógica de negocios y, en la mayoría de los casos, incluso las vistas. Si tiene una aplicación en Angular 1, puede traducirla fácilmente a Angular 2+, React, Svelte, WebComponents o incluso a su biblioteca de presentaciones. Si tiene una aplicación en Angular 2+, pero no tiene especialistas, puede transferir fácilmente la aplicación a una biblioteca más popular sin tener que reescribir la lógica empresarial. Pero al final, olvídate por completo del problema de la migración del marco al marco. ¿Qué tipo de magia es esta?

¿Qué es la arquitectura limpia?


Para entender esto, es mejor leer el libro de Martin Robert "Arquitectura limpia» ( por Robert C.Martin "Arquitectura limpia» ). Un breve extracto del cual se da en el artículo como referencia .

Las ideas principales incrustadas en la arquitectura:

  1. Independencia del marco. La arquitectura no depende de la existencia de ninguna biblioteca. Esto le permite utilizar el marco como una herramienta, en lugar de exprimir su sistema dentro de sus limitaciones.
  2. Testabilidad Las reglas comerciales se pueden probar sin una interfaz de usuario, base de datos, servidor web o cualquier otro componente externo.
  3. Independencia de la IU. La interfaz de usuario se puede cambiar fácilmente sin cambiar el resto del sistema. Por ejemplo, la interfaz web puede ser reemplazada por la consola, sin cambiar las reglas de negocio.
  4. Independencia de la base de datos. Puede intercambiar Oracle o SQL Server por MongoDB, BigTable, CouchDB u otra cosa. Sus reglas de negocio no están relacionadas con la base de datos.
  5. Independencia de cualquier servicio externo. De hecho, sus reglas comerciales simplemente no saben nada sobre el mundo exterior.

Las ideas descritas en este libro durante muchos años han sido la base para construir aplicaciones complejas en varios campos.

Esta flexibilidad se logra dividiendo la aplicación en capas de Servicio, Repositorio, Modelo. Agregué el enfoque MVC a Clean Architecture y obtuve las siguientes capas:

  • Ver : muestra datos al cliente, en realidad visualiza el estado de la lógica al cliente.
  • Controlador : es responsable de interactuar con el usuario a través de IO (entrada-salida).
  • Servicio : es responsable de la lógica empresarial y su reutilización entre componentes.
  • Repositorio : responsable de recibir datos de fuentes externas, como una base de datos, api, almacenamiento local, etc.
  • Modelos : es responsable de transferir datos entre capas y sistemas, así como de la lógica de procesamiento de estos datos.

El propósito de cada capa se discute a continuación.

¿Quién es arquitectura pura?


El desarrollo web ha recorrido un largo camino, desde simples secuencias de comandos jquery hasta el desarrollo de grandes aplicaciones de SPA. Y ahora las aplicaciones web se han vuelto tan grandes que la cantidad de lógica empresarial se ha vuelto comparable o incluso superior a las aplicaciones móviles, de escritorio y de servidor.

Para los desarrolladores que escriben aplicaciones complejas y grandes, así como también transfieren la lógica empresarial del servidor a las aplicaciones web para ahorrar en el costo de los servidores, Clean Architecture ayudará a organizar el código y escalar sin problemas a gran escala.

Al mismo tiempo, si su tarea es solo el diseño y la animación de las páginas de destino, entonces Clean Architecture simplemente no tiene dónde insertar. Si su lógica de negocios está en el back-end y su tarea es obtener los datos, mostrarlos al cliente y procesar el clic en el botón, entonces no sentirá la flexibilidad de Clean Architecture, pero puede ser un excelente trampolín para el crecimiento explosivo de la aplicación.

¿Dónde ya se aplica?


La arquitectura pura no está vinculada a ningún marco, plataforma o lenguaje de programación en particular. Durante décadas, se ha utilizado para escribir aplicaciones de escritorio. Su implementación de referencia se puede encontrar en los marcos para aplicaciones de servidor Asp.Net Core, Java Spring y NestJS. También es muy popular al escribir aplicaciones iOs y Android. Pero en el desarrollo web, apareció en una forma extremadamente infructuosa en los marcos angulares.

Como yo mismo no solo soy Typecript, sino también un desarrollador de C #, por ejemplo, tomaré la implementación de referencia de esta arquitectura para Asp.Net Core.

Aquí hay una aplicación de muestra simplificada:

Aplicación de muestra en 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 no comprende que no dice nada malo, lo analizaremos en partes de cada fragmento.

Se da un ejemplo para una aplicación Asp.Net Core, pero para Java Spring, WinForms, Android, React la arquitectura y el código serán los mismos, solo cambiará el idioma y el trabajo con la vista (si corresponde).

Aplicación web


El único marco que intentó usar Clean Architecture fue Angular. Pero resultó horrible, que en 1, que en 2+.

Y hay muchas razones para esto:

  1. Marco monolítico angular. Y este es su principal problema. Si no le gusta algo, tiene que ahogarse a diario y no hay nada que pueda hacer al respecto. No solo hay muchos problemas en él, sino que también contradice la ideología de la arquitectura pura.
  2. DI. , Javascript.
  3. . JSX. , 6, . .
  4. . Rollup ES2015 2 4 , angular 9.
  5. Y muchos más problemas. En general, hasta la tecnología moderna angular funciona con un retraso de 5 años en relación con React.

Pero, ¿qué pasa con otros marcos? React, Vue, Preact, Mithril y otros son bibliotecas de presentación exclusivas y no proporcionan ninguna arquitectura ... pero ya tenemos la arquitectura ... ¡queda por ensamblar todo en un todo!

Empezamos a crear una aplicación.


Consideraremos Pure Architecture por el ejemplo de una aplicación ficticia que esté lo más cerca posible de una aplicación web real. Esta es una oficina en la compañía de seguros que muestra el perfil del usuario, los eventos asegurados, las tarifas de seguro propuestas y las herramientas para trabajar con estos datos.

Prototipo de aplicación

En el ejemplo, solo se implementará una pequeña parte de lo funcional, pero desde allí puede comprender dónde y cómo colocar el resto de lo funcional. Comencemos a crear la aplicación desde la capa Controlador y conectemos la capa Vista al final. Y en el curso de la creación, consideramos cada capa con más detalle.

Patrón controlador


Controlador : es responsable de la interacción del usuario con la aplicación. Puede ser un clic en un botón en una página web, una aplicación de escritorio, una aplicación móvil o ingresar un comando en una consola Linux, o una solicitud de red, o cualquier otro evento IO que ingrese a la aplicación.

El controlador más simple en una arquitectura limpia es el siguiente:

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

}

Su tarea es recibir un evento del usuario e iniciar procesos comerciales. En el caso ideal, el controlador no sabe nada sobre View, y luego puede reutilizarse entre plataformas, como Web, React-Native o Electron.

Ahora escribamos un controlador para nuestra aplicación. Su tarea es obtener un perfil de usuario, tarifas disponibles y ofrecer la mejor tarifa al usuario:

UserPageController. Controlador con lógica empresarial
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;
            });
        }
    }

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


Tenemos un controlador normal sin una arquitectura limpia, si lo heredamos de React.Component obtenemos un componente funcional con lógica. Muchos desarrolladores de aplicaciones web escriben, pero este enfoque tiene muchos inconvenientes importantes. La principal es la incapacidad de reutilizar la lógica entre componentes. Después de todo, la tarifa recomendada se puede mostrar no solo en su cuenta personal, sino también en la página de destino y en muchos otros lugares para atraer a un cliente al servicio.

Para poder reutilizar la lógica entre componentes, es necesario colocarla en una capa especial llamada Servicio.

Patrón de servicio


Servicio : responsable de toda la lógica empresarial de la aplicación. Si el Controlador necesitaba recibir, procesar y enviar algunos datos, lo hace a través del Servicio. Si varios controladores necesitan la misma lógica, funcionan con el Servicio. Pero la capa de servicio en sí misma no debe saber nada sobre la capa de controlador y vista y el entorno en el que funciona.

Pasemos la lógica del controlador al servicio e implementemos el servicio en el controlador:

UserPageController. Controlador sin lógica empresarial
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. Servicio para trabajar con perfil de usuario
export class UserProfilService {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

Servicio Arancelario. Servicio para trabajar con tarifas.
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;
        });
    }
    
    /**
     * ...       
     */
}


Ahora, si varios controladores necesitan obtener un perfil de usuario o tarifas, pueden reutilizar la misma lógica de los servicios. En los servicios, lo principal es no olvidarse de los principios SÓLIDOS y que cada servicio es responsable de su área de responsabilidad. En este caso, un servicio es responsable de trabajar con el perfil del usuario, y otro servicio es responsable de trabajar con las tarifas.

Pero, ¿qué sucede si la fuente de datos cambia, por ejemplo, fetch puede cambiar a websocket o grps o la base de datos, y los datos reales deben reemplazarse por datos de prueba? Y en general, ¿por qué la lógica de negocios necesita saber algo sobre una fuente de datos? Para resolver estos problemas, hay una capa de repositorio.

Patrón de repositorio


Repositorio : responsable de la comunicación con el almacén de datos. El almacenamiento puede ser un servidor, base de datos, memoria, almacenamiento local, almacenamiento de sesiones o cualquier otro almacenamiento. Su tarea es abstraer la capa de Servicio de la implementación de almacenamiento específica.

Hagamos solicitudes de red de los servicios en el repositorio, mientras que el controlador no cambia:
UserProfilService. Servicio para trabajar con perfil de usuario
import { UserProfilRepository } from "./UserProfilRepository";

export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository =
        new UserProfilRepository();

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

UserProfilRepository. Servicio para trabajar con almacenamiento de perfil de usuario
export class UserProfilRepository {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

Servicio Arancelario. Servicio para trabajar con tarifas.
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;
        });
    }
    
    /**
     * ...       
     */
}

TariffRepository. Repositorio para trabajar con almacenamiento de tarifas
export class TariffRepository {
    //  
    public async getTariffs(): Promise<any> {
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    /**
     * ...        
     */
}


Ahora es suficiente escribir una solicitud de datos una vez y cualquier servicio podrá reutilizar esta solicitud. Más adelante veremos un ejemplo de cómo redefinir el repositorio sin tocar el código de servicio e implementar el repositorio mocha para las pruebas.

En el servicio UserProfilService, puede parecer que no es necesario y el controlador puede acceder directamente al repositorio de datos, pero esto no es así. En cualquier momento, los requisitos pueden aparecer o cambiar en la capa empresarial, se puede requerir una solicitud adicional o se pueden enriquecer los datos. Por lo tanto, incluso cuando no hay lógica en la capa de servicio, se debe preservar la cadena Controlador - Servicio - Repositorio. Esta es una contribución a tu mañana.

Es hora de averiguar qué tipo de repositorio se configura, si son correctos. La capa Modelos es responsable de esto.

Modelos: DTO, Entidades, ViewModels


Modelos : es responsable de la descripción de las estructuras con las que funciona la aplicación. Dicha descripción ayuda enormemente a los nuevos desarrolladores de proyectos a comprender con qué está trabajando la aplicación. Además, es muy conveniente usarlo para construir bases de datos o validar datos almacenados en el modelo.

Los modelos se dividen en diferentes patrones según el tipo de uso:
  • Entidades : son responsables de trabajar con la base de datos y son una estructura que repite una tabla o documento en la base de datos.
  • DTO (objeto de transferencia de datos) : se utilizan para transferir datos entre diferentes capas de la aplicación.
  • ViewModel : contiene información previamente preparada necesaria para mostrar en la vista.


Agregue el modelo de perfil de usuario y otros modelos a la aplicación, y deje que las otras capas sepan que ahora estamos trabajando no con un objeto abstracto, sino con un perfil muy específico:
UserPageController. En lugar de cualquiera, se utilizan los modelos descritos.
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. En lugar de cualquiera, especifique el modelo devuelto
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();
    }
    
    /**
     * ...        
     */
}

Servicio Arancelario. En lugar de cualquiera, especifique el modelo devuelto
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. En lugar de cualquiera, especifique el modelo devuelto
import { UserProfileDto } from "./UserProfileDto";

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

TariffRepository. En lugar de cualquiera, especifique el modelo devuelto
import { TariffDto } from "./TariffDto";

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

    /**
     * ...        
     */
}

UserProfileDto. Un modelo con una descripción de los datos con los que trabajamos.
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;
    }

}

TarifaDto. Un modelo con una descripción de los datos con los que trabajamos.
export class TariffDto {
    public ageFrom: number = 0;
    public ageTo: number = 0;
    public price: number = 0;
}


Ahora, sin importar en qué capa de la aplicación nos encontremos, sabemos exactamente con qué datos estamos trabajando. Además, debido a la descripción del modelo, encontramos un error en nuestro servicio. En la lógica del servicio, se usó la propiedad userProfile.age, que en realidad no existe, pero tiene una fecha de nacimiento. Y para calcular la edad, debe llamar al método de modelo userProfile.getAge ().

Pero hay un problema. Si intentamos utilizar los métodos del modelo que proporcionó el repositorio actual, obtendremos una excepción. El caso es que los métodos response.json () y JSON.parse ()No devuelve nuestro modelo, sino un objeto JSON, que de ninguna manera está asociado con nuestro modelo. Puede verificar esto si ejecuta el comando userProfile instanceof UserProfileDto, obtiene una declaración falsa. Para convertir los datos recibidos de una fuente externa al modelo descrito, hay un proceso de deserialización de datos.

Deserialización de datos


Deserialización : el proceso de restaurar la estructura necesaria a partir de una secuencia de bytes. Si los datos contienen información no especificada en los modelos, se ignorará. Si hay información en los datos que contradice la descripción del modelo, se producirá un error de deserialización.

Y lo más interesante aquí es que al diseñar el ES2015 y agregar la palabra clave de clase , olvidaron agregar deserialización ... El hecho de que en todos los idiomas está listo para usar, en ES2015 simplemente olvidaron ...

Para resolver este problema, escribí una biblioteca para la deserialización serializable TS , un artículo sobre el cual se puede leer en este enlace . El propósito de esto es devolver la funcionalidad perdida.

Agregue soporte de deserialización en el modelo y la deserialización misma al repositorio:
TariffRepository. Agregar un proceso de deserialización
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); //  
    }
    
    /**
     * ...        
     */
}

TariffRepository. Agregar un proceso de deserialización
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. Agregar soporte de deserialización
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;
    }

}

TarifaDto. Agregar soporte de deserialización
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;

}


Ahora, en todas las capas de la aplicación, puede estar absolutamente seguro de que estamos trabajando con los modelos que esperamos. En la vista, el controlador y otras capas, puede llamar a los métodos del modelo descrito.

¿Para qué sirven Serializable y jsonProperty?
Serializable — Ecmascript Typescript. jsonProperty , Typescript uniontypes. .

Ahora tenemos una aplicación casi terminada. Es hora de probar la lógica escrita en las capas Controlador, Servicio y Modelos. Para hacer esto, necesitamos devolver datos de prueba especialmente preparados en la capa de Repositorio en lugar de una solicitud real al servidor. Pero cómo reemplazar el repositorio sin tocar el código que entra en producción. Hay un patrón de inyección de dependencia para esto.

Inyección de dependencia - Inyección de dependencia


Inyección de dependencias : inyecta dependencias en las capas Contoller, Servicio, Repositorio y le permite anular estas dependencias fuera de estas capas.

En el programa, la capa del controlador depende de la capa de servicio, y depende de la capa del repositorio. En la forma actual, las capas mismas causan sus dependencias a través de la creación de instancias. Y para redefinir la dependencia, la capa necesita establecer esta dependencia desde el exterior. Hay muchas formas de hacer esto, pero la más popular es pasar la dependencia como parámetro en el constructor.

Luego, crear un programa con todas las dependencias se verá así:
var programm = new IndexPageController(new ProfileService(new ProfileRepository()));

De acuerdo, se ve horrible. Incluso teniendo en cuenta que solo hay dos dependencias en el programa, ya se ve horrible. Qué decir sobre los programas en los que cientos y miles de dependencias.

Para resolver el problema, necesita una herramienta especial, pero para esto necesita encontrarla. Si recurrimos a la experiencia de otras plataformas, por ejemplo, Asp.Net Core, entonces el registro de dependencias se produce en la etapa de inicialización del programa y se ve así:
DI.register(IProfileService,ProfileService);

y luego, al crear el controlador, el propio marco creará e implementará esta dependencia.

Pero hay tres problemas importantes:
  1. Cuando se transcribe el Script mecanografiado en Javascript, no quedan rastros de las interfaces.
  2. Todo lo que cayó en el clásico DI permanece en él para siempre. Es muy difícil limpiarlo durante la refactorización. Y en una aplicación web, debe guardar cada byte.
  3. Casi todas las bibliotecas de vistas no usan DI, y los diseñadores de controladores están ocupados con los parámetros.


En aplicaciones web, DI se usa solo en Angular 2+. En Angular 1, al registrar dependencias, en lugar de una interfaz, se utilizó una cadena; en InversifyJS, se utiliza Symbol en lugar de la interfaz. Y todo esto se implementa tan terriblemente que es mejor tener muchas novedades como en el primer ejemplo de esta sección que estas soluciones.

Para resolver los tres problemas, se inventó mi propio DI y la solución me ayudó a encontrar el marco Java Spring y su decorador con cable automático. La descripción de cómo funciona esta DI se puede encontrar en el artículo en el enlace y en el repositorio de GitHub .

Es hora de aplicar la DI resultante en nuestra aplicación.

Poniendolo todo junto


Para implementar DI en todas las capas, agregaremos un decorador de reflexión, lo que hará que el mecanografiado genere metainformación adicional sobre los tipos de dependencia. En el controlador donde necesita llamar a las dependencias, colgaremos el decorador con cable automático. Y en el lugar donde se inicializa el programa, determinamos en qué entorno qué dependencia se implementará.

Para el repositorio UserProfilRepository, cree el mismo repositorio, pero con datos de prueba en lugar de la solicitud real. Como resultado, obtenemos el siguiente código:
Main.ts. Ubicación de inicialización del programa
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";

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

UserPageController. Dependencia a través del decorador con cable
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. Introducción a la reflexión y la generación de dependencia.
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();
    }

    /**
     * ...        
     */
}

Servicio Arancelario. Introducción a la reflexión y la generación de dependencia.
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. Introduciendo la generación de reflexión
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. Nuevo repositorio para pruebas
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. Introduciendo la generación de reflexión
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);
        });
    }

    /**
     * ...        
     */
}


Ahora, en cualquier parte del programa, existe la oportunidad de cambiar la implementación de cualquier lógica. En nuestro ejemplo, en lugar de una solicitud de perfil de usuario real al servidor en el entorno de prueba, se utilizarán los datos de prueba.

En la vida real, los reemplazos se pueden encontrar en cualquier lugar, por ejemplo, puede cambiar la lógica en un servicio, implementar el servicio anterior en producción y, en la refactorización, ya es nuevo. Realice pruebas A / B con lógica empresarial, cambie la base de datos basada en documentos a relacional y, en general, cambie la solicitud de red a sockets web. Y todo esto sin detener el desarrollo para reescribir la solución.

Es hora de ver el resultado del programa. Hay una capa de Vista para esto.

Vista de implementación


La capa Vista es responsable de presentar los datos que están contenidos en la capa Controlador al usuario. En el ejemplo, usaré React para esto, pero en su lugar puede haber cualquier otro, por ejemplo Preact, Svelte, Vue, Mithril, WebComponent o cualquier otro.

Para hacer esto, simplemente herede nuestro controlador de React.Component y agregue un método de representación con la representación de la vista:

Main.ts. Comienza a dibujar un componente Reaccionar
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. Hereda de React.Component y agrega un método de representación
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>
            </>
        );
    }

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


Al agregar solo dos líneas y una plantilla de presentación, nuestro controlador se convirtió en un componente de reacción con lógica de trabajo.

¿Por qué se llama forceUpdate en lugar de setState?
forceUpdate , setState. setState, Redux, mobX ., . React Preact forceUpdate, ChangeDetectorRef.detectChanges(), Mithril redraw . Observable , MobX Angular, . .

Pero incluso en una implementación de este tipo, resultó que estábamos vinculados a la biblioteca React con su ciclo de vida, la implementación de la vista y el principio de invalidar la vista, lo que contradice el concepto de Arquitectura limpia. Y la lógica y el diseño están en el mismo archivo, lo que complica el trabajo paralelo de la fuente y el desarrollador.

Separación de controlador y vista


Para resolver ambos problemas, colocamos la capa de vista en un archivo separado y, en lugar del componente Reaccionar, hacemos el componente base que abstraerá nuestro controlador de una biblioteca de presentación específica. Al mismo tiempo, describimos los atributos que puede tomar el componente.

Recibimos los siguientes cambios:
UserPageView. Llevaron la vista a un archivo separado
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. Tomar vista y reaccionar a un archivo separado
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);
        }
    }

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

Componente base Un componente que nos abstrae de un marco particular.
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", {}, "  ");
        }
    }

}


Ahora nuestra vista está en un archivo separado, y el controlador no sabe nada al respecto, excepto que lo está. Tal vez no debería inyectar la vista a través de la propiedad del controlador, sino hacerlo a través del decorador como lo hace Angular, pero este es un tema para pensar.

El componente básico también contiene una abstracción del ciclo de vida del marco. Son diferentes en todos los marcos, pero están en todos los marcos. Angular es ngOnInit, ngOnChanges, ngOnDestroy. En React and Preact, este es componentDidMount, shouldComponentUpdate, componentWillUnmount. En Vue, esto se crea, actualiza, destruye. En Mithril es oncreate, onupdate, onremove. En WebComponents, esto está conectadoCallback, attributeChangedCallback, desconectadoCallback. Y así en todas las bibliotecas. La mayoría incluso tiene la misma interfaz o una similar.

Además, ahora los componentes de la biblioteca se pueden ampliar con su propia lógica para su posterior reutilización entre todos los componentes. Por ejemplo, la introducción de herramientas para análisis, monitoreo, registro, etc.

Nos fijamos en el resultado


Solo queda evaluar lo que sucedió. Todo el programa tiene la siguiente forma final:
Main.ts. El archivo desde el cual se inicia el programa
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. Representación de uno de los componentes del programa.
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 lógica de uno de los componentes para la interacción del usuario.
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);
        }
    }

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

Componente base Clase base para todos los componentes del programa.
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. Servicio para reutilizar la lógica entre componentes para trabajar con un perfil de usuario
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();
    }

    /**
     * ...        
     */
}

Servicio Arancelario. Servicio para reutilizar lógica entre componentes para trabajar con tarifas
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. Repositorio para recibir un perfil del servidor, verificarlo y validarlo
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;

}


Como resultado, obtuvimos una aplicación escalable modular con una cantidad muy pequeña de repetitivo (1 componente básico, 3 líneas para implementar dependencias por clase) y una sobrecarga muy baja (en realidad solo para implementar dependencias, todo lo demás es lógico). Además, no estamos vinculados a ninguna biblioteca de presentación. Cuando Angular 1 murió, muchos comenzaron a reescribir aplicaciones en React. Cuando los desarrolladores de Angular 2 se agotaron, muchas compañías comenzaron a sufrir debido a la velocidad de desarrollo. Cuando React muera nuevamente, tendrá que reescribir las soluciones vinculadas a su marco y ecosistema. Pero con Chita Architecture puede olvidarse de atar el marco.

¿Cuál es la ventaja sobre Redux?


Para entender la diferencia, veamos cómo se comporta Redux cuando la aplicación crece.
Redux

Como puede ver en el diagrama, con el crecimiento de las aplicaciones de Redux escaladas verticalmente, la Tienda y el número de Reductores también aumenta y se convierte en un cuello de botella. Y la cantidad de gastos generales para reconstruir la Tienda y encontrar el Reductor correcto está comenzando a exceder la carga útil.

Puede verificar la relación entre gastos generales y carga útil en una aplicación de tamaño mediano con una simple prueba.
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

Volver a crear la Tienda en 100 propiedades tomó 8 veces más tiempo que la lógica misma. Con 1000 elementos, esto ya es 50 veces más. Además, una acción del usuario puede generar toda una cadena de acciones, cuya llamada es difícil de atrapar y depurar. Ciertamente, puede argumentar que 0.04 ms para recrear la Tienda es muy pequeño y no se ralentizará. Pero 0.04 ms está en el procesador Core i7 y para una acción. Dado que los procesadores móviles más débiles y el hecho de que una sola acción del usuario puede generar docenas de acciones, todo esto lleva al hecho de que los cálculos no caben en 16 ms y se crea la sensación de que la aplicación se está ralentizando.

Comparemos cómo crece la aplicación Clean Architecture:
Arquitectura pura

Como se puede ver debido a la separación de la lógica y las responsabilidades entre las capas, la aplicación se escala horizontalmente. Si algún componente necesita procesar los datos, recurrirá al servicio correspondiente sin tocar los servicios no relacionados con su tarea. Después de recibir los datos, solo se volverá a dibujar un componente. La cadena de procesamiento de datos es muy corta y obvia, y para un máximo de 4 saltos de código, puede encontrar la lógica necesaria y depurarla. Además, si dibujamos una analogía con un muro de ladrillos, entonces podemos eliminar cualquier ladrillo de este muro y reemplazarlo por otro sin afectar la estabilidad de este muro.

Bonus 1: funcionalidad mejorada de los componentes del framework


Una ventaja adicional era la capacidad de complementar o cambiar el comportamiento de los componentes de la biblioteca de presentación. Por ejemplo, una reacción en caso de un error en la vista no representa la aplicación completa, si se realiza una pequeña revisión:
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}`
            );
        }
    }

}

Ahora la reacción no dibujará solo el componente en el que ocurrió el error. De la misma manera simple, puede agregar monitoreo, análisis y otros nishtyaki.

Bonus 2: Validación de datos


Además de describir datos y transferir datos entre capas, los modelos tienen un gran margen de oportunidad. Por ejemplo, si conecta la biblioteca de validación de clase , simplemente colgando los decoradores, puede validar los datos en estos modelos, incluidos con un poco de refinamiento, puede validar formularios web.

Bonus 3: Crear entidades


Además, si necesita trabajar con una base de datos local, puede conectar la biblioteca de tipos y sus modelos se convertirán en entidades mediante las cuales se generará y ejecutará la base de datos.

Gracias por la atención


Si le gustó el artículo o el enfoque, haga clic en Me gusta y no tenga miedo de experimentar. Si es un experto en Redux y no le gusta la disidencia, explique en los comentarios cómo escala, prueba y valida los datos en su aplicación.

All Articles