Java設計模式學習1:創建型與結構型
設計模式介紹
首先要明白設計模式到底是什么,設計模式是對大家實際工作中寫的各種代碼進行高層次抽象的總結,其中最出名的當屬 Gang of Four (GoF) 的分類了,他們將設計模式分類為 23 種經典的模式,根據用途和具體情況我們又可以分為三大類,分別為創建型模式、結構型模式和行為型模式。
設計模式重要原則
這些原則將貫通全文:
面向接口編程,而不是面向實現。這個很重要,也是優雅的、可擴展的代碼的第一步,這就不需要多說了吧。
職責單一原則。每個類都應該只有一個單一的功能,并且該功能應該由這個類完全封裝起來。
對修改關閉,對擴展開放。對修改關閉是說,辛辛苦苦加班寫出來的代碼,該實現的功能和該修復的 bug 都完成了,別人可不能說改就改;對擴展開放就比較好理解了,也就是說在我們寫好的代碼基礎上,很容易實現擴展。
創建型模式
創建型模式的作用就是創建對象,說到創建一個對象,最熟悉的就是 new 一個對象,然后 set 相關屬性。但是,在很多場景下,我們需要給客戶端提供更加友好的創建對象的方式,尤其是那種我們定義了類,但是需要提供給其他開發者用的時候。
1.1簡單工廠模式
簡單地說,簡單工廠模式通常就是這樣,一個工廠類 XxxFactory,里面有一個靜態方法,根據我們不同的參數,返回不同的派生自同一個父類(或實現同一接口)的實例對象。我們職責單一原則,一個類只提供一種功能,FoodFactory 的功能就是只要負責生產各種 Food。
1.2 工廠模式
簡單工廠模式很簡單,如果它能滿足我們的需要,我覺得就不要折騰了。之所以需要引入工廠模式,是因為我們往往需要使用兩個或兩個以上的工廠。
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; } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
客戶端調用如下:
public class APP { public static void main(String[] args) { // 先選擇一個具體的工廠 FoodFactory factory = new ChineseFoodFactory(); // 由第一步的工廠產生具體的對象,不同的工廠造出不一樣的對象 Food food = factory.makeFood("A"); } }
1
2
3
4
5
6
7
8
其中,ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food。
雖然都是調用 makeFood(“A”) 制作 A 類食物,但是,不同的工廠生產出來的完全不一樣。
第一步,我們需要選取合適的工廠,然后第二步基本上和簡單工廠一樣。
核心在于,我們需要在第一步選好我們需要的工廠。比如,我們有 LogFactory 接口,實現類有 FileLogFactory 和 KafkaLogFactory,分別對應將日志寫入文件和寫入 Kafka 中,顯然,我們客戶端第一步就需要決定到底要實例化 FileLogFactory 還是 KafkaLogFactory,這將決定之后的所有的操作。
1.3抽象工廠模式
當涉及到產品族的時候,就需要引入抽象工廠模式了。
一個經典的例子是造一臺電腦。我們先不引入抽象工廠模式,看看怎么實現。
因為電腦是由許多的構件組成的,我們將 CPU 和主板進行抽象,然后 CPU 由 CPUFactory 生產,主板由 MainBoardFactory 生產,然后,我們再將 CPU 和主板搭配起來組合在一起,如下圖:
// 得到 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);
1
2
3
4
5
6
7
8
9
10
單獨看 CPU 工廠和主板工廠,它們分別是前面我們說的工廠模式。這種方式也容易擴展,因為要給電腦加硬盤的話,只需要加一個 HardDiskFactory 和相應的實現即可,不需要修改現有的工廠。
但是,這種方式有一個問題,那就是如果 Intel 家產的 CPU 和 AMD 產的主板不能兼容使用,那么這代碼就容易出錯,因為客戶端并不知道它們不兼容,也就會錯誤地出現隨意組合。
下面就是我們要說的產品族的概念,它代表了組成某個產品的一系列附件的集合,當涉及到這種產品族的問題的時候,就需要抽象工廠模式來支持了。我們不再定義 CPU 工廠、主板工廠、硬盤工廠、顯示屏工廠等等,我們直接定義電腦工廠,每個電腦工廠負責生產所有的設備,這樣能保證肯定不存在兼容問題。
這個時候,對于客戶端來說,不再需要單獨挑選 CPU 廠商、主板廠商、硬盤廠商等,直接選擇一家品牌工廠,品牌工廠會負責生產所有的東西,而且能保證肯定是兼容可用的。
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); }
1
2
3
4
5
6
7
8
9
10
11
12
13
當然,抽象工廠的問題也是顯而易見的,比如我們要加個顯示器,就需要修改所有的工廠,給所有的工廠都加上制造顯示器的方法。這有點違反了對修改關閉,對擴展開放這個設計原則。
1.4單例模式
單例模式是一種很基礎的設計模式,在面試時可能會被要求手寫不同類型的單例模式代碼,主要有三種模式:
餓漢模式:
//餓漢模式,很餓很著急,所以類加載時即創建實例對象 public class Singleton1 { private static Singleton1 singleton = new Singleton1(); private Singleton1(){ } public static Singleton1 getInstance(){ return singleton; } }
1
2
3
4
5
6
7
8
9
飽漢模式
public class SingleTon { private static SingleTon instance; private SingleTon() { } public static SingleTon getInstance() { if (instance == null) { instance = new SingleTon(); } return instance; } }
1
2
3
4
5
6
7
8
9
10
11
雙重鎖模式
//飽漢模式的雙重鎖模式,提高效率 public class Singleton3 { // 靜態實例變量加上volatile private static volatile SingleTon instance; // 私有化構造函數 private SingleTon() { } // 雙重檢查鎖 public static SingleTon getInstance() { if (instance == null) { synchronized(Singleton.class){ if(instance == null){ instance = new SingleTon(); } } } return instance; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
幾種模式的比較:
經典寫法:
懶漢和餓漢的最大區別就在于創建對象時候是否實例化;有new就是餓漢,反之就是懶漢,跟加不加synchronized ,沒有關系
1、餓漢模式是線程安全的,因為實例對象在類加載過程中就會被創建,在getInstance()方法中只是直接返回對象引用。之所以被稱為“餓漢”,是因為這種模式創建實例對象比較“急”,真的是餓壞了……
好處:簡單明了,無需關注線程安全問題。
缺點:如果在一個大環境下使用了過多的餓漢單例,則會生產出過多的實例對象,無論你是否要使用他們。
2、飽漢模式不是線程安全的,因為是在需要的時候才會產生實例對象,生產之前會判斷對象引用是否為空,這里,如果多個線程同時進入判斷,就會生成多個實例對象,這是不符合單例的思想的。所以飽漢模式為了保證線程安全,就用synchronized關鍵字標識了方法。之所以被稱為“飽漢”,因為它很飽,不急著生產實例,在需要的時候才會生產。
好處:延時加載,用的時候才會生產對象。
缺點:需要保證同步,付出效率的代價。
3、雙重鎖模式,是飽漢模式的優化,進行雙重判斷,當已經創建過實例對象后就無需加鎖,提高效率。也是一種推薦使用的方式。
放入一篇詳細的CSDN介紹:
https://blog.csdn.net/yin767833376/article/details/54379345
1.5建造者模式
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; } // 靜態方法,用于生成一個 Builder,這個不一定要有,不過寫這個方法是一個很好的習慣, // 有些代碼要求別人寫 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() { } // 鏈式調用設置各個屬性值,返回 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() 方法負責將 UserBuilder 中設置好的屬性“復制”到 User 中。 // 當然,可以在 “復制” 之前做點檢驗 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); } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
核心是:先把所有的屬性都設置給 Builder,然后 build() 方法的時候,將這些屬性復制給實際產生的對象。
看看客戶端的調用:
public class APP { public static void main(String[] args) { User d = User.builder() .name("f") .password("12345") .age(25) .build(); } }
1
2
3
4
5
6
7
8
9
建造者模式的鏈式寫法很吸引人,但是,多寫了很多“無用”的 builder 的代碼,感覺這個模式沒什么用。不過,當屬性很多,而且有些必填,有些選填的時候,這個模式會使代碼清晰很多。我們可以在 Builder 的構造方法中強制讓調用者提供必填字段,還有,在 build() 方法中校驗各個參數比在 User 的構造方法中校驗,代碼要優雅一些。
當然,如果你只是想要鏈式寫法,不想要建造者模式,有個很簡單的辦法,User 的 getter 方法不變,所有的 setter 方法都讓其 return this 就可以了,然后就可以像下面這樣調用:
User user = new User().setName("").setPassword("").setAge(20);
1
1.6原型模式
創建型模式的最后一個設計模式了。
原型模式很簡單:有一個原型實例,基于這個原型實例產生新的實例,也就是“克隆”了。
Object 類中有一個 clone() 方法,它用于生成一個新的對象,當然,如果我們要調用這個方法,java 要求我們的類必須先實現 Cloneable 接口,此接口沒有定義任何方法,但是不這么做的話,在 clone() 的時候,會拋出 CloneNotSupportedException 異常。
protected native Object clone() throws CloneNotSupportedException;
1
java 的克隆是淺克隆,碰到對象引用的時候,克隆出來的對象和原對象中的引用將指向同一個對象。通常實現深克隆的方法是將對象進行序列化,然后再進行反序列化。這里序列化和反序列化可以自己CSDN研究。
創建型模式的總結
創建型模式總體上比較簡單,它們的作用就是為了產生實例對象,算是各種工作的第一步了,因為我們寫的是面向對象的代碼,所以我們第一步當然是需要創建一個對象了。
簡單工廠模式最簡單;工廠模式在簡單工廠模式的基礎上增加了選擇工廠的維度,需要第一步選擇合適的工廠;抽象工廠模式有產品族的概念,如果各個產品是存在兼容性問題的,就要用抽象工廠模式。單例模式就不說了,為了保證全局使用的是同一對象,一方面是安全性考慮,一方面是為了節省資源;建造者模式專門對付屬性很多的那種類,為了讓代碼更優美;原型模式用得最少,了解和 Object 類中的 clone() 方法相關的知識即可。
結構型模式
結構型模式旨在通過改變代碼結構來達到解耦的目的,使得我們的代碼容易維護和擴展。
耦合:
解耦:假設生產者和消費者分別是兩個類。如果讓生產者直接調用消費者的某個方法,那么生產者對于消費者就會產生依賴(也就是耦合)。將來如果消費者的代碼發生變化,可能會影響到生產者。而如果兩者都依賴于某個緩沖區,兩者之間不直接依賴,耦合也就相應降低了。生產者直接調用消費者的某個方法,還有另一個弊端。由于函數調用是同步的(或者叫阻塞的),在消費者的方法沒有返回之前,生產者只好一直等在那邊。萬一消費者處理數據很慢,生產者就會白白糟蹋大好時光。緩沖區還有另一個好處。如果制造數據的速度時快時慢,緩沖區的好處就體現出來了。當數據制造快的時候,消費者來不及處理,未處理的數據可以暫時存在緩沖區中。等生產者的制造速度慢下來,消費者再慢慢處理掉。
這里放一篇對于高級術語的連接文章:耦合、解耦
2.1. 代理模式
代理模式:代理模式顧名思義,就是在根本目標之前加上擴展,加上擴展之后的產物就是代理對象了,整個模式就是代理模式了。
一個代理來隱藏具體實現類的實現細節,通常還用于在真實的實現的前后添加一部分邏輯。
既然說是代理,那就要對客戶端隱藏真實實現,由代理來負責客戶端的所有請求。當然,代理只是個代理,它不會完成實際的業務邏輯,而是一層皮而已,但是對于客戶端來說,它必須表現得就是客戶端需要的真實實現。
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; } } // 代理要表現得“就像是”真實實現類,所以需要實現 FoodService 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; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
客戶端調用,注意,我們要用代理來實例化接口:
FoodService foodService = new FoodServiceProxy(); foodService.makeChicken();
1
2
代理模式說白了就是做 “方法包裝” 或做 “方法增強”。在面向切面編程中,其實就是動態代理的過程。比如 Spring 中,我們自己不定義代理類,但是 Spring 會幫我們動態來定義代理,然后把我們定義在 @Before、@After、@Around 中的代碼邏輯動態添加到代理中。
說到動態代理,又可以展開說,Spring 中實現動態代理有兩種,一種是如果我們的類定義了接口,如 UserService 接口和 UserServiceImpl 實現,那么采用 JDK 的動態代理,感興趣的讀者可以去看看 java.lang.reflect.Proxy 類的源碼;另一種是我們自己沒有定義接口的,Spring 會采用 CGLIB 進行動態代理,它是一個 jar 包,性能還不錯。
2.2適配器模式
適配器模式做的就是,有一個接口需要實現,但是我們現成的對象都不滿足,需要加一層適配器來進行適配。
適配器模式總體來說分三種:默認適配器模式、對象適配器模式、類適配器模式。先不急著分清楚這幾個,先看看例子再說。
2.2.1. 默認適配器模式
用 Appache commons-io 包中的 FileAlterationListener 做例子,此接口定義了很多的方法,用于對文件或文件夾進行監控,一旦發生了對應的操作,就會觸發相應的方法。
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); }
1
2
3
4
5
6
7
8
9
10
此接口的一大問題是抽象方法太多了,如果我們要用這個接口,意味著我們要實現每一個抽象方法,如果我們只是想要監控文件夾中的文件創建和文件刪除事件,可是我們還是不得不實現所有的方法,很明顯,這不是我們想要的。
所以,我們需要下面的一個適配器,它用于實現上面的接口,但是所有的方法都是空方法,這樣,我們就可以轉而定義自己的類來繼承下面這個類即可。
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) { } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
然后可以定義以下類,我們僅僅需要實現我們想實現的方法就可以了:
public class FileMonitor extends FileAlterationListenerAdaptor { public void onFileCreate(final File file) { // 文件創建 doSomething(); } public void onFileDelete(final File file) { // 文件刪除 doSomething(); } }
1
2
3
4
5
6
7
8
9
10
11
2.2.2 對象適配器模式
來看一個《Head First 設計模式》中的一個例子,我稍微修改了一下,看看怎么將雞適配成鴨,這樣雞也能當鴨來用。因為,現在鴨這個接口,我們沒有合適的實現類可以用,所以需要適配器。
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("雞也會飛哦"); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
鴨接口有 fly() 和 quare() 兩個方法,雞 Cock 如果要冒充鴨,fly() 方法是現成的,但是雞不會鴨的呱呱叫,沒有 quack() 方法。這個時候就需要適配了:
// 毫無疑問,首先,這個適配器肯定需要 implements Duck,這樣才能當做鴨來用 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(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
客戶端調用很簡單了:
public static void main(String[] args) { // 有一只野雞 Cock wildCock = new WildCock(); // 成功將野雞適配成鴨 Duck duck = new CockAdapter(wildCock); ... }
1
2
3
4
5
6
7
到這里,大家也就知道了適配器模式是怎么回事了。無非是我們需要一只鴨,但是我們只有一只雞,這個時候就需要定義一個適配器,由這個適配器來充當鴨,但是適配器里面的方法還是由雞來實現的。
2.2.3 類適配器模式
看到這個圖,大家應該很容易理解的吧,通過繼承的方法,適配器自動獲得了所需要的大部分方法。這個時候,客戶端使用更加簡單,直接 Target t = new SomeAdapter(); 就可以了。
適配器模式總結
類適配和對象適配的異同
一個采用繼承,一個采用組合;
類適配屬于靜態實現,對象適配屬于組合的動態實現,對象適配需要多實例化一個對象。
總體來說,對象適配用得比較多。
橋梁模式
橋梁模式雖然不是一個使用頻率很高的模式,但是熟悉這個模式對于理解面向對象的設計原則,包括“開-閉”原則以及組合/聚合復用原則都很有幫助。理解好這兩個原則,有助于形成正確的設計思想和培養良好的設計風格。
橋梁模式的用意是“將抽象化(Abstraction)與實現化(Implementation)脫耦,使得二者可以獨立地變化”。這句話很短,但是第一次讀到這句話的人很可能都會思考良久而不解其意。
這句話有三個關鍵詞,也就是抽象化、實現化和脫耦。理解這三個詞所代表的概念是理解橋梁模式用意的關鍵。
橋梁模式問題引入
例子1:
現需要提供大中小3種型號的畫筆,能夠繪制5種不同顏色,如果使用蠟筆,我們需要準備3*5=15支蠟筆,也就是說必須準備15個具體的蠟筆類。而如果使用毛筆的話,只需要3種型號的毛筆,外加5個顏料盒,用3+5=8個類就可以實現15支蠟筆的功能。實際上,蠟筆和毛筆的關鍵一個區別就在于筆和顏色是否能夠分離。
例子2:
設想如果要繪制矩形、圓形、橢圓、正方形,我們至少需要4個形狀類,但是如果繪制的圖形需要具有不同的顏色,如紅色、綠色、藍色等,此時至少有如下兩種設計方案:
第一種設計方案是為每一種形狀都提供一套各種顏色的版本。
這種設計方案就是類似于實例一中的蠟筆,顏色和形狀緊密結合起來,必須為每一種形狀準備各種顏色的版本,加入我們現在要求加入一種顏色藍色,那么每一種形狀都需要修改,所以這種設計方案的缺點是顯而易見的,一是不符合“開-閉”原則,二是需要的類非常多,編碼重復性較高。
第二種設計方案是根據實際需要對形狀和顏色進行組合。
上下兩部分圖是分別從不同的角度描述這種方案,上邊的圖說的是如何利用形狀和顏色進行組合,下邊的圖說的是各個類的繼承和組合關系。第二種方案需要為所有的圖形聲明一個共同的父類,為所有的顏色聲明一個父類,兩個父類各有自己的具體實現,我們需要的產品就是有兩種具體的產品進行組合得到的。這樣在加入新的顏色或者形狀的時候不用修改其他的類,而且大大的減少了代碼量。而這第二種方案就是我們今天要討論的橋梁模式。
三、什么是橋梁模式
上邊的蠟筆和圖形的例子他們都有一個共同的特點就是他們都有兩個變化因素,蠟筆是粗細和形狀,圖形是形狀和顏色,不管是毛筆還是圖形的第二種解決方案他們比較好的原因都是將這兩種變化因素分開了,使得兩種因素可以獨立的變化。蠟筆的顏色和蠟筆本身是分不開的,所以就造成必須使用15支色彩、大小各異的蠟筆來繪制圖畫。而毛筆與顏料能夠很好的分開,各自獨立變化,便簡化了操作。
詳細文章請見:
https://blog.csdn.net/xingjiarong/article/details/50132727?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163185494316780262521538%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=163185494316780262521538&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-50132727.first_rank_v2_pc_rank_v29&utm_term=%E6%A1%A5%E6%A2%81%E6%A8%A1%E5%BC%8F&spm=1018.2226.3001.4187
直接上代碼。
我們首先需要一個橋梁,它是一個接口,定義提供的接口方法。
public interface DrawAPI { public void draw(int radius, int x, int y); }
1
2
3
然后是一系列實現類:
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); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
定義一個抽象類,此類的實現類都需要使用 DrawAPI:
public abstract class Shape { protected DrawAPI drawAPI; protected Shape(DrawAPI drawAPI) { this.drawAPI = drawAPI; } public abstract void draw(); }
1
2
3
4
5
6
7
定義抽象類的子類
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); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
來看客戶端演示:
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(); }
1
2
3
4
5
6
2.4 裝飾模式
從名字來簡單解釋下裝飾器。既然說是裝飾,那么往往就是添加小功能這種,而且,我們要滿足可以添加多個小功能。最簡單的,代理模式就可以實現功能的增強,但是代理不容易實現多個功能的增強,當然你可以說用代理包裝代理的多層包裝方式,但是那樣的話代碼就復雜了。
從圖中可以看到,接口 Component 其實已經有了 ConcreteComponentA 和 ConcreteComponentB 兩個實現類了,但是,如果我們要增強這兩個實現類的話,我們就可以采用裝飾模式,用具體的裝飾器來裝飾實現類,以達到增強的目的。
最近大街上流行起來了“快樂檸檬”,我們把快樂檸檬的飲料分為三類:紅茶、綠茶、咖啡,在這三大類的基礎上,又增加了許多的口味,什么金桔檸檬紅茶、金桔檸檬珍珠綠茶、芒果紅茶、芒果綠茶、芒果珍珠紅茶、烤珍珠紅茶、烤珍珠芒果綠茶、椰香胚芽咖啡、焦糖可可咖啡等等,每家店都有很長的菜單,但是仔細看下,其實原料也沒幾樣,但是可以搭配出很多組合,如果顧客需要,很多沒出現在菜單中的飲料他們也是可以做的。
在這個例子中,紅茶、綠茶、咖啡是最基礎的飲料,其他的像金桔檸檬、芒果、珍珠、椰果、焦糖等都屬于裝飾用的。當然,在開發中,我們確實可以像門店一樣,開發這些類:LemonBlackTea、LemonGreenTea、MangoBlackTea、MangoLemonGreenTea…但是,很快我們就發現,這樣子干肯定是不行的,這會導致我們需要組合出所有的可能,而且如果客人需要在紅茶中加雙份檸檬怎么辦?三份檸檬怎么辦?
首先,定義飲料抽象基類:
public abstract class Beverage { // 返回描述 public abstract String getDescription(); // 返回價格 public abstract double cost(); }
1
2
3
4
5
6
然后是三個基礎飲料實現類,紅茶、綠茶和咖啡:
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; } } ...// 咖啡省略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
定義調料,也就是裝飾者的基類,此類必須繼承自 Beverage:
public abstract class Condiment extends Beverage { }
1
2
3
然后我們來定義檸檬、芒果等具體的調料,它們屬于裝飾者,毫無疑問,這些調料肯定都需要繼承調料 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; // 加檸檬需要 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; // 加芒果需要 3 元 } } ...// 給每一種調料都加一個類
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
客戶端調用:
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()); //"綠茶, 加檸檬, 加芒果 價格:¥16" 如果我們需要 芒果-珍珠-雙份檸檬-紅茶: Beverage beverage = new Mongo(new Pearl(new Lemon(new Lemon(new BlackTea())))); }
1
2
3
4
5
6
7
8
9
10
11
12
13
2.5 門面模式
門面模式(也叫外觀模式,Facade Pattern)在許多源碼中有使用,比如 slf4j 就可以理解為是門面模式的應用。這是一個簡單的設計模式,我們直接上代碼再說吧。
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(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
客戶端怎么調用:
public static void main(String[] args) { ShapeMaker shapeMaker = new ShapeMaker(); // 客戶端調用現在更加清晰了 shapeMaker.drawCircle(); shapeMaker.drawRectangle(); shapeMaker.drawSquare(); }
1
2
3
4
5
6
7
8
2.6 組合模式
組合模式用于表示具有層次結構的數據,使得我們對單個對象和組合對象的訪問具有一致性。
直接看一個例子吧,每個員工都有姓名、部門、薪水這些屬性,同時還有下屬員工集合(雖然可能集合為空),而下屬員工和自己的結構是一樣的,也有姓名、部門這些屬性,同時也有他們的下屬員工集合。
public class Employee { private String name; private String dept; private int salary; private List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
結構型模式總結
代理模式是做方法增強的,適配器模式是把雞包裝成鴨這種用來適配接口的,橋梁模式做到了很好的解耦,裝飾模式從名字上就看得出來,適合于裝飾類或者說是增強類的場景,門面模式的優點是客戶端不需要關心實例化過程,只要調用需要的方法即可,組合模式用于描述具有層次結構的數據,享元模式是為了在特定的場景中緩存已經創建的對象,用于提高性能。
Java 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。