前端的設計模式系列-裝飾器模式
代碼也寫了幾年了,設計模式處于看了忘,忘了看的狀態(tài),最近對設計模式有了點感覺,索性就再學習總結下吧。
大部分講設計模式的文章都是使用的 Java、C++ 這樣的以類為基礎的靜態(tài)類型語言,作為前端開發(fā)者,js 這門基于原型的動態(tài)語言,函數(shù)成為了一等公民,在實現(xiàn)一些設計模式上稍顯不同,甚至簡單到不像使用了設計模式,有時候也會產(chǎn)生些困惑。
下面按照「場景」-「設計模式定義」- 「代碼實現(xiàn)」- 「易混設計模式」-「總」的順序來總結一下,如有不當之處,歡迎交流討論。
場景
微信小程序定義一個頁面是通過微信提供的Page 方法,然后傳入一個配置對象進去。
Page({
data: { // 參與頁面渲染的數(shù)據(jù)
logs: []
},
onLoad: function () {
// 頁面渲染后 執(zhí)行
}
})
如果我們有個需求是在每個頁面加載的時候上報一些自定義數(shù)據(jù)。
最直接的當然是去每個頁面加就好了,但上報數(shù)據(jù)的邏輯是一致的,一個一個加有些傻了,這里就可以用到裝飾器模式了。
裝飾器模式
看下維基百科的定義。
裝飾器(修飾)模式,是面向?qū)ο蟪淌筋I域中,一種動態(tài)地往一個類別中添加新的行為的設計模式。就功能而言,修飾模式相比生成子類別更為靈活,這樣可以給某個對象而不是整個類別添加一些功能。
看一下 UML 類圖和次序圖。
當訪問 Component1 中的operation 方法時,會先調(diào)用預先定義的兩個裝飾器 Decorator1 和 Decorator2 中的 operation 方法,執(zhí)行一些額外操作,最后再執(zhí)行原始的 operation 方法。
舉一個簡單的例子:
買奶茶的話可以額外加珍珠、椰果等,不同小料有不同的價格、也可以自由組合,此時就可以用到裝飾器模式,對原始奶茶進行加料、算價。
原始的奶茶有一個接口和類。
interface MilkTea {
public double getCost(); // 返回奶茶的價格
public String getIngredients(); // 返回奶茶的原料
}
class SimpleMilkTea implements MilkTea {
@Override
public double getCost() {
return 10;
}
@Override
public String getIngredients() {
return "MilkTea";
}
}
下邊引入裝飾器,進行加料。
// 添加一個裝飾器的抽象類
abstract class MilkTeaDecorator implements MilkTea {
private final MilkTea decoratedMilkTea;
public MilkTeaDecorator(MilkTea c) {
this.decoratedMilkTea = c;
}
@Override
public double getCost() {
return decoratedMilkTea.getCost();
}
@Override
public String getIngredients() {
return decoratedMilkTea.getIngredients();
}
}
// 添加珍珠
class WithPearl extends MilkTeaDecorator {
public WithPearl(MilkTea c) {
super(c); // 調(diào)用父類構造函數(shù)
}
@Override
public double getCost() {
// 調(diào)用父類方法
return super.getCost() + 2;
}
@Override
public String getIngredients() {
// 調(diào)用父類方法
return super.getIngredients() + ", 加珍珠";
}
}
// 添加椰果
class WithCoconut extends MilkTeaDecorator {
public WithCoconut(MilkTea c) {
super(c);
}
@Override
public double getCost() {
return super.getCost() + 1;
}
@Override
public String getIngredients() {
return super.getIngredients() + ", 加椰果";
}
}
讓我們測試一下,
public class Main {
public static void printInfo(MilkTea c) {
System.out.println("價格: " + c.getCost() + "; 加料: " + c.getIngredients());
}
public static void main(String[] args) {
MilkTea c = new SimpleMilkTea();
printInfo(c); // 價格: 10.0; 加料: MilkTea
c = new WithPearl(new SimpleMilkTea());
printInfo(c); // 價格: 12.0; 加料: MilkTea, 加珍珠
c = new WithCoconut(new WithPearl(new SimpleMilkTea()));
printInfo(c); // 價格: 13.0; 加料: MilkTea, 加珍珠, 加椰果
}
}
未來如果需要新增一種小料,只需要新寫一個裝飾器類,并且可以和之前的小料隨意搭配。
// 添加冰淇淋
class WithCream extends MilkTeaDecorator {
public WithCream(MilkTea c) {
super(c);
}
@Override
public double getCost() {
return super.getCost() + 5;
}
@Override
public String getIngredients() {
return super.getIngredients() + ", 加冰淇淋";
}
}
public class Main {
public static void printInfo(MilkTea c) {
System.out.println("價格: " + c.getCost() + "; 加料: " + c.getIngredients());
}
public static void main(String[] args) {
c = new WithCoconut(new WithCream(new WithPearl(new SimpleMilkTea())));
printInfo(c); // 價格: 18.0; 加料: MilkTea, 加珍珠, 加冰淇淋, 加椰果
}
}
讓我們用 js 改寫一下,達到同樣的效果。
const SimpleMilkTea = () => {
return {
getCost() {
return 10;
},
getIngredients() {
return "MilkTea";
},
};
};
// 加珍珠
const WithPearl = (milkTea) => {
return {
getCost() {
return milkTea.getCost() + 2;
},
getIngredients() {
return milkTea.getIngredients() + ", 加珍珠";
},
};
};
// 加椰果
const WithCoconut = (milkTea) => {
return {
getCost() {
return milkTea.getCost() + 1;
},
getIngredients() {
return milkTea.getIngredients() + ", 加椰果";
},
};
};
// 加冰淇淋
const WithCream = (milkTea) => {
return {
getCost() {
return milkTea.getCost() + 5;
},
getIngredients() {
return milkTea.getIngredients() + ", 加冰淇淋";
},
};
};
// test
const printInfo = (c) => {
console.log(
"價格: " + c.getCost() + "; 加料: " + c.getIngredients()
);
};
let c = SimpleMilkTea();
printInfo(c); // 價格: 10; 加料: MilkTea
c = WithPearl(SimpleMilkTea());
printInfo(c); // 價格: 12; 加料: MilkTea, 加珍珠
c = WithCoconut(WithPearl(SimpleMilkTea()));
printInfo(c); // 價格: 13; 加料: MilkTea, 加珍珠, 加椰果
c = WithCoconut(WithCream(WithPearl(SimpleMilkTea())));
printInfo(c); // 價格: 18; 加料: MilkTea, 加珍珠, 加冰淇淋, 加椰果
沒有再定義類和接口,js 中用函數(shù)直接表示。
原始的 SimpleMilkTea 方法返回一個奶茶對象,然后又定義了三個裝飾函數(shù),傳入一個奶茶對象,返回一個裝飾后的對象。
代碼實現(xiàn)
回到文章最開頭的場景,我們需要為每個頁面加載的時候上報一些自定義數(shù)據(jù)。其實我們只需要引入一個裝飾函數(shù),將傳入的 option 進行裝飾返回即可。
const Base = (option) => {
const { onLoad ...rest } = option;
return {
...rest,
// 重寫 onLoad 方法
onLoad(...args) {
// 增加路由字段
this.reportData(); // 上報數(shù)據(jù)
// onLoad
if (typeof onLoad === 'function') {
onLoad.call(this, ...args);
}
}
reportData() {
// 做一些事情
}
}
然后回到原始頁面增加 Base 的調(diào)用即可。
Page(Base({
data: { // 參與頁面渲染的數(shù)據(jù)
logs: []
},
onLoad: function () {
// 頁面渲染后 執(zhí)行
}
})
同理,利用裝飾器模式我們也可以對其它生命周期統(tǒng)一插入我們需要做的事情,而不需要業(yè)務方自己再寫一遍。
在大團隊的話,每個業(yè)務方可能都需要在小程序生命周期做一些事情,此時只需要利用裝飾器模式,編寫一個裝飾函數(shù),然后在業(yè)務代碼中調(diào)用即可。
最終的業(yè)務代碼可能會裝飾很多層,最終才傳給小程序 Page 函數(shù)。
Page(Base(Log(Ce({
data: { // 參與頁面渲染的數(shù)據(jù)
logs: []
},
onLoad: function () {
// 頁面渲染后 執(zhí)行
}
})
易混設計模式
如果之前看過代理模式,到這里可能會有一些困惑,因為和代理模式的作用很像,都是對原有對象進行包裝,增強原有對象。
但還是有很大的不同點:
代理模式中,我們是直接將原對象封裝到代理對象之中,對于業(yè)務方并不關系原始對象,直接使用代理對象即可。
裝飾器模式中,我們只提供了裝飾函數(shù),輸入原始對象,輸出增強對象。輸出的增強對象,還可以接著傳入到新的裝飾器函數(shù)中繼續(xù)增強。對于業(yè)務方,可以隨意組合裝飾函數(shù),但得有一個最最開始的原始對象。
再具體點:
代理模式的話,對象之間的依賴關系已經(jīng)寫死了,原始對象 A,新增代理對象A1, A1 的基礎上再新增代理對象A2。如果我們不想要 A1 新增的功能了,我們并不能直接使用 A2 ,因為A2 已經(jīng)包含了 A1 的功能,我們只能在 A 的基礎上再新寫一個代理對象A3。
而裝飾器模式,我們只提供裝飾函數(shù)A1,裝飾函數(shù) A2,然后對原始對象進行裝飾 A2(A1(A))。如果不想要 A1新增的功能,只需要把 A1 這個裝飾器去掉,調(diào)用 A2(A) 即可。
所以使用代理模式還是使用裝飾器模式,取決于我們是要把所有功能包裝后最終產(chǎn)出一個對象給業(yè)務方使用,還是提供許多功能,讓業(yè)務方自由組合。
總
裝飾器模式同樣踐行了「單一職責原則」,可以把對象/函數(shù)的各個功能獨立出來,降低它們之間的耦合性。
業(yè)務開發(fā)中,如果某個對象/函數(shù)擁有了太多功能,可以考慮使用裝飾器模式進行拆分。