這需求快讓我崩潰了,不過幸虧我懂裝飾器模式
目的
裝飾器模式(Decorator Pattern) 的目的非常簡單,那就是:
在不修改原有代碼的情況下增加邏輯。 |
這句話聽起來可能有些矛盾,既然都要增加邏輯了,怎么可能不去修改原有的代碼?但 SOLID (向對象設計5大重要原則)的開放封閉原則就是在試圖解決這個問題,其內容是不去改動已經寫好的核心邏輯,但又能夠擴充新邏輯,也就是對擴展開放,對修改關閉。
舉個例子,假如產品的需求是實現一個專門在瀏覽器的控制臺中輸出文本的功能,你可能會這樣做:
- class Printer {
- print(text) {
- console.log(text);
- }
- }
- const printer = new Printer();
- printer.print('something'); // something
在你滿意的看著自己的成果時,產品過來說了一句:“我覺得顏色不夠突出,還是把它改成黃色的吧!”
小菜一碟!你自信的打開百度一通操作之后,把代碼改成了下面這樣子:
- class Printer {
- print(text) {
- console.log(`%c${text}`,'color: yellow;');
- }
- }
但產品看了看又說:“這個字體有點太小了,再大一點,最好是高端大氣上檔次那種。
”好吧。。。“你強行控制著自己拿刀的沖動,一邊琢磨多大的字體才是高端大氣上檔次,一邊修改 print 的代碼:
- class Printer {
- print(text) {
- console.log(`%c${text}`,'color: yellow;font-size: 36px;');
- }
- }
這次改完你之后你心中已經滿是 mmp 了,而且偷偷給產品貼了個標簽:
你無法保證這次是最后的修改,而且也可能會不只一個產品來對你指手劃腳。你呆呆的看著顯示器,直到電腦進入休眠模式,屏幕中映出你那張苦大仇深的臉,想著不斷變得亂七八糟的 print 方法,不知道該怎么去應付那些永無休止的需求。。。
在上面的例子中,最開始的 Printer 按照需求寫出它應該要有的邏輯,那就是在控制臺中輸出一些文本。換句話說,當寫完“在控制臺中輸出一些文本”這段邏輯后,就能將 Printer 結束了,因為它就是 Printer 的全部邏輯了。那在這個情況下該如何改變字體或是顏色的邏輯呢?
這時你該需要裝飾器模式了。
Decorator Pattern(裝飾器模式)
首先修改原來的 Printer,使它可以支持擴充樣式:
- class Printer {
- print(text = '', style = '') {
- console.log(`%c${text}`, style);
- }
- }
之后分別創建改變字體和顏色的裝飾器:
- const yellowStyle = (printer) => ({
- ...printer,
- print: (text = '', style = '') => {
- printer.print(text, `${style}color: yellow;`);
- }
- });
- const boldStyle = (printer) => ({
- ...printer,
- print: (text = '', style = '') => {
- printer.print(text, `${style}font-weight: bold;`);
- }
- });
- const bigSizeStyle = (printer) => ({
- ...printer,
- print: (text = '', style = '') => {
- printer.print(text, `${style}font-size: 36px;`);
- }
- });
代碼中的 yellowStyle、boldStyle 和 bigSizeStyle 分別是給 print 方法的裝飾器,它們都會接收 printer,并以 printer 為基礎復制出一個一樣的對象出來并返回,而返回的 printer 與原來的區別是,各自 Decorator 都會為 printer 的 print 方法加上各自裝飾的邏輯(例如改變字體、顏色或字號)后再調用 printer 的 print。
使用方式如下:
只要把所有裝飾的邏輯抽出來,就能夠自由的搭配什么時候要輸出什么樣式,加入要再增加一個斜體樣式,也只需要再新增一個裝飾器就行了,不需要改動原來的 print 邏輯。
不過要注意的是上面的代碼只是簡單的把 Object 用解構復制,如果在 prototype 上存在方法就有可能會出錯,所以要深拷貝一個新對象的話,還需要另外編寫邏輯:
- const copyObj = (originObj) => {
- const originPrototype = Object.getPrototypeOf(originObj);
- let newObj = Object.create(originPrototype);
- const originObjOwnProperties = Object.getOwnPropertyNames(originObj);
- originObjOwnProperties.forEach((property) => {
- const prototypeDesc = Object.getOwnPropertyDescriptor(originObj, property);
- Object.defineProperty(newObj, property, prototypeDesc);
- });
- return newObj;
- }
然后裝飾器內改使上面代碼中的 copyObj,就能正確復制相同的對象了:
- const yellowStyle = (printer) => {
- const decorator = copyObj(printer);
- decorator.print = (text = '', style = '') => {
- printer.print(text, `${style}color: yellow;`);
- };
- return decorator;
- };
其他案例
因為我們用的語言是 JavaScript,所以沒有用到類,只是簡單的裝飾某個方個方法,比如下面這個用來發布文章的 publishArticle:
- const publishArticle = () => {
- console.log('發布文章');
- };
如果你想要再發布文章之后在 微博或QQ空間之類的平臺上發個動態,那又該怎么處理呢?是像下面的代碼這樣嗎?
- const publishArticle = () => {
- console.log('發布文章');
- console.log('發 微博 動態');
- console.log('發 QQ空間 動態');
- };
這樣顯然不好!publishArticle 應該只需要發布文章的邏輯就夠了!而且如果之后第三方服務平臺越來越多,那 publishArticle 就會陷入一直加邏輯一直爽的情況,在明白了裝飾器模式后就不能再這樣做了!
所以把這個需求套上裝飾器:
- const publishArticle = () => {
- console.log('發布文章');
- };
- const publishWeibo = (publish) => (...args) => {
- publish(args);
- console.log('發 微博 動態');
- };
- const publishQzone = (publish) => (...args) => {
- publish(args);
- console.log('發 QQ空間 動態');
- };
- const publishArticleAndWeiboAndQzone = publishWeibo(publishQzone(publishArticle));
前面 Printer 的例子是復制一個對象并返回,但如果是方法就不用復制了,只要確保每個裝飾器都會返回一個新方法,然后會去執行被裝飾的方法就行了。
總結
裝飾器模式是一種非常有用的設計模式,在項目中也會經常用到,當需求變動時,覺得某個邏輯很多余,那么直接不裝飾它就行了,也不需要去修改實現邏輯的代碼。每一個裝飾器都做他自己的事情,與其他裝飾器互不影響。