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