設(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è)計模式的六大原則
- 開閉原則(Open Close Principle)
開閉原則的意思是:對擴(kuò)展開放,對修改關(guān)閉。在程序需要進(jìn)行拓展的時候,不能去修改原有的代碼,實現(xiàn)一個熱插拔的效果。簡言之,是為了使程序的擴(kuò)展性好,易于維護(hù)和升級。想要達(dá)到這樣的效果,我們需要使用接口和抽象類,后面的具體設(shè)計中我們會提到這點。 - 里氏代換原則(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ī)范。 - 依賴倒轉(zhuǎn)原則(Dependence Inversion Principle)
這個原則是開閉原則的基礎(chǔ),具體內(nèi)容:針對接口編程,依賴于抽象而不依賴于具體。 - 接口隔離原則(Interface Segregation Principle)
這個原則的意思是:使用多個隔離的接口,比使用單個接口要好。它還有另外一個意思是:降低類之間的耦合度。由此可見,其實設(shè)計模式就是從大型軟件架構(gòu)出發(fā)、便于升級和維護(hù)的軟件設(shè)計思想,它強(qiáng)調(diào)降低依賴,降低耦合。 - 迪米特法則,又稱最少知道原則(Demeter Principle)
最少知道原則是指:一個實體應(yīng)當(dāng)盡量少地與其他實體之間發(fā)生相互作用,使得系統(tǒng)功能模塊相對獨立。 - 合成復(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,這就不多說了,性能太差。
嵌套類最經(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();
}
可能大家看上面一步步還不是特別清晰,我把所有的東西整合到一張圖上:
