面試官:我不想聽(tīng)單例、工廠了,跟我說(shuō)說(shuō)裝飾器模式吧!
我草草地估算了一下,基本上80% Java 候選人的簡(jiǎn)歷上,在專業(yè)技能欄上都會(huì)寫(xiě)上這么一條:
熟悉常用的GOF設(shè)計(jì)模式,可在實(shí)際業(yè)務(wù)場(chǎng)景中進(jìn)行合理運(yùn)用;
如果面試官恰好看到了這條專業(yè)技能,問(wèn)道:“那你說(shuō)一下,都熟悉并使用過(guò)哪些設(shè)計(jì)模式呢?”
然后,絕大多數(shù)候選人都會(huì)回答說(shuō):“嗯,熟悉單例模式和工廠模式?!?/span>
面試官接著問(wèn)道:“還有其他的嗎?”
候選人一般會(huì)說(shuō):“嗯,還有代理模式、策略模式這些吧,其實(shí)平時(shí)用到的也不是很多?!?/span>
此時(shí),若面試官繼續(xù)追問(wèn):“裝飾器模式有沒(méi)有了解過(guò)?”
候選人往往會(huì)發(fā)愣一下,然后說(shuō):“嗯,這個(gè)設(shè)計(jì)模式也聽(tīng)過(guò),但沒(méi)太深入了解?!?/span>
嗯,本文我們以真實(shí)場(chǎng)景帶入的方式來(lái)講解一下,有用且有趣的“裝飾器”模式。
接下來(lái)話不多說(shuō),Show me the case。
業(yè)務(wù)背景
某大型在線教育學(xué)習(xí)平臺(tái),其學(xué)生端最重要的功能就是展示學(xué)生的課程列表,學(xué)生可點(diǎn)擊課程列表中的某個(gè)課程進(jìn)教室上課,還可以查看這節(jié)課對(duì)應(yīng)的課件、課前預(yù)習(xí)和課后作業(yè)等。
如下圖所示:
當(dāng)然,真實(shí)的業(yè)務(wù)場(chǎng)景還是要復(fù)雜很多的,比如:英語(yǔ)課程的 PC 端按照上圖中的展示方式即可,而英語(yǔ)課程 iPad 端的產(chǎn)品經(jīng)理,則希望在已經(jīng)上過(guò)的課程中,加上老師給學(xué)生的打分。
數(shù)學(xué)課程與英語(yǔ)課程也有不同的地方,課程卡片上需要展示教師的標(biāo)簽,如:名師、活躍、名校畢業(yè)、教齡長(zhǎng)、好評(píng)多等;
最近新推出的繪畫(huà)課程,不但需要在課程卡片上展示教師的標(biāo)簽,而且為了鼓勵(lì)學(xué)生更多地上課學(xué)習(xí),會(huì)在課程卡片上展示一個(gè)完課獎(jiǎng)品。
當(dāng)然,我僅僅是舉個(gè)例子,實(shí)際的課表展示邏輯會(huì)復(fù)雜很多。
代碼質(zhì)量問(wèn)題
說(shuō)說(shuō)目前這塊的代碼實(shí)現(xiàn)情況。
最開(kāi)始的時(shí)候,公司只有英語(yǔ)課程,且 PC 端和 iPad 端的課表展示邏輯是一樣的。
代碼demo如下:
public class Curriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對(duì)應(yīng)的課前預(yù)習(xí)");
System.out.println("展示對(duì)應(yīng)的課后作業(yè)");
System.out.println("展示對(duì)應(yīng)的課件");
}
}
后來(lái),英語(yǔ)課程的 PC 端和 iPad 端的課表展示邏輯不一樣了,iPad 端的課表展示需要加上老師給學(xué)生的打分,代碼實(shí)現(xiàn)如下:
public class Curriculum {
public void query(int studentID, int origin) {
System.out.println("展示課表");
System.out.println("展示對(duì)應(yīng)的課前預(yù)習(xí)");
System.out.println("展示對(duì)應(yīng)的課后作業(yè)");
System.out.println("展示對(duì)應(yīng)的課件");
//英語(yǔ)課程iPad端
if(origin == 1){
System.out.println("展示對(duì)應(yīng)的學(xué)生評(píng)分");
}
}
}
再后來(lái),又增加了需要展示老師標(biāo)簽的數(shù)學(xué)課程,以及增加老師標(biāo)簽和完課獎(jiǎng)品的繪畫(huà)課程,都在一個(gè)類(lèi)中以 if else 分支判斷的方式來(lái)實(shí)現(xiàn),代碼的可讀性和可維護(hù)性就太差了。
于是,負(fù)責(zé)維護(hù)這塊業(yè)務(wù)代碼的工程師干脆一刀切,直接寫(xiě)成了四套代碼。
英語(yǔ)課程PC端:
public class EnglishPCCurriculum {
public List query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對(duì)應(yīng)的課前預(yù)習(xí)");
System.out.println("展示對(duì)應(yīng)的課后作業(yè)");
System.out.println("展示對(duì)應(yīng)的課件");
}
}
英語(yǔ)課程iPad端:
public class EnglishIPadCurriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對(duì)應(yīng)的課前預(yù)習(xí)");
System.out.println("展示對(duì)應(yīng)的課后作業(yè)");
System.out.println("展示對(duì)應(yīng)的課件");
System.out.println("展示對(duì)應(yīng)的學(xué)生評(píng)分");
}
}
數(shù)學(xué)課程:
public class MathCurriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對(duì)應(yīng)的課前預(yù)習(xí)");
System.out.println("展示對(duì)應(yīng)的課后作業(yè)");
System.out.println("展示對(duì)應(yīng)的課件");
System.out.println("展示對(duì)應(yīng)的老師標(biāo)簽");
}
}
繪畫(huà)課程:
public class DrawCurriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對(duì)應(yīng)的課前預(yù)習(xí)");
System.out.println("展示對(duì)應(yīng)的課后作業(yè)");
System.out.println("展示對(duì)應(yīng)的課件");
System.out.println("展示對(duì)應(yīng)的老師標(biāo)簽");
System.out.println("展示對(duì)應(yīng)的完課獎(jiǎng)品");
}
}
劃重點(diǎn),代碼按照上述方式實(shí)現(xiàn),有何問(wèn)題?
在《重構(gòu)—改善既有代碼的設(shè)計(jì)》一書(shū)中,有兩種非常常見(jiàn)的Bad Smell(糟糕的代碼),叫做 “過(guò)長(zhǎng)的方法” 和 “重復(fù)的代碼” 。
其實(shí)問(wèn)題還是挺大的,我們上面的代碼只是實(shí)現(xiàn)了一個(gè)demo而已,如果是真實(shí)的代碼,這個(gè)查詢課表的query()方法實(shí)現(xiàn)了太多的業(yè)務(wù)邏輯,一定命中了“過(guò)長(zhǎng)的方法”這個(gè)Bad Semll。
而且,上面這四個(gè)類(lèi)中的query()方法,在實(shí)現(xiàn)展示課表、作業(yè)、預(yù)習(xí)、課件業(yè)務(wù)無(wú)邏輯的時(shí)候,也命中了“重復(fù)的代碼”這個(gè)Bad Semll。
除此之外,這段代碼還命中了一種叫做 “發(fā)散式變化” 的 Bad Smell。
發(fā)散式變化的定義是,一個(gè)類(lèi)被錨定了多個(gè)變化,當(dāng)這些變化中的任意一個(gè)發(fā)生時(shí),就必須對(duì)類(lèi)進(jìn)行修改。這說(shuō)明該類(lèi)承擔(dān)的職責(zé)過(guò)多,不符合單一職責(zé)的設(shè)計(jì)原則。
而上面這四個(gè)類(lèi)的query()方法中,從頭到尾實(shí)現(xiàn)了整個(gè)課表展示的邏輯,只要課表、作業(yè)、預(yù)習(xí)、課件等任意邏輯發(fā)生變化都需要對(duì)這個(gè)類(lèi)進(jìn)行修改,確實(shí)承擔(dān)的職責(zé)過(guò)多了。
接下來(lái),我們看看如何這塊代碼進(jìn)行重構(gòu),使其實(shí)現(xiàn)方式更具可維護(hù)性和可擴(kuò)展性。
裝飾器模式
裝飾器模式(Decorator Pattern),在不改變一個(gè)現(xiàn)有對(duì)象結(jié)構(gòu)的情況下,為其動(dòng)態(tài)地增加一些額外的職責(zé)。
裝飾器模式的優(yōu)點(diǎn)在于:
- 可動(dòng)態(tài)地為現(xiàn)有對(duì)象增加額外的職責(zé),無(wú)需改動(dòng)原來(lái)的代碼,具備更好的靈活性和可擴(kuò)展性,且符合開(kāi)閉原則。
- 每種額外的職責(zé)都被實(shí)現(xiàn)為一個(gè)單獨(dú)且通用的裝飾器,符合單一職責(zé),解決了“過(guò)長(zhǎng)的方法”、“重復(fù)的代碼”和“發(fā)散式變化”等Bad Smell。
其類(lèi)結(jié)構(gòu)圖如下:
圖片
Component:定義了被裝飾對(duì)象和裝飾器都需要實(shí)現(xiàn)的接口。
ConcreteComponent:被裝飾對(duì)象,需要提供業(yè)務(wù)邏輯的核心功能。
Decorator:抽象裝飾器,可通過(guò)其子類(lèi)進(jìn)行額外功能職責(zé)的擴(kuò)展。
ConcreteDecorator:具體裝飾類(lèi),對(duì)被裝飾對(duì)象進(jìn)行額外功能職責(zé)的擴(kuò)展。
代碼重構(gòu)優(yōu)化
接下來(lái)我們通過(guò)裝飾器模式將代碼進(jìn)行重構(gòu)優(yōu)化。
Curriculum接口:
public interface Curriculum {
public void query(int studentID);
}
Curriculum具體實(shí)現(xiàn):
public class ConcreteCurriculum implements Curriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對(duì)應(yīng)的課前預(yù)習(xí)");
System.out.println("展示對(duì)應(yīng)的課后作業(yè)");
System.out.println("展示對(duì)應(yīng)的課件");
}
}
Curriculum的抽象裝飾器:
public abstract class CurriculumDecorator implements Curriculum {
protected Curriculum curriculum;
public CurriculumDecorator(Curriculum curriculum){
this.curriculum = curriculum;
}
public void query(int studentID){
curriculum.query(studentID);
}
}
Curriculum的評(píng)分裝飾器:
public class ScoreDecorator extends CurriculumDecorator{
public ScoreDecorator(Curriculum curriculum) {
super(curriculum);
}
@Override
public void query(int studentID) {
curriculum.query(studentID);
System.out.println("展示對(duì)應(yīng)的學(xué)生評(píng)分");
}
}
Curriculum的老師標(biāo)簽裝飾器:
public class LabelDecorator extends CurriculumDecorator{
public LabelDecorator(Curriculum curriculum) {
super(curriculum);
}
@Override
public void query(int studentID) {
curriculum.query(studentID);
System.out.println("展示對(duì)應(yīng)的老師標(biāo)簽");
}
}
Curriculum的獎(jiǎng)品裝飾器:
public class GiftDecorator extends CurriculumDecorator{
public GiftDecorator(Curriculum curriculum) {
super(curriculum);
}
@Override
public void query(int studentID) {
curriculum.query(studentID);
System.out.println("展示對(duì)應(yīng)的完課獎(jiǎng)品");
}
}
Demo:
public class Demo {
public static void main(String[] args) {
Curriculum curriculum = new ConcreteCurriculum();
CurriculumDecorator scoreDecorator = new ScoreDecorator(new ConcreteCurriculum());
CurriculumDecorator labelDecorator = new LabelDecorator(new ConcreteCurriculum());
CurriculumDecorator giftLabelDecorator = new GiftDecorator(labelDecorator);
System.out.println("英語(yǔ)PC端課表展示");
curriculum.query(123);
System.out.println();
System.out.println("英語(yǔ)iPad端課表展示");
scoreDecorator.query(123);
System.out.println();
System.out.println("數(shù)學(xué)課表展示");
labelDecorator.query(123);
System.out.println();
System.out.println("繪畫(huà)課表展示");
giftLabelDecorator.query(123);
}
}
執(zhí)行結(jié)果:
英語(yǔ)PC端課表展示
展示課表
展示對(duì)應(yīng)的課前預(yù)習(xí)
展示對(duì)應(yīng)的課后作業(yè)
展示對(duì)應(yīng)的課件
英語(yǔ)iPad端課表展示
展示課表
展示對(duì)應(yīng)的課前預(yù)習(xí)
展示對(duì)應(yīng)的課后作業(yè)
展示對(duì)應(yīng)的課件
展示對(duì)應(yīng)的學(xué)生評(píng)分
數(shù)學(xué)課表展示
展示課表
展示對(duì)應(yīng)的課前預(yù)習(xí)
展示對(duì)應(yīng)的課后作業(yè)
展示對(duì)應(yīng)的課件
展示對(duì)應(yīng)的老師標(biāo)簽
繪畫(huà)課表展示
展示課表
展示對(duì)應(yīng)的課前預(yù)習(xí)
展示對(duì)應(yīng)的課后作業(yè)
展示對(duì)應(yīng)的課件
展示對(duì)應(yīng)的老師標(biāo)簽
展示對(duì)應(yīng)的完課獎(jiǎng)品
至此,展示課表業(yè)務(wù)場(chǎng)景的代碼改造完畢。
有的同學(xué)可能會(huì)問(wèn),為什么不通過(guò)繼承的方式進(jìn)行實(shí)現(xiàn)呢?
其原因在于,繼承的方式不如這種動(dòng)態(tài)組合的方式靈活,也很難實(shí)現(xiàn)這種細(xì)粒度的代碼復(fù)用。
舉個(gè)例子:如果數(shù)學(xué)和繪畫(huà)課程又新增了需求,需要額外展示對(duì)應(yīng)的輔修資料,但英語(yǔ)課程則不需要展示這類(lèi)信息,那按照繼承的方式應(yīng)該如何實(shí)現(xiàn)呢?