一.引子-先来安利一款好游戏
《塞尔达传说-荒野之息》,这款于2017年3月3日由任天堂(“民间高手”)发售在自家主机平台WIIU和SWITCH上的单机RPG游戏,可谓是跨时代的“神做”了。第一次制做“开放类”游戏的任天堂就教科书般的定义了这类游戏应该如何制做。 前端
而这个游戏真正吸引个人地方是他的细节,举个栗子,《荒野之息》中的世界有天气和温度两个概念,会下雨打雷,有严寒酷暑,可是这些天气不想大多数游戏同样,只是简单的背景,而是实实在在会影响主角林克(Link)每个操做。好比,下雨天去登山会打滑;打雷天若是身上有金属装备会被雷劈(木制装备则没事!);严寒中会慢慢流失体力(穿上一件保暖衣就解决了);酷暑中使用爆炸箭则会原地爆炸!等等;
就是这些细节让这个游戏世界显的无比真实又有趣。
二.问题-如何设计这样的游戏代码?
做为程序猿,玩游戏之余不由会思考,这样的游戏代码应该如何设计编写? 好比“攀爬”这个动做,须要判断攀爬的位置,林克的装备(有些装备能让你爬的更快),当时的天气,林克的体力等等众多条件,里面确定参杂的无数if else,更况且这只是其中一个简单的操做,拓展到所有游戏,其复杂的不可想象。 显然这样的设计是不行的。 那咱们假设“攀爬”的方法只专心处理攀爬这件事(有体力就能成功,反之失败),其余判断在方法外部执行,好比判断天气,装备,位置等等,这样就符合了程序设计的单一职责和低耦合等原则,而且判断天气的方法还能够拿去别的地方复用,加强了代码的复用度和可测试度,彷佛可行! 那应该如何设计这样的代码呢?这就引出了咱们今天的主角-装饰器模式。react
三.主角-装饰器模式(decorator)
根据GoF在《设计模式:可复用面向对象软件的基础》(如下简称《设计模式》)一书中对装饰器模式定义:装饰器模式又称包装模式(“wrapper”),目的是以对用户透明的方式扩展对象的功能,是继承的一种代替方案。 一块儿划重点:git
- 对用户透明:通常指被装饰过的对象的对外接口不变,“攀爬”被怎么装饰都仍是“攀爬”。
- 扩展对象的功能:通常指修改或添加对象功能,好比林克在雪地就能够用盾牌滑雪,平地则没有这个能力。
- 继承的一种代替方案:熟悉面向对象的同窗必定对继承并不陌生,这里咱们重点谈谈继承自己的一些缺点:1)继承中子类和超类存在强耦合性,超类的修改会影响所有子类;2)超类对子类是“白盒复用”,子类必须了解超类的所有实现,破坏了封装性。3)当项目庞大时,继承会使得子类爆发性增加,好比《荒野之息》中存在料理系统,任意两种食材都可以搭配出一款料理,假定有10中可使用食材,使用继承的方式就要构建10*10=100个子类表示料理结果,而装饰器模式仅仅使用10+1=11个子类就能够完成以上工做。(还包括了任意种食材的混合,事实上游戏中的确能够。) 最后,总结一下装饰器模式的特色:不改变对象自身的基础上,在程序运行时给对象添加某种功能,一句话:锦上添花。(想一想《王者荣耀》中最赚钱的皮肤,怎么全是游戏,喂!)
四.场景-面向切片编程(AOP)
说到装饰器,最经典的应用场景就是面向切片编程(Aspect Oriented Programming,如下简称AOP),AOP适合某些具备横向逻辑(可切片)的应用,好比提交表单,点击提交按钮之后执行的逻辑是:上报点击 -> 校验数据 -> 提交数据 -> 上报结果 。能够看到,首尾的上报日志功能和核心业务逻辑并无直接关系,而且几乎全部表单提交都须要上报日志的功能,所以,上报日志,这个功能就能够单独抽象出来,最后在程序运行(或编译)时动态织入业务逻辑中。相似的功能还有:数据校验,权限控制,异常处理,缓存管理等等。 AOP的优势是能够保持业务逻辑模块的纯净和高内聚,同时方便功能复用,经过装饰器就能够很方便的把功能模块装饰到主业务逻辑中去。github
五.应用-前端开发中的应用
接下来咱们一块儿看看具体装饰器模式是如何在前端开发中应用的。 Talk is cheap, show me the code! (屁话少说,放码过来!) 在JS中改变一个对象再简单不过了。 ajax
得力于JS是一门基于原型的弱类型语言,给对象添加或修改功能都十分容易,所以传统的面向对象中的装饰器模式在JS中的应用并不太多(ES6正式提出class之后场景有所增长)。 咱们先简单模拟一下面向对象中的装饰器模式。 假设咱们要开发一个飞机大战的游戏,飞机能够切换装备的武器,发射不一样的子弹。
咱们先实现一个飞机的类,并实现一个fire方法。 接着,咱们实现一个发射导弹的装饰器类
这个类接收一个飞机实例,而且从新实现了fire方法,在方法内部先调用原来实例的fire方法,接着扩展此方法,增长了发射导弹的功能。 相似的咱们再实现一个发射原子弹的装饰器。
最后咱们看一下应该如何使用这两个装饰器。
能够看到,通过两个装饰器装饰后的plane实例,再调用fire方法时,就能够同时发射三种子弹了。而装饰器自己并无直接改写Plane类,只是加强了它的fire方法,对plane实例的使用者也是透明的。 接下来咱们看一看如何应用装饰器在JS中实现AOP编程。 首先咱们扩展一下函数的原型,让每一个函数均可以被装饰。咱们给函数增长一个before和after方法,这两个方法各自接收一个新的函数,并保证新函数在原函数以前(before)或以后(after)执行。
这里须要注意的是新函数和原函数具备相同this和参数。 有了两个方法,之前不少复杂的需求就变得很简单了。
栗子一:挂载多个onload函数
一般状况下,window.onload只能挂载一个回调函数,重复声明回调函数,后面的会把以前声明的覆盖掉,有了after之后,这个麻烦解决了。 npm
栗子二:日志上报
栗子三:追加(改变)参数
好比,为了增长安全性,给全部接口都增长一个token参数,若是不实用AOP,咱们只能改ajax方法了。可是有了AOP,就能够像下面这样操做。 编程
原理就是before函数和原函数接收相同的this和参数,而且before会在原函数以前执行。 其实AOP在前端项目中的应用场景还不少,好比校验表单参数,异常处理,数据缓存,本地持久化等,这里不在一一举例了。 有些同窗对直接改写函数的原型比较抵触,这里咱们也给出函数式的before实现。
六.ES7-@decorator语法
在JS将来的标准(ES7)中,装饰器也已被加入到了提案中。 前端同窗都知道jQuery最大的特色就是它链式调用的API设计,其核心是每一个方法都返回this,也就是jQuery对象实例,咱们不妨先实现一个高阶函数,用于实现链式调用。 设计模式
fluent函数接收一个函数fn做为参数,返回一个新的函数,在新函数内部经过apply调用fn,并最终返回上下文this。有了这个函数,咱们就能够很方便的给任意对象的方法添加链式调用。
接下来,咱们看看如何使用ES7的@decorator语法来简化上面的代码,先来看一下结果。
熟悉JAVA的同窗一眼就看出这不是注解写法么,没错,ES7中的@decorator正是参考了Python和JAVA语法设计出来的。@后面的fluentDecorate是一个装饰器函数,这个函数接收三个参数,分别是target,name和descriptor,这三个参数和Object.defineProperty方法的参数彻底相同,实际上@decorator也正是这个方法的语法糖而已。 值得注意的是@decorator不止能够做用在对象或类的方法上面,还能够直接做用在类(class)上,区别是装饰函数的第一个参数target不一样,看成用在方法上时,target指向对象自己,而看成用在类时target指向类(class),而且name和descriptor都是undefined。 如下给出fluentDecorate函数的完整实现。
一般咱们能够把这个装饰函数再抽象一下,让他成为一个高阶函数,能够接收咱们最开始定义的fluent函数或者其余函数(好比截流函数等),而后返回一个用这个函数装饰的新装饰函数,更具备通用型。
@decorator到目前为止还只是个提案,没有任何浏览器支持了这个语法,可是好在可使用Babel以插件(transform-decorators-legacy)的形式在本身的项目中体验。 注意,@decorator只能做用于类和类的方法上,不能用于普通函数,由于函数存在变量提高,而类是不会提高的。
七.组件-装饰器在React项目中的应用
最后结合目前前端最火的框架React,来看看装饰器是如何在组件上使用的。 回到最开始的假设,如何开发出《荒野之息》这样细节丰富的游戏,下面咱们就使用React搭配装饰器来模拟一下游戏中的细节实现。 咱们先实现一个Person组件,用来代指游戏的主角,这个组件能够接收名字,生命值,攻击类等初始化参数,并在一个卡片中展现这些参数,当生命值为0时,会提示“游戏结束”。而且在卡片中放置一个“JUMP”按钮,用点击按钮模拟主角跳跃的交互。 浏览器
组件调用:
实现结果以下,是否是很抽象?哈哈!
接下来咱们想要模拟游戏中的天气和温度变化,须要实现一个“天然环境”的组件Natural,这个组件自身有天气(wat)和温度(tep)两个状态(state),而且能够经过输入改变这两个状态,咱们以前建立的Person组件做为后代插入这个组件中,而且接收Natural的wat和tep状态做为属性。
好了,咱们的实验页面就完成了,最终效果以下,上面能够经过进度条和单选按钮改变天气和温度,改变后的结果经过props传递给游戏主角。
可是如今改变温度和天气对主角并不会形成任何影响,接下来咱们想在不改变原有Person组件的前提下,实现两个功能:第一,当温度大于50度或者小于10度的时候,主角生命值慢慢降低;第二当天气是雨天的时候,主角每跳跃3次就失败1次。 先来实现第一个功能,温度太高和太低时,主角生命值慢慢减小。咱们的思路是实现一个装饰器,用这个装饰器在外部装饰Person组件,使得这个组件能够感知温度变化。先给出实现:
仔细观察decorateTep函数,它接收一个组件(A)做为参数,返回一个新的React组件(B),在B内部维护了一个hp和tep状态 ,在tep处于临界值时,改变B的hp,最后render时用B的hp代替原来的hp属性传递给A组件。 这不是就是高阶组件(HOC)么?!没错,当装饰器去装饰一个组件时,它的实现和高阶组件彻底一致。经过返回一个新组件的方式去加强原有组件的能力,这也符合React提倡的组件组合的设计模式(注意不是mixin或者继承),decorateTep的使用方法很简单,一行代码搞定:
接下来咱们来实现第二个功能,下雨时跳跃会偶尔失败,这里咱们换一个策略,再也不装饰Person组件,而是装饰组件内部的onJump跳跃方法。代码以下:
区别以前的decorateTep,这个decorateWat装饰器的重点是第三个参数descriptor,以前提到,descriptor参数是被装饰方法的描述对象,它的value属性指向的就是原方法(onJump),这里咱们用变量method保存原方法,同时使用i记录点击次数,经过闭包延长这两个变量的生命周期,最后实现一个新的方法代替原方法,在新方法内部经过apply调用原方法并重置变量i,注意decorateWat最后返回的是改变之后的descriptor对象。 通过装饰器装饰过的onJump方法以下:
好了,接下来就是见证奇迹的时刻!
八.轮子-经常使用装饰器库
事实上如今已经有不少开源装饰器的库能够拿来使用,如下是质量较好的轮子,但愿能够给你们提供帮助。 core-decorators lodash-decorators react-decoration缓存
九.参考-相关资料阅读
所有演示源代码 五分钟让你明白为何塞尔达能够夺得年度游戏 《荒野之息》中46个精彩的小细节 日亚上一位玩家对《荒野之息》的评价 面向切片编程 《JavaScript 设计模式与开发实践》曾探;人民邮电出版社 《JavaScript 高级程序设计(第三版)》Zakas;人民邮电出版社 《ES 6 标准入门(第二版)》阮一峰;电子工业出版社 最后,若有不对的地方,欢迎各位小伙伴留言拍砖,大家的支持是我继续的最大动力! 谢谢你们!
做者:TNFE 朱雀