緣起
近期產(chǎn)品提了個類似支付寶收能量活動的需求,其中有一項(xiàng)需求是獲得能量球需要完成一些任務(wù), 比如:分享活動頁面可以獲得a能量,完成場景開啟可以獲得b能量,操作遙控器可以獲得c能量等等……
這些任務(wù)都是產(chǎn)品已經(jīng)存在的功能策橘,如果更改每一項(xiàng)產(chǎn)生能量球任務(wù)的功能炸渡,則可能需要不斷的 repeat 自己,增大代碼量的同時丽已,后期如果變更需求或者產(chǎn)生bug也需要重復(fù)的體力勞動蚌堵。 這些都是我不期望看到的。那么有沒有一種優(yōu)雅的解決方案呢?
其實(shí)能量球任務(wù) 和 產(chǎn)品已存在的功能是沒有必然關(guān)系的吼畏,只是因?yàn)檫@個活動才有了那么一絲絲關(guān)聯(lián)督赤。 所以沒有必要耦合在一起,恰好近期在看設(shè)計(jì)模式泻蚊,裝飾器模式給了我一些靈感:
裝飾器模式(Decorator Pattern)允許向一個現(xiàn)有的對象添加新的功能躲舌,同時又不改變其結(jié)構(gòu)。這種類型的設(shè)計(jì)模式屬于結(jié)構(gòu)型模式性雄,它是作為現(xiàn)有的類的一個包裝没卸。
這種模式創(chuàng)建了一個裝飾類,用來包裝原有的類秒旋,并在保持類方法簽名完整性的前提下约计,提供了額外的功能。
何不寫一個能量球任務(wù)的裝飾器迁筛,裝飾一下需要完成任務(wù)的功能方法煤蚌,這樣既減少了代碼量、又不會repeat自己细卧,是一種比較優(yōu)雅的解決方案尉桩。
性空
既然使用裝飾器模式,那么必須先寫出一個能量球任務(wù)裝飾器來酒甸。
V1版本如下所示:
// 收能量任務(wù)裝飾器
function getPowerTask (taskCode) {
return function (target, property, descriptor) {
const rawFunc = descriptor.value;
descriptor.value = async (...args) => {
console.log(target, `property=${property}`, descriptor)
await powerModel.finishTask(taskCode);
await rawFunc.apply(target, args);
}
return descriptor;
}
}
一個活動頁面分享成功后調(diào)用能量球任務(wù)裝飾器的代碼如下所示:
export default class ShareTask extends Component {
……
shareView() {
const shareMod = this.getShareMod();
const shareOpt = this.getShareOpts();
shareMod.share(shareOpt).then(
res => this.shareAfter(),
err => console.log(err)
)
}
@getPowerTask('share')
async shareAfter() {
console.log('shareAfter');
await this.props.shareAfter();
}
……
render() {
……
}
}
結(jié)果是喜憂參半魄健, 喜的是可以成功的觸發(fā)收能量任務(wù),憂的是shareAfter方法內(nèi) 獲取this.props
的結(jié)果是 undefined插勤。
為啥是這樣的結(jié)果呢沽瘦,后來打斷點(diǎn)調(diào)試發(fā)現(xiàn) target 是 Component 原型, 并不是 Component 實(shí)例农尖。 后來翻閱資料發(fā)現(xiàn)其實(shí) Decorator 只是ES7的語法糖析恋,類方法上的裝飾器底層還是基于 Object.defineProperty 來實(shí)現(xiàn)的, 上面的裝飾器使用babel翻譯成ES5其實(shí)就是如下執(zhí)行的:
getPowerTask(ShareTask.prototype, 'shareAfter', descriptor);
// 類似于
Object.defineProperty(ShareTask.prototype, 'shareAfter', descriptor);
把 shareTask.proptype 的 shareAfter 方法重新定義成了我們的方法盛卡。 既然裝飾器只是一層封裝助隧,那么當(dāng)方法具體執(zhí)行的時候,是不是就是在 實(shí)例里面了滑沧, 考慮到這樣并村,我對裝飾器進(jìn)行了以下改裝:
1、箭頭方法的this指向是在方法定義的地方滓技,這里this需要指向執(zhí)行的地方哩牍,所以改成普通方法
2、rawFunc.apply 的上下文改成this令漂,即棄用原型對象膝昆,改成執(zhí)行上下文對象
V2收能量裝飾器
function getPowerTask (taskCode) {
return function (target, property, descriptor) {
const rawFunc = descriptor.value;
descriptor.value = async function(...args) => {
console.log(target, `property=${property}`, descriptor)
await powerModel.finishTask(taskCode);
await rawFunc.apply(this, args);
}
return descriptor;
}
}
后來試了一下V2 版本丸边, 果然成功的獲取到了 props
屬性。
總結(jié)
此次成功的實(shí)踐了裝飾器模式荚孵,雖說軟件工程沒有銀彈妹窖, 這次開發(fā)也算是一次尋找“鉛彈”的過程吧,下面總結(jié)一些裝飾器填坑的經(jīng)驗(yàn):
- this指向問題
Object.defineProperty方法是對類原型的方法進(jìn)行重新定義收叶,執(zhí)行的時候還是在類實(shí)例下執(zhí)行骄呼。如果想封裝類的原有方法同時想同時使用類的實(shí)例,descriptor.value
必須定義一個普通函數(shù)滔驾,不能使用箭頭函數(shù)谒麦。實(shí)例執(zhí)行的時候this的指向就是當(dāng)前實(shí)例類
- 普通函數(shù)不能使用裝飾器
因?yàn)閖avascript在生成上下文對象的時候存在函數(shù)聲明提升,所以對普通函數(shù)裝飾失效哆致。
- 不要手里有錘子绕德,全世界去找釘子
他山之石
- 阮一峰裝飾器實(shí)例文檔
- 某大神