Construindo uma estratégia anticorrosiva de front-end baseada em Observable

Introdução: O ciclo de vida e iteração do negócio To B geralmente dura muitos anos. Com a iteração e evolução do produto, os relacionamentos front-end e back-end centrados em chamadas de interface se tornarão muito complexos. Após muitos anos de iterações, qualquer modificação na interface pode causar problemas imprevistos ao produto. Nesse caso, torna-se muito importante construir um aplicativo de front-end mais robusto para garantir a robustez e a escalabilidade do front-end em iterações de longo prazo. Este artigo se concentrará em como usar estratégias anticorrosivas de interface para evitar ou reduzir o impacto de alterações de interface no front-end.

Autor | Xie Yadong
Fonte | Alibaba Technology Public Account

O ciclo de vida e iteração do negócio To B geralmente dura muitos anos. Com a iteração e evolução do produto, os relacionamentos de front-end e back-end centrados em chamadas de interface se tornarão muito complicados. Após muitos anos de iterações, qualquer modificação na interface pode causar problemas imprevistos ao produto. Nesse caso, torna-se muito importante construir um aplicativo de front-end mais robusto para garantir a robustez e a escalabilidade do front-end em iterações de longo prazo. Este artigo se concentrará em como usar estratégias anticorrosivas de interface para evitar ou reduzir o impacto de alterações de interface no front-end.

Dilema e Dilema

Para explicar mais claramente os problemas enfrentados pelo front-end, tomamos como exemplo a página de painel comum no negócio Para B. Esta página contém três partes de exibição de informações: memória disponível, memória usada e proporção de memória usada.

Neste momento, as dependências entre os componentes front-end e as interfaces são mostradas na figura a seguir.

Quando a interface retorna o ajuste de estrutura, a forma como o componente MemoryFree chama a interface precisa ser ajustada. Da mesma forma, MemoryUsage e MemoryUsagePercent precisam ser modificados para funcionar.

Pode haver centenas de interfaces enfrentadas pelo verdadeiro negócio To B, e a lógica de integração de componentes e interfaces é muito mais complicada do que os exemplos acima.

Após vários anos ou até iterações mais longas, a interface irá gerar gradualmente várias versões. Considerando a estabilidade da interface e os hábitos do usuário, o front-end geralmente depende de várias versões da interface para construir a interface ao mesmo tempo. Quando algumas interfaces precisam ser ajustadas offline ou alteradas, o front-end precisa re-entender a lógica de negócios e fazer muitos ajustes na lógica do código para garantir a operação estável da interface.

As alterações comuns de interface que afetam o front-end incluem, mas não se limitam a:

  • ajuste de campo de retorno
  • mudança de chamada
  • Coexistência de várias versões

Quando o front-end se depara com um negócio baseado em plataforma, esses problemas se tornam mais difíceis. Os produtos de plataforma encapsularão um ou mais mecanismos subjacentes. Por exemplo, plataformas de aprendizado de máquina podem ser criadas com base em mecanismos de aprendizado de máquina, como TensorFlow e Pytorch, e plataformas de computação em tempo real podem ser criadas com base em mecanismos de computação como Flink e Spark.

Embora a plataforma encapsule a maioria das interfaces do mecanismo na camada superior, é inevitável que algumas interfaces de baixo nível sejam transmitidas diretamente de forma transparente para o front end. mudanças de interface da plataforma, mas também enfrenta o motor de código aberto.Desafios colocados pelas mudanças de interface.

A situação que o front-end enfrenta é determinada pelos relacionamentos exclusivos de front-end e back-end. Diferente de outros campos, no negócio To B, o front-end geralmente aceita o fornecimento do fornecedor back-end como um cliente downstream e, em alguns casos, torna-se um seguidor do back-end.

No relacionamento cliente/fornecedor, o front-end é downstream e a equipe de back-end é upstream.O conteúdo da interface e o tempo de lançamento são geralmente determinados pela equipe de back-end.

No relacionamento do seguidor, a equipe de back-end upstream não fará nenhum ajuste de acordo com as necessidades da equipe de front-end, e o front-end só pode se adequar aos modelos upstream e back-end. Isso geralmente acontece quando o front-end não pode exercer influência sobre a equipe de back-end upstream, como quando o front-end precisa projetar a interface com base na interface do projeto de código aberto ou quando o modelo do back-end equipe é muito madura e difícil de modificar.

O autor de Clean Architecture descreveu esse dilema de projeto de arquitetura incorporada, que é muito semelhante ao dilema que descrevemos acima.

O software deve ser uma coisa de longa duração, e o firmware se tornará obsoleto à medida que o hardware evoluir, mas a realidade é que, embora o software em si não se desgaste com o tempo, o hardware e seu firmware sim. Obsoleto com o tempo, o software precisa ser alterado em conformidade.

Seja um relacionamento cliente/fornecedor ou um relacionamento de seguidores, assim como o software não pode determinar o desenvolvimento e a iteração do hardware, é difícil ou impossível para o front-end determinar o design do mecanismo e da interface, embora o próprio front-end o faça. não muda com o tempo. Não disponível, mas o mecanismo de tecnologia e as interfaces relacionadas ficarão desatualizados com o tempo, e o código de front-end apodrecerá gradualmente com a substituição iterativa do mecanismo de tecnologia e, eventualmente, será forçado a ser reescrito.

Design de duas camadas anticorrosivas

Antes do nascimento do Windows, os engenheiros introduziram o conceito de HAL (Hardware Abstraction Layer) para resolver os problemas de manutenção de hardware, firmware e software mencionados acima. não precisa de modificações frequentes devido a alterações de hardware ou firmware.

A ideia de design do HAL também é chamada de Anticorruption Layer in Domain Driven Design (DDD). Dentre os diversos mapeamentos de contexto definidos pelo DDD, a camada anticorrupção é a mais defensiva. É frequentemente usado quando a equipe downstream precisa evitar preferências técnicas externas ou intrusão do modelo de domínio e pode ajudar a isolar bem o modelo upstream do modelo downstream.

Podemos introduzir o conceito de camada anticorrosiva no front-end para reduzir ou evitar o impacto das alterações na interface de mapeamento de contexto do back-end atual no código do front-end.

Existem muitas maneiras de implementar camadas anticorrosivas na indústria.GraphQL e BFF, que têm sido populares nos últimos anos, podem ser usados ​​como alternativas, mas a seleção de tecnologia também é limitada por cenários de negócios. Completamente diferente do negócio To C, no negócio To B, o relacionamento entre front-end e back-end é geralmente um relacionamento cliente/fornecedor ou seguidor/seguidor. Sob esse relacionamento, tornou-se irreal esperar que o back-end coopere com o front-end para transformar a interface com o GraphQL, e a construção do BFF geralmente requer recursos de implantação adicionais e custos de operação e manutenção.

Nos casos acima, construir uma camada anticorrosiva no navegador é uma solução mais viável, mas construir uma camada anticorrosiva no navegador também enfrenta desafios.

Seja React, Angular ou Vue, existem inúmeras soluções de camada de dados, desde Mobx, Redux, Vuex, etc. a camada de visualização?Desacoplamento completo? A solução Observable representada por RxJS pode ser a melhor escolha neste momento.

RxJS é a implementação JavaScript do projeto ReactiveX, que originalmente era uma extensão do LINQ e foi desenvolvido por uma equipe liderada pelo arquiteto da Microsoft Erik Meijer. O objetivo deste projeto é fornecer uma interface de programação consistente para ajudar os desenvolvedores a lidar com fluxos de dados assíncronos com mais facilidade. Atualmente, o RxJS é frequentemente usado como uma ferramenta de desenvolvimento de programação reativa em desenvolvimento, mas no cenário de construção de uma camada anticorrosiva, a solução Observable representada pelo RxJS também pode desempenhar um papel importante.

Escolhemos o RxJS principalmente com base nas seguintes considerações:

  • A capacidade de unificar diferentes fontes de dados: o RxJS pode converter websockets, solicitações http e até ações do usuário, cliques em páginas etc. em um objeto observável unificado.
  • A capacidade de unificar diferentes tipos de dados: RxJS unifica dados assíncronos e síncronos em objetos observáveis.
  • Recursos avançados de processamento de dados: o RxJS fornece uma variedade de operadores Operadores, que podem pré-processar Observáveis ​​antes de assinar.
  • Não invade a arquitetura front-end: o Observable do RxJS pode ser convertido de e para o Promise, o que significa que todos os conceitos do RxJS podem ser completamente encapsulados na camada de dados, e somente o Promise pode ser exposto à camada de visualização.

Quando o RxJS é introduzido para converter todos os tipos de interfaces em objetos Observable, os componentes de visualização front-end irão depender apenas de Observables e serão desacoplados dos detalhes da implementação da interface. Promessas, e o que é obtido na camada de visualização é puro Promessas podem ser usadas com qualquer esquema e estrutura de camada de dados.

Além de converter para Promise, os desenvolvedores também podem misturar com soluções RxJS na camada de renderização, como rxjs-hooks, para uma melhor experiência de desenvolvimento.

Implementação de três camadas anticorrosivas

Referindo-se ao design da camada anticorrosão acima, implementamos o código da camada anticorrosão com RxJS Observable como o núcleo no projeto do painel no início.

O código central da camada anticorrosiva é o seguinte

export function getMemoryFreeObservable(): Observable<number> {
  return fromFetch("/api/v1/memory/free").pipe(mergeMap((res) => res.json()));
}

export function getMemoryUsageObservable(): Observable<number> {
  return fromFetch("/api/v1/memory/usage").pipe(mergeMap((res) => res.json()));
}

export function getMemoryUsagePercent(): Promise<number> {
  return lastValueFrom(forkJoin([getMemoryFreeObservable(), getMemoryUsageObservable()]).pipe(
    map(([usage, free]) => +((usage / (usage + free)) * 100).toFixed(2))
  ));
}

export function getMemoryFree(): Promise<number> {
  return lastValueFrom(getMemoryFreeObservable());
}

export function getMemoryUsage(): Promise<number> {
  return lastValueFrom(getMemoryUsageObservable());
}

O código de implementação de MemoryUsagePercent é o seguinte: Neste momento, o componente não dependerá mais da interface específica, mas dependerá diretamente da implementação da camada anticorrosiva.

function MemoryUsagePercent() {
  const [usage, setUsage] = useState<number>(0);
  useEffect(() => {
    (async () => {
      const result = await getMemoryUsagePercent();
      setUsage(result);
    })();
  }, []);
  return <div>Usage: {usage} %</div>;
}

export default MemoryUsagePercent;

1 Ajuste de campo de retorno

Quando o campo de retorno é alterado, a camada anticorrosiva pode interceptar efetivamente o impacto da interface no componente. Quando os dados de retorno de /api/v2/quota/free e /api/v2/quota/usage são alterados para o seguinte estrutura

{
  requestId: string;
  data: number;
}

Precisamos apenas ajustar as duas linhas de código da camada anticorrosiva.Observe que, neste momento, o getMemoryUsagePercent empacotado por nossa camada superior é baseado em Observable, portanto, nenhuma alteração é necessária.

export function getMemoryUsageObservable(): Observable<number> {
  return fromFetch("/api/v2/memory/free").pipe(
     mergeMap((res) => res.json()),
+    map((data) => data.data)
  );
}

export function getMemoryUsageObservable(): Observable<number> {
  return fromFetch("/api/v2/memory/usage").pipe(
     mergeMap((res) => res.json()),
+    map((data) => data.data)
  );
}

Na camada anticorrosiva Observable, haverá dois designs de Observable de alto nível e Observable de baixo nível. No exemplo acima, Free Observable e Usage Observable são encapsulamento de baixo nível, enquanto Percent Observable usa Free e Usage Observable para alta -level design. Encapsulamento de alto nível. Quando o encapsulamento de baixo nível é alterado, devido às características do próprio Observable, o encapsulamento de alto nível geralmente não precisa ser alterado. Este também é um benefício adicional trazido a nós pelo camada anticorrosiva.

2 O método de chamada é alterado

A camada anticorrosiva também pode desempenhar um papel quando o método de invocação é alterado. /api/v3/memory retorna diretamente os dados de uso e gratuito.O formato da interface é o seguinte.

{
  requestId: string;
  data: {
    free: number;
    usage: number;
  }
}

O código da camada anticorrosiva só precisa ser atualizado da seguinte forma para garantir que o código da camada componente não precise ser modificado.

export function getMemoryObservable(): Observable<{ free: number; usage: number }> {
  return fromFetch("/api/v3/memory").pipe(
    mergeMap((res) => res.json()),
    map((data) => data.data)
  );
}

export function getMemoryFreeObservable(): Observable<number> {
  return getMemoryObservable().pipe(map((data) => data.free));
}

export function getMemoryUsageObservable(): Observable<number> {
  return getMemoryObservable().pipe(map((data) => data.usage));
}

export function getMemoryUsagePercent(): Promise<number> {
  return lastValue(getMemoryObservable().pipe(
    map(({ usage, free }) => +((usage / (usage + free)) * 100).toFixed(2))
  ));
}

3 Coexistência de várias versões

Quando o código front-end precisa ser implantado em vários ambientes, a interface v3 está disponível em alguns ambientes, enquanto apenas a interface v2 é implantada em alguns ambientes. Neste momento, ainda podemos proteger as diferenças de ambiente no anticorrosão camada.

export function getMemoryLegacyObservable(): Observable<{ free: number; usage: number }> {
  const legacyUsage = fromFetch("/api/v2/memory/usage").pipe(
    mergeMap((res) => res.json())
  );
  const legacyFree = fromFetch("/api/v2/memory/free").pipe(
    mergeMap((res) => res.json())
  );
  return forkJoin([legacyUsage, legacyFree], (usage, free) => ({
    free: free.data.free,
    usage: usage.data.usage,
  }));
}

export function getMemoryObservable(): Observable<{ free: number; usage: number }> {
  const current = fromFetch("/api/v3/memory").pipe(
    mergeMap((res) => res.json()),
    map((data) => data.data)
  );
  return race(getMemoryLegacyObservable(), current);
}

export function getMemoryFreeObservable(): Observable<number> {
  return getMemoryObservable().pipe(map((data) => data.free));
}

export function getMemoryUsageObservable(): Observable<number> {
  return getMemoryObservable().pipe(map((data) => data.usage));
}

export function getMemoryUsagePercent(): Promise<number> {
  return lastValue(getMemory().pipe(
    map(({ usage, free }) => +((usage / (usage + free)) * 100).toFixed(2))
  ));
}

Através do operador de corrida, quando a interface de qualquer versão da v2 e v3 estiver disponível, a camada anticorrosiva pode funcionar normalmente, e a camada de componentes não precisa mais se atentar ao impacto da interface pelo ambiente.

Quatro aplicações adicionais

A camada anticorrosiva não é apenas uma camada adicional de encapsulamento e isolamento da interface, mas também possui as seguintes funções.

1 Mapeamento de conceito

A semântica da interface e a semântica dos dados exigidos pelo front-end às vezes não correspondem completamente.Ao chamar a interface diretamente na camada do componente, todos os desenvolvedores precisam saber o suficiente sobre o mapeamento semântico entre a interface e a interface. Com a camada anticorrosão, o método de chamada fornecido pela camada anticorrosão contém a semântica real dos dados, reduzindo o custo de compreensão secundária dos desenvolvedores.

2 Adaptação de formato

Em muitos casos, a estrutura de dados e o formato retornados pela interface não correspondem ao formato de dados exigido pelo front-end.Ao adicionar lógica de conversão de dados à camada anticorrosiva, a intrusão de dados da interface no código de negócios pode ser reduzida. No caso acima, encapsulamos o retorno de dados de getMemoryUsagePercent, para que a camada de componente possa usar diretamente os dados de porcentagem sem convertê-los novamente.

3 Cache de Interface

No caso em que vários serviços dependem da mesma interface, podemos adicionar lógica de cache através da camada anticorrosiva, reduzindo efetivamente a pressão de chamada da interface.

Semelhante à adaptação de formato, encapsular a lógica de cache na camada anticorrosão pode evitar o cache secundário de dados pela camada de componente e pode gerenciar centralmente os dados armazenados em cache para reduzir a complexidade do código.Um exemplo simples de cache é o seguinte.

class CacheService {
  private cache: { [key: string]: any } = {};
  getData() {
    if (this.cache) {
      return of(this.cache);
    } else {
      return fromFetch("/api/v3/memory").pipe(
        mergeMap((res) => res.json()),
        map((data) => data.data),
        tap((data) => {
          this.cache = data;
        })
      );
    }
  }
}

4 Estabilidade inferior

Quando a estabilidade da interface é ruim, a prática usual é lidar com o erro de resposta na camada de componente, este tipo de lógica geralmente é complicado e o custo de manutenção da camada de componente será alto. Podemos verificar a estabilidade através da camada anticorrosiva. Quando a interface falha, podemos retornar os dados de negócios finais. Como os dados finais são mantidos uniformemente na camada anticorrosiva, testes e modificações subsequentes serão mais conveniente. Na camada anticorrosão de coexistência de várias versões acima, adicione o seguinte código, neste momento, mesmo que as interfaces v2 e v3 não possam retornar dados, o front-end ainda poderá permanecer disponível.

  return race(getMemoryLegacy(), current).pipe(
+   catchError(() => of({ usage: '-', free: '-' }))
  );

5 Depuração e testes conjuntos

A interface e o front-end podem ser desenvolvidos em paralelo. No momento, nenhuma interface de back-end real está disponível para desenvolvimento de front-end. Comparado com a maneira tradicional de construir uma API simulada, é mais conveniente simular os dados diretamente na camada anticorrosiva.

export function getMemoryFree(): Observable<number> {
  return of(0.8);
}

export function getMemoryUsage(): Observable<number> {
  return of(1.2);
}

export function getMemoryUsagePercent(): Observable<number> {
  return forkJoin([getMemoryUsage(), getMemoryFree()]).pipe(
    map(([usage, free]) => +((usage / (usage + free)) * 100).toFixed(2))
  );
}

Os dados simulados na camada anticorrosão também podem ser usados ​​para testar a página, como o impacto de simular uma grande quantidade de dados no desempenho da página.

export function getLargeList(): Observable<string[]> {
  const options = [];
  for (let i = 0; i < 100000; i++) {
    const value = `${i.toString(36)}${i}`;
    options.push(value);
  }
  return of(options);
}

Cinco Resumo

Neste artigo abordamos o seguinte:

  1. Quais são os dilemas e os motivos pelos quais o front-end enfrenta mudanças frequentes de interface?
  2. Ideia de design e seleção de tecnologia de camada anticorrosiva
  3. Exemplo de código de implementação de camada anticorrosiva usando Observable
  4. Papel adicional da camada anticorrosiva

Os leitores devem observar que é razoável introduzir uma camada anticorrosiva de front-end apenas em cenários específicos, ou seja, o front-end está em uma relação de seguidor ou fornecedor/cliente e enfrenta um grande número de interfaces que não podem garantir estabilidade e compatibilidade. Se a camada anticorrosiva puder ser construída no Gateway de back-end ou o número de interfaces for pequeno, o custo adicional da introdução da camada anticorrosiva superará os benefícios.

O RxJS fornece mais recursos observáveis ​​no cenário de construção da camada anticorrosiva. Se os leitores não precisarem de ferramentas complexas de conversão de operadores, eles também poderão criar soluções de construção observáveis ​​por conta própria. Na verdade, apenas 100 linhas de código podem ser usadas para implementar  mini-rxjs - StackBlitz .

A arquitetura front-end transformada não dependerá mais diretamente da implementação da interface e não invadirá o design da camada de dados front-end existente. Também pode realizar mapeamento de conceito, adaptação de formato, cache de interface, análise de estabilidade e auxiliar na depuração conjunta e testando. Todo o código de exemplo deste artigo está disponível no repositório  GitHub - vthinkxie/rxjs-acl: Anti Corruption Layer with RxJS  .

Link original

Este artigo é conteúdo original do Alibaba Cloud e não pode ser reproduzido sem permissão. 

Acho que você gosta

Origin blog.csdn.net/yunqiinsight/article/details/123686863
Recomendado
Clasificación