Arquitetura de rede para aplicativos da Web

Quero compartilhar com você uma abordagem que venho usando há muitos anos no desenvolvimento de aplicativos, incluindo aplicativos da Web. Muitos desenvolvedores de desktop, servidor e aplicativos móveis estão familiarizados com essa abordagem. é fundamental ao criar esses aplicativos, no entanto, ele é muito mal representado na Web, embora definitivamente existam pessoas que desejam usar essa abordagem. Além disso, o editor de código VS é escrito sobre essa abordagem .

Arquitetura pura

Como resultado da aplicação dessa abordagem, você se livrará de uma estrutura específica. Você pode alternar facilmente a biblioteca de visualizações dentro de seu aplicativo, por exemplo, React, Preact, Vue, Mithril sem reescrever a lógica de negócios e, na maioria dos casos, até visualizações. Se você possui um aplicativo no Angular 1, pode traduzi-lo facilmente para Angular 2+, React, Svelte, WebComponents ou até sua biblioteca de apresentações. Se você possui um aplicativo no Angular 2+, mas não há especialistas, pode transferi-lo facilmente para uma biblioteca mais popular sem precisar reescrever a lógica comercial. Mas, no final, esqueça completamente o problema da migração de uma estrutura para outra. Que tipo de mágica é essa?

O que é arquitetura limpa


Para entender isso, é melhor ler o livro de Martin Robert "Arquitetura Limpa» ( de Robert C.Martin "Arquitetura Limpa» ). Um breve trecho do qual é dado no artigo por referência .

As principais idéias incorporadas na arquitetura:

  1. Independência da estrutura. A arquitetura não depende da existência de nenhuma biblioteca. Isso permite que você use a estrutura como uma ferramenta, em vez de restringir seu sistema às limitações.
  2. Testabilidade. As regras de negócios podem ser testadas sem uma interface com o usuário, banco de dados, servidor da Web ou qualquer outro componente externo.
  3. Independência da interface do usuário. A interface do usuário pode ser facilmente alterada sem alterar o restante do sistema. Por exemplo, a interface da web pode ser substituída pelo console, sem alterar as regras de negócios.
  4. Independência do banco de dados. Você pode trocar o Oracle ou o SQL Server pelo MongoDB, BigTable, CouchDB ou qualquer outra coisa. Suas regras de negócios não estão relacionadas ao banco de dados.
  5. Independência de qualquer serviço externo. De fato, suas regras de negócios simplesmente não sabem nada sobre o mundo exterior.

As idéias descritas neste livro há muitos anos têm sido a base para a criação de aplicativos complexos em vários campos.

Essa flexibilidade é obtida dividindo o aplicativo nas camadas Serviço, Repositório e Modelo. Adicionei a abordagem MVC à arquitetura limpa e obtive as seguintes camadas:

  • Exibir - exibe dados para o cliente, na verdade visualiza o estado da lógica para o cliente.
  • Controlador - é responsável por interagir com o usuário por meio de E / S (entrada e saída).
  • Serviço - é responsável pela lógica de negócios e sua reutilização entre componentes.
  • Repositório - responsável por receber dados de fontes externas, como banco de dados, API, armazenamento local etc.
  • Modelos - é responsável pela transferência de dados entre camadas e sistemas, bem como pela lógica de processamento desses dados.

O objetivo de cada camada é discutido abaixo.

Quem é a arquitetura pura


O desenvolvimento da Web percorreu um longo caminho, desde o script jquery simples até o desenvolvimento de grandes aplicativos SPA. E agora os aplicativos da Web se tornaram tão grandes que a quantidade de lógica de negócios se tornou comparável ou até superior aos aplicativos de servidor, desktop e móvel.

Para desenvolvedores que escrevem aplicativos grandes e complexos, bem como transferem a lógica de negócios do servidor para aplicativos da Web para economizar no custo dos servidores, o Pure Architecture ajudará a organizar o código e a escalar sem problemas em grande escala.

Ao mesmo tempo, se sua tarefa é apenas layout e animação de páginas de destino, o Clean Architecture simplesmente não tem onde inserir. Se sua lógica de negócios estiver no back-end e sua tarefa for obter os dados, exibi-los para o cliente e processar o clique no botão, você não sentirá a flexibilidade da Arquitetura Limpa, mas pode ser um excelente trampolim para o crescimento explosivo do aplicativo.

Onde já aplicado?


A arquitetura pura não está vinculada a nenhuma estrutura, plataforma ou linguagem de programação específica. Durante décadas, tem sido usado para escrever aplicativos de desktop. Sua implementação de referência pode ser encontrada nas estruturas para aplicativos de servidor Asp.Net Core, Java Spring e NestJS. Também é muito popular ao escrever aplicativos iOS e Android. Mas no desenvolvimento da Web, ele apareceu de uma forma extremamente malsucedida nas estruturas Angular.

Como eu não sou apenas o Typecript, mas também um desenvolvedor de C #, por exemplo, adotarei a implementação de referência dessa arquitetura para o Asp.Net Core.

Aqui está um aplicativo de amostra simplificado:

Aplicativo de exemplo no 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; }
    }


Se você não entender que isso não diz nada de ruim, analisaremos isso em partes de cada fragmento.

Um exemplo é dado para um aplicativo Asp.Net Core, mas para Java Spring, WinForms, Android, React, a arquitetura e o código serão os mesmos, apenas o idioma e o trabalho com a exibição (se houver) serão alterados.

Aplicação Web


A única estrutura que tentou usar a arquitetura limpa foi o Angular. Mas acabou horrível, que em 1, que em 2+.

E há muitas razões para isso:

  1. Estrutura monolítica angular. E este é o seu principal problema. Se você não gosta de algo, precisa se engasgar diariamente, e não há nada que possa fazer. Não apenas há muitos problemas, mas também contradiz a ideologia da arquitetura pura.
  2. DI. , Javascript.
  3. . JSX. , 6, . .
  4. . Rollup ES2015 2 4 , angular 9.
  5. E muitos mais problemas. Em geral, até a tecnologia moderna angular rola com um atraso de 5 anos em relação ao React.

Mas e outras estruturas? React, Vue, Preact, Mithril e outros são exclusivamente bibliotecas de apresentação e não fornecem nenhuma arquitetura ... mas já temos a arquitetura ... resta reunir tudo em um único todo!

Começamos a criar um aplicativo


Consideraremos o Pure Architecture pelo exemplo de um aplicativo fictício o mais próximo possível de um aplicativo da Web real. Este é um escritório da companhia de seguros que exibe o perfil do usuário, eventos segurados, taxas de seguro propostas e ferramentas para trabalhar com esses dados.

Protótipo de aplicação

No exemplo, apenas uma pequena parte do funcional será implementada, mas a partir dele você poderá entender onde e como posicionar o restante do funcional. Vamos começar a criar o aplicativo a partir da camada Controller e conectar a camada View no final. E no decorrer da criação, consideramos cada camada com mais detalhes.

Padrão do Controlador


Controlador - é responsável pela interação do usuário com o aplicativo. Pode ser um clique em um botão em uma página da web, aplicativo de desktop, aplicativo móvel ou digitar um comando em um console Linux ou uma solicitação de rede ou qualquer outro evento de entrada / saída que entrar no aplicativo.

O controlador mais simples em uma arquitetura limpa é o seguinte:

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

}

Sua tarefa é receber um evento do usuário e iniciar processos de negócios. No caso ideal, o Controlador não sabe nada sobre o View e pode ser reutilizado entre plataformas, como Web, React-Native ou Electron.

Agora vamos escrever um controlador para nosso aplicativo. Sua tarefa é obter um perfil de usuário, tarifas disponíveis e oferecer a melhor tarifa ao usuário:

UserPageController. Controlador com lógica de negócios
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;
            });
        }
    }

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


Temos um controlador regular sem uma arquitetura limpa; se o herdarmos do React.Component, obteremos um componente funcional com lógica. Muitos desenvolvedores de aplicativos da Web escrevem, mas essa abordagem tem muitas desvantagens significativas. O principal é a incapacidade de reutilizar a lógica entre os componentes. Afinal, a tarifa recomendada pode ser exibida não apenas em sua conta pessoal, mas também na página de destino e em muitos outros lugares para atrair um cliente para o serviço.

Para poder reutilizar a lógica entre os componentes, é necessário colocá-la em uma camada especial chamada Serviço.

Padrão de Serviço


Serviço - responsável por toda a lógica comercial do aplicativo. Se o Controlador precisou receber, processar, enviar alguns dados - isso é feito através do Serviço. Se vários controladores precisarem da mesma lógica, eles trabalharão com o Serviço. Mas a camada de serviço em si não deve saber nada sobre a camada Controller and View e o ambiente em que trabalha.

Vamos mover a lógica do controlador para o serviço e implementar o serviço no controlador:

UserPageController. Controlador sem lógica de negócios
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. Serviço para trabalhar com perfil de usuário
export class UserProfilService {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

TariffService. Serviço para trabalhar com 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;
        });
    }
    
    /**
     * ...       
     */
}


Agora, se vários controladores precisarem obter um perfil de usuário ou tarifas, eles poderão reutilizar a mesma lógica dos serviços. Nos serviços, o principal é não esquecer os princípios do SOLID e que cada serviço é responsável por sua área de responsabilidade. Nesse caso, um serviço é responsável por trabalhar com o perfil do usuário e outro serviço é responsável por trabalhar com tarifas.

Mas e se a fonte de dados mudar, por exemplo, a busca puder mudar para websocket ou grps ou o banco de dados, e os dados reais precisarem ser substituídos pelos dados de teste? E, em geral, por que a lógica comercial precisa saber algo sobre uma fonte de dados? Para resolver esses problemas, existe uma camada de repositório.

Padrão de Repositório


Repositório - responsável pela comunicação com o data warehouse. O armazenamento pode ser um servidor, banco de dados, memória, armazenamento local, armazenamento de sessão ou qualquer outro armazenamento. Sua tarefa é abstrair a camada de Serviço da implementação de armazenamento específica.

Vamos fazer solicitações de rede de serviços no repositório, enquanto o controlador não muda:
UserProfilService. Serviço para trabalhar com perfil de usuário
import { UserProfilRepository } from "./UserProfilRepository";

export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository =
        new UserProfilRepository();

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

UserProfilRepository. Serviço para trabalhar com armazenamento de perfil de usuário
export class UserProfilRepository {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

TariffService. Serviço para trabalhar com 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;
        });
    }
    
    /**
     * ...       
     */
}

Repositório Tarifário. Repositório para trabalhar com armazenamento tarifário
export class TariffRepository {
    //  
    public async getTariffs(): Promise<any> {
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    /**
     * ...        
     */
}


Agora basta escrever uma solicitação de dados uma vez e qualquer serviço poderá reutilizá-la. Posteriormente, veremos um exemplo de como redefinir o repositório sem tocar no código de serviço e implementar o repositório mocha para teste.

No serviço UserProfilService, pode parecer que não é necessário e o controlador pode acessar diretamente o repositório para obter dados, mas não é assim. A qualquer momento na camada de negócios, os requisitos podem aparecer ou mudar, uma solicitação adicional pode ser necessária ou os dados podem ser enriquecidos. Portanto, mesmo quando não há lógica na camada de serviço, a cadeia Controlador - Serviço - Repositório deve ser preservada. Esta é uma contribuição para o seu amanhã.

É hora de descobrir que tipo de repositório é definido, se eles estão corretos. A camada de modelos é responsável por isso.

Modelos: DTO, Entidades, Modelos de exibição


Modelos - é responsável pela descrição das estruturas com as quais o aplicativo trabalha. Essa descrição ajuda muito os novos desenvolvedores de projetos a entender com o que o aplicativo está trabalhando. Além disso, é muito conveniente usá-lo para criar bancos de dados ou validar dados armazenados no modelo.

Os modelos são divididos em diferentes padrões, dependendo do tipo de uso:
  • Entidades - são responsáveis ​​por trabalhar com o banco de dados e são uma estrutura que repete uma tabela ou documento no banco de dados.
  • DTO (Objeto de transferência de dados) - são usados ​​para transferir dados entre diferentes camadas do aplicativo.
  • ViewModel - contém informações pré-preparadas necessárias para exibição na exibição.


Inclua o modelo de perfil de usuário e outros modelos no aplicativo e informe as outras camadas que agora estamos trabalhando não com um objeto abstrato, mas com um perfil muito específico:
UserPageController. Em vez de qualquer, os modelos descritos são usados.
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. Em vez de qualquer, especifique o modelo retornado
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. Em vez de qualquer, especifique o modelo retornado
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. Em vez de qualquer, especifique o modelo retornado
import { UserProfileDto } from "./UserProfileDto";

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

Repositório Tarifário. Em vez de qualquer, especifique o modelo retornado
import { TariffDto } from "./TariffDto";

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

    /**
     * ...        
     */
}

UserProfileDto. Um modelo com uma descrição dos dados com os quais trabalhamos
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;
    }

}

TariffDto. Um modelo com uma descrição dos dados com os quais trabalhamos
export class TariffDto {
    public ageFrom: number = 0;
    public ageTo: number = 0;
    public price: number = 0;
}


Agora, independentemente da camada do aplicativo em que estamos, sabemos exatamente com quais dados estamos trabalhando. Além disso, devido à descrição do modelo, encontramos um erro em nosso serviço. Na lógica do serviço, foi usada a propriedade userProfile.age, que na verdade não existe, mas tem uma data de nascimento. E para calcular a idade, você deve chamar o método do modelo userProfile.getAge ().

Mas há um problema. Se tentarmos usar os métodos do modelo fornecido pelo repositório atual, obteremos uma exceção. O problema é que os métodos response.json () e JSON.parse ()Ele não retorna nosso modelo, mas um objeto JSON, que não está de forma alguma associado ao nosso modelo. Você pode verificar isso se executar o comando userProfile instanceof UserProfileDto, obtendo uma declaração falsa. Para converter os dados recebidos de uma fonte externa para o modelo descrito, há um processo de desserialização de dados.

Desserialização de dados


Desserialização - o processo de restaurar a estrutura necessária a partir de uma sequência de bytes. Se os dados contiverem informações não especificadas nos modelos, elas serão ignoradas. Se houver informações nos dados que contradizem a descrição do modelo, ocorrerá um erro de desserialização.

E o mais interessante aqui é que, ao projetar o ES2015 e adicionar a palavra-chave class , eles esqueceram de desserializar ... O fato de que em todos os idiomas está fora da caixa, no ES2015 eles simplesmente esqueceram ...

Para resolver esse problema, escrevi uma biblioteca para desserialização TS-Serializable , um artigo sobre o qual pode ser lido neste link . O objetivo é retornar a funcionalidade perdida.

Inclua suporte de desserialização no modelo e desserialização em si no repositório:
Repositório Tarifário. Adicionar um processo de desserialização
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); //  
    }
    
    /**
     * ...        
     */
}

Repositório Tarifário. Adicionar um processo de desserialização
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. Adicionando suporte à desserialização
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. Adicionando suporte à desserialização
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;

}


Agora, em todas as camadas do aplicativo, você pode ter certeza absoluta de que estamos trabalhando com os modelos que esperamos. Na visualização, controlador e outras camadas, você pode chamar os métodos do modelo descrito.

O que são Serializable e jsonProperty?
Serializable — Ecmascript Typescript. jsonProperty , Typescript uniontypes. .

Agora temos uma aplicação quase finalizada. É hora de testar a lógica escrita nas camadas Controller, Service e Models. Para fazer isso, precisamos retornar dados de teste especialmente preparados na camada Repositório, em vez de uma solicitação real para o servidor. Mas como substituir o Repositório sem tocar no código que entra em produção. Existe um padrão de injeção de dependência para isso.

Injeção de Dependência - Injeção de Dependência


Injeção de Dependência - injeta dependências nas camadas Contoller, Service, Repository e permite substituir essas dependências fora dessas camadas.

No programa, a camada Controller depende da camada Service e da camada Repository. Na forma atual, as próprias camadas causam suas dependências através da instanciação. E, para redefinir a dependência, a camada precisa definir essa dependência de fora. Existem várias maneiras de fazer isso, mas o mais popular é passar a dependência como um parâmetro no construtor.

Em seguida, criar um programa com todas as dependências ficará assim:
var programm = new IndexPageController(new ProfileService(new ProfileRepository()));

Concordo - parece horrível. Mesmo levando em conta que existem apenas duas dependências no programa, ele já parece horrível. O que dizer sobre programas em que centenas e milhares de dependências.

Para resolver o problema, você precisa de uma ferramenta especial, mas para isso precisa encontrá-la. Se nos voltarmos para a experiência de outras plataformas, por exemplo, o Asp.Net Core, então o registro de dependências ocorre no estágio de inicialização do programa e fica assim:
DI.register(IProfileService,ProfileService);

e, em seguida, ao criar o controlador, a própria estrutura criará e implementará essa dependência.

Mas existem três problemas significativos:
  1. Ao transpilar o Typecript em Javascript, não há nenhum rastro nas interfaces.
  2. Tudo o que caiu no DI clássico permanece nele para sempre. É muito difícil limpá-lo durante a refatoração. E em um aplicativo da web, você precisa salvar todos os bytes.
  3. Quase todas as bibliotecas de exibição não usam DI, e os projetistas de controladores estão ocupados com os parâmetros.


Em aplicativos da Web, o DI é usado apenas no Angular 2+. No Angular 1, ao registrar dependências, em vez de uma interface, uma string era usada; no InversifyJS, Symbol é usado no lugar da interface. E tudo isso é implementado de maneira tão terrível que é melhor ter muitos novos, como no primeiro exemplo desta seção, do que essas soluções.

Para resolver todos os três problemas, meu próprio DI foi inventado e a solução para ele me ajudou a encontrar a estrutura do Java Spring e seu decorador com conexão automática. A descrição de como essa DI funciona pode ser encontrada no artigo no link e no repositório GitHub .

É hora de aplicar o DI resultante em nosso aplicativo.

Juntando tudo


Para implementar o DI em todas as camadas, adicionaremos um decorador de reflexão, que fará com que o texto datilografado gere meta-informações adicionais sobre tipos de dependência. No controlador em que você precisa chamar as dependências, desligaremos o decorador com conexão automática. E no local em que o programa foi inicializado, determinamos em qual ambiente qual dependência será implementada.

Para o repositório UserProfilRepository, crie o mesmo repositório, mas com dados de teste em vez da solicitação real. Como resultado, obtemos o seguinte código:
Main.ts. Local de Inicialização do Programa
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";

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

UserPageController. Dependência através do decorador autowired
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. Introdução à geração de reflexão e dependência
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. Introdução à geração de reflexão e dependência
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. Apresentando a geração de reflexão
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. Novo repositório para teste
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); //   
    }

    /**
     * ...        
     */
}

Repositório Tarifário. Apresentando a geração de reflexão
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);
        });
    }

    /**
     * ...        
     */
}


Agora, em qualquer lugar do programa, há uma oportunidade de alterar a implementação de qualquer lógica. Em nosso exemplo, em vez de uma solicitação real de perfil de usuário para o servidor no ambiente de teste, os dados de teste serão usados.

Na vida real, as substituições podem ser encontradas em qualquer lugar, por exemplo, você pode alterar a lógica em um serviço, implementando o serviço antigo na produção e, na refatoração, ele já é novo. Realize testes A / B com lógica comercial, altere o banco de dados baseado em documentos para relacional e geralmente altere a solicitação de rede para soquetes da web. E tudo isso sem interromper o desenvolvimento para reescrever a solução.

É hora de ver o resultado do programa. Há uma camada de visualização para isso.

Visão de Implementação


A camada View é responsável por apresentar os dados que estão contidos na camada Controller ao usuário. No exemplo, usarei React para isso, mas em seu lugar pode haver qualquer outro, por exemplo, Preact, Svelte, Vue, Mithril, WebComponent ou qualquer outro.

Para fazer isso, simplesmente herde nosso controlador de React.Component e adicione um método de renderização a ele com a representação da visualização:

Main.ts. Começa a desenhar um componente 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. Herda de React.Component e adiciona um método de renderização
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>
            </>
        );
    }

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


Ao adicionar apenas duas linhas e um modelo de apresentação, nosso controlador se transformou em um componente de reação com lógica de trabalho.

Por que o forceUpdate é chamado em vez de setState?
forceUpdate , setState. setState, Redux, mobX ., . React Preact forceUpdate, ChangeDetectorRef.detectChanges(), Mithril redraw . Observable , MobX Angular, . .

Mas, mesmo em tal implementação, verificamos que vinculamos à biblioteca React com seu ciclo de vida, implementação da visualização e o princípio de invalidar a visualização, o que contradiz o conceito de Arquitetura Limpa. E a lógica e o layout estão no mesmo arquivo, o que complica o trabalho paralelo do compositor e desenvolvedor.

Separação de Controlador e Visão


Para resolver os dois problemas, colocamos a camada de visualização em um arquivo separado e, em vez do componente React, criamos o componente base que abstrairá nosso controlador de uma biblioteca de apresentação específica. Ao mesmo tempo, descrevemos os atributos que o componente pode receber.

Recebemos as seguintes alterações:
UserPageView. Eles levaram a visualização para um arquivo 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. Veja e reaja a um arquivo 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);
        }
    }

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

BaseComponent Um componente que nos abstrai de uma estrutura específica
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", {}, "  ");
        }
    }

}


Agora nossa visão está em um arquivo separado, e o controlador não sabe nada sobre isso, exceto que está. Talvez você não deva injetar a vista através da propriedade do controlador, mas fazê-lo através do decorador como o Angular, mas este é um tópico para alguma reflexão.

O componente básico também contém uma abstração do ciclo de vida da estrutura. Eles são diferentes em todas as estruturas, mas são em todas as estruturas. Angular é ngOnInit, ngOnChanges, ngOnDestroy. Em React e Preact, este é componentDidMount, shouldComponentUpdate, componentWillUnmount. No Vue, isso é criado, atualizado, destruído. No Mithril, ele é criado, atualizado, removido. Em WebComponents, isso é conectadoCallback, attributeChangedCallback ,connectedCallback. E assim em todas as bibliotecas. A maioria ainda tem a mesma interface ou similar.

Além disso, agora os componentes da biblioteca podem ser expandidos com sua própria lógica para posterior reutilização entre todos os componentes. Por exemplo, a introdução de ferramentas para análise, monitoramento, registro etc.

Nós olhamos para o resultado


Resta apenas avaliar o que aconteceu. Todo o programa tem a seguinte forma final:
Main.ts. O arquivo a partir do qual o programa é iniciado
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. Representação de um dos componentes do 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. A lógica de um dos componentes para interação do usuário
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 base para todos os componentes do 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. Serviço para reutilizar a lógica entre componentes para trabalhar com um perfil de usuário
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. Serviço para reutilizar a lógica entre componentes para trabalhar com 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. Repositório para receber um perfil do servidor, verificando e validando
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, obtivemos um aplicativo escalável modular com uma quantidade muito pequena de clichê (1 componente básico, 3 linhas para implementar dependências por classe) e sobrecarga muito baixa (na verdade, apenas para implementar dependências, tudo o resto é lógico). Além disso, não estamos vinculados a nenhuma biblioteca de apresentações. Quando o Angular 1 morreu, muitos começaram a reescrever aplicativos no React. Quando os desenvolvedores do Angular 2 acabaram, muitas empresas começaram a sofrer por causa da velocidade do desenvolvimento. Quando o React morrer novamente, você terá que reescrever as soluções vinculadas à sua estrutura e ecossistema. Mas com a Chita Architecture você pode esquecer de amarrar a estrutura.

Qual é a vantagem sobre o Redux?


Para entender a diferença, vamos ver como o Redux se comporta quando o aplicativo cresce.
Restaurado

Como você pode ver no diagrama, com o crescimento dos aplicativos Redux dimensionados verticalmente, a Store e o número de redutores também aumentam e se transformam em gargalo. E a quantidade de despesas gerais para reconstruir a loja e encontrar o redutor certo está começando a exceder a carga útil.

Você pode verificar a taxa de sobrecarga para carga útil em um aplicativo de tamanho médio com um teste simples.
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

Para recriar a Loja em 100 propriedades, levou 8 vezes mais tempo que a própria lógica. Com 1000 elementos, isso já é 50 vezes mais. Além disso, uma ação do usuário pode gerar toda uma cadeia de ações, cuja chamada é difícil de capturar e depurar. Você certamente pode argumentar que 0,04 ms para recriar a loja é muito pequeno e não diminui a velocidade. Mas 0,04 ms está no processador Core i7 e por uma ação. Dado os processadores móveis mais fracos e o fato de que uma única ação do usuário pode gerar dezenas de ações, tudo isso leva ao fato de que os cálculos não cabem em 16 ms e é criado o sentimento de que o aplicativo está diminuindo a velocidade.

Vamos comparar como o aplicativo Clean Architecture cresce:
Arquitetura pura

Como pode ser visto devido à separação da lógica e das responsabilidades entre as camadas, o aplicativo é escalonado horizontalmente. Se algum componente precisar processar os dados, ele retornará ao serviço correspondente sem tocar nos serviços não relacionados à sua tarefa. Após receber os dados, apenas um componente será redesenhado. A cadeia de processamento de dados é muito curta e óbvia e, para um máximo de 4 saltos de código, você pode encontrar a lógica necessária e depurá-la. Além disso, se traçarmos uma analogia com uma parede de tijolos, podemos remover qualquer tijolo dessa parede e substituí-lo por outro sem afetar a estabilidade dessa parede.

Bônus 1: Funcionalidade aprimorada dos componentes da estrutura


Um bônus foi a capacidade de complementar ou alterar o comportamento dos componentes da biblioteca de apresentações. Por exemplo, uma reação no caso de um erro na exibição não renderiza o aplicativo inteiro, se uma pequena revisão for feita:
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}`
            );
        }
    }

}

Agora a reação não atrairá apenas o componente em que o erro ocorreu. Da mesma maneira simples, você pode adicionar monitoramento, análises e outros nishtyaki.

Bônus 2: Validação de Dados


Além de descrever e transferir dados entre camadas, os modelos têm uma grande margem de oportunidade. Por exemplo, se você conectar a biblioteca de validador de classe , simplesmente suspendendo os decoradores, poderá validar os dados nesses modelos, incluindo com um pouco de refinamento, você pode validar formulários da web.

Bônus 3: Criando Entidades


Além disso, se você precisar trabalhar com um banco de dados local, poderá conectar a biblioteca typeorm e seus modelos se transformarão em entidades pelas quais o banco de dados será gerado e executado.

Obrigado pela atenção


Se você gostou do artigo ou da abordagem, curta e não tenha medo de experimentar. Se você é adepto do Redux e não gosta de discordar, explique nos comentários como dimensiona, testa e valida os dados em seu aplicativo.

All Articles