一.引子-先來安利一款好遊戲
《塞爾達傳說-荒野之息》,這款於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 朱雀