优雅的现代JavaScript设计模式: 冰冻工厂
栏目: JavaScript · 发布时间: 6年前
内容简介:原文地址从上个世纪九十末开始,我就开始断断续续的从事JavaScript的开发工作.初始,我并不喜欢它.但是自从了解了ES2015(也叫ES6),我开始认为JavaScript是一个强大而且杰出的动态编程语言.随着时间流逝,我掌握了几种能够代码更加简洁,可测试以及更加有表达力的编码模式.现在,我将把这些模式分享与你.
原文地址 Elegant patterns in modern JavaScript: Ice Factory
从上个世纪九十末开始,我就开始断断续续的从事JavaScript的开发工作.初始,我并不喜欢它.但是自从了解了ES2015(也叫ES6),我开始认为JavaScript是一个强大而且杰出的动态编程语言.
随着时间流逝,我掌握了几种能够代码更加简洁,可测试以及更加有表达力的编码模式.现在,我将把这些模式分享与你.
我第一个介绍的模式是RORO(稍后会翻译).如果你没有阅读过它,请不要担心,因为这不会影响这篇文章的阅读,你可以在其他的时候阅读它.
今天,我将会给你介绍 冰冻工厂 模式.
冰冻工厂只是一个函数,它能够创建并且返回一个不可变对象.我们将在后面解释这个定义,首先,让我们看看为什么这个模式如此的有用.
JavaScript的class并不完美.
通常来说,我们都会把一些相关的函数聚合在一个对象中.例如,在一款电子商务的app中,我们可能有一个 cart
对象,它暴露了 addProduct
和 removeProduct
两个函数.我们可以通过 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
创建的对象,继承于这个 class
的 prototype
.因此,修改这个类的原型,将会影响所有通过这个类创建的对象,即使这个修改是在对象创建之后.
看看这个例子:
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中的 new
和 this
有时候会反直觉,奇怪,如彩虹陷阱一般
冰冻工厂模式来拯救你
正如我之前所说的那样, 一个冰工厂是一个创建并且返回不可变对象的函数 .通过冰工厂模式,我们的购物车例子改写成如下模式:
// 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 .
我的冰冻工厂模式应用在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,我们可能很快会意识到,添加和删除商品的概念会不断的冒出来.
伴随着我们的购物车对象,我们可能会有一个 类别 对象和一个 订单 对象.所有的这些对象都可能暴露不同版本的 addProduct
和 removeProduct
函数.
我们都知道,复制重复代码是不好的行为,所以我们最终可能会尝试创建一个类似 商品列表 的对象,我们的 购物车 , 类别 以及 订单 对象都继承于它.
但是,除了通过继承一个 商品列表 对象来扩展我们的对象,我们还可以采用另外一个理论,它来自于一本非常有影响力的书,是这么写的:
“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 } } 复制代码
实际上,在这篇文章的早些时候的版本,我就是这么做的.但是后来我发现这有些危险(这里相关有解释).所以,我们最好还是通过对象的属性的方式进行组合.
太棒了,我已经把我的想法传递给你了
当我们学习一些新的知识,特别是一些类似架构和设计这类复杂的内容的时候,我们更希望有简单可遵循的铁律.我们想听到类似 总要这么做 和 永远不要这么做 的话.
但是随时我工作时间的增长,我越来越意识到不存在 总要 和 永远不要 .只有 选择 和 权衡 .
通过 冰冻工厂 的方式创建对象会比普通的使用 class
消耗更多的内存和降低性能.
在我上面所描述的例子中,这不会有什么影响,即使它们运行起来比 class
慢, 冰冻工厂 模式仍旧是非常快的.
如果你发现你需要在一瞬间创建成百上千个对象,或者你工作的团队对于能耗以及内存消耗非常敏感,那么你可能需要使用 class
而不是 冰冻工厂 模式.
记着,首先是构建你的app和防止过早的优化.在大多数时候,对象的创建都不是瓶颈.
虽然我在这里抱怨,但是 class
并不总是那么糟糕. 你不应该因为一个框架或者类库使用了 class
就否定它.实际上,Dan Abramov曾经在他的文章 How to use Classes and Sleep at Night 有过非常精彩的探讨.
最后,我想和你介绍一些我在这些代码例子中所用到的一些个人习惯:
- 我使用函数语句而不是函数表达式
- 我把函数的返回语句放在了函数的顶端(通过上面定律使得这变得可能)
- 我把我们的工厂函数称作
makeX
, 而不是createX
或者buildX
或者其他. - 我的工厂函数使用了一个单一的,解构对象作为参数
- 我不用分号( Crockford 也不赞成使用分号 )
- 还有...
你可能喜欢其他的代码风格,那都是可以的.风格并不是设计模式,不需要严格的遵守.
这里,相信我们已经明确的了解了,冰冻工厂模式的定义是 使用一个函数来创建和返回一个不可变对象 .具体怎么写这个函数取决于你.
如果你觉得这篇文章非常有用,请点关注并收藏,并且转发给你的朋友们,让他们也能够了解.
以上所述就是小编给大家介绍的《优雅的现代JavaScript设计模式: 冰冻工厂》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 设计模式-享元设计模式
- 设计模式(四)Singleton设计模式
- 设计模式之建造者设计模式
- 设计模式之单例设计模式
- 设计模式(2)- 深入浅出设计模式 阅读笔记
- 设计模式(1)- 深入浅出设计模式 阅读笔记
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。