优雅的现代JavaScript设计模式: 冰冻工厂

栏目: JavaScript · 发布时间: 5年前

内容简介:原文地址从上个世纪九十末开始,我就开始断断续续的从事JavaScript的开发工作.初始,我并不喜欢它.但是自从了解了ES2015(也叫ES6),我开始认为JavaScript是一个强大而且杰出的动态编程语言.随着时间流逝,我掌握了几种能够代码更加简洁,可测试以及更加有表达力的编码模式.现在,我将把这些模式分享与你.

原文地址 Elegant patterns in modern JavaScript: Ice Factory

从上个世纪九十末开始,我就开始断断续续的从事JavaScript的开发工作.初始,我并不喜欢它.但是自从了解了ES2015(也叫ES6),我开始认为JavaScript是一个强大而且杰出的动态编程语言.

随着时间流逝,我掌握了几种能够代码更加简洁,可测试以及更加有表达力的编码模式.现在,我将把这些模式分享与你.

我第一个介绍的模式是RORO(稍后会翻译).如果你没有阅读过它,请不要担心,因为这不会影响这篇文章的阅读,你可以在其他的时候阅读它.

今天,我将会给你介绍 冰冻工厂 模式.

冰冻工厂只是一个函数,它能够创建并且返回一个不可变对象.我们将在后面解释这个定义,首先,让我们看看为什么这个模式如此的有用.

JavaScript的class并不完美.

通常来说,我们都会把一些相关的函数聚合在一个对象中.例如,在一款电子商务的app中,我们可能有一个 cart 对象,它暴露了 addProductremoveProduct 两个函数.我们可以通过 cart.addProduct() 以及 cart.removeProduct() 来调用他们.

如果你曾经写过以类为中心的面向对象的语言,例如 Java 或者C#, 这可能会使你感觉非常亲切自然.

如果你是一个新手, 没关系,现在你已经见到了 cart.addProduct() 这个语句.对于这种写法,我个人持保留态度.

我们该如何创建一个好的 cart 对象呢?第一个与现在JavaScript相关的直觉应该是使用 class .看起来就像这样:

// ShoppingCart.js
export default class ShoppingCart {
  constructor({db}) {
    this.db = db
  }
  
  addProduct (product) {
    this.db.push(product)
  }
  
  empty () {
    this.db = []
  }
  get products () {
    return Object
      .freeze([...this.db])
  }
  removeProduct (id) {
    // remove a product 
  }
  // other methods
}
// someOtherModule.js
const db = [] 
const cart = new ShoppingCart({db})
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
})
复制代码

注: 为了简单的缘故,我使用一个数组作为数据库 db .在实际代码中,这个应该是类似 Model 或者 Repo 这些能够和真实数据库交互的对象.

不幸的是,虽然这段代码看起来非常棒,但是JavaScript中 class 的行为可能和你想的不太一样.

如果你稍不注意,JavaScript会反咬你一口.

例如, 通过 new 关键字创建的对象是可以修改的.因此,你能够对一个方法 重新赋值

const db = []
const cart = new ShoppingCart({db})
cart.addProduct = () => 'nope!' 
// No Error on the line above!
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
}) // output: "nope!" FTW?
复制代码

更加糟糕的是,通过 new 创建的对象,继承于这个 classprototype .因此,修改这个类的原型,将会影响所有通过这个类创建的对象,即使这个修改是在对象创建之后.

看看这个例子:

const cart = new ShoppingCart({db: []})
const other = new ShoppingCart({db: []})
ShoppingCart.prototype
  .addProduct = () => ‘nope!’
// No Error on the line above!
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
}) // output: "nope!"
other.addProduct({ 
  name: 'bar', 
  price: 8.88
}) // output: "nope!"
复制代码

实际上,JavaScript中, this 是动态绑定的.如果我们把 cart 对象的方法传递出去,将会导致失去 this 的引用.这一点非常违反直觉的,同时会招来许多麻烦,

一个常见的陷进是我们把一个实例的方法绑定成一个事件的处理函数. 以我们的 cart.empty 方法为例.

empty () {
    this.db = []
  }
复制代码

如果我们直接把这个方法绑定成我们页面的按钮点击事件...

<button id="empty">
  Empty cart
</button>
复制代码
document
  .querySelector('#empty')
  .addEventListener(
    'click', 
    cart.empty
  )
复制代码

当用户点击这个 empty 按钮的时候,他们的购物车仍旧是满的,并没有被清空.

这个失败是静默的,因为 this 将会指向这个 button ,而不是指向 cart .因此,我们的 cart.empty 方法最后会给 button 创建一个新的属性 db 并且赋值为 [] ,而不是影响 cart 对象中的 db .

这种类型的bug可能会让你奔溃,因为并没有错误发生,你通常的直觉告诉你这应该是对的,但是实际上不是.

为了让它能够正常的工作,我们可以这么做:

document
  .querySelector("#empty")
  .addEventListener(
    "click", 
    () => cart.empty()
  )
复制代码

我认为 Mattias Petter Johansson 说的非常好:

JavaScript中的 newthis 有时候会反直觉,奇怪,如彩虹陷阱一般

冰冻工厂模式来拯救你

正如我之前所说的那样, 一个冰工厂是一个创建并且返回不可变对象的函数 .通过冰工厂模式,我们的购物车例子改写成如下模式:

// makeShoppingCart.js
export default function makeShoppingCart({
  db
}) {
  return Object.freeze({
    addProduct,
    empty,
    getProducts,
    removeProduct,
    // others
  })
  function addProduct (product) {
    db.push(product)
  }
  
  function empty () {
    db = []
  }
  function getProducts () {
    return Object
      .freeze([...db])
  }
  function removeProduct (id) {
    // remove a product
  }
  // other functions
}
// someOtherModule.js
const db = []
const cart = makeShoppingCart({ db })
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
})
复制代码

需要注意的事,我们奇怪的彩虹陷阱已经没有了:

  • 我们不再需要 new 我们仅仅是调用一个普通的JavaScript函数来创建我们的 cart 对象.

  • 我们不再需要 this 我们的成员函数能够直接访问 db 对象.

  • 我们的 cart 对象是完完全全的不可变. Object.freeze() 冻结了 cart 对象,因此不能够对其添加新的属性,修改或者删除已经存在的属性以及原型链也无法修改.只需要记住, Object.freeze() 是浅层的,所以如果我们返回的对象包含了数组或者其他的对象,我们必须保证 Object.freeze() 也对它们产生了作用.同样的,我们所使用的 ES模块 也是不可变的.你需要使用严格模式,防止重新赋值能够报错而不是静默的失败.

私密性

另外一个冰工厂模式的优势就是他们能够拥有私有成员.我们看如下例子

function makeThing(spec) {
  const secret = 'shhh!'
  return Object.freeze({
    doStuff
  })
  function doStuff () {
    // 我们可以在这里使用 spec 和 secret变量
  }
}
// secret 在这里无法被访问
const thing = makeThing()
thing.secret // undefined

复制代码

JavaScript使用闭包来完成这个功能,相关的资料你可以在MDN上面查询.

公认的定律

即使工厂模式已经存在JavaScript里面很久了,但是冰工厂模式仍旧被强烈的推荐.Douglas Crockford在这个视频中就展示了相关的代码(视频需要科学上网).

这段是Crockford演示的代码,他把这个创建对象的函数称之为 constructor .

优雅的现代JavaScript设计模式: 冰冻工厂

我的冰冻工厂模式应用在Crockford的例子上,代码看起来像是这样.

function makeSomething({ member }) {
  const { other } = makeSomethingElse() 
  
  return Object.freeze({ 
    other,
    method
  }) 
  function method () {
    // code that uses "member"
  }
}
复制代码

我利用函数变量提升的优势,把返回的语句放在了接近顶部的位置,这样读者在开始阅读代码直接之前,能够有一个概览.

我同时也把 spec 参数进行了解构,并且把模式改名成了 冰冻工厂 ,这个名字更加方便记忆同时也防止和ES6中的 constructor 弄混.但实际上,它们是同一个东西.

因此,我由衷的说一句,感谢你,Mr.Crockford

注: 这里值得一提的事,Crockford认为函数的变量提升是JavaScript的弊端,因而可能认为的版本不正确.我在这篇文章谈到了我的理解,更详细的,在这篇评论中.

继承怎么办?

当我们持续的构建我们的电子商务app,我们可能很快会意识到,添加和删除商品的概念会不断的冒出来.

伴随着我们的购物车对象,我们可能会有一个 类别 对象和一个 订单 对象.所有的这些对象都可能暴露不同版本的 addProductremoveProduct 函数.

我们都知道,复制重复代码是不好的行为,所以我们最终可能会尝试创建一个类似 商品列表 的对象,我们的 购物车 , 类别 以及 订单 对象都继承于它.

但是,除了通过继承一个 商品列表 对象来扩展我们的对象,我们还可以采用另外一个理论,它来自于一本非常有影响力的书,是这么写的:

“Favor object composition over class inheritance.” – Design Patterns: Elements of Reusable Object-Oriented Software.

我们应该更多的采用对象组合而不是继承

  • 设计模式

这里附上这本书的链接设计模式

实际上,这本书的作者,我们俗称的四人帮之一,还说到

“…our experience is that designers overuse inheritance as a reuse technique, and designs are often made more reusable (and simpler) by depending more on object composition.” 我们的经验是 程序员 过度的使用继承作为复用的手段,但是通过对象组合的模式来设计会使得复用更加的广泛和简单.

因此,我们的 商品列表 工厂将是这样:

function makeProductList({ productDb }) {
  return Object.freeze({
    addProduct,
    empty,
    getProducts,
    removeProduct,
    // others
  )}
 
  // addProduct 以及其他函数的定义…
}
复制代码

然后,我们的 购物车 工厂将长成这样:

function makeShoppingCart(productList) {
  return Object.freeze({
    items: productList,
    someCartSpecificMethod,
    // …
)}
function someCartSpecificMethod () {
  // code 
  }
}
复制代码

然后,我们可以把商品列表传入到我们的购物车中,就像这样:

const productDb = []
const productList = makeProductList({ productDb })
const cart = makeShoppingCart(productList)
复制代码

我们将可以通过 items 属性来使用 productList .如下所示:

cart.items.addProduct()
复制代码

我们也可以尝试通过方法的合并,把整个 productList 对象融入到我们的购物车对象中.就像这样

function makeShoppingCart({ 
  addProduct,
  empty,
  getProducts,
  removeProduct,
  …others
}) {
  return Object.freeze({
    addProduct,
    empty,
    getProducts,
    removeProduct,
    someOtherMethod,
    …others
)}
function someOtherMethod () {
  // code 
  }
}
复制代码

实际上,在这篇文章的早些时候的版本,我就是这么做的.但是后来我发现这有些危险(这里相关有解释).所以,我们最好还是通过对象的属性的方式进行组合.

太棒了,我已经把我的想法传递给你了

优雅的现代JavaScript设计模式: 冰冻工厂

当我们学习一些新的知识,特别是一些类似架构和设计这类复杂的内容的时候,我们更希望有简单可遵循的铁律.我们想听到类似 总要这么做永远不要这么做 的话.

但是随时我工作时间的增长,我越来越意识到不存在 总要永远不要 .只有 选择权衡 .

通过 冰冻工厂 的方式创建对象会比普通的使用 class 消耗更多的内存和降低性能.

在我上面所描述的例子中,这不会有什么影响,即使它们运行起来比 class 慢, 冰冻工厂 模式仍旧是非常快的.

如果你发现你需要在一瞬间创建成百上千个对象,或者你工作的团队对于能耗以及内存消耗非常敏感,那么你可能需要使用 class 而不是 冰冻工厂 模式.

记着,首先是构建你的app和防止过早的优化.在大多数时候,对象的创建都不是瓶颈.

虽然我在这里抱怨,但是 class 并不总是那么糟糕. 你不应该因为一个框架或者类库使用了 class 就否定它.实际上,Dan Abramov曾经在他的文章 How to use Classes and Sleep at Night 有过非常精彩的探讨.

最后,我想和你介绍一些我在这些代码例子中所用到的一些个人习惯:

你可能喜欢其他的代码风格,那都是可以的.风格并不是设计模式,不需要严格的遵守.

这里,相信我们已经明确的了解了,冰冻工厂模式的定义是 使用一个函数来创建和返回一个不可变对象 .具体怎么写这个函数取决于你.

如果你觉得这篇文章非常有用,请点关注并收藏,并且转发给你的朋友们,让他们也能够了解.


以上所述就是小编给大家介绍的《优雅的现代JavaScript设计模式: 冰冻工厂》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

鳥哥的Linux私房菜(第四版)

鳥哥的Linux私房菜(第四版)

鳥哥 / 碁峰資訊股份有限公司 / 2016-1-25 / TWD 980.00

本書前三版均蟬聯電腦專業書籍Linux暢銷排行榜Top1,為地表最暢銷的Linux中文書籍! 您是有意學習Linux的小菜鳥,卻不知如何下手?您是遨遊Linux的老鳥,想要一本資料豐富的工具書?本書絕對是最佳選擇! ※鳥哥傾囊相授,內容由淺入深 書中包含了鳥哥從完全不懂Linux到現在的所有歷程,鳥哥將這幾年來的所知所學傾囊相授,以最淺顯易懂的文字帶領您進入Linux的世界。 ......一起来看看 《鳥哥的Linux私房菜(第四版)》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换