面試官:我不想聽單例、工廠了,跟我說說裝飾器模式吧!
我草草地估算了一下,基本上80% Java 候選人的簡歷上,在專業(yè)技能欄上都會寫上這么一條:
熟悉常用的GOF設(shè)計模式,可在實際業(yè)務(wù)場景中進行合理運用;
如果面試官恰好看到了這條專業(yè)技能,問道:“那你說一下,都熟悉并使用過哪些設(shè)計模式呢?”
然后,絕大多數(shù)候選人都會回答說:“嗯,熟悉單例模式和工廠模式?!?/span>
面試官接著問道:“還有其他的嗎?”
候選人一般會說:“嗯,還有代理模式、策略模式這些吧,其實平時用到的也不是很多?!?/span>
此時,若面試官繼續(xù)追問:“裝飾器模式有沒有了解過?”
候選人往往會發(fā)愣一下,然后說:“嗯,這個設(shè)計模式也聽過,但沒太深入了解。”
嗯,本文我們以真實場景帶入的方式來講解一下,有用且有趣的“裝飾器”模式。
接下來話不多說,Show me the case。
業(yè)務(wù)背景
某大型在線教育學習平臺,其學生端最重要的功能就是展示學生的課程列表,學生可點擊課程列表中的某個課程進教室上課,還可以查看這節(jié)課對應(yīng)的課件、課前預習和課后作業(yè)等。
如下圖所示:

當然,真實的業(yè)務(wù)場景還是要復雜很多的,比如:英語課程的 PC 端按照上圖中的展示方式即可,而英語課程 iPad 端的產(chǎn)品經(jīng)理,則希望在已經(jīng)上過的課程中,加上老師給學生的打分。
數(shù)學課程與英語課程也有不同的地方,課程卡片上需要展示教師的標簽,如:名師、活躍、名校畢業(yè)、教齡長、好評多等;
最近新推出的繪畫課程,不但需要在課程卡片上展示教師的標簽,而且為了鼓勵學生更多地上課學習,會在課程卡片上展示一個完課獎品。
當然,我僅僅是舉個例子,實際的課表展示邏輯會復雜很多。
代碼質(zhì)量問題
說說目前這塊的代碼實現(xiàn)情況。
最開始的時候,公司只有英語課程,且 PC 端和 iPad 端的課表展示邏輯是一樣的。
代碼demo如下:
public class Curriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對應(yīng)的課前預習");
System.out.println("展示對應(yīng)的課后作業(yè)");
System.out.println("展示對應(yīng)的課件");
}
}后來,英語課程的 PC 端和 iPad 端的課表展示邏輯不一樣了,iPad 端的課表展示需要加上老師給學生的打分,代碼實現(xiàn)如下:
public class Curriculum {
public void query(int studentID, int origin) {
System.out.println("展示課表");
System.out.println("展示對應(yīng)的課前預習");
System.out.println("展示對應(yīng)的課后作業(yè)");
System.out.println("展示對應(yīng)的課件");
//英語課程iPad端
if(origin == 1){
System.out.println("展示對應(yīng)的學生評分");
}
}
}再后來,又增加了需要展示老師標簽的數(shù)學課程,以及增加老師標簽和完課獎品的繪畫課程,都在一個類中以 if else 分支判斷的方式來實現(xiàn),代碼的可讀性和可維護性就太差了。
于是,負責維護這塊業(yè)務(wù)代碼的工程師干脆一刀切,直接寫成了四套代碼。
英語課程PC端:
public class EnglishPCCurriculum {
public List query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對應(yīng)的課前預習");
System.out.println("展示對應(yīng)的課后作業(yè)");
System.out.println("展示對應(yīng)的課件");
}
}英語課程iPad端:
public class EnglishIPadCurriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對應(yīng)的課前預習");
System.out.println("展示對應(yīng)的課后作業(yè)");
System.out.println("展示對應(yīng)的課件");
System.out.println("展示對應(yīng)的學生評分");
}
}數(shù)學課程:
public class MathCurriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對應(yīng)的課前預習");
System.out.println("展示對應(yīng)的課后作業(yè)");
System.out.println("展示對應(yīng)的課件");
System.out.println("展示對應(yīng)的老師標簽");
}
}繪畫課程:
public class DrawCurriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對應(yīng)的課前預習");
System.out.println("展示對應(yīng)的課后作業(yè)");
System.out.println("展示對應(yīng)的課件");
System.out.println("展示對應(yīng)的老師標簽");
System.out.println("展示對應(yīng)的完課獎品");
}
}劃重點,代碼按照上述方式實現(xiàn),有何問題?
在《重構(gòu)—改善既有代碼的設(shè)計》一書中,有兩種非常常見的Bad Smell(糟糕的代碼),叫做 “過長的方法” 和 “重復的代碼” 。
其實問題還是挺大的,我們上面的代碼只是實現(xiàn)了一個demo而已,如果是真實的代碼,這個查詢課表的query()方法實現(xiàn)了太多的業(yè)務(wù)邏輯,一定命中了“過長的方法”這個Bad Semll。
而且,上面這四個類中的query()方法,在實現(xiàn)展示課表、作業(yè)、預習、課件業(yè)務(wù)無邏輯的時候,也命中了“重復的代碼”這個Bad Semll。
除此之外,這段代碼還命中了一種叫做 “發(fā)散式變化” 的 Bad Smell。
發(fā)散式變化的定義是,一個類被錨定了多個變化,當這些變化中的任意一個發(fā)生時,就必須對類進行修改。這說明該類承擔的職責過多,不符合單一職責的設(shè)計原則。
而上面這四個類的query()方法中,從頭到尾實現(xiàn)了整個課表展示的邏輯,只要課表、作業(yè)、預習、課件等任意邏輯發(fā)生變化都需要對這個類進行修改,確實承擔的職責過多了。
接下來,我們看看如何這塊代碼進行重構(gòu),使其實現(xiàn)方式更具可維護性和可擴展性。
裝飾器模式
裝飾器模式(Decorator Pattern),在不改變一個現(xiàn)有對象結(jié)構(gòu)的情況下,為其動態(tài)地增加一些額外的職責。
裝飾器模式的優(yōu)點在于:
- 可動態(tài)地為現(xiàn)有對象增加額外的職責,無需改動原來的代碼,具備更好的靈活性和可擴展性,且符合開閉原則。
- 每種額外的職責都被實現(xiàn)為一個單獨且通用的裝飾器,符合單一職責,解決了“過長的方法”、“重復的代碼”和“發(fā)散式變化”等Bad Smell。
其類結(jié)構(gòu)圖如下:
圖片
Component:定義了被裝飾對象和裝飾器都需要實現(xiàn)的接口。
ConcreteComponent:被裝飾對象,需要提供業(yè)務(wù)邏輯的核心功能。
Decorator:抽象裝飾器,可通過其子類進行額外功能職責的擴展。
ConcreteDecorator:具體裝飾類,對被裝飾對象進行額外功能職責的擴展。
代碼重構(gòu)優(yōu)化
接下來我們通過裝飾器模式將代碼進行重構(gòu)優(yōu)化。
Curriculum接口:
public interface Curriculum {
public void query(int studentID);
}Curriculum具體實現(xiàn):
public class ConcreteCurriculum implements Curriculum {
public void query(int studentID) {
System.out.println("展示課表");
System.out.println("展示對應(yīng)的課前預習");
System.out.println("展示對應(yīng)的課后作業(yè)");
System.out.println("展示對應(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的評分裝飾器:
public class ScoreDecorator extends CurriculumDecorator{
public ScoreDecorator(Curriculum curriculum) {
super(curriculum);
}
@Override
public void query(int studentID) {
curriculum.query(studentID);
System.out.println("展示對應(yīng)的學生評分");
}
}Curriculum的老師標簽裝飾器:
public class LabelDecorator extends CurriculumDecorator{
public LabelDecorator(Curriculum curriculum) {
super(curriculum);
}
@Override
public void query(int studentID) {
curriculum.query(studentID);
System.out.println("展示對應(yīng)的老師標簽");
}
}Curriculum的獎品裝飾器:
public class GiftDecorator extends CurriculumDecorator{
public GiftDecorator(Curriculum curriculum) {
super(curriculum);
}
@Override
public void query(int studentID) {
curriculum.query(studentID);
System.out.println("展示對應(yī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("英語PC端課表展示");
curriculum.query(123);
System.out.println();
System.out.println("英語iPad端課表展示");
scoreDecorator.query(123);
System.out.println();
System.out.println("數(shù)學課表展示");
labelDecorator.query(123);
System.out.println();
System.out.println("繪畫課表展示");
giftLabelDecorator.query(123);
}
}執(zhí)行結(jié)果:
英語PC端課表展示
展示課表
展示對應(yīng)的課前預習
展示對應(yīng)的課后作業(yè)
展示對應(yīng)的課件
英語iPad端課表展示
展示課表
展示對應(yīng)的課前預習
展示對應(yīng)的課后作業(yè)
展示對應(yīng)的課件
展示對應(yīng)的學生評分
數(shù)學課表展示
展示課表
展示對應(yīng)的課前預習
展示對應(yīng)的課后作業(yè)
展示對應(yīng)的課件
展示對應(yīng)的老師標簽
繪畫課表展示
展示課表
展示對應(yīng)的課前預習
展示對應(yīng)的課后作業(yè)
展示對應(yīng)的課件
展示對應(yīng)的老師標簽
展示對應(yīng)的完課獎品至此,展示課表業(yè)務(wù)場景的代碼改造完畢。
有的同學可能會問,為什么不通過繼承的方式進行實現(xiàn)呢?
其原因在于,繼承的方式不如這種動態(tài)組合的方式靈活,也很難實現(xiàn)這種細粒度的代碼復用。
舉個例子:如果數(shù)學和繪畫課程又新增了需求,需要額外展示對應(yīng)的輔修資料,但英語課程則不需要展示這類信息,那按照繼承的方式應(yīng)該如何實現(xiàn)呢?






























