JavaScript 中的外观模式构建我们的外观

2025-06-07

JavaScript 中的外观模式

建立我们的门面

在构建应用程序时,我们经常会遇到外部 API 的问题。有些 API 的方法很简单,而有些 API 的方法却非常复杂。将它们统一到一个通用接口下,是外观模式的用途之一。

假设我们正在构建一个显示电影、电视节目、音乐和书籍信息的应用程序。每个信息都有不同的供应商。它们使用不同的方法实现,有各种不同的需求等等。我们必须记住或记录如何查询每种类型。

或者我们呢?

外观模式解决了这样的问题。这是一个通用接口,无论底层使用什么,它都具有相同的方法。

我准备了四种不同的资源服务实现方式:

class FetchMusic {
  get resources() {
    return [
      { id: 1, title: "The Fragile" },
      { id: 2, title: "Alladin Sane" },
      { id: 3, title: "OK Computer" }
    ];
  }

  fetch(id) {
    return this.resources.find(item => item.id === id);
  }
}

class GetMovie {
  constructor(id) {
    return this.resources.find(item => item.id === id);
  }

  get resources() {
    return [
      { id: 1, title: "Apocalypse Now" },
      { id: 2, title: "Die Hard" },
      { id: 3, title: "Big Lebowski" }
    ];
  }
}

const getTvShow = function(id) {
  const resources = [
    { id: 1, title: "Twin Peaks" },
    { id: 2, title: "Luther" },
    { id: 3, title: "The Simpsons" }
  ];

  return resources.find(item => item.id === 1);
};

const booksResource = [
  { id: 1, title: "Ulysses" },
  { id: 2, title: "Ham on Rye" },
  { id: 3, title: "Quicksilver" }
];
Enter fullscreen mode Exit fullscreen mode

它们使用不同的模式命名,实现方式有好有坏,工作量有大有小。为了避免过于复杂,我使用了通用响应格式的简单示例。但无论如何,这很好地说明了问题。

我们的立面设计

要创建外观,首先我们需要了解每个供应商的各个方面。如果需要额外的授权、更多参数等,则必须实现。这是额外的功能,当与不需要它的供应商一起使用时,可以丢弃。

外观的构建块是公共接口。无论你想查询哪个资源,都应该只使用一种方法。当然,在它下面可能还有更多方法,但公共访问应该受到限制并且易于使用。

首先,我们应该确定公共 API 的形式。对于这个例子来说,一个 getter 就足够了。这里唯一的区别是媒体类型——书籍、电影等等。所以类型将成为我们的基础。

接下来,我们来看看资源之间的共同点。每个资源都可以通过 ID 进行查询。因此,我们的 getter 应该接受一个参数,即 ID。

建立我们的门面

(我决定为此使用一个类,但这不是必需的。由对象文字或甚至函数集合组成的模块可能就足够了。尽管如此,我喜欢这种符号。)

class CultureFasade {
  constructor(type) {
    this.type = type;
  }
}
Enter fullscreen mode Exit fullscreen mode

首先,我们在构造函数中定义类型。这意味着每个外观实例都会返回不同的类型。我知道这看起来可能有点多余,但比起每次都使用单个函数实例并传递多个参数来说,这样做更方便。

好的,接下来就是定义我们的公共方法和私有方法。为了标注“私有”方法,我使用了著名的_而不是#,因为 CodePen 还不支持它。

正如我们之前所说,唯一的公共方法应该是我们的 getter。

class CultureFacade {
  constructor(type) {
    this.type = type;
  }

  get(id) {
    return id;
  }
}
Enter fullscreen mode Exit fullscreen mode

基础实现(框架)已经完成了。现在,让我们开始讨论类的真正核心部分——私有 getter。

首先,我们需要确定每个资源是如何查询的:

  • 音乐需要一个新的实例,然后在方法中传递ID get
  • Movie的每个实例返回数据,初始化时需要ID;
  • TV Show 只是一个接受 ID 并返回数据的单一函数;
  • 书籍只是一种资源,需要我们自己去查询。

我知道这一步看起来很繁琐,也没有必要,但请注意,现在我们真的不需要再去想任何事情了。概念阶段在设计和构建过程中非常重要

好的,音乐开始吧。

class CultureFacade {
  ...

  _findMusic(id) {
    const db = new FetchMusic();
    return db.fetch(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

我们已经创建了一个简单的方法,它完全按照我们之前描述的方式执行。剩下的三个只是例行公事。

class CultureFacade {
  ...

  _findMusic(id) {
    const db = new FetchMusic();
    return db.fetch(id);
  }

  _findMovie(id) {
    return new GetMovie(id);
  }

  _findTVShow(id) {
    return getTvShow(id);
  }

  _findBook(id) {
    return booksResource.find(item => item.id === id);
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,我们已经有了查询数据库的所有方法。

获取公共 API

作为一名程序员,我学到的最重要的事情之一就是永远不要依赖你的供应商。你永远不知道会发生什么。他们可能会受到攻击、关闭,你的公司可能会停止支付服务费用等等。

了解了这一点,我们的 getter 也应该使用一种外观。它应该尝试获取数据,而不是假设它会成功。

那么,让我们编写这样的方法。

class CultureFacade {
  ...

  get _error() {
    return { status: 404, error: `No item with this id found` };
  }

  _tryToReturn(func, id) {
    const result = func.call(this, id);

    return new Promise((ok, err) => !!result
      ? ok(result)
      : err(this._error));
  }
}
Enter fullscreen mode Exit fullscreen mode

我们先在这里停一下。如您所见,此方法也是私有的。为什么?公共方法不会从中受益。它需要了解其他私有方法。接下来,它需要两个参数——funcid。后者很明显,而前者则不然。好的,所以这将接受一个要运行的函数(或者更确切地说是我们类的方法)。如您所见,执行被分配给result变量。接下来,我们检查它是否成功,并返回一个。为什么要使用这种复杂的构造?使用甚至简单的语法Promise,Promise 非常易于调试和执行async/awaitthen/catch

哦,还有错误。没什么大不了的,只是一个 getter 返回了一条消息。这个可以更复杂一些,包含更多信息等等。我没有实现任何花哨的功能,因为实际上并不需要,而且我们的供应商也没有任何错误可以参考。

好的,我们现在有了什么?用于查询供应商的私有方法。我们尝试查询的内部外观。以及我们的公共 getter 框架。让我们将它扩展成一个完整的实体。

由于我们依赖于预定义类型,因此我们将使用非常强大的switch语句。

class CultureFacade {
  constructor(type) {
    this.type = type;
  }

  get(id) {
    switch (this.type) {
      case "music": {
        return this._tryToReturn(this._findMusic, id);
      }

      case "movie": {
        return this._tryToReturn(this._findMovie, id);
      }

      case "tv": {
        return this._tryToReturn(this._findTVShow, id);
      }

      case "book": {
        return this._tryToReturn(this._findBook, id);
      }

      default: {
        throw new Error("No type set!");
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

关于定义字符串类型的说明

我们的类型是手写的。这不是最佳实践。它应该放在一边定义,这样就不会出现拼写错误。为什么不呢,那就这样做吧。

const TYPE_MUSIC = "music";
const TYPE_MOVIE = "movie";
const TYPE_TV = "tv";
const TYPE_BOOK = "book";

class CultureFacade {
  constructor(type) {
    this.type = type;
  }

  get(id) {
    switch (this.type) {
      case TYPE_MUSIC: {
        return this._tryToReturn(this._findMusic, id);
      }

      case TYPE_MOVIE: {
        return this._tryToReturn(this._findMovie, id);
      }

      case TYPE_TV: {
        return this._tryToReturn(this._findTVShow, id);
      }

      case TYPE_BOOK: {
        return this._tryToReturn(this._findBook, id);
      }

      default: {
        throw new Error("No type set!");
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

这些类型应该被导出并在整个应用程序范围内使用。

用法

好了,看来我们搞定了。我们去试试吧!

const music = new CultureFacade(TYPE_MUSIC);
music.get(3)
    .then(data => console.log(data))
    .catch(e => console.error(e));
Enter fullscreen mode Exit fullscreen mode

使用 实现起来非常简单then/catch。它只是输出了我们正在寻找的专辑,在本例中是 Radiohead 的《OK Computer》。顺便说一句,这首歌很棒。

好的,但我们也尝试获取错误信息。我们的供应商在缺少所请求的资源时,根本无法真正告知任何信息。但我们可以!

const movies = new CultureFacade(TYPE_MOVIE);
movie.get(5)
    .then(data => console.log(data))
    .catch(e => console.log(e));
Enter fullscreen mode Exit fullscreen mode

这到底是什么?哦,控制台报错,说“找不到这个 id 的条目”。其实,这是一个 JSON 兼容的对象!耶!

如你所见,如果使用得当,外观模式会非常强大。当你有多个类似的数据源、类似的操作等等,并且想要统一使用时,它会非常有用。

所有代码均可在CodePen上找到。

文章来源:https://dev.to/tomekbuszewski/facade-pattern-in-javascript-3on4
PREV
基本身份验证、SAML、密钥、OAuth、JWT 和令牌 Quicky 基本身份验证 SAML OAuth 2.0 API 密钥资源
NEXT
为什么 (! + [] + [] + ![]).length 是 9