Java基礎(chǔ)(九)

設(shè)計模式

設(shè)計模式(Design pattern)代表了最佳的實踐,通常被有經(jīng)驗的面向?qū)ο蟮能浖_發(fā)人員所采用。設(shè)計模式是軟件開發(fā)人員在軟件開發(fā)過程中面臨的一般問題的解決方案。這些解決方案是眾多軟件開發(fā)人員經(jīng)過相當(dāng)長的一段時間的試驗和錯誤總結(jié)出來的。

設(shè)計模式是一套被反復(fù)使用的、多數(shù)人知曉的、經(jīng)過分類編目的、代碼設(shè)計經(jīng)驗的總結(jié)。使用設(shè)計模式是為了重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設(shè)計模式于己于他人于系統(tǒng)都是多贏的,設(shè)計模式使代碼編制真正工程化,設(shè)計模式是軟件工程的基石,如同大廈的一塊塊磚石一樣。項目中合理地運(yùn)用設(shè)計模式可以完美地解決很多問題,每種模式在現(xiàn)實中都有相應(yīng)的原理來與之對應(yīng),每種模式都描述了一個在我們周圍不斷重復(fù)發(fā)生的問題,以及該問題的核心解決方案,這也是設(shè)計模式能被廣泛應(yīng)用的原因。

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

什么是 GOF(四人幫,全拼 Gang of Four)?

在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名為 Design Patterns - Elements of Reusable Object-Oriented Software(中文譯名:設(shè)計模式 - 可復(fù)用的面向?qū)ο筌浖兀?/code> 的書,該書首次提到了軟件開發(fā)中設(shè)計模式的概念。
四位作者合稱 GOF(四人幫,全拼 Gang of Four)。他們所提出的設(shè)計模式主要是基于以下的面向?qū)ο笤O(shè)計原則。

  • 對接口編程而不是對實現(xiàn)編程。
  • 優(yōu)先使用對象組合而不是繼承。

設(shè)計模式的使用

設(shè)計模式在軟件開發(fā)中的兩個主要用途。

開發(fā)人員的共同平臺

設(shè)計模式提供了一個標(biāo)準(zhǔn)的術(shù)語系統(tǒng),且具體到特定的情景。例如,單例設(shè)計模式意味著使用單個對象,這樣所有熟悉單例設(shè)計模式的開發(fā)人員都能使用單個對象,并且可以通過這種方式告訴對方,程序使用的是單例模式。

最佳的實踐

設(shè)計模式已經(jīng)經(jīng)歷了很長一段時間的發(fā)展,它們提供了軟件開發(fā)過程中面臨的一般問題的最佳解決方案。學(xué)習(xí)這些模式有助于經(jīng)驗不足的開發(fā)人員通過一種簡單快捷的方式來學(xué)習(xí)軟件設(shè)計。

設(shè)計模式的類型

根據(jù)設(shè)計模式的參考書 Design Patterns - Elements of Reusable Object-Oriented Software(中文譯名:設(shè)計模式 - 可復(fù)用的面向?qū)ο筌浖兀?中所提到的,總共有 23 種設(shè)計模式。這些模式可以分為三大類:創(chuàng)建型模式(Creational Patterns)、結(jié)構(gòu)型模式(Structural Patterns)、行為型模式(Behavioral Patterns)。當(dāng)然,我們還會討論另一類設(shè)計模式:J2EE 設(shè)計模式。

下面用一個圖片來整體描述一下設(shè)計模式之間的關(guān)系:

設(shè)計模式的六大原則

  1. 開閉原則(Open Close Principle)
    開閉原則的意思是:對擴(kuò)展開放,對修改關(guān)閉。在程序需要進(jìn)行拓展的時候,不能去修改原有的代碼,實現(xiàn)一個熱插拔的效果。簡言之,是為了使程序的擴(kuò)展性好,易于維護(hù)和升級。想要達(dá)到這樣的效果,我們需要使用接口和抽象類,后面的具體設(shè)計中我們會提到這點。
  2. 里氏代換原則(Liskov Substitution Principle)
    里氏代換原則是面向?qū)ο笤O(shè)計的基本原則之一。 里氏代換原則中說,任何基類可以出現(xiàn)的地方,子類一定可以出現(xiàn)。LSP 是繼承復(fù)用的基石,只有當(dāng)派生類可以替換掉基類,且軟件單位的功能不受到影響時,基類才能真正被復(fù)用,而派生類也能夠在基類的基礎(chǔ)上增加新的行為。里氏代換原則是對開閉原則的補(bǔ)充。實現(xiàn)開閉原則的關(guān)鍵步驟就是抽象化,而基類與子類的繼承關(guān)系就是抽象化的具體實現(xiàn),所以里氏代換原則是對實現(xiàn)抽象化的具體步驟的規(guī)范。
  3. 依賴倒轉(zhuǎn)原則(Dependence Inversion Principle)
    這個原則是開閉原則的基礎(chǔ),具體內(nèi)容:針對接口編程,依賴于抽象而不依賴于具體。
  4. 接口隔離原則(Interface Segregation Principle)
    這個原則的意思是:使用多個隔離的接口,比使用單個接口要好。它還有另外一個意思是:降低類之間的耦合度。由此可見,其實設(shè)計模式就是從大型軟件架構(gòu)出發(fā)、便于升級和維護(hù)的軟件設(shè)計思想,它強(qiáng)調(diào)降低依賴,降低耦合。
  5. 迪米特法則,又稱最少知道原則(Demeter Principle)
    最少知道原則是指:一個實體應(yīng)當(dāng)盡量少地與其他實體之間發(fā)生相互作用,使得系統(tǒng)功能模塊相對獨立。
  6. 合成復(fù)用原則(Composite Reuse Principle)
    合成復(fù)用原則是指:盡量使用合成/聚合的方式,而不是使用繼承。
  • 創(chuàng)建型模式-->對象怎么來
  • 結(jié)構(gòu)型模式-->對象和誰有關(guān)
  • 行為型模式-->對象與對象在干嘛
  • J2EE 模式-->對象合起來要干嘛(表現(xiàn)層,文中表示層個人感覺用的不準(zhǔn)確)java是面向?qū)ο蟮恼Z言,所以要搞好對象,模式(套路)就是用來更加好的搞對象滴。

創(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)同一接口)的實例對象。

我們強(qiáng)調(diào)職責(zé)單一原則,一個類只提供一種功能,F(xiàn)oodFactory 的功能就是只要負(fù)責(zé)生產(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();
        // 由第一步的工廠產(chǎn)生具體的對象,不同的工廠造出不一樣的對象
        Food food = factory.makeFood("A");
    }
}

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

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

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

雖然簡單,不過我也把所有的構(gòu)件都畫到一張圖上

抽象工廠模式

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

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

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

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

// 得到 Intel 的 CPU
CPUFactory cpuFactory = new IntelCPUFactory();
CPU cpu = intelCPUFactory.makeCPU();

// 得到 AMD 的主板
MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
MainBoard mainBoard = mainBoardFactory.make();

// 組裝 CPU 和主板
Computer computer = new Computer(cpu, mainBoard);

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

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

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

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

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

public static void main(String[] args) {
    // 第一步就要選定一個“大廠”
    ComputerFactory cf = new AmdFactory();
    // 從這個大廠造 CPU
    CPU cpu = cf.makeCPU();
    // 從這個大廠造主板
    MainBoard board = cf.makeMainBoard();
      // 從這個大廠造硬盤
      HardDisk hardDisk = cf.makeHardDisk();

    // 將同一個廠子出來的 CPU、主板、硬盤組裝在一起
    Computer result = new Computer(cpu, board, hardDisk);
}

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

單例模式

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

餓漢模式最簡單:

public class Singleton {
    // 首先,將 new Singleton() 堵死
    private Singleton() {};
    // 創(chuàng)建私有靜態(tài)實例,意味著這個類第一次使用的時候就會進(jìn)行創(chuàng)建
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }
    // 瞎寫一個靜態(tài)方法。這里想說的是,如果我們只是要調(diào)用 Singleton.getDate(...),
    // 本來是不想要生成 Singleton 實例的,不過沒辦法,已經(jīng)生成了
    public static Date getDate(String mode) {return new Date();}
}

構(gòu)造方法私有后子類想繼承必須給父類添加一個非私有的構(gòu)造方法,同時子類也必須使用相同參數(shù)的構(gòu)造方法,不然會報錯

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

飽漢模式最容易出錯:

public class Singleton {
    // 首先,也是先堵死 new Singleton() 這條路
    private Singleton() {}
    // 和餓漢模式相比,這邊不需要先實例化出來,注意這里的 volatile,它是必須的
    private static volatile Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            // 加鎖
            synchronized (Singleton.class) {
                // 這一次判斷也是必須的,不然會有并發(fā)問題
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

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

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

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

volatile 相關(guān)整理

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

public class Singleton3 {

    private Singleton3() {}
    // 主要是使用了 嵌套類可以訪問外部類的靜態(tài)屬性和靜態(tài)方法 的特性
    private static class Holder {
        private static Singleton3 instance = new Singleton3();
    }
    public static Singleton3 getInstance() {
        return Holder.instance;
    }
}

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

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

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

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

經(jīng)驗之談:一般情況下,不建議使用餓漢方式,建議使用飽漢方式。只有在要明確實現(xiàn) lazy loading 效果時,才會使用嵌套類方式。如果涉及到反序列化創(chuàng)建對象時,可以嘗試使用枚舉方式。如果有其他特殊的需求,可以考慮使用雙檢鎖方式。

建造者模式

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

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

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

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

class User {
    // 下面是“一堆”的屬性
    private String name;
    private String password;
    private String nickName;
    private int age;

    // 構(gòu)造方法私有化,不然客戶端就會直接調(diào)用構(gòu)造方法了
    private User(String name, String password, String nickName, int age) {
        this.name = name;
        this.password = password;
        this.nickName = nickName;
        this.age = age;
    }
    // 靜態(tài)方法,用于生成一個 Builder,這個不一定要有,不過寫這個方法是一個很好的習(xí)慣,
    // 有些代碼要求別人寫 new User.UserBuilder().a()...build() 看上去就沒那么好
    public static UserBuilder builder() {
        return new UserBuilder();
    }

    public static class UserBuilder {
        // 下面是和 User 一模一樣的一堆屬性
        private String  name;
        private String password;
        private String nickName;
        private int age;

        private UserBuilder() {
        }

        // 鏈?zhǔn)秸{(diào)用設(shè)置各個屬性值,返回 this,即 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;
        }

        // build() 方法負(fù)責(zé)將 UserBuilder 中設(shè)置好的屬性“復(fù)制”到 User 中。
        // 當(dāng)然,可以在 “復(fù)制” 之前做點檢驗
        public User build() {
            if (name == null || password == null) {
                throw new RuntimeException("用戶名和密碼必填");
            }
            if (age <= 0 || age >= 150) {
                throw new RuntimeException("年齡不合法");
            }
            // 還可以做賦予”默認(rèn)值“的功能
              if (nickName == null) {
                nickName = name;
            }
            return new User(name, password, nickName, age);
        }
    }
}

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

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

public class APP {
    public static void main(String[] args) {
        User d = User.builder()
                .name("foo")
                .password("pAss12345")
                .age(25)
                .build();
    }
}

說實話,建造者模式的鏈?zhǔn)?/code>寫法很吸引人,但是,多寫了很多“無用”的 builder 的代碼,感覺這個模式?jīng)]什么用。不過,當(dāng)屬性很多,而且有些必填,有些選填的時候,這個模式會使代碼清晰很多。我們可以在Builder 的構(gòu)造方法中強(qiáng)制讓調(diào)用者提供必填字段,還有,在 build() 方法中校驗各個參數(shù)比在 User 的構(gòu)造方法中校驗,代碼要優(yōu)雅一些。

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

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

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

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

User user = new User().setName("").setPassword("").setAge(20);
很多人是這么用的,但是筆者覺得其實這種寫法非常地不優(yōu)雅,不是很推薦使用。

Lombok是一個Java庫,能自動插入編輯器并構(gòu)建工具,簡化Java開發(fā)。通過添加注解的方式,不需要為類編寫getter或eques方法,同時可以自動化日志變量。

原型模式

這是我要說的創(chuàng)建型模式的最后一個設(shè)計模式了。

原型模式很簡單:有一個原型實例,基于這個原型實例產(chǎn)生新的實例,也就是“克隆”了。

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

protected native Object clone() throws CloneNotSupportedException;

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

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

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

創(chuàng)建型模式總體上比較簡單,它們的作用就是為了產(chǎn)生實例對象,算是各種工作的第一步了,因為我們寫的是面向?qū)ο蟮拇a,所以我們第一步當(dāng)然是需要創(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)來達(dá)到解耦的目的,使得我們的代碼容易維護(hù)和擴(kuò)展。

代理模式

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

既然說是代理,那就要對客戶端隱藏真實實現(xiàn),由代理來負(fù)責(zé)客戶端的所有請求。當(dāng)然,代理只是個代理,它不會完成實際的業(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;
    }
}

// 代理要表現(xiàn)得“就像是”真實實現(xiàn)類,所以需要實現(xiàn) FoodService
public class FoodServiceProxy implements FoodService {

    // 內(nèi)部一定要有一個真實的實現(xiàn)類,當(dāng)然也可以通過構(gòu)造方法注入
    private FoodService foodService = new FoodServiceImpl();

    public Food makeChicken() {
        System.out.println("我們馬上要開始制作雞肉了");

        // 如果我們定義這句為核心代碼的話,那么,核心代碼是真實實現(xiàn)類做的,
        // 代理只是在核心代碼前后做些“無足輕重”的事情
        Food food = foodService.makeChicken();

        System.out.println("雞肉制作完成啦,加點胡椒粉"); // 增強(qiáng)
          food.addCondiment("pepper");

        return food;
    }
    public Food makeNoodle() {
        System.out.println("準(zhǔn)備制作拉面~");
        Food food = foodService.makeNoodle();
        System.out.println("制作完成啦")
        return food;
    }
}

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

// 這里用代理類來實例化
FoodService foodService = new FoodServiceProxy();
foodService.makeChicken();

我們發(fā)現(xiàn)沒有,代理模式說白了就是做 “方法包裝” 或做 “方法增強(qiáng)”。在面向切面編程中,其實就是動態(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 進(jìn)行動態(tài)代理,它是一個 jar 包,性能還不錯。

適配器模式

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

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

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

默認(rèn)適配器模式

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

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

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) {
        // 文件創(chuàng)建
        doSomething();
    }

    public void onFileDelete(final File file) {
        // 文件刪除
        doSomething();
    }
}

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

對象適配器模式

來看一個《Head First 設(shè)計模式》中的一個例子,我稍微修改了一下,看看怎么將雞適配成鴨,這樣雞也能當(dāng)鴨來用。因為,現(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() 方法。這個時候就需要適配了:

// 毫無疑問,首先,這個適配器肯定需要 implements Duck,這樣才能當(dāng)做鴨來用
public class CockAdapter implements Duck {

    Cock cock;
    // 構(gòu)造方法中需要一個雞的實例,此類就是將這只雞適配成鴨來用
      public CockAdapter(Cock cock) {
        this.cock = cock;
    }

    // 實現(xiàn)鴨的呱呱叫方法
    @Override
      public void quack() {
        // 內(nèi)部其實是一只雞的咕咕叫
        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);
      ...
}

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

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

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

類適配器模式

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

適配器模式總結(jié)

  • 類適配和對象適配的異同

一個采用繼承,一個采用組合;

類適配屬于靜態(tài)實現(xiàn),對象適配屬于組合的動態(tài)實現(xiàn),對象適配需要多實例化一個對象。

總體來說,對象適配用得比較多。

適配器模式和代理模式的異同

比較這兩種模式,其實是比較對象適配器模式和代理模式,在代碼結(jié)構(gòu)上,它們很相似,都需要一個具體的實現(xiàn)類的實例。但是它們的目的不一樣,代理模式做的是增強(qiáng)原方法的活;適配器做的是適配的活,為的是提供“把雞包裝成鴨,然后當(dāng)做鴨來使用”,而雞和鴨它們之間原本沒有繼承關(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("用藍(lán)色筆畫圖,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();
}

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容