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" }
];
它们使用不同的模式命名,实现方式有好有坏,工作量有大有小。为了避免过于复杂,我使用了通用响应格式的简单示例。但无论如何,这很好地说明了问题。
我们的立面设计
要创建外观,首先我们需要了解每个供应商的各个方面。如果需要额外的授权、更多参数等,则必须实现。这是额外的功能,当与不需要它的供应商一起使用时,可以丢弃。
外观的构建块是公共接口。无论你想查询哪个资源,都应该只使用一种方法。当然,在它下面可能还有更多方法,但公共访问应该受到限制并且易于使用。
首先,我们应该确定公共 API 的形式。对于这个例子来说,一个 getter 就足够了。这里唯一的区别是媒体类型——书籍、电影等等。所以类型将成为我们的基础。
接下来,我们来看看资源之间的共同点。每个资源都可以通过 ID 进行查询。因此,我们的 getter 应该接受一个参数,即 ID。
建立我们的门面
(我决定为此使用一个类,但这不是必需的。由对象文字或甚至函数集合组成的模块可能就足够了。尽管如此,我喜欢这种符号。)
class CultureFasade {
constructor(type) {
this.type = type;
}
}
首先,我们在构造函数中定义类型。这意味着每个外观实例都会返回不同的类型。我知道这看起来可能有点多余,但比起每次都使用单个函数实例并传递多个参数来说,这样做更方便。
好的,接下来就是定义我们的公共方法和私有方法。为了标注“私有”方法,我使用了著名的_
而不是#
,因为 CodePen 还不支持它。
正如我们之前所说,唯一的公共方法应该是我们的 getter。
class CultureFacade {
constructor(type) {
this.type = type;
}
get(id) {
return id;
}
}
基础实现(框架)已经完成了。现在,让我们开始讨论类的真正核心部分——私有 getter。
首先,我们需要确定每个资源是如何查询的:
- 音乐需要一个新的实例,然后在方法中传递ID
get
; - Movie的每个实例返回数据,初始化时需要ID;
- TV Show 只是一个接受 ID 并返回数据的单一函数;
- 书籍只是一种资源,需要我们自己去查询。
我知道这一步看起来很繁琐,也没有必要,但请注意,现在我们真的不需要再去想任何事情了。概念阶段在设计和构建过程中非常重要。
好的,音乐开始吧。
class CultureFacade {
...
_findMusic(id) {
const db = new FetchMusic();
return db.fetch(id);
}
}
我们已经创建了一个简单的方法,它完全按照我们之前描述的方式执行。剩下的三个只是例行公事。
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);
}
}
现在,我们已经有了查询数据库的所有方法。
获取公共 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));
}
}
我们先在这里停一下。如您所见,此方法也是私有的。为什么?公共方法不会从中受益。它需要了解其他私有方法。接下来,它需要两个参数——func
和id
。后者很明显,而前者则不然。好的,所以这将接受一个要运行的函数(或者更确切地说是我们类的方法)。如您所见,执行被分配给result
变量。接下来,我们检查它是否成功,并返回一个。为什么要使用这种复杂的构造?使用甚至简单的语法Promise
,Promise 非常易于调试和执行。async/await
then/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!");
}
}
}
}
关于定义字符串类型的说明
我们的类型是手写的。这不是最佳实践。它应该放在一边定义,这样就不会出现拼写错误。为什么不呢,那就这样做吧。
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!");
}
}
}
}
这些类型应该被导出并在整个应用程序范围内使用。
用法
好了,看来我们搞定了。我们去试试吧!
const music = new CultureFacade(TYPE_MUSIC);
music.get(3)
.then(data => console.log(data))
.catch(e => console.error(e));
使用 实现起来非常简单then/catch
。它只是输出了我们正在寻找的专辑,在本例中是 Radiohead 的《OK Computer》。顺便说一句,这首歌很棒。
好的,但我们也尝试获取错误信息。我们的供应商在缺少所请求的资源时,根本无法真正告知任何信息。但我们可以!
const movies = new CultureFacade(TYPE_MOVIE);
movie.get(5)
.then(data => console.log(data))
.catch(e => console.log(e));
这到底是什么?哦,控制台报错,说“找不到这个 id 的条目”。其实,这是一个 JSON 兼容的对象!耶!
—
如你所见,如果使用得当,外观模式会非常强大。当你有多个类似的数据源、类似的操作等等,并且想要统一使用时,它会非常有用。
—
所有代码均可在CodePen上找到。
文章来源:https://dev.to/tomekbuszewski/facade-pattern-in-javascript-3on4