随着时间的推移调整 JavaScript 抽象

Avatar of Kaloyan Kosev
Kaloyan Kosev

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

即使您没有阅读我的文章 在处理远程数据时 JavaScript 抽象的重要性,您可能也已经相信可维护性和可扩展性对您的项目非常重要,而实现这一目标的途径就是引入抽象

在本篇文章中,让我们假设在 JavaScript 中,抽象就是一个模块

模块的初始实现仅仅是其漫长(且希望持久)生命周期过程的开始。我认为模块的生命周期中存在 3 个主要事件

  1. 模块的引入。 初始实现以及在项目中重复使用它的过程。
  2. 修改模块。 随着时间的推移调整模块。
  3. 删除模块。

在我的 上一篇文章 中,重点仅仅放在第一个事件上。在本文中,更多地思考第二个事件。

处理模块的更改是我经常遇到的一个痛点。与引入模块相比,开发人员维护或更改模块的方式对于保持项目的可维护性和可扩展性同样重要,甚至更为重要。我见过一个编写良好且抽象的模块随着时间的推移被更改彻底破坏。有时,我就是造成这些灾难性更改的人!

当我说灾难性时,我指的是从可维护性和可扩展性的角度来看是灾难性的。我理解,从接近截止日期和发布必须正常工作的功能的角度来看,放慢速度思考更改的所有潜在影响并不总是可行的。

开发人员的更改可能不是最佳的原因不胜枚举。我想特别强调一个

以可维护的方式进行更改的技巧

以下是如何像专业人士一样开始进行更改的方法。

让我们从一个代码示例开始:一个 API 模块。我选择它是因为在启动项目时,与外部 API 通信是我定义的第一个基本抽象之一。其理念是将所有与 API 相关的配置和设置(如基本 URL、错误处理逻辑等)存储在此模块中。

让我们只引入一个设置,API.url,一个私有方法,API._handleError(),以及一个公共方法,API.get()

class API {
  constructor() {
    this.url = 'http://whatever.api/v1/';
  }

  /**
   * Fetch API's specific way to check
   * whether an HTTP response's status code is in the successful range.
   */
  _handleError(_res) {
      return _res.ok ? _res : Promise.reject(_res.statusText);
  }

  /**
   * Get data abstraction
   * @return {Promise}
   */
  get(_endpoint) {
      return window.fetch(this.url + _endpoint, { method: 'GET' })
          .then(this._handleError)
          .then( res => res.json())
          .catch( error => {
              alert('So sad. There was an error.');
              throw new Error(error);
          });
  }
};

在此模块中,我们唯一的公共方法API.get()返回一个 Promise。在我们所有需要获取远程数据的地方,我们使用 API 模块抽象而不是直接通过window.fetch()调用 Fetch API。例如,获取用户信息API.get('user')或当前天气预报API.get('weather')。此实现的重要之处在于Fetch API 与我们的代码没有紧密耦合

现在,假设有一个更改请求!我们的技术负责人要求我们切换到另一种获取远程数据的方法。我们需要切换到 Axios。我们如何应对这一挑战?

在我们开始讨论方法之前,让我们首先总结一下哪些内容保持不变以及哪些内容发生了更改

  1. 更改:在我们的公共API.get()方法中
    • 我们需要将window.fetch()调用更改为axios()。并且我们需要再次返回一个 Promise,以保持我们实现的一致性。Axios 基于 Promise。太棒了!
    • 我们服务器的响应是 JSON。使用 Fetch API 链一个.then( res => res.json())语句来解析我们的响应数据。使用 Axios,服务器提供的响应位于data属性下,我们不需要解析它。因此,我们需要将 .then 语句更改为.then( res => res.data )
  2. 更改:在我们的私有API._handleError方法中
    • 对象响应中缺少ok布尔标志。但是,存在statusText属性。我们可以挂钩它。如果其值为'OK',则一切正常。

      旁注:是的,在 Fetch API 中将ok设置为true与在 Axios 的statusText中具有'OK'并不相同。但让我们保持简单,并且为了避免过于广泛,让我们保持现状,不引入任何高级错误处理。

  3. 无更改API.url保持不变,以及我们catch错误并alert它们的方式。

一切清晰!现在让我们深入研究应用这些更改的实际方法。

方法 1:删除代码。编写代码。

class API {
  constructor() {
    this.url = 'http://whatever.api/v1/'; // says the same
  }

  _handleError(_res) {
      // DELETE: return _res.ok ? _res : Promise.reject(_res.statusText);
      return _res.statusText === 'OK' ? _res : Promise.reject(_res.statusText);
  }

  get(_endpoint) {
      // DELETE: return window.fetch(this.url + _endpoint, { method: 'GET' })
      return axios.get(this.url + _endpoint)
          .then(this._handleError)
          // DELETE: .then( res => res.json())
          .then( res => res.data)
          .catch( error => {
              alert('So sad. There was an error.');
              throw new Error(error);
          });
  }
};

听起来很合理。提交。推送。合并。完成。

但是,在某些情况下,这可能不是一个好主意。想象一下以下情况:切换到 Axios 后,您发现存在一个功能无法使用 XMLHttpRequests(Axios 获取资源方法的接口),但之前使用 Fetch 的新式浏览器 API 时工作正常。我们现在该怎么办?

我们的技术负责人说,让我们在此特定用例中使用旧的 API 实现,并在其他所有地方继续使用 Axios。你该怎么办?在您的源代码控制历史记录中查找旧的 API 模块。还原。在此处添加if语句。在我看来,这听起来不太好。

必须有一种更简单、更易维护和可扩展的方式来进行更改!是的,确实有。

方法 2:重构代码。编写适配器!

有一个传入的更改请求!让我们重新开始,而不是删除代码,而是将 Fetch 的特定逻辑移动到另一个抽象中,该抽象将充当所有 Fetch 特定内容的适配器(或包装器)。

对于那些熟悉适配器模式(也称为包装器模式)的人来说,是的,这正是我们的目标!如果您对所有细节感兴趣,请参阅 此处提供的优秀极客介绍

以下是计划

步骤 1

将 API 模块中所有 Fetch 特定的行重构到一个新的抽象FetchAdapter中。

class FetchAdapter {
  _handleError(_res) {
      return _res.ok ? _res : Promise.reject(_res.statusText);
  }

  get(_endpoint) {
      return window.fetch(_endpoint, { method: 'GET' })
          .then(this._handleError)
          .then( res => res.json());
  }
};

步骤 2

通过删除 Fetch 特定的部分并保持其他所有内容不变来重构 API 模块。添加FetchAdapter作为依赖项(以某种方式)。

class API {
  constructor(_adapter = new FetchAdapter()) {
    this.adapter = _adapter;

    this.url = 'http://whatever.api/v1/';
  }

  get(_endpoint) {
      return this.adapter.get(_endpoint)
          .catch( error => {
              alert('So sad. There was an error.');
              throw new Error(error);
          });
  }
};

现在情况不同了!架构已以某种方式更改,您可以处理获取资源的不同机制(适配器)。最后一步:您猜对了!编写一个AxiosAdapter

const AxiosAdapter = {
  _handleError(_res) {
      return _res.statusText === 'OK' ? _res : Promise.reject(_res.statusText);
  },

  get(_endpoint) {
      return axios.get(_endpoint)
          .then(this._handleError)
          .then( res => res.data);
  }
};

并在 API 模块中,将默认adapter切换到 Axios。

class API {
  constructor(_adapter = new /*FetchAdapter()*/ AxiosAdapter()) {
    this.adapter = _adapter;

    /* ... */
  }
  /* ... */
};

太棒了!如果我们需要在此特定用例中使用旧的 API 实现,并在其他所有地方继续使用 Axios,该怎么办?没问题!

// Import your modules however you like, just an example.
import API from './API';
import FetchAdapter from './FetchAdapter';

// Uses the AxiosAdapter (the default one)
const API = new API();
API.get('user');

// Uses the FetchAdapter
const legacyAPI = new API(new FetchAdapter());
legacyAPI.get('user');

因此,下次您需要对项目进行更改时,请评估哪种方法更有意义

  • 删除代码。编写代码
  • 重构代码。编写适配器。

根据您的特定用例仔细判断。过度适配代码库并引入太多抽象可能会导致复杂性增加,这也不好。

适配愉快!