Building a front-end anti-corrosion strategy based on Observable

Introduction: The life cycle and iteration of the To B business usually lasts for many years. With the iteration and evolution of the product, the front-end and back-end relationships centered on interface calls will become very complex. After many years of iterations, any modification to the interface can cause unforeseen problems to the product. In this case, it becomes very important to build a more robust front-end application to ensure the robustness and scalability of the front-end under long-term iterations. This article will focus on how to use interface anti-corrosion strategies to avoid or reduce the impact of interface changes on the front end.

Author | Xie Yadong
Source | Alibaba Technology Public Account

The life cycle and iteration of the To B business usually lasts for many years. With the iteration and evolution of the product, the front-end and back-end relationships centered on interface calls will become very complicated. After many years of iterations, any modification to the interface can cause unforeseen problems to the product. In this case, it becomes very important to build a more robust front-end application to ensure the robustness and scalability of the front-end under long-term iterations. This article will focus on how to use interface anti-corrosion strategies to avoid or reduce the impact of interface changes on the front end.

Dilemma and Dilemma

In order to explain the problems faced by the front-end more clearly, we take the common dashboard page in the To B business as an example. This page contains three parts of information display: available memory, used memory, and used memory ratio.

At this time, the dependencies between front-end components and interfaces are shown in the following figure.

When the interface returns structure adjustment, the way the MemoryFree component calls the interface needs to be adjusted. Likewise, MemoryUsage and MemoryUsagePercent have to be modified to work.

There may be hundreds of interfaces faced by the real To B business, and the integration logic of components and interfaces is far more complicated than the above examples.

After several years or even longer iterations, the interface will gradually generate multiple versions. Considering the stability of the interface and user habits, the front-end often relies on multiple versions of the interface to build the interface at the same time. When some interfaces need to be adjusted offline or changed, the front end needs to re-understand the business logic and make a lot of code logic adjustments to ensure the stable operation of the interface.

Common interface changes that affect the front end include but are not limited to:

  • return field adjustment
  • change in call
  • Coexistence of multiple versions

When the front end is faced with a platform-based business, such problems will become more difficult. Platform products will encapsulate one or more underlying engines. For example, machine learning platforms may be built based on machine learning engines such as TensorFlow and Pytorch, and real-time computing platforms may be built based on computing engines such as Flink and Spark.

Although the platform will encapsulate most of the interfaces of the engine in the upper layer, it is inevitable that some low-level interfaces will be directly transparently transmitted to the front end. At this time, the front end not only has to deal with the interface changes of the platform, but also faces the open source engine. Challenges posed by interface changes.

The predicament the front end faces is determined by the unique front-end and back-end relationships. Different from other fields, in the To B business, the front-end usually accepts the supply of the back-end supplier as a downstream customer, and in some cases becomes a follower of the back-end.

In the customer/supplier relationship, the front-end is downstream and the back-end team is upstream. The interface content and launch time are usually determined by the back-end team.

In the follower relationship, the upstream back-end team will not make any adjustments according to the needs of the front-end team, and the front-end can only conform to the upstream and back-end models. This usually happens when the front-end cannot exert influence on the upstream back-end team, such as when the front-end needs to design the interface based on the interface of the open source project, or when the model of the back-end team is very mature and difficult to modify.

The author of Clean Architecture has described such an embedded architecture design dilemma, which is very similar to the dilemma we described above.

Software is supposed to be a long-lived thing, and firmware will become obsolete as hardware evolves, but the reality is that while software itself doesn't wear out over time, hardware and its firmware do. Obsolete over time, the software needs to be changed accordingly.

Whether it is a customer/supplier relationship or a follower relationship, just as software cannot determine the development and iteration of hardware, it is difficult or impossible for the front end to determine the design of the engine and interface, although the front end itself does not change over time. Not available, but the technology engine and related interfaces will become outdated over time, and the front-end code will gradually rot with the iterative replacement of the technology engine, and will eventually be forced to be rewritten.

Two anti-corrosion layer design

Before the birth of Windows, engineers introduced the concept of HAL (Hardware Abstraction Layer) in order to solve the above-mentioned maintainability problems of hardware, firmware and software. HAL provides services for software and shields the implementation details of hardware, so that software does not have to Frequent modifications due to hardware or firmware changes.

The design idea of ​​HAL is also called Anticorruption Layer in Domain Driven Design (DDD). Among the various context mappings defined by DDD, the anti-corruption layer is the most defensive. It is often used when the downstream team needs to prevent external technical preferences or domain model intrusion, and can help to isolate the upstream model from the downstream model well.

We can introduce the concept of anti-corrosion layer in the front-end to reduce or avoid the impact of changes in the context mapping interface of the current back-end on the front-end code.

There are many ways to implement anti-corrosion layers in the industry. Both GraphQL and BFF, which have been popular in recent years, can be used as alternatives, but technology selection is also limited by business scenarios. Completely different from the To C business, in the To B business, the relationship between the front and back ends is usually a customer/supplier or a follower/follower relationship. Under this relationship, it has become unrealistic to hope that the back-end cooperates with the front-end to transform the interface with GraphQL, and the construction of BFF generally requires additional deployment resources and operation and maintenance costs.

In the above cases, building an anti-corrosion layer on the browser side is a more feasible solution, but building an anti-corrosion layer in the browser also faces challenges.

Whether it is React, Angular or Vue, there are countless data layer solutions, from Mobx, Redux, Vuex, etc., these data layer solutions will actually invade the view layer. Is there an anti-corrosion layer solution that can be combined with the view layer? Complete decoupling? The Observable solution represented by RxJS may be the best choice at this time.

RxJS is the JavaScript implementation of the ReactiveX project, which was originally an extension to LINQ and was developed by a team led by Microsoft architect Erik Meijer. The goal of this project is to provide a consistent programming interface to help developers handle asynchronous data streams more easily. At present, RxJS is often used as a reactive programming development tool in development, but in the scenario of building an anti-corrosion layer, the Observable solution represented by RxJS can also play a huge role.

We chose RxJS mainly based on the following considerations:

  • The ability to unify different data sources: RxJS can convert websockets, http requests, and even user actions, page clicks, etc. into a unified Observable object.
  • The ability to unify different types of data: RxJS unifies asynchronous and synchronous data into Observable objects.
  • Rich data processing capabilities: RxJS provides a wealth of Operator operators, which can pre-process Observables before subscribing.
  • Does not invade the front-end architecture: RxJS's Observable can be converted to and from Promise, which means that all concepts of RxJS can be completely encapsulated in the data layer, and only Promise can be exposed to the view layer.

When RxJS is introduced to convert all types of interfaces into Observable objects, the front-end view components will only rely on Observables, and will be decoupled from the details of the interface implementation. At the same time, Observables can be converted to and from Promises, and what is obtained at the view layer is pure Promises can be used with any data layer scheme and framework.

In addition to converting to Promise, developers can also mix with RxJS solutions in the rendering layer, such as rxjs-hooks, for a better development experience.

Implementation of three anti-corrosion layers

Referring to the above anti-corrosion layer design, we implement the anti-corrosion layer code with RxJS Observable as the core in the dashboard project at the beginning.

The core code of the anti-corrosion layer is as follows

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());
}

The implementation code of MemoryUsagePercent is as follows. At this time, the component will no longer depend on the specific interface, but directly depend on the implementation of the anti-corrosion layer.

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 Return field adjustment

When the return field is changed, the anti-corrosion layer can effectively intercept the impact of the interface on the component. When the return data of /api/v2/quota/free and /api/v2/quota/usage is changed to the following structure

{
  requestId: string;
  data: number;
}

We only need to adjust the two lines of code of the anti-corrosion layer. Note that at this time, the getMemoryUsagePercent packaged by our upper layer is based on Observable so no changes are required.

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)
  );
}

In the Observable anti-corrosion layer, there will be two designs of high-level Observable and low-level Observable. In the above example, Free Observable and Usage Observable are low-level encapsulation, while Percent Observable uses Free and Usage Observable for high-level design. High-level encapsulation. When the low-level encapsulation is changed, due to the characteristics of Observable itself, the high-level encapsulation often does not need to be changed. This is also an additional benefit brought to us by the anti-corrosion layer.

2 The calling method is changed

The anti-corrosion layer can also play a role when the invocation method changes. /api/v3/memory directly returns the data of free and usage. The interface format is as follows.

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

The anti-corrosion layer code only needs to be updated as follows to ensure that the component layer code does not need to be modified.

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 Coexistence of multiple versions

When the front-end code needs to be deployed in multiple environments, the v3 interface is available in some environments, while only the v2 interface is deployed in some environments. At this time, we can still shield the environment differences in the anti-corrosion layer.

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))
  ));
}

Through the race operator, when the interface of any version of v2 and v3 is available, the anti-corrosion layer can work normally, and the component layer no longer needs to pay attention to the impact of the interface by the environment.

Four additional applications

The anti-corrosion layer is not only an additional layer of encapsulation and isolation of the interface, but also has the following functions.

1 Concept Mapping

The interface semantics and the semantics of the data required by the front-end sometimes do not completely correspond. When calling the interface directly at the component layer, all developers need to know enough about the semantic mapping between the interface and the interface. With the anti-corrosion layer, the calling method provided by the anti-corrosion layer contains the real semantics of the data, reducing the secondary understanding cost of developers.

2 Format adaptation

In many cases, the data structure and format returned by the interface do not match the data format required by the front end. By adding data conversion logic to the anti-corrosion layer, the intrusion of interface data to business code can be reduced. In the above case, we encapsulate the data return of getMemoryUsagePercent, so that the component layer can directly use the percentage data without converting it again.

3 Interface Cache

In the case where multiple services depend on the same interface, we can add caching logic through the anti-corrosion layer, thereby effectively reducing the calling pressure of the interface.

Similar to format adaptation, encapsulating the caching logic in the anti-corrosion layer can avoid the secondary caching of data by the component layer, and can centrally manage the cached data to reduce the complexity of the code. A simple caching example is as follows.

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 Stability bottom

When the interface stability is poor, the usual practice is to deal with the response error at the component layer. This kind of logic is usually complicated, and the maintenance cost of the component layer will be high. We can check the stability through the anti-corrosion layer. When the interface fails, we can return the bottom-line business data. Since the bottom-line data is uniformly maintained in the anti-corrosion layer, subsequent testing and modification will be more convenient. In the multi-version coexistence anti-corrosion layer above, add the following code, at this time, even if the v2 and v3 interfaces cannot return data, the front end can still remain available.

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

5 Joint debugging and testing

The interface and front-end may be developed in parallel. At this time, no real back-end interface is available for front-end development. Compared with the traditional way of building mock api, it is more convenient to mock the data directly in the anti-corrosion layer.

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))
  );
}

Mock data in the anti-corrosion layer can also be used to test the page, such as the impact of mocking a large amount of data on page performance.

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);
}

Five Summary

In this article we cover the following:

  1. What are the dilemmas and reasons why the front end faces frequent interface changes?
  2. Design idea and technology selection of anti-corrosion layer
  3. Code example of implementing anti-corrosion layer using Observable
  4. Additional role of anti-corrosion layer

Readers should note that it is reasonable to introduce a front-end anti-corrosion layer only in specific scenarios, that is, the front-end is in a follower or supplier/customer relationship and faces a large number of interfaces that cannot guarantee stability and compatibility. If the anti-corrosion layer can be built on the back-end Gateway, or the number of interfaces is small, the additional cost of introducing the anti-corrosion layer will outweigh the benefits.

RxJS provides more Observable capabilities in the anti-corrosion layer construction scenario. If readers do not need complex operators conversion tools, they can also build Observable construction solutions by themselves. In fact, only 100 lines of code can be used to implement  mini-rxjs - StackBlitz .

The transformed front-end architecture will no longer directly depend on the interface implementation, and will not invade the existing front-end data layer design. It can also undertake concept mapping, format adaptation, interface caching, stability analysis, and assist in joint debugging and testing. All sample code in this article is available in the repository  GitHub - vthinkxie/rxjs-acl: Anti Corruption Layer with RxJS  .

Original link

This article is original content of Alibaba Cloud and may not be reproduced without permission. 

Guess you like

Origin blog.csdn.net/yunqiinsight/article/details/123686863