<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文學會設(shè)計模式,太詳細了!

          共 29501字,需瀏覽 60分鐘

           ·

          2021-11-16 08:39

            Java大聯(lián)盟

            幫助萬千Java學習者持續(xù)成長

          關(guān)注



          https://javadoop.com/post/design-pattern


          B 站搜索:楠哥教你學Java

          獲取更多優(yōu)質(zhì)視頻教程


          設(shè)計模式是對大家實際工作中寫的各種代碼進行高層次抽象的總結(jié),其中最出名的當屬 Gang of Four (GoF) 的分類了,他們將設(shè)計模式分類為 23 種經(jīng)典的模式,根據(jù)用途我們又可以分為三大類,分別為創(chuàng)建型模式、結(jié)構(gòu)型模式和行為型模式。

          有一些重要的設(shè)計原則在開篇和大家分享下,這些原則將貫通全文:

          1. 面向接口編程,而不是面向?qū)崿F(xiàn)。這個很重要,也是優(yōu)雅的、可擴展的代碼的第一步,這就不需要多說了吧。

          2. 職責單一原則。每個類都應該只有一個單一的功能,并且該功能應該由這個類完全封裝起來。

          3. 對修改關(guān)閉,對擴展開放。對修改關(guān)閉是說,我們辛辛苦苦加班寫出來的代碼,該實現(xiàn)的功能和該修復的 bug 都完成了,別人可不能說改就改;對擴展開放就比較好理解了,也就是說在我們寫好的代碼基礎(chǔ)上,很容易實現(xiàn)擴展。

          創(chuàng)建型模式比較簡單,但是會比較沒有意思,結(jié)構(gòu)型和行為型比較有意思。

          創(chuàng)建型模式

          創(chuàng)建型模式的作用就是創(chuàng)建對象,說到創(chuàng)建一個對象,最熟悉的就是 new 一個對象,然后 set 相關(guān)屬性。但是,在很多場景下,我們需要給客戶端提供更加友好的創(chuàng)建對象的方式,尤其是那種我們定義了類,但是需要提供給其他開發(fā)者用的時候。

          簡單工廠模式

          和名字一樣簡單,非常簡單,直接上代碼吧:

          public class FoodFactory {
          public static Food makeFood(String name) { if (name.equals("noodle")) { Food noodle = new LanZhouNoodle(); noodle.addSpicy("more"); return noodle; } else if (name.equals("chicken")) { Food chicken = new HuangMenChicken(); chicken.addCondiment("potato"); return chicken; } else { return null; } }}

          其中,LanZhouNoodle 和 HuangMenChicken 都繼承自 Food。

          簡單地說,簡單工廠模式通常就是這樣,一個工廠類 XxxFactory,里面有一個靜態(tài)方法,根據(jù)我們不同的參數(shù),返回不同的派生自同一個父類(或?qū)崿F(xiàn)同一接口)的實例對象。

          我們強調(diào)職責單一原則,一個類只提供一種功能,F(xiàn)oodFactory 的功能就是只要負責生產(chǎn)各種 Food。

          工廠模式

          簡單工廠模式很簡單,如果它能滿足我們的需要,我覺得就不要折騰了。之所以需要引入工廠模式,是因為我們往往需要使用兩個或兩個以上的工廠。

          public interface FoodFactory {    Food makeFood(String name);}public class ChineseFoodFactory implements FoodFactory {
          @Override public Food makeFood(String name) { if (name.equals("A")) { return new ChineseFoodA(); } else if (name.equals("B")) { return new ChineseFoodB(); } else { return null; } }}public class AmericanFoodFactory implements FoodFactory {
          @Override public Food makeFood(String name) { if (name.equals("A")) { return new AmericanFoodA(); } else if (name.equals("B")) { return new AmericanFoodB(); } else { return null; } }}

          其中,ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food。

          客戶調(diào)用:

          public class APP {    public static void main(String[] args) {                FoodFactory factory = new ChineseFoodFactory();                Food food = factory.makeFood("A");    }}

          雖然都是調(diào)用 makeFood("A")  制作 A 類食物,但是,不同的工廠生產(chǎn)出來的完全不一樣。

          第一步,我們需要選取合適的工廠,然后第二步基本上和簡單工廠一樣。

          核心在于,我們需要在第一步選好我們需要的工廠。比如,我們有 LogFactory 接口,實現(xiàn)類有 FileLogFactory 和 KafkaLogFactory,分別對應將日志寫入文件和寫入 Kafka 中,顯然,我們客戶端第一步就需要決定到底要實例化 FileLogFactory 還是 KafkaLogFactory,這將決定之后的所有的操作。

          雖然簡單,不過我也把所有的構(gòu)件都畫到一張圖上,這樣讀者看著比較清晰:

          抽象工廠模式

          當涉及到產(chǎn)品族的時候,就需要引入抽象工廠模式了。

          一個經(jīng)典的例子是造一臺電腦。我們先不引入抽象工廠模式,看看怎么實現(xiàn)。

          因為電腦是由許多的構(gòu)件組成的,我們將 CPU 和主板進行抽象,然后 CPU 由 CPUFactory 生產(chǎn),主板由 MainBoardFactory 生產(chǎn),然后,我們再將 CPU 和主板搭配起來組合在一起,如下圖:

          這個時候的客戶端調(diào)用是這樣的:

          CPUFactory cpuFactory = new IntelCPUFactory();CPU cpu = intelCPUFactory.makeCPU();
          MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();MainBoard mainBoard = mainBoardFactory.make();
          Computer computer = new Computer(cpu, mainBoard);

          單獨看 CPU 工廠和主板工廠,它們分別是前面我們說的工廠模式。這種方式也容易擴展,因為要給電腦加硬盤的話,只需要加一個 HardDiskFactory 和相應的實現(xiàn)即可,不需要修改現(xiàn)有的工廠。

          但是,這種方式有一個問題,那就是如果 Intel 家產(chǎn)的 CPU 和 AMD 產(chǎn)的主板不能兼容使用,那么這代碼就容易出錯,因為客戶端并不知道它們不兼容,也就會錯誤地出現(xiàn)隨意組合。

          下面就是我們要說的產(chǎn)品族的概念,它代表了組成某個產(chǎn)品的一系列附件的集合:

          當涉及到這種產(chǎn)品族的問題的時候,就需要抽象工廠模式來支持了。我們不再定義 CPU 工廠、主板工廠、硬盤工廠、顯示屏工廠等等,我們直接定義電腦工廠,每個電腦工廠負責生產(chǎn)所有的設(shè)備,這樣能保證肯定不存在兼容問題。

          這個時候,對于客戶端來說,不再需要單獨挑選 CPU廠商、主板廠商、硬盤廠商等,直接選擇一家品牌工廠,品牌工廠會負責生產(chǎn)所有的東西,而且能保證肯定是兼容可用的。

          public static void main(String[] args) {        ComputerFactory cf = new AmdFactory();        CPU cpu = cf.makeCPU();        MainBoard board = cf.makeMainBoard();            HardDisk hardDisk = cf.makeHardDisk();
          Computer result = new Computer(cpu, board, hardDisk);}

          當然,抽象工廠的問題也是顯而易見的,比如我們要加個顯示器,就需要修改所有的工廠,給所有的工廠都加上制造顯示器的方法。這有點違反了對修改關(guān)閉,對擴展開放這個設(shè)計原則。

          單例模式

          單例模式用得最多,錯得最多。

          餓漢模式最簡單:

          public class Singleton {        private Singleton() {};        private static Singleton instance = new Singleton();
          public static Singleton getInstance() { return instance; } public static Date getDate(String mode) {return new Date();}}

          很多人都能說出餓漢模式的缺點,可是我覺得生產(chǎn)過程中,很少碰到這種情況:你定義了一個單例的類,不需要其實例,可是你卻把一個或幾個你會用到的靜態(tài)方法塞到這個類中。

          飽漢模式最容易出錯:

          public class Singleton {        private Singleton() {}        private static volatile Singleton instance = null;
          public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}

          雙重檢查,指的是兩次檢查 instance 是否為 null。

          volatile 在這里是需要的,希望能引起讀者的關(guān)注。

          很多人不知道怎么寫,直接就在 getInstance() 方法簽名上加上 synchronized,這就不多說了,性能太差。

          嵌套類最經(jīng)典,以后大家就用它吧:

          public class Singleton3 {
          private Singleton3() {} private static class Holder { private static Singleton3 instance = new Singleton3(); } public static Singleton3 getInstance() { return Holder.instance; }}

          注意,很多人都會把這個嵌套類說成是靜態(tài)內(nèi)部類,嚴格地說,內(nèi)部類和嵌套類是不一樣的,它們能訪問的外部類權(quán)限也是不一樣的。

          最后,我們說一下枚舉,枚舉很特殊,它在類加載的時候會初始化里面的所有的實例,而且 JVM 保證了它們不會再被實例化,所以它天生就是單例的。

          雖然我們平時很少看到用枚舉來實現(xiàn)單例,但是在 RxJava 的源碼中,有很多地方都用了枚舉來實現(xiàn)單例。

          建造者模式

          經(jīng)常碰見的 XxxBuilder 的類,通常都是建造者模式的產(chǎn)物。建造者模式其實有很多的變種,但是對于客戶端來說,我們的使用通常都是一個模式的:

          Food food = new FoodBuilder().a().b().c().build();Food food = Food.builder().a().b().c().build();

          套路就是先 new 一個 Builder,然后可以鏈式地調(diào)用一堆方法,最后再調(diào)用一次 build() 方法,我們需要的對象就有了。

          來一個中規(guī)中矩的建造者模式:

          class User {        private String name;    private String password;    private String nickName;    private int age;
          private User(String name, String password, String nickName, int age) { this.name = name; this.password = password; this.nickName = nickName; this.age = age; } public static UserBuilder builder() { return new UserBuilder(); }
          public static class UserBuilder { private String name; private String password; private String nickName; private int age;
          private UserBuilder() { }
          public UserBuilder name(String name) { this.name = name; return this; }
          public UserBuilder password(String password) { this.password = password; return this; }
          public UserBuilder nickName(String nickName) { this.nickName = nickName; return this; }
          public UserBuilder age(int age) { this.age = age; return this; }
          public User build() { if (name == null || password == null) { throw new RuntimeException("用戶名和密碼必填"); } if (age <= 0 || age >= 150) { throw new RuntimeException("年齡不合法"); } if (nickName == null) { nickName = name; } return new User(name, password, nickName, age); } }}

          核心是:先把所有的屬性都設(shè)置給 Builder,然后 build() 方法的時候,將這些屬性復制給實際產(chǎn)生的對象。

          看看客戶端的調(diào)用:

          public class APP {    public static void main(String[] args) {        User d = User.builder()                .name("foo")                .password("pAss12345")                .age(25)                .build();    }}
          說實話,建造者模式的鏈式寫法很吸引人,但是,多寫了很多“無用”的 builder 的代碼,感覺這個模式?jīng)]什么用。不過,當屬性很多,而且有些必填,有些選填的時候,這個模式會使代碼清晰很多。我們可以在 Builder 的構(gòu)造方法中強制讓調(diào)用者提供必填字段,還有,在 build() 方法中校驗各個參數(shù)比在 User 的構(gòu)造方法中校驗,代碼要優(yōu)雅一些。

          題外話,強烈建議讀者使用 lombok,用了 lombok 以后,上面的一大堆代碼會變成如下這樣:

          @Builderclass User {    private String  name;    private String password;    private String nickName;    private int age;}


          怎么樣,省下來的時間是不是又可以干點別的了。

          當然,如果你只是想要鏈式寫法,不想要建造者模式,有個很簡單的辦法,User 的 getter 方法不變,所有的 setter 方法都讓其 return this 就可以了,然后就可以像下面這樣調(diào)用:

          User user = new User().setName("").setPassword("").setAge(20);


          很多人是這么用的,但是筆者覺得其實這種寫法非常地不優(yōu)雅,不是很推薦使用。

          原型模式

          這是我要說的創(chuàng)建型模式的最后一個設(shè)計模式了。原型模式很簡單:有一個原型實例,基于這個原型實例產(chǎn)生新的實例,也就是“克隆”了。

          Object 類中有一個 clone() 方法,它用于生成一個新的對象,當然,如果我們要調(diào)用這個方法,java 要求我們的類必須先實現(xiàn) Cloneable 接口,此接口沒有定義任何方法,但是不這么做的話,在 clone() 的時候,會拋出 CloneNotSupportedException 異常。

          protected native Object clone() throws CloneNotSupportedException;

          java 的克隆是淺克隆,碰到對象引用的時候,克隆出來的對象和原對象中的引用將指向同一個對象。通常實現(xiàn)深克隆的方法是將對象進行序列化,然后再進行反序列化。

          原型模式了解到這里我覺得就夠了,各種變著法子說這種代碼或那種代碼是原型模式,沒什么意義。

          創(chuàng)建型模式總結(jié)

          創(chuàng)建型模式總體上比較簡單,它們的作用就是為了產(chǎn)生實例對象,算是各種工作的第一步了,因為我們寫的是面向?qū)ο?/strong>的代碼,所以我們第一步當然是需要創(chuàng)建一個對象了。

          簡單工廠模式最簡單;工廠模式在簡單工廠模式的基礎(chǔ)上增加了選擇工廠的維度,需要第一步選擇合適的工廠;抽象工廠模式有產(chǎn)品族的概念,如果各個產(chǎn)品是存在兼容性問題的,就要用抽象工廠模式。單例模式就不說了,為了保證全局使用的是同一對象,一方面是安全性考慮,一方面是為了節(jié)省資源;建造者模式專門對付屬性很多的那種類,為了讓代碼更優(yōu)美;原型模式用得最少,了解和 Object 類中的 clone() 方法相關(guān)的知識即可。

          結(jié)構(gòu)型模式

          前面創(chuàng)建型模式介紹了創(chuàng)建對象的一些設(shè)計模式,這節(jié)介紹的結(jié)構(gòu)型模式旨在通過改變代碼結(jié)構(gòu)來達到解耦的目的,使得我們的代碼容易維護和擴展。

          代理模式

          第一個要介紹的代理模式是最常使用的模式之一了,用一個代理來隱藏具體實現(xiàn)類的實現(xiàn)細節(jié),通常還用于在真實的實現(xiàn)的前后添加一部分邏輯。

          既然說是代理,那就要對客戶端隱藏真實實現(xiàn),由代理來負責客戶端的所有請求。當然,代理只是個代理,它不會完成實際的業(yè)務(wù)邏輯,而是一層皮而已,但是對于客戶端來說,它必須表現(xiàn)得就是客戶端需要的真實實現(xiàn)。

          理解代理這個詞,這個模式其實就簡單了。

          public interface FoodService {
              Food makeChicken();
              Food makeNoodle();
          }

          public class FoodServiceImpl implements FoodService { public Food makeChicken() { Food f = new Chicken() f.setChicken("1kg"); f.setSpicy("1g"); f.setSalt("3g"); return f; } public Food makeNoodle() { Food f = new Noodle(); f.setNoodle("500g"); f.setSalt("5g"); return f; }}

          public class FoodServiceProxy implements FoodService {
          private FoodService foodService = new FoodServiceImpl();
          public Food makeChicken() { System.out.println("我們馬上要開始制作雞肉了");
          Food food = foodService.makeChicken();
          System.out.println("雞肉制作完成啦,加點胡椒粉"); food.addCondiment("pepper");
          return food; } public Food makeNoodle() { System.out.println("準備制作拉面~"); Food food = foodService.makeNoodle(); System.out.println("制作完成啦") return food; }}

          客戶端調(diào)用,注意,我們要用代理來實例化接口:

          FoodService foodService = new FoodServiceProxy();foodService.makeChicken();

          我們發(fā)現(xiàn)沒有,代理模式說白了就是做 “方法包裝” 或做 “方法增強”。在面向切面編程中,其實就是動態(tài)代理的過程。比如 Spring 中,我們自己不定義代理類,但是 Spring 會幫我們動態(tài)來定義代理,然后把我們定義在 @Before、@After、@Around 中的代碼邏輯動態(tài)添加到代理中。

          說到動態(tài)代理,又可以展開說,Spring 中實現(xiàn)動態(tài)代理有兩種,一種是如果我們的類定義了接口,如 UserService 接口和 UserServiceImpl 實現(xiàn),那么采用 JDK 的動態(tài)代理,感興趣的讀者可以去看看 java.lang.reflect.Proxy 類的源碼;另一種是我們自己沒有定義接口的,Spring 會采用 CGLIB 進行動態(tài)代理,它是一個 jar 包,性能還不錯。

          適配器模式

          說完代理模式,說適配器模式,是因為它們很相似,這里可以做個比較。

          適配器模式做的就是,有一個接口需要實現(xiàn),但是我們現(xiàn)成的對象都不滿足,需要加一層適配器來進行適配。

          適配器模式總體來說分三種:默認適配器模式、對象適配器模式、類適配器模式。先不急著分清楚這幾個,先看看例子再說。

          默認適配器模式

          首先,我們先看看最簡單的適配器模式默認適配器模式(Default Adapter)是怎么樣的。

          我們用 Appache commons-io 包中的 FileAlterationListener 做例子,此接口定義了很多的方法,用于對文件或文件夾進行監(jiān)控,一旦發(fā)生了對應的操作,就會觸發(fā)相應的方法。

          public interface FileAlterationListener {    void onStart(final FileAlterationObserver observer);    void onDirectoryCreate(final File directory);    void onDirectoryChange(final File directory);    void onDirectoryDelete(final File directory);    void onFileCreate(final File file);    void onFileChange(final File file);    void onFileDelete(final File file);    void onStop(final FileAlterationObserver observer);}

          此接口的一大問題是抽象方法太多了,如果我們要用這個接口,意味著我們要實現(xiàn)每一個抽象方法,如果我們只是想要監(jiān)控文件夾中的文件創(chuàng)建文件刪除事件,可是我們還是不得不實現(xiàn)所有的方法,很明顯,這不是我們想要的。

          所以,我們需要下面的一個適配器,它用于實現(xiàn)上面的接口,但是所有的方法都是空方法,這樣,我們就可以轉(zhuǎn)而定義自己的類來繼承下面這個類即可。

          public class FileAlterationListenerAdaptor implements FileAlterationListener {
          public void onStart(final FileAlterationObserver observer) { }
          public void onDirectoryCreate(final File directory) { }
          public void onDirectoryChange(final File directory) { }
          public void onDirectoryDelete(final File directory) { }
          public void onFileCreate(final File file) { }
          public void onFileChange(final File file) { }
          public void onFileDelete(final File file) { }
          public void onStop(final FileAlterationObserver observer) { }}

          比如我們可以定義以下類,我們僅僅需要實現(xiàn)我們想實現(xiàn)的方法就可以了:

          public class FileMonitor extends FileAlterationListenerAdaptor {    public void onFileCreate(final File file) {                doSomething();    }
          public void onFileDelete(final File file) { doSomething(); }}

          當然,上面說的只是適配器模式的其中一種,也是最簡單的一種,無需多言。下面,再介紹“正統(tǒng)的”適配器模式。

          對象適配器模式

          來看一個《Head First 設(shè)計模式》中的一個例子,我稍微修改了一下,看看怎么將雞適配成鴨,這樣雞也能當鴨來用。因為,現(xiàn)在鴨這個接口,我們沒有合適的實現(xiàn)類可以用,所以需要適配器。

          public interface Duck {    public void quack();     public void fly(); }
          public interface Cock { public void gobble(); public void fly(); }
          public class WildCock implements Cock { public void gobble() { System.out.println("咕咕叫"); } public void fly() { System.out.println("雞也會飛哦"); }}

          鴨接口有 fly() 和 quare() 兩個方法,雞 Cock 如果要冒充鴨,fly() 方法是現(xiàn)成的,但是雞不會鴨的呱呱叫,沒有 quack() 方法。這個時候就需要適配了:

          public class CockAdapter implements Duck {
          Cock cock; public CockAdapter(Cock cock) { this.cock = cock; }
          @Override public void quack() { cock.gobble(); }
          @Override public void fly() { cock.fly(); }}

          客戶端調(diào)用很簡單了:

          public static void main(String[] args) {          Cock wildCock = new WildCock();            Duck duck = new CockAdapter(wildCock);      ...}

          到這里,大家也就知道了適配器模式是怎么回事了。無非是我們需要一只鴨,但是我們只有一只雞,這個時候就需要定義一個適配器,由這個適配器來充當鴨,但是適配器里面的方法還是由雞來實現(xiàn)的。

          我們用一個圖來簡單說明下:

          上圖應該還是很容易理解的,我就不做更多的解釋了。下面,我們看看類適配模式怎么樣的。

          類適配器模式

          廢話少說,直接上圖:

          看到這個圖,大家應該很容易理解的吧,通過繼承的方法,適配器自動獲得了所需要的大部分方法。這個時候,客戶端使用更加簡單,直接 Target t = new SomeAdapter(); 就可以了。

          適配器模式總結(jié)

          1. 類適配和對象適配的異同
          一個采用繼承,一個采用組合;
          類適配屬于靜態(tài)實現(xiàn),對象適配屬于組合的動態(tài)實現(xiàn),對象適配需要多實例化一個對象。
          總體來說,對象適配用得比較多。
          2. 適配器模式和代理模式的異同
          比較這兩種模式,其實是比較對象適配器模式和代理模式,在代碼結(jié)構(gòu)上,它們很相似,都需要一個具體的實現(xiàn)類的實例。但是它們的目的不一樣,代理模式做的是增強原方法的活;適配器做的是適配的活,為的是提供“把雞包裝成鴨,然后當做鴨來使用”,而雞和鴨它們之間原本沒有繼承關(guān)系。

          橋梁模式

          理解橋梁模式,其實就是理解代碼抽象和解耦。

          我們首先需要一個橋梁,它是一個接口,定義提供的接口方法.

          public interface DrawAPI {   public void draw(int radius, int x, int y);}

          然后是一系列實現(xiàn)類:

          public class RedPen implements DrawAPI {    @Override    public void draw(int radius, int x, int y) {        System.out.println("用紅色筆畫圖,radius:" + radius + ", x:" + x + ", y:" + y);    }}public class GreenPen implements DrawAPI {    @Override    public void draw(int radius, int x, int y) {        System.out.println("用綠色筆畫圖,radius:" + radius + ", x:" + x + ", y:" + y);    }}public class BluePen implements DrawAPI {    @Override    public void draw(int radius, int x, int y) {        System.out.println("用藍色筆畫圖,radius:" + radius + ", x:" + x + ", y:" + y);    }}

          定義一個抽象類,此類的實現(xiàn)類都需要使用 DrawAPI:

          public abstract class Shape {    protected DrawAPI drawAPI;    protected Shape(DrawAPI drawAPI) {        this.drawAPI = drawAPI;    }    public abstract void draw();}

          定義抽象類的子類:

          public class Circle extends Shape {    private int radius;    public Circle(int radius, DrawAPI drawAPI) {        super(drawAPI);        this.radius = radius;    }    public void draw() {        drawAPI.draw(radius, 0, 0);    }}
          public class Rectangle extends Shape { private int x; private int y; public Rectangle(int x, int y, DrawAPI drawAPI) { super(drawAPI); this.x = x; this.y = y; } public void draw() { drawAPI.draw(0, x, y); }}

          最后,我們來看客戶端演示:

          public static void main(String[] args) {    Shape greenCircle = new Circle(10, new GreenPen());    Shape redRectangle = new Rectangle(4, 8, new RedPen());    greenCircle.draw();    redRectangle.draw();}

          可能大家看上面一步步還不是特別清晰,我把所有的東西整合到一張圖上:

          這回大家應該就知道抽象在哪里,怎么解耦了吧。橋梁模式的優(yōu)點也是顯而易見的,就是非常容易進行擴展。

          本節(jié)引用了這里的例子,并對其進行了修改。

          裝飾模式

          要把裝飾模式說清楚明白,不是件容易的事情。也許讀者知道 Java IO 中的幾個類是典型的裝飾模式的應用,但是讀者不一定清楚其中的關(guān)系,也許看完就忘了,希望看完這節(jié)后,讀者可以對其有更深的感悟。

          首先,我們先看一個簡單的圖,看這個圖的時候,了解下層次結(jié)構(gòu)就可以了:

          我們來說說裝飾模式的出發(fā)點,從圖中可以看到,接口 Component 其實已經(jīng)有了 ConcreteComponentAConcreteComponentB 兩個實現(xiàn)類了,但是,如果我們要增強這兩個實現(xiàn)類的話,我們就可以采用裝飾模式,用具體的裝飾器來裝飾實現(xiàn)類,以達到增強的目的。

          從名字來簡單解釋下裝飾器。既然說是裝飾,那么往往就是添加小功能這種,而且,我們要滿足可以添加多個小功能。最簡單的,代理模式就可以實現(xiàn)功能的增強,但是代理不容易實現(xiàn)多個功能的增強,當然你可以說用代理包裝代理的多層包裝方式,但是那樣的話代碼就復雜了。

          首先明白一些簡單的概念,從圖中我們看到,所有的具體裝飾者們 ConcreteDecorator* 都可以作為 Component 來使用,因為它們都實現(xiàn)了 Component 中的所有接口。它們和 Component 實現(xiàn)類 ConcreteComponent* 的區(qū)別是,它們只是裝飾者,起裝飾作用,也就是即使它們看上去牛逼轟轟,但是它們都只是在具體的實現(xiàn)中加了層皮來裝飾而已。

          注意這段話中混雜在各個名詞中的 Component 和 Decorator,別搞混了。

          下面來看看一個例子,先把裝飾模式弄清楚,然后再介紹下 java io 中的裝飾模式的應用。

          最近大街上流行起來了“快樂檸檬”,我們把快樂檸檬的飲料分為三類:紅茶、綠茶、咖啡,在這三大類的基礎(chǔ)上,又增加了許多的口味,什么金桔檸檬紅茶、金桔檸檬珍珠綠茶、芒果紅茶、芒果綠茶、芒果珍珠紅茶、烤珍珠紅茶、烤珍珠芒果綠茶、椰香胚芽咖啡、焦糖可可咖啡等等,每家店都有很長的菜單,但是仔細看下,其實原料也沒幾樣,但是可以搭配出很多組合,如果顧客需要,很多沒出現(xiàn)在菜單中的飲料他們也是可以做的。

          在這個例子中,紅茶、綠茶、咖啡是最基礎(chǔ)的飲料,其他的像金桔檸檬、芒果、珍珠、椰果、焦糖等都屬于裝飾用的。當然,在開發(fā)中,我們確實可以像門店一樣,開發(fā)這些類:LemonBlackTea、LemonGreenTea、MangoBlackTea、MangoLemonGreenTea......但是,很快我們就發(fā)現(xiàn),這樣子干肯定是不行的,這會導致我們需要組合出所有的可能,而且如果客人需要在紅茶中加雙份檸檬怎么辦?三份檸檬怎么辦?

          不說廢話了,上代碼。

          首先,定義飲料抽象基類:

          public abstract class Beverage {            public abstract String getDescription();            public abstract double cost();}

          然后是三個基礎(chǔ)飲料實現(xiàn)類,紅茶、綠茶和咖啡:

          public class BlackTea extends Beverage {      public String getDescription() {        return "紅茶";    }      public double cost() {        return 10;    }}public class GreenTea extends Beverage {    public String getDescription() {        return "綠茶";    }      public double cost() {        return 11;    }}...

          定義調(diào)料,也就是裝飾者的基類,此類必須繼承自 Beverage:

          public abstract class Condiment extends Beverage {
          }

          然后我們來定義檸檬、芒果等具體的調(diào)料,它們屬于裝飾者,毫無疑問,這些調(diào)料肯定都需要繼承調(diào)料 Condiment 類:

          public class Lemon extends Condiment {    private Beverage bevarage;            public Lemon(Beverage bevarage) {        this.bevarage = bevarage;    }    public String getDescription() {                return bevarage.getDescription() + ", 加檸檬";    }    public double cost() {                return beverage.cost() + 2;     }}
          public class Mango extends Condiment { private Beverage bevarage; public Mango(Beverage bevarage) { this.bevarage = bevarage; } public String getDescription() { return bevarage.getDescription() + ", 加芒果"; } public double cost() { return beverage.cost() + 3; }}...

          看客戶端調(diào)用:

          public static void main(String[] args) {        Beverage beverage = new GreenTea();        beverage = new Lemon(beverage);     beverage = new Mongo(beverage); 
          System.out.println(beverage.getDescription() + " 價格:¥" + beverage.cost()); }

          如果我們需要 芒果-珍珠-雙份檸檬-紅茶

          Beverage beverage = new Mongo(new Pearl(new Lemon(new Lemon(new BlackTea()))));

          是不是很變態(tài)?

          看看下圖可能會清晰一些:

          到這里,大家應該已經(jīng)清楚裝飾模式了吧。

          下面,我們再來說說 java IO 中的裝飾模式。看下圖 InputStream 派生出來的部分類:

          我們知道 InputStream 代表了輸入流,具體的輸入來源可以是文件(FileInputStream)、管道(PipedInputStream)、數(shù)組(ByteArrayInputStream)等,這些就像前面奶茶的例子中的紅茶、綠茶,屬于基礎(chǔ)輸入流。

          FilterInputStream 承接了裝飾模式的關(guān)鍵節(jié)點,它的實現(xiàn)類是一系列裝飾器,比如 BufferedInputStream 代表用緩沖來裝飾,也就使得輸入流具有了緩沖的功能,LineNumberInputStream 代表用行號來裝飾,在操作的時候就可以取得行號了,DataInputStream 的裝飾,使得我們可以從輸入流轉(zhuǎn)換為 java 中的基本類型值。

          當然,在 java IO 中,如果我們使用裝飾器的話,就不太適合面向接口編程了,如:

          InputStream inputStream = new LineNumberInputStream(new BufferedInputStream(new FileInputStream("")));

          這樣的結(jié)果是,InputStream 還是不具有讀取行號的功能,因為讀取行號的方法定義在 LineNumberInputStream 類中。

          我們應該像下面這樣使用:

          InputStream inputStream = new LineNumberInputStream(new BufferedInputStream(new FileInputStream("")));

          所以說嘛,要找到純的嚴格符合設(shè)計模式的代碼還是比較難的。

          門面模式

          門面模式(也叫外觀模式,F(xiàn)acade Pattern)在許多源碼中有使用,比如 slf4j 就可以理解為是門面模式的應用。這是一個簡單的設(shè)計模式,我們直接上代碼再說吧。

          首先,我們定義一個接口:

          public interface Shape {   void draw();}

          定義幾個實現(xiàn)類:

          public class Circle implements Shape {    @Override    public void draw() {       System.out.println("Circle::draw()");    }}
          public class Rectangle implements Shape { @Override public void draw() { System.out.println("Rectangle::draw()"); }}

          客戶端調(diào)用:

          public static void main(String[] args) {          Shape circle = new Circle();      circle.draw();
          Shape rectangle = new Rectangle(); rectangle.draw();}

          以上是我們常寫的代碼,我們需要畫圓就要先實例化圓,畫長方形就需要先實例化一個長方形,然后再調(diào)用相應的 draw() 方法。

          下面,我們看看怎么用門面模式來讓客戶端調(diào)用更加友好一些。

          我們先定義一個門面:

          public class ShapeMaker {   private Shape circle;   private Shape rectangle;   private Shape square;
          public ShapeMaker() { circle = new Circle(); rectangle = new Rectangle(); square = new Square(); }

          public void drawCircle(){ circle.draw(); } public void drawRectangle(){ rectangle.draw(); } public void drawSquare(){ square.draw(); }}

          看看現(xiàn)在客戶端怎么調(diào)用:

          public static void main(String[] args) {  ShapeMaker shapeMaker = new ShapeMaker();
          shapeMaker.drawCircle(); shapeMaker.drawRectangle(); shapeMaker.drawSquare(); }

          門面模式的優(yōu)點顯而易見,客戶端不再需要關(guān)注實例化時應該使用哪個實現(xiàn)類,直接調(diào)用門面提供的方法就可以了,因為門面類提供的方法的方法名對于客戶端來說已經(jīng)很友好了。

          組合模式

          組合模式用于表示具有層次結(jié)構(gòu)的數(shù)據(jù),使得我們對單個對象和組合對象的訪問具有一致性。

          直接看一個例子吧,每個員工都有姓名、部門、薪水這些屬性,同時還有下屬員工集合(雖然可能集合為空),而下屬員工和自己的結(jié)構(gòu)是一樣的,也有姓名、部門這些屬性,同時也有他們的下屬員工集合。

          public class Employee {   private String name;   private String dept;   private int salary;   private List<Employee> subordinates; 
          public Employee(String name,String dept, int sal) { this.name = name; this.dept = dept; this.salary = sal; subordinates = new ArrayList<Employee>(); }
          public void add(Employee e) { subordinates.add(e); }
          public void remove(Employee e) { subordinates.remove(e); }
          public List<Employee> getSubordinates(){ return subordinates; }
          public String toString(){ return ("Employee :[ Name : " + name + ", dept : " + dept + ", salary :" + salary+" ]"); } }

          通常,這種類需要定義 add(node)、remove(node)、getChildren() 這些方法。

          這說的其實就是組合模式,這種簡單的模式我就不做過多介紹了,相信各位讀者也不喜歡看我寫廢話。

          享元模式

          英文是 Flyweight Pattern,不知道是誰最先翻譯的這個詞,感覺這翻譯真的不好理解,我們試著強行關(guān)聯(lián)起來吧。Flyweight 是輕量級的意思,享元分開來說就是 共享 元器件,也就是復用已經(jīng)生成的對象,這種做法當然也就是輕量級的了。

          復用對象最簡單的方式是,用一個 HashMap 來存放每次新生成的對象。每次需要一個對象的時候,先到 HashMap 中看看有沒有,如果沒有,再生成新的對象,然后將這個對象放入 HashMap 中。

          這種簡單的代碼我就不演示了。

          結(jié)構(gòu)型模式總結(jié)

          前面,我們說了代理模式、適配器模式、橋梁模式、裝飾模式、門面模式、組合模式和享元模式。讀者是否可以分別把這幾個模式說清楚了呢?在說到這些模式的時候,心中是否有一個清晰的圖或處理流程在腦海里呢?

          代理模式是做方法增強的,適配器模式是把雞包裝成鴨這種用來適配接口的,橋梁模式做到了很好的解耦,裝飾模式從名字上就看得出來,適合于裝飾類或者說是增強類的場景,門面模式的優(yōu)點是客戶端不需要關(guān)心實例化過程,只要調(diào)用需要的方法即可,組合模式用于描述具有層次結(jié)構(gòu)的數(shù)據(jù),享元模式是為了在特定的場景中緩存已經(jīng)創(chuàng)建的對象,用于提高性能。

          行為型模式

          行為型模式關(guān)注的是各個類之間的相互作用,將職責劃分清楚,使得我們的代碼更加地清晰。

          策略模式

          策略模式太常用了,所以把它放到最前面進行介紹。它比較簡單,我就不廢話,直接用代碼說事吧。

          下面設(shè)計的場景是,我們需要畫一個圖形,可選的策略就是用紅色筆來畫,還是綠色筆來畫,或者藍色筆來畫。

          首先,先定義一個策略接口:

          public interface Strategy {   public void draw(int radius, int x, int y);}

          然后我們定義具體的幾個策略:

          public class RedPen implements Strategy {   @Override   public void draw(int radius, int x, int y) {      System.out.println("用紅色筆畫圖,radius:" + radius + ", x:" + x + ", y:" + y);   }}public class GreenPen implements Strategy {   @Override   public void draw(int radius, int x, int y) {      System.out.println("用綠色筆畫圖,radius:" + radius + ", x:" + x + ", y:" + y);   }}public class BluePen implements Strategy {   @Override   public void draw(int radius, int x, int y) {      System.out.println("用藍色筆畫圖,radius:" + radius + ", x:" + x + ", y:" + y);   }}

          使用策略的類:

          public class Context {   private Strategy strategy;
          public Context(Strategy strategy){ this.strategy = strategy; }
          public int executeDraw(int radius, int x, int y){ return strategy.draw(radius, x, y); }}

          客戶端演示:

          public static void main(String[] args) {    Context context = new Context(new BluePen());       context.executeDraw(10, 0, 0);}

          放到一張圖上,讓大家看得清晰些:

          這個時候,大家有沒有聯(lián)想到結(jié)構(gòu)型模式中的橋梁模式,它們其實非常相似,我把橋梁模式的圖拿過來大家對比下:

          要我說的話,它們非常相似,橋梁模式在左側(cè)加了一層抽象而已。橋梁模式的耦合更低,結(jié)構(gòu)更復雜一些。

          觀察者模式

          觀察者模式對于我們來說,真是再簡單不過了。無外乎兩個操作,觀察者訂閱自己關(guān)心的主題和主題有數(shù)據(jù)變化后通知觀察者們。

          首先,需要定義主題,每個主題需要持有觀察者列表的引用,用于在數(shù)據(jù)變更的時候通知各個觀察者:

          public class Subject {    private List<Observer> observers = new ArrayList<Observer>();    private int state;    public int getState() {        return state;    }    public void setState(int state) {        this.state = state;                notifyAllObservers();    }        public void attach(Observer observer) {        observers.add(observer);    }        public void notifyAllObservers() {        for (Observer observer : observers) {            observer.update();        }    }}

          定義觀察者接口:

          public abstract class Observer {    protected Subject subject;    public abstract void update();}

          其實如果只有一個觀察者類的話,接口都不用定義了,不過,通常場景下,既然用到了觀察者模式,我們就是希望一個事件出來了,會有多個不同的類需要處理相應的信息。比如,訂單修改成功事件,我們希望發(fā)短信的類得到通知、發(fā)郵件的類得到通知、處理物流信息的類得到通知等。

          我們來定義具體的幾個觀察者類:

          public class BinaryObserver extends Observer {        public BinaryObserver(Subject subject) {        this.subject = subject;                this.subject.attach(this);    }        @Override    public void update() {        String result = Integer.toBinaryString(subject.getState());        System.out.println("訂閱的數(shù)據(jù)發(fā)生變化,新的數(shù)據(jù)處理為二進制值為:" + result);    }}
          public class HexaObserver extends Observer { public HexaObserver(Subject subject) { this.subject = subject; this.subject.attach(this); } @Override public void update() { String result = Integer.toHexString(subject.getState()).toUpperCase(); System.out.println("訂閱的數(shù)據(jù)發(fā)生變化,新的數(shù)據(jù)處理為十六進制值為:" + result); }}

          客戶端使用也非常簡單:

          public static void main(String[] args) {        Subject subject1 = new Subject();        new BinaryObserver(subject1);    new HexaObserver(subject1);       subject.setState(11);}

          output:

          訂閱的數(shù)據(jù)發(fā)生變化,新的數(shù)據(jù)處理為二進制值為:1011
          訂閱的數(shù)據(jù)發(fā)生變化,新的數(shù)據(jù)處理為十六進制值為:B

          當然,jdk 也提供了相似的支持,具體的大家可以參考 java.util.Observable 和 java.util.Observer 這兩個類。

          實際生產(chǎn)過程中,觀察者模式往往用消息中間件來實現(xiàn),如果要實現(xiàn)單機觀察者模式,筆者建議讀者使用 Guava 中的 EventBus,它有同步實現(xiàn)也有異步實現(xiàn),本文主要介紹設(shè)計模式,就不展開說了。

          還有,即使是上面的這個代碼,也會有很多變種,大家只要記住核心的部分,那就是一定有一個地方存放了所有的觀察者,然后在事件發(fā)生的時候,遍歷觀察者,調(diào)用它們的回調(diào)函數(shù)。

          責任鏈模式

          責任鏈通常需要先建立一個單向鏈表,然后調(diào)用方只需要調(diào)用頭部節(jié)點就可以了,后面會自動流轉(zhuǎn)下去。比如流程審批就是一個很好的例子,只要終端用戶提交申請,根據(jù)申請的內(nèi)容信息,自動建立一條責任鏈,然后就可以開始流轉(zhuǎn)了。

          有這么一個場景,用戶參加一個活動可以領(lǐng)取獎品,但是活動需要進行很多的規(guī)則校驗然后才能放行,比如首先需要校驗用戶是否是新用戶、今日參與人數(shù)是否有限額、全場參與人數(shù)是否有限額等等。設(shè)定的規(guī)則都通過后,才能讓用戶領(lǐng)走獎品。

          如果產(chǎn)品給你這個需求的話,我想大部分人一開始肯定想的就是,用一個 List 來存放所有的規(guī)則,然后 foreach 執(zhí)行一下每個規(guī)則就好了。不過,讀者也先別急,看看責任鏈模式和我們說的這個有什么不一樣?

          首先,我們要定義流程上節(jié)點的基類:

          public abstract class RuleHandler {        protected RuleHandler successor;
          public abstract void apply(Context context);
          public void setSuccessor(RuleHandler successor) { this.successor = successor; }
          public RuleHandler getSuccessor() { return successor; }}

          接下來,我們需要定義具體的每個節(jié)點了。

          校驗用戶是否是新用戶:

          public class NewUserRuleHandler extends RuleHandler {    public void apply(Context context) {        if (context.isNewUser()) {                        if (this.getSuccessor() != null) {                this.getSuccessor().apply(context);            }        } else {            throw new RuntimeException("該活動僅限新用戶參與");        }    }}

          校驗用戶所在地區(qū)是否可以參與:

          public class LocationRuleHandler extends RuleHandler {    public void apply(Context context) {        boolean allowed = activityService.isSupportedLocation(context.getLocation);        if (allowed) {            if (this.getSuccessor() != null) {                this.getSuccessor().apply(context);            }        } else {            throw new RuntimeException("非常抱歉,您所在的地區(qū)無法參與本次活動");        }    }}

          校驗獎品是否已領(lǐng)完:

          public class LimitRuleHandler extends RuleHandler {    public void apply(Context context) {        int remainedTimes = activityService.queryRemainedTimes(context);         if (remainedTimes > 0) {            if (this.getSuccessor() != null) {                this.getSuccessor().apply(userInfo);            }        } else {            throw new RuntimeException("您來得太晚了,獎品被領(lǐng)完了");        }    }}

          客戶端:

          public static void main(String[] args) {    RuleHandler newUserHandler = new NewUserRuleHandler();    RuleHandler locationHandler = new LocationRuleHandler();    RuleHandler limitHandler = new LimitRuleHandler();
          locationHandler.setSuccessor(limitHandler);
          locationHandler.apply(context);}

          代碼其實很簡單,就是先定義好一個鏈表,然后在通過任意一節(jié)點后,如果此節(jié)點有后繼節(jié)點,那么傳遞下去。

          至于它和我們前面說的用一個 List 存放需要執(zhí)行的規(guī)則的做法有什么異同,留給讀者自己琢磨吧。

          模板方法模式

          在含有繼承結(jié)構(gòu)的代碼中,模板方法模式是非常常用的。

          通常會有一個抽象類:

          public abstract class AbstractTemplate {        public void templateMethod() {        init();        apply();         end();     }
          protected void init() { System.out.println("init 抽象層已經(jīng)實現(xiàn),子類也可以選擇覆寫"); }
          protected abstract void apply();
          protected void end() { }}

          模板方法中調(diào)用了 3 個方法,其中 apply() 是抽象方法,子類必須實現(xiàn)它,其實模板方法中有幾個抽象方法完全是自由的,我們也可以將三個方法都設(shè)置為抽象方法,讓子類來實現(xiàn)。也就是說,模板方法只負責定義第一步應該要做什么,第二步應該做什么,第三步應該做什么,至于怎么做,由子類來實現(xiàn)。

          我們寫一個實現(xiàn)類:

          public class ConcreteTemplate extends AbstractTemplate {    public void apply() {        System.out.println("子類實現(xiàn)抽象方法 apply");    }
          public void end() { System.out.println("我們可以把 method3 當做鉤子方法來使用,需要的時候覆寫就可以了"); }}

          客戶端調(diào)用演示:

          public static void main(String[] args) {    AbstractTemplate t = new ConcreteTemplate();        t.templateMethod();}

          代碼其實很簡單,基本上看到就懂了,關(guān)鍵是要學會用到自己的代碼中。

          狀態(tài)模式

          廢話我就不說了,我們說一個簡單的例子。商品庫存中心有個最基本的需求是減庫存和補庫存,我們看看怎么用狀態(tài)模式來寫。

          核心在于,我們的關(guān)注點不再是 Context 是該進行哪種操作,而是關(guān)注在這個 Context 會有哪些操作。

          定義狀態(tài)接口:

          public interface State {    public void doAction(Context context);}

          定義減庫存的狀態(tài):

          public class DeductState implements State {
          public void doAction(Context context) { System.out.println("商品賣出,準備減庫存"); context.setState(this);
          }
          public String toString() { return "Deduct State"; }}

          定義補庫存狀態(tài):

          public class RevertState implements State {
          public void doAction(Context context) { System.out.println("給此商品補庫存"); context.setState(this);
          }
          public String toString() { return "Revert State"; }}

          前面用到了 context.setState(this),我們來看看怎么定義 Context 類:

          public class Context {    private State state;      private String name;      public Context(String name) {        this.name = name;    }
          public void setState(State state) { this.state = state; } public void getState() { return this.state; }}

          我們來看下客戶端調(diào)用,大家就一清二楚了:

          public static void main(String[] args) {        Context context = new Context("iPhone X");          State revertState = new RevertState();      revertState.doAction(context);          State deductState = new DeductState();      deductState.doAction(context);
          }

          讀者可能會發(fā)現(xiàn),在上面這個例子中,如果我們不關(guān)心當前 context 處于什么狀態(tài),那么 Context 就可以不用維護 state 屬性了,那樣代碼會簡單很多。

          不過,商品庫存這個例子畢竟只是個例,我們還有很多實例是需要知道當前 context 處于什么狀態(tài)的。

          行為型模式總結(jié)

          行為型模式部分介紹了策略模式、觀察者模式、責任鏈模式、模板方法模式和狀態(tài)模式,其實,經(jīng)典的行為型模式還包括備忘錄模式、命令模式等,但是它們的使用場景比較有限,而且本文篇幅也挺大了,我就不進行介紹了。

          總結(jié)

          學習設(shè)計模式的目的是為了讓我們的代碼更加的優(yōu)雅、易維護、易擴展。這次整理這篇文章,讓我重新審視了一下各個設(shè)計模式,對我自己而言收獲還是挺大的。我想,文章的最大收益者一般都是作者本人,為了寫一篇文章,需要鞏固自己的知識,需要尋找各種資料,而且,自己寫過的才最容易記住,也算是我給讀者的建議吧。


          推薦閱讀

          1、搞定數(shù)據(jù)庫索引,不怕面試官問了!

          2、Spring Boot+Vue項目實戰(zhàn)

          3、一文搞懂前后端分離

          4、快速上手Spring Boot+Vue前后端分離


          楠哥簡介

          資深 Java 工程師,微信號 southwindss

          《Java零基礎(chǔ)實戰(zhàn)》一書作者

          騰訊課程官方 Java 面試官今日頭條認證大V

          GitChat認證作者,B站認證UP主(楠哥教你學Java)

          致力于幫助萬千 Java 學習者持續(xù)成長。




          有收獲,就在看 
          瀏覽 69
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  影音先锋男人资源站亚洲AV | 黄色一级视频免费观看 | 一级黄色免费 | 久久99精品波多野结衣一区 | 欧美亚洲日韩中文在线 |