設計模式
1.UML圖
統一建模語言是用來設計軟件的可視化建模語言。定義了用例圖、類圖、對象圖、狀態圖、活動圖、時序圖、協作圖、構件圖、部署圖等 9 種圖。
1.1類圖
1.1.1類的表示方式
在UML類圖中,類使用包含類名、屬性(field) 和方法(method) 且帶有分割線的矩形來表示,比如下圖表示一個Employee類,它包含name,age和address這3個屬性,以及work()方法
屬性/方法名稱前加的加號和減號表示了這個屬性/方法的可見性,UML類圖中表示可見性的符號有三種:
-
+:表示public
-
-:表示private
-
#:表示protected
屬性的完整表示方式是: 可見性 名稱 :類型 [ = 缺省值]
方法的完整表示方式是: 可見性 名稱(參數列表) [ : 返回類型]
1.1.2類與類之間關系的表示方式
關聯關系:
對象之間的引用;表示一類對象與另一類對象之間的聯系,比如老師和學生。然后也分為一般關聯關系、聚合關系和組合關系。
一般關聯關系分為單向關聯,雙向關聯,自關聯。
1,單向關聯
單向關聯用一個帶箭頭的實線表示。如圖Customer類有Address的成員變量類,使得每個顧客都有一個地址。
2,雙向關聯
雙向關聯就是雙方各自持有對方類型的成員變量。雙向關聯用一個不帶箭頭的直線表示,顧客可以有多個產品,每個產品又可以表示自己被那個顧客買走。
3,自關聯
自關聯在UML類圖中用一個帶有箭頭且指向自身的線表示,也就是“自己包含自己”。
聚合關系
聚合關系是關聯關系的一種,是強關聯關系,是整體和部分之間的關系。
通過成員對象來實現的,其中成員對象是整體對象的一部分,但是成員對象可以脫離整體對象而獨立存在。例如,學校與老師的關系,學校包含老師,但如果學校停辦了,老師依然存在。
聚合關系可以用帶空心菱形的實線來表示,菱形指向整體。
組合關系
組合表示類之間的整體與部分的關系,但它是一種更強烈的聚合關系。
一旦整體對象不存在,部分對象也將不存在,部分對象不能脫離整體對象而存在。例如,頭和嘴的關系,沒有了頭,嘴也就不存在了。
組合關系用帶實心菱形的實線來表示,菱形指向整體。
依賴關系
它是對象之間耦合度最弱的一種關聯方式,是臨時性的關聯。
類的方法里通過局部變量,方法參數或者對靜態方法的調用來訪問另一個類(被依賴類)中的某些方法來完成一些職責。
依賴關系使用帶箭頭的虛線來表示,箭頭從使用類指向被依賴的類。例如司機駕駛汽車,依賴汽車才能駕駛:
繼承關系
繼承關系是對象之間耦合度最大的一種關系,表示一般與特殊的關系,是父類與子類之間的關系,是一種繼承關系。
泛化關系用帶空心三角箭頭的實線來表示,箭頭從子類指向父類。代碼使用面對對象的繼承機制實現即可,Student 類和 Teacher 類都是 Person 類的子類。
實現關系
接口與實現類之間的關系。類實現了接口,類中的操作實現了接口中所聲明的所有的抽象操作。
實現關系使用帶空心三角箭頭的虛線來表示,箭頭從實現類指向接口。汽車和船實現了交通工具:
2 . 軟件設計原則
提高軟件系統的可維護性和可復用性,增加軟件的可擴展性和靈活性
2.1 開閉原則
對擴展開放,對修改關閉。即在程序需要進行拓展的時候,不能去修改原有的代碼。使程序的擴展性好,易于維護和升級。
多使用接口和抽象類。抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟件架構的穩定。而軟件中易變的細節可以從抽象派生來的實現類來進行擴展,當軟件需要發生變化時,只需要根據需求重新派生一個實現類來擴展就可以了。
2.2 里氏代換原則
里氏代換原則:任何基類可以出現的地方,子類一定可以出現。簡單理解就是:子類繼承父類時,除添加新的方法完成新增功能外,盡量不要重寫父類的方法。
2.3 依賴倒轉原則
高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。簡單的說就是要求對抽象進行編程,不要對實現進行編程,這樣就降低了客戶與實現模塊間的耦合。
例如:讓用戶程序依賴于抽象,實現的細節也依賴于抽象。即使實現細節不斷變動,只要抽象不變,客戶程序就不需要變化。
3.4 接口隔離原則
客戶端不應該被迫依賴于它不使用的方法;一個類對另一個類的依賴應該建立在最小的接口上。
這種時候可以將接口,按照功能細分;然后依賴具體的功能。
3.5 迪米特法則
迪米特法則又叫最少知識原則。
其含義是:如果兩個軟件實體無須直接通信,那么就不應當發生直接的相互調用,可以通過第三方轉發該調用。其目的是降低類之間的耦合度,提高模塊的相對獨立性。
第三方:當前對象本身、當前對象的成員對象、當前對象所創建的對象、當前對象的方法參數等,這些對象同當前對象存在關聯、聚合或組合關系,可以直接訪問這些對象的方法。
3.6 合成復用原則
合成復用原則是指:盡量先使用組合或者聚合等關聯關系來實現,其次才考慮使用繼承關系來實現。
通常類的復用分為繼承復用和合成復用兩種。
繼承復用雖然有簡單和易實現的優點,但它也存在以下缺點:
-
繼承復用破壞了類的封裝性。因為繼承會將父類的實現細節暴露給子類,父類對子類是透明的,所以這種復用又稱為“白箱”復用。
-
子類與父類的耦合度高。父類的實現的任何改變都會導致子類的實現發生變化,這不利于類的擴展與維護。
-
它限制了復用的靈活性。從父類繼承而來的實現是靜態的,在編譯時已經定義,所以在運行時不可能發生變化。
采用組合或聚合復用時,可以將已有對象納入新對象中,使之成為新對象的一部分,新對象可以調用已有對象的功能,它有以下優點:
-
它維持了類的封裝性。因為成分對象的內部細節是新對象看不見的,所以這種復用又稱為“黑箱”復用。
-
對象間的耦合度低。可以在類的成員位置聲明抽象。
-
復用的靈活性高。這種復用可以在運行時動態進行,新對象可以動態地引用與成分對象類型相同的對象。
3 . 設計模式分類
-
創建型模式
用于描述“怎樣創建對象”,它的主要特點是“將對象的創建與使用分離”。有單例、原型、工廠方法、抽象工廠、建造者等 5 種創建型模式。
-
結構型模式
用于描述如何將類或對象按某種布局組成更大的結構。有代理、適配器、橋接、裝飾、外觀、享元、組合等 7 種結構型模式。
-
行為型模式
用于描述類或對象之間怎樣相互協作共同完成單個對象無法單獨完成的任務,以及怎樣分配職責。有模板方法、策略、命令、職責鏈、狀態、觀察者、中介者、迭代器、訪問者、備忘錄、解釋器等 11 種行為型模式。
4 . 創建者模式
創建型模式的主要關注點是“怎樣創建對象?”,它的主要特點是“將對象的創建與使用分離”。
這樣可以降低系統的耦合度,使用者不需要關注對象的創建細節。
創建型模式分為:
-
單例模式
-
工廠方法模式
-
抽象工程模式
-
原型模式
-
建造者模式
這里主要講常用的單例模式喝工廠方法模式
4.1 單例設計模式
它提供了一種創建對象的最佳方式。保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。
這樣做主要是利用全局訪問點,在任何位置都可以訪問到相同的實例,方便數據共享。確保一致性,避免競態條件(多線程環境下,可以避免由于多個線程同時創建對象而導致的競態條件。)
使用場景:
-
當一個類只應該有一個實例,且客戶端應該能夠從全局訪問該實例時,可以考慮使用單例模式。
-
當需要控制資源的分配,限制實例的數量時,例如數據庫連接池。
-
當希望避免頻繁創建和銷毀對象以提高性能時。
這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
實現:
1.?使用枚舉(Enum)實現單例模式 (枚舉方式屬于惡漢式方式)
public enum Singleton {INSTANCE;// 在枚舉中添加您需要的方法和屬性public void doSomething() {System.out.println("Singleton instance is doing something.");}
}
?2.? 懶漢式(靜態內部類方式):
靜態內部類單例模式中實例由內部類創建,由于 JVM 在加載外部類的過程中, 是不會加載靜態內部類的, 只有內部類的屬性/方法被調用時才會被加載, 并初始化其靜態屬性。靜態屬性由于被 static
修飾,保證只被實例化一次,并且嚴格保證實例化順序。
/*** 靜態內部類方式*/
public class Singleton {//私有構造方法private Singleton() {}private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}//對外提供靜態方法獲取該對象public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}
第一次加載Singleton類時不會去初始化INSTANCE,只有第一次調用getInstance,虛擬機加載SingletonHolder,并初始化INSTANCE,這樣不僅能確保線程安全,也能保證 Singleton 類的唯一性。
單例模式有:餓漢式:類加載就會導致該單實例對象被創建
懶漢式:類加載不會導致該單實例對象被創建,而是首次使用該對象時才會創建
餓漢式和懶漢式也有很多種方式,但是都存在著一些問題。詳情可以看單例模式單獨的介紹。
3. 懶漢式-方式3(雙重檢查鎖):
再來討論一下懶漢模式中加鎖的問題,對于 getInstance()
方法來說,絕大部分的操作都是讀操作,讀操作是線程安全的,所以我們沒必讓每個線程必須持有鎖才能調用該方法,我們需要調整加鎖的時機。由此也產生了一種新的實現模式:雙重檢查鎖模式
/*** 雙重檢查方式*/
public class Singleton { //私有構造方法private Singleton() {}private static Singleton instance;//對外提供靜態方法獲取該對象public static Singleton getInstance() {//第一次判斷,如果instance不為null,不進入搶鎖階段,直接返回實例if(instance == null) {synchronized (Singleton.class) {//搶到鎖之后再次判斷是否為nullif(instance == null) {instance = new Singleton();}}}return instance;}
}
雙重檢查鎖模式是一種非常好的單例實現模式,解決了單例、性能、線程安全問題,上面的雙重檢測鎖模式看上去完美無缺,其實是存在問題,在多線程的情況下,可能會出現空指針問題,出現問題的原因是JVM在實例化對象的時候會進行優化和指令重排序操作。
要解決雙重檢查鎖模式帶來空指針異常的問題,只需要使用 volatile
關鍵字, volatile
關鍵字可以保證可見性和有序性。
/*** 雙重檢查方式*/
public class Singleton {//私有構造方法private Singleton() {}private static volatile Singleton instance;//對外提供靜態方法獲取該對象public static Singleton getInstance() {//第一次判斷,如果instance不為null,不進入搶鎖階段,直接返回實際if(instance == null) {synchronized (Singleton.class) {//搶到鎖之后再次判斷是否為空if(instance == null) {instance = new Singleton();}}}return instance;}
}
小結:
添加 volatile
關鍵字之后的雙重檢查鎖模式是一種比較好的單例實現模式,能夠保證在多線程的情況下線程安全也不會有性能問題。
存在的問題:
使用序列化和反射可以破壞除枚舉外的單例模式方法。
問題的解決:
序列化、反序列方式破壞單例模式的解決方法:
在Singleton類中添加readResolve()
方法,在反序列化時被反射調用,如果定義了這個方法,就返回這個方法的值,如果沒有定義,則返回新new出來的對象。
public class Singleton implements Serializable {//私有構造方法private Singleton() {}private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}//對外提供靜態方法獲取該對象public static Singleton getInstance() {return SingletonHolder.INSTANCE;}/*** 下面是為了解決序列化反序列化破解單例模式*/private Object readResolve() {return SingletonHolder.INSTANCE;}
}
-
源碼解析:
ObjectInputStream類
public final Object readObject() throws IOException, ClassNotFoundException{...// if nested read, passHandle contains handle of enclosing objectint outerHandle = passHandle;try {Object obj = readObject0(false);//重點查看readObject0方法..... }private Object readObject0(boolean unshared) throws IOException {...try {switch (tc) {...case TC_OBJECT:return checkResolve(readOrdinaryObject(unshared));//重點查看readOrdinaryObject方法...}} finally {depth--;bin.setBlockDataMode(oldMode);} }private Object readOrdinaryObject(boolean unshared) throws IOException {...//isInstantiable 返回true,執行 desc.newInstance(),通過反射創建新的單例類,obj = desc.isInstantiable() ? desc.newInstance() : null; ...// 在Singleton類中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法執行結果為trueif (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {// 通過反射調用 Singleton 類中的 readResolve 方法,將返回值賦值給rep變量// 這樣多次調用ObjectInputStream類中的readObject方法,繼而就會調用我們定義的readResolve方法,所以返回的是同一個對象。Object rep = desc.invokeReadResolve(obj);...}return obj; }
-
反射方式破解單例的解決方法:
public class Singleton {//私有構造方法private Singleton() {/*反射破解單例模式需要添加的代碼*/if(instance != null) {throw new RuntimeException();}}private static volatile Singleton instance;//對外提供靜態方法獲取該對象public static Singleton getInstance() {if(instance != null) {return instance;}synchronized (Singleton.class) {if(instance != null) {return instance;}instance = new Singleton();return instance;}}
}
說明:
這種方式比較好理解。當通過反射方式調用構造方法進行創建創建時,直接拋異常。不運行此中操作。
經驗之談:一般情況下,因為懶漢式的其他方式存在但容易產生垃圾對象,線程不安全的問題。明確實現 lazy loading 效果時,可以使用靜態內部類的方式。如果涉及到反序列化創建對象時,可以嘗試使用枚舉方式。如果有其他特殊的需求,可以考慮使用雙檢鎖方式。
4.2 工廠模式
其主要目的是封裝對象的創建過程,使客戶端代碼和具體的對象實現解耦。這樣子就不用每次都new對象,更換對象的話,所有new對象的地方也要修改,違背了開閉原則(對擴展開放,對修改關閉)。使用工廠來生產對象,更換對象也直接在工廠更換即可。
工廠模式的主要好處包括:
-
解耦合:工廠模式將對象的創建過程與客戶端代碼分離,客戶端不需要知道具體的對象是如何創建的,只需要通過工廠方法獲取對象即可,從而降低了代碼之間的耦合度。
-
靈活性:由于工廠負責創建對象,客戶端可以通過工廠方法獲取不同的對象實例,而無需關心具體的實現細節,從而提高了系統的靈活性。
-
可擴展性:如果需要添加新的產品類型,只需在工廠中添加相應的產品創建邏輯,而不需要修改客戶端代碼,這樣可以很方便地擴展系統的功能。
-
統一管理:工廠模式將對象的創建集中在一個地方,便于統一管理和維護,提高了代碼的可維護性。
使用場景:
-
當一個系統需要創建多個類型的對象,并且這些對象之間存在著共同的接口時,可以考慮使用工廠模式。
-
當客戶端不需要知道具體的對象是如何創建的,只需要獲取對象實例時,可以使用工廠模式。
-
當系統需要動態地決定創建哪種類型的對象時,可以使用工廠模式。
工廠模式包含以下幾個核心角色:
-
抽象產品(Abstract Product):定義了產品的共同接口或抽象類。它可以是具體產品類的父類或接口,規定了產品對象的共同方法。
-
具體產品(Concrete Product):實現了抽象產品接口,定義了具體產品的特定行為和屬性。
-
抽象工廠(Abstract Factory):聲明了創建產品的抽象方法,可以是接口或抽象類。它可以有多個方法用于創建不同類型的產品。
-
具體工廠(Concrete Factory):實現了抽象工廠接口,負責實際創建具體產品的對象。
這里介紹三種工廠:
-
簡單工廠模式(不屬于GOF的23種經典設計模式)
-
工廠方法模式
-
抽象工廠模式
4.2.1?簡單工廠模式
簡單工廠不是一種設計模式,反而比較像是一種編程習慣。
結構:
簡單工廠包含如下角色:
-
抽象產品 :定義了產品的規范,描述了產品的主要特性和功能。
-
具體產品 :實現或者繼承抽象產品的子類
-
具體工廠 :提供了創建產品的方法,調用者通過該方法來獲取產品。
使用場景:
-
當對象的創建邏輯相對簡單,并且不需要頻繁地進行變更時,可以考慮使用簡單工廠模式。
-
在客戶端只知道所需產品的名稱或類型,而不需要關心產品的創建過程時,可以使用簡單工廠模式。
實現思路:
// 抽象產品接口
interface Product {void show();
}// 具體產品類A
class ConcreteProductA implements Product {@Overridepublic void show() {System.out.println("This is product A.");}
}// 具體產品類B
class ConcreteProductB implements Product {@Overridepublic void show() {System.out.println("This is product B.");}
}// 簡單工廠類
class SimpleFactory {public static Product createProduct(String type) {if ("A".equals(type)) {return new ConcreteProductA();} else if ("B".equals(type)) {return new ConcreteProductB();}return null;}
}// 客戶端
public class Client {public static void main(String[] args) {Product productA = SimpleFactory.createProduct("A");productA.show();Product productB = SimpleFactory.createProduct("B");productB.show();}
}
上面的工廠類創建對象的功能定義為靜態的,這個屬于是靜態工廠模式,當然你也可以不設置為靜態的。
優缺點:
優點:
-
簡單工廠模式中,客戶端通過工廠類的靜態方法來獲取產品實例,而不需要直接實例化具體產品類。如果要實現新產品直接修改工廠類,而不需要在原代碼中修改。
缺點:
-
工廠類負責創建所有產品,因此如果系統需要添加新的產品類型,需要修改工廠類,違反了開放封閉原則。
4.2.2?工廠方法模式
使用工廠方法模式可以完美的解決簡單工廠模式的缺點,完全遵循開閉原則。
概念
定義一個用于創建對象的接口,讓子類決定實例化哪個產品類對象。工廠方法使一個產品類的實例化延遲到其工廠的子類。
結構
工廠方法模式的主要角色:
-
抽象工廠(Abstract Factory):提供了創建產品的接口,調用者通過它訪問具體工廠的工廠方法來創建產品。
-
具體工廠(ConcreteFactory):主要是實現抽象工廠中的抽象方法,完成具體產品的創建。
-
抽象產品(Product):定義了產品的規范,描述了產品的主要特性和功能。
-
具體產品(ConcreteProduct):實現了抽象產品角色所定義的接口,由具體工廠來創建,它同具體工廠之間一一對應。
圖例
使用工廠方法模式對上例進行改進:
工廠方法模式適用于需要創建一系列相關對象的情況
// 抽象產品接口
interface Product {void show();
}// 具體產品類A
class ConcreteProductA implements Product {@Overridepublic void show() {System.out.println("This is product A.");}
}// 具體產品類B
class ConcreteProductB implements Product {@Overridepublic void show() {System.out.println("This is product B.");}
}// 抽象工廠類
interface Factory {Product createProduct();
}// 具體工廠類A,負責創建產品A
class ConcreteFactoryA implements Factory {@Overridepublic Product createProduct() {return new ConcreteProductA();}
}// 具體工廠類B,負責創建產品B
class ConcreteFactoryB implements Factory {@Overridepublic Product createProduct() {return new ConcreteProductB();}
}// 客戶端
public class Client {public static void main(String[] args) {Factory factoryA = new ConcreteFactoryA();Product productA = factoryA.createProduct();productA.show();Factory factoryB = new ConcreteFactoryB();Product productB = factoryB.createProduct();productB.show();}
}
于是乎要增加產品類時只要相應地增加工廠類,不需要修改工廠類的代碼了,這樣就解決了簡單工廠模式的缺點。
使用場景:
-
當需要創建的對象是一個具體的產品,但是不確定具體產品的類型時,可以使用工廠方法模式。
-
在工廠類中定義一個創建產品的抽象方法,由子類負責實現具體產品的創建過程,從而實現了產品的創建和客戶端的解耦。
優缺點:
優點:
-
工廠方法模式中,客戶端通過調用工廠類的方法來創建產品,具體產品的創建邏輯由子類實現,不同的產品由不同的工廠子類負責創建。
-
工廠方法模式符合開放封閉原則,因為客戶端可以通過新增工廠子類來添加新的產品類型,而無需修改原有的代碼。
缺點:
-
每增加一個產品就要增加一個具體產品類和一個對應的具體工廠類,這增加了系統的復雜度。
4.2.3?抽象工廠模式
抽象工廠模式通常涉及一族相關的產品,每個具體工廠類負責創建該族中的具體產品。
使用場景:
-
當一個系統需要創建一系列相互關聯或相互依賴的產品對象時,可以考慮使用抽象工廠模式。
-
抽象工廠模式提供了一個創建一組相關或相互依賴對象的接口,客戶端可以通過該接口來創建產品族中的不同產品,而不需要關心具體的產品實現。
所以由此也可看出,普通工廠模式,工廠方法模式都只是單一產品類的工廠;而很多時候我們需要綜合性的,需要生產多等級產品的工廠。下圖所示橫軸是產品等級,也就是同一類產品;縱軸是產品族,也就是同一品牌的產品,同一品牌的產品產自同一個工廠:
結構
抽象工廠模式的主要角色如下:
-
抽象工廠(Abstract Factory):提供了創建產品的接口,它包含多個創建產品的方法,可以創建多個不同等級的產品。
-
具體工廠(Concrete Factory):主要是實現抽象工廠中的多個抽象方法,完成具體產品的創建。
-
抽象產品(Product):定義了產品的規范,描述了產品的主要特性和功能,抽象工廠模式有多個抽象產品。
-
具體產品(ConcreteProduct):實現了抽象產品角色所定義的接口,由具體工廠來創建,它同具體工廠之間是多對一的關系。
代碼案例:
// 抽象產品接口
interface Product {void show();
}// 具體產品類A
class ConcreteProductA implements Product {@Overridepublic void show() {System.out.println("This is product A.");}
}// 具體產品類B
class ConcreteProductB implements Product {@Overridepublic void show() {System.out.println("This is product B.");}
}// 抽象工廠接口
interface AbstractFactory {Product createProductA();Product createProductB();
}// 具體工廠類,負責創建產品A和產品B
class ConcreteFactory implements AbstractFactory {@Overridepublic Product createProductA() {return new ConcreteProductA();}@Overridepublic Product createProductB() {return new ConcreteProductB();}
}// 客戶端
public class Client {public static void main(String[] args) {AbstractFactory factory = new ConcreteFactory();Product productA = factory.createProductA();productA.show();Product productB = factory.createProductB();productB.show();}
}
-
使用場景:
-
當需要創建的對象是一系列相互關聯或相互依賴的產品族時,如電器工廠中的電視機、洗衣機、空調等。
-
系統中有多個產品族,但每次只使用其中的某一族產品。如有人只喜歡穿某一個品牌的衣服和鞋。
-
系統中提供了產品的類庫,且所有產品的接口相同,客戶端不依賴產品實例的創建細節和內部結構。
-
優缺點
優點:
當一個產品族中的多個對象被設計成一起工作時,它能保證客戶端始終只使用同一個產品族中的對象。
缺點:
當產品族中需要增加一個新的產品時,所有的工廠類都需要進行修改。
模式擴展 (利用反射機制來創建對象)
簡單工廠+配置文件解除耦合
可以通過工廠模式+配置文件的方式解除工廠對象和產品對象的耦合。
通過使用配置文件,將創建對象的參數存儲在外部配置文件中,可以在不修改客戶端代碼的情況下,通過修改配置文件來改變對象的創建方式。這樣就可以實現對創建邏輯的解耦合,客戶端不需要知道具體的創建方式,只需要從工廠類獲取對象即可。
具體實現步驟如下:
-
在配置文件中配置需要創建的對象的類名或者類型。
-
在簡單工廠類中讀取配置文件,并根據配置的信息來創建對應的對象。
假設有一個配置文件 config.properties
,內容如下:
product.type=ConcreteProductA
創建簡單工廠類 SimpleFactory.java
,用于讀取配置文件并根據配置創建對象:
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;public class SimpleFactory {public static Product createProduct() {Properties properties = new Properties();try (InputStream inputStream = SimpleFactory.class.getResourceAsStream("config.properties")) {properties.load(inputStream);String productType = properties.getProperty("product.type");if ("ConcreteProductA".equals(productType)) {return new ConcreteProductA();} else if ("ConcreteProductB".equals(productType)) {return new ConcreteProductB();}} catch (IOException e) {e.printStackTrace();}return null;}
}
5. 結構型模式
結構型模式描述如何將類或對象按某種布局組成更大的結構。它分為類結構型模式和對象結構型模式,前者采用繼承機制來組織接口和類,后者釆用組合或聚合來組合對象。
由于組合關系或聚合關系比繼承關系耦合度低,滿足“合成復用原則”,所以對象結構型模式比類結構型模式具有更大的靈活性。
結構型模式分為以下 7 種:
-
代理模式
-
適配器模式
-
裝飾者模式
-
橋接模式
-
外觀模式
-
組合模式
-
享元模式
日常業務主要用到適配器模式和裝飾者模式,其他模式都是在特定情況下使用。
5.1 適配器模式
適配器模式是一種結構型設計模式,其主要作用是解決兩個不兼容接口之間的兼容性問題。適配器模式通過引入一個適配器來將一個類的接口轉換成客戶端所期望的另一個接口,從而讓原本由于接口不匹配而無法協同工作的類能夠協同工作。
結構
適配器模式(Adapter)包含以下主要角色:
-
目標(Target)接口:當前系統業務所期待的接口,它可以是抽象類或接口。
-
適配者(Adaptee)類:它是被訪問和適配的現存組件庫中的組件接口。
-
適配器(Adapter)類:它是一個轉換器,通過繼承或引用適配者的對象,把適配者接口轉換成目標接口,讓客戶按目標接口的格式訪問適配者。
圖例:
AudioPlayer實現了 MediaPlayer 接口,只可以播放 mp3 。實現了 AdvancedMediaPlayer 接口的類則可以播放 vlc 和 mp4 格式的文件。可以創建一個實現了 MediaPlayer 接口的適配器類 MediaAdapter,并使用 AdvancedMediaPlayer 的實現類對象來播放所需的格式。AdapterPatternDemo 類則可以使用 AudioPlayer 類來播放各種格式的音頻。
對象適配器模式代碼案例:
// 目標接口
interface MediaPlayer {void play(String audioType, String filename);
}// 適配器接口
interface AdvancedMediaPlayer {void playVlc(String filename);void playMp4(String filename);
}// 適配器類
class MediaAdapter implements MediaPlayer {private AdvancedMediaPlayer advancedMediaPlayer;public MediaAdapter(String audioType) {if (audioType.equalsIgnoreCase("vlc")) {advancedMediaPlayer = new VlcPlayer();} else if (audioType.equalsIgnoreCase("mp4")) {advancedMediaPlayer = new Mp4Player();}}@Overridepublic void play(String audioType, String filename) {if (audioType.equalsIgnoreCase("vlc")) {advancedMediaPlayer.playVlc(filename);} else if (audioType.equalsIgnoreCase("mp4")) {advancedMediaPlayer.playMp4(filename);}}
}// 具體實現類
class AudioPlayer implements MediaPlayer {MediaAdapter mediaAdapter;@Overridepublic void play(String audioType, String filename) {if (audioType.equalsIgnoreCase("mp3")) {System.out.println("Playing mp3 file. Name: " + filename);} else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {mediaAdapter = new MediaAdapter(audioType);mediaAdapter.play(audioType, filename);} else {System.out.println("Invalid media. " + audioType + " format not supported");}}
}class VlcPlayer implements AdvancedMediaPlayer {@Overridepublic void playVlc(String filename) {System.out.println("Playing vlc file. Name: " + filename);}@Overridepublic void playMp4(String filename) {// Do nothing}
}class Mp4Player implements AdvancedMediaPlayer {@Overridepublic void playVlc(String filename) {// Do nothing}@Overridepublic void playMp4(String filename) {System.out.println("Playing mp4 file. Name: " + filename);}
}// 使用示例
public class Main {public static void main(String[] args) {AudioPlayer audioPlayer = new AudioPlayer();audioPlayer.play("mp3", "song.mp3");audioPlayer.play("vlc", "movie.vlc");audioPlayer.play("mp4", "video.mp4");}
}
適配器模式有類適配器模式和對象適配器模式;這里使用對象適配器模式主要是類適配器模式違背了合成復用原則,它限制了適配器類只能適配一個具體的被適配者類。且Java 不支持多重繼承,因此在 Java 中一般使用接口來實現類似的功能
比如下面類適配器,采用的是繼承:
// 適配器類(類適配器)
class MediaAdapter extends Mp4Player implements MediaPlayer {@Overridepublic void play(String audioType, String filename) {if (audioType.equalsIgnoreCase("vlc")) {playVlc(filename);} else if (audioType.equalsIgnoreCase("mp4")) {playMp4(filename);}}
}
當然,也有接口適配器模式,不過使用相對較少。當一個接口擁有許多方法,但實現類只需要實現其中一部分方法時,可以使用接口適配器模式,提供一個抽象適配器類實現該接口,并提供默認實現,從而避免實現類需要實現大量空方法。
使用場景:
-
當需要使用一個已經存在的類,但是它的接口不符合當前需求時,可以考慮使用適配器模式。
-
當需要復用一些已經存在的類,但是接口與其他類不兼容時,可以考慮使用適配器模式。
-
當需要創建一個可復用的類,該類可以與不相關或不可預見的類協同工作時,可以考慮使用適配器模式。
5.2 裝飾器模式
它允許你在不改變對象結構的情況下,動態地將新功能附加到對象上。
結構:
-
抽象組件(Component):定義了原始對象和裝飾器對象的公共接口或抽象類,可以是具體組件類的父類或接口。
-
具體組件(Concrete Component):是被裝飾的原始對象,它定義了需要添加新功能的對象。
-
抽象裝飾器(Decorator):繼承自抽象組件,它包含了一個抽象組件對象,并定義了與抽象組件相同的接口,同時可以通過組合方式持有其他裝飾器對象。
-
具體裝飾器(Concrete Decorator):實現了抽象裝飾器的接口,負責向抽象組件添加新的功能。具體裝飾器通常會在調用原始對象的方法之前或之后執行自己的操作。
圖例:
平常假如要加一個配料,都需要修改餐品的源代碼,但是隨著配料的增多, 類會變得越來越龐大,等下類爆炸了。
public class Main {public static void main(String[] args) {FriedNoodles friedNoodles = new FriedNoodles();friedNoodles.addBacon();friedNoodles.addEgg();friedNoodles.addFish();System.out.println("Cost: $" + friedNoodles.getCost());}
}
案例:
假設有一個簡單的咖啡店系統,其中有一個 Coffee
接口表示咖啡,它有一個方法 getCost()
來獲取咖啡的價格。現在我們要給咖啡添加一些額外的配料,比如牛奶、摩卡和奶泡。
// 咖啡接口
interface Coffee {double getCost();
}// 具體咖啡類
class SimpleCoffee implements Coffee {@Overridepublic double getCost() {return 1.0;}
}// 裝飾者抽象類
abstract class CoffeeDecorator implements Coffee {protected final Coffee decoratedCoffee;public CoffeeDecorator(Coffee decoratedCoffee) {this.decoratedCoffee = decoratedCoffee;}public double getCost() {return decoratedCoffee.getCost();}
}// 具體裝飾者類
class Milk extends CoffeeDecorator {public Milk(Coffee decoratedCoffee) {super(decoratedCoffee);}@Overridepublic double getCost() {return super.getCost() + 0.5;}
}class Mocha extends CoffeeDecorator {public Mocha(Coffee decoratedCoffee) {super(decoratedCoffee);}@Overridepublic double getCost() {return super.getCost() + 1.0;}
}class Foam extends CoffeeDecorator {public Foam(Coffee decoratedCoffee) {super(decoratedCoffee);}@Overridepublic double getCost() {return super.getCost() + 0.3;}
}// 使用示例
public class Main {public static void main(String[] args) {Coffee coffee = new SimpleCoffee();coffee = new Milk(coffee);coffee = new Mocha(coffee);coffee = new Foam(coffee);System.out.println("Cost: $" + coffee.getCost());}
}
通過組合不同的裝飾者,可以在不改變原有咖啡對象的情況下,動態地添加額外的功能和費用。
使用場景:
-
動態地給對象添加功能:當需要給對象動態地添加一些額外的功能,而且這些功能可以獨立于該對象的創建。
-
當不能采用繼承的方式對系統進行擴充或者采用繼承不利于系統擴展和維護時。
不能采用繼承的情況主要有兩類:
-
第一類是系統中存在大量獨立的擴展,為支持每一種組合將產生大量的子類,使得子類數目呈爆炸性增長;
-
第二類是因為類定義不能繼承(如final類)
-
6. 行為型模式
行為型模式用于描述程序在運行時復雜的流程控制,即描述多個類或對象之間怎樣相互協作共同完成單個對象都無法單獨完成的任務,它涉及算法與對象間職責的分配。
行為型模式分為類行為模式和對象行為模式,前者采用繼承機制來在類間分派行為,后者采用組合或聚合在對象間分配行為。由于組合關系或聚合關系比繼承關系耦合度低,滿足“合成復用原則”,所以對象行為模式比類行為模式具有更大的靈活性。
行為型模式分為:
-
模板方法模式
-
策略模式
-
命令模式
-
職責鏈模式
-
狀態模式
-
觀察者模式
-
中介者模式
-
迭代器模式
-
訪問者模式
-
備忘錄模式
-
解釋器模式
常用的模式主要是策略模式(常用),觀察者模式,模板方法模式。
6.1 模板方法模式
模板方法模式是一種行為型設計模式,它定義了一個算法的骨架,將一些步驟延遲到子類中實現。這種模式允許子類在不改變算法結構的情況下重新定義算法的某些步驟。
結構
-
抽象類(Abstract Class):負責給出一個算法的輪廓和骨架。它由一個模板方法和若干個基本方法構成。其中包含了一些基本操作的步驟,有些步驟由具體子類實現。
-
模板方法:定義了算法的骨架,按某種順序調用其包含的基本方法。
-
基本方法:是實現算法各個步驟的方法,是模板方法的組成部分。基本方法又可以分為三種:
-
抽象方法(Abstract Method) :一個抽象方法由抽象類聲明、由其具體子類實現。
-
具體方法(Concrete Method) :一個具體方法由一個抽象類或具體類聲明并實現,其子類可以進行覆蓋也可以直接繼承。
-
鉤子方法(Hook Method) :在抽象類中已經實現,包括用于判斷的邏輯方法和需要子類重寫的空方法兩種。
一般鉤子方法是用于判斷的邏輯方法,這類方法名一般為isXxx,返回值類型為boolean類型。
-
-
-
具體子類(Concrete Class):實現抽象類中所定義的抽象方法和鉤子方法,它們是一個頂級邏輯的組成步驟。
案例:
你制作一個飲料,步驟是確定的,像燒水; 釀造;倒入杯中,添加調味品。燒水和倒杯是固定的基本操作,釀造和添加調味料這個則是通過具體的情況來定的。
代碼實現:
// 抽象類
abstract class Beverage {// 模板方法,定義了算法的骨架public final void prepareBeverage() {boilWater();brew();pourInCup();addCondiments();}// 抽象方法,由子類實現abstract void brew();abstract void addCondiments();// 公共方法,由父類實現void boilWater() {System.out.println("Boiling water");}void pourInCup() {System.out.println("Pouring into cup");}
}// 具體類1
class Coffee extends Beverage {@Overridevoid brew() {System.out.println("Dripping coffee through filter");}@Overridevoid addCondiments() {System.out.println("Adding sugar and milk");}
}// 具體類2
class Tea extends Beverage {@Overridevoid brew() {System.out.println("Steeping the tea");}@Overridevoid addCondiments() {System.out.println("Adding lemon");}
}// 使用示例
public class Main {public static void main(String[] args) {Beverage coffee = new Coffee();coffee.prepareBeverage();System.out.println();Beverage tea = new Tea();tea.prepareBeverage();}
}
注意:為防止惡意操作,一般模板方法都加上 final 關鍵詞。
使用場景:
-
當有一系列算法步驟,其中有一部分是固定的,但是另一部分需要在子類中具體實現時,可以考慮使用模板方法模式。
-
當需要在不同的子類中重用相同的算法框架時,可以使用模板方法模式。
以下是模板方法模式在開發后臺管理系統中的使用場景示例:
-
權限管理: 在后臺管理系統中,通常需要對不同用戶或用戶組的權限進行管理。模板方法模式可以定義一個權限管理的骨架,包括權限驗證、權限分配等操作,而具體的權限驗證和分配操作可以交由子類實現。
-
數據的增刪改查: 后臺管理系統通常需要對數據進行增加、刪除、修改、查詢等操作。可以使用模板方法模式定義一個數據操作的骨架,包括數據的驗證、數據的持久化等步驟,而具體的數據操作可以由子類實現。
-
數據的導入導出: 后臺管理系統可能需要支持數據的導入導出功能,例如從 Excel 文件中導入數據到數據庫,或者將數據庫中的數據導出為 Excel 文件。可以使用模板方法模式定義一個數據導入導出的骨架,包括數據格式的驗證、數據的轉換等步驟,而具體的導入導出操作可以由子類實現。
-
日志記錄: 后臺管理系統通常需要記錄用戶的操作日志,例如登錄日志、操作日志等。可以使用模板方法模式定義一個日志記錄的骨架,包括日志的格式化、日志的存儲等步驟,而具體的日志記錄操作可以由子類實現。
優缺點:
優點:
-
提高代碼復用性
將相同部分的代碼放在抽象的父類中,而將不同的代碼放入不同的子類中。
-
實現了反向控制
通過一個父類調用其子類的操作,通過對子類的具體實現擴展不同的行為,實現了反向控制 ,并符合“開閉原則”。
缺點:
-
對每個不同的實現都需要定義一個子類,這會導致類的個數增加,系統更加龐大,設計也更加抽象。
-
父類中的抽象方法由子類實現,子類執行的結果會影響父類的結果,這導致一種反向的控制結構,它提高了代碼閱讀的難度。
6.2 策略模式
策略模式是一種行為型設計模式,它定義了一系列算法,將每個算法封裝到具有共同接口的獨立類中,并且使它們可以相互替換。策略模式可以讓算法的變化獨立于使用算法的客戶端。
主要解決:在有多種算法相似的情況下,使用 if...else 所帶來的復雜和難以維護。
結構
策略模式的主要角色如下:
-
抽象策略(Strategy)類:這是一個抽象角色,通常由一個接口或抽象類實現。所有具體策略類都實現了該接口。
-
具體策略(Concrete Strategy)類:實現了抽象策略定義的接口,提供具體的算法實現或行為。
-
環境/上下文(Context)類:持有一個策略類的引用,負責將客戶端的請求委派給具體的策略對象。
圖例:
在 Java 中使用策略模式的寫法:
-
定義策略接口:創建一個接口,用于定義所有具體策略類的公共行為。
-
創建具體策略類:實現策略接口,并提供具體的算法實現。
-
創建上下文類:維護一個對策略接口的引用,并提供方法來設置和切換不同的具體策略類。
-
客戶端使用:在客戶端代碼中,創建上下文對象,并設置具體的策略類,然后調用上下文對象的方法來執行具體的算法。
// 1. 定義策略接口
interface PaymentStrategy {void pay(double amount);
}// 2. 創建具體策略類
class AliPayStrategy implements PaymentStrategy {@Overridepublic void pay(double amount) {System.out.println("Paid " + amount + " via AliPay.");}
}class WeChatPayStrategy implements PaymentStrategy {@Overridepublic void pay(double amount) {System.out.println("Paid " + amount + " via WeChatPay.");}
}// 3. 創建上下文類
class PaymentContext {private PaymentStrategy paymentStrategy;public void setPaymentStrategy(PaymentStrategy paymentStrategy) {this.paymentStrategy = paymentStrategy;}public void makePayment(double amount) {paymentStrategy.pay(amount);}
}// 4. 客戶端使用
public class Main {public static void main(String[] args) {PaymentContext paymentContext = new PaymentContext();// 使用支付寶支付paymentContext.setPaymentStrategy(new AliPayStrategy());paymentContext.makePayment(100.0);// 使用微信支付paymentContext.setPaymentStrategy(new WeChatPayStrategy());paymentContext.makePayment(50.0);}
}
代碼中創建了策略接口 PaymentStrategy
和兩個具體策略類 AliPayStrategy
和 WeChatPayStrategy
。然后,創建了上下文類 PaymentContext
,它維護了一個對策略接口的引用,并提供了設置和執行具體策略的方法。最后,在客戶端 Main
類中,創建了 PaymentContext
的實例,并設置了具體的支付策略,然后進行支付操作。
使用場景:
-
當有多個相關的類只有行為或算法上稍有不同的情況下,可以考慮使用策略模式。它將算法的變化獨立封裝到各自的策略類中,易于擴展和維護。
-
一個系統需要動態地在幾種算法中選擇一種時,可以將這些行為封裝成不同的策略類,并在需要時動態切換。
-
系統中各算法彼此完全獨立,且要求對客戶隱藏具體算法的實現細節時。
-
一個類定義了多種行為,并且這些行為在這個類的操作中以多個條件語句的形式出現,可將每個條件分支移入它們各自的策略類中以代替這些條件語句。
注意事項:如果一個系統的策略多于四個,就需要考慮使用混合模式,解決策略類膨脹的問題。
混合模式:
混合模式是指在策略模式中引入了簡單工廠模式或者享元模式等其他設計模式,來減少策略類的數量,簡化系統的結構。
舉例來說,假設一個系統有多種支付方式,除了支付寶支付和微信支付之外,還有銀行卡支付、信用卡支付等多種支付方式。如果每種支付方式都對應一個具體的策略類,隨著支付方式的增加,策略類的數量會急劇增加,導致類膨脹問題。為了解決這個問題,可以引入簡單工廠模式,將支付方式的創建交給一個工廠類來完成;同時,如果某些支付方式具有相似的功能,可以使用享元模式來共享相同的部分,減少策略對象的數量。
代碼案例:
// 1. 定義策略接口
interface PaymentStrategy {void pay(double amount);
}// 2. 創建具體策略類
class AliPayStrategy implements PaymentStrategy {@Overridepublic void pay(double amount) {System.out.println("Paid " + amount + " via AliPay.");}
}class WeChatPayStrategy implements PaymentStrategy {@Overridepublic void pay(double amount) {System.out.println("Paid " + amount + " via WeChatPay.");}
}class BankCardPayStrategy implements PaymentStrategy {@Overridepublic void pay(double amount) {System.out.println("Paid " + amount + " via BankCard.");}
}class CreditCardPayStrategy implements PaymentStrategy {@Overridepublic void pay(double amount) {System.out.println("Paid " + amount + " via CreditCard.");}
}// 3. 創建簡單工廠類
class PaymentStrategyFactory {private static final Map<String, PaymentStrategy> strategies = new HashMap<>();static {strategies.put("AliPay", new AliPayStrategy());strategies.put("WeChatPay", new WeChatPayStrategy());// 可以添加更多支付方式的策略對象}public static PaymentStrategy getPaymentStrategy(String type) {return strategies.get(type);}
}// 4. 客戶端使用
public class Main {public static void main(String[] args) {PaymentStrategy aliPayStrategy = PaymentStrategyFactory.getPaymentStrategy("AliPay");aliPayStrategy.pay(100.0);PaymentStrategy weChatPayStrategy = PaymentStrategyFactory.getPaymentStrategy("WeChatPay");weChatPayStrategy.pay(50.0);PaymentStrategy bankCardPayStrategy = PaymentStrategyFactory.getPaymentStrategy("BankCard");bankCardPayStrategy.pay(80.0);PaymentStrategy creditCardPayStrategy = PaymentStrategyFactory.getPaymentStrategy("CreditCard");creditCardPayStrategy.pay(120.0);}
}
6.3 觀察者模式
觀察者模式是一種行為型設計模式,它定義了一種一對多的依賴關系,使得當一個對象的狀態發生變化時,其相關依賴對象都會得到通知并自動更新,如同發布-訂閱模式。常見的情況如:公眾號更新內容,所有的關注用戶都能自動收到信息。
結構:
-
抽象主題(Subject):也稱為被觀察者,把所有觀察者對象保存在一個集合里。主題可以提供接口去添加、刪除和通知觀察者的方法。
-
抽象觀察者(Observer):抽象觀察者是接收主題通知的對象。觀察者需要實現一個更新方法,當收到主題的通知時,調用該方法進行更新操作。
-
具體主題(Concrete Subject):具體主題是主題的具體實現類。該角色將有關狀態存入具體觀察者對象,在具體主題的內部狀態發生改變時,給所有注冊過的觀察者發送通知。
-
具體觀察者(Concrete Observer):具體觀察者是抽象觀察者的具體實現類。它實現了更新方法,定義了在收到主題通知時需要執行的具體操作。
觀察者模式通過將主題和觀察者解耦,實現了對象之間的松耦合。當主題的狀態發生改變時,所有依賴于它的觀察者都會收到通知并進行相應的更新。
圖例:
代碼案例:
import java.util.ArrayList;
import java.util.List;// 主題接口(被觀察者)
interface Subject {void registerObserver(Observer observer);void removeObserver(Observer observer);void notifyObservers();
}// 具體主題(具體的被觀察者)
class ConcreteSubject implements Subject {private List<Observer> observers = new ArrayList<>();private int state;public int getState() {return state;}public void setState(int state) {this.state = state;notifyObservers();}@Overridepublic void registerObserver(Observer observer) {observers.add(observer);}@Overridepublic void removeObserver(Observer observer) {observers.remove(observer);}@Overridepublic void notifyObservers() {for (Observer observer : observers) {observer.update();}}
}// 觀察者接口
interface Observer {void update();
}// 具體觀察者
class ConcreteObserver implements Observer {private ConcreteSubject subject;public ConcreteObserver(ConcreteSubject subject) {this.subject = subject;this.subject.registerObserver(this);}@Overridepublic void update() {System.out.println("State changed: " + subject.getState());}
}// 使用示例
public class Main {public static void main(String[] args) {ConcreteSubject subject = new ConcreteSubject();ConcreteObserver observer1 = new ConcreteObserver(subject);ConcreteObserver observer2 = new ConcreteObserver(subject);// 改變主題狀態subject.setState(10);subject.setState(20);}
}
在這個代碼中,Subject
是主題接口,定義了注冊、移除和通知觀察者的方法。ConcreteSubject
是具體的主題類,維護了觀察者列表,并在狀態改變時通知觀察者。Observer
是觀察者接口,定義了觀察者需要實現的更新方法。ConcreteObserver
是具體的觀察者類,實現了更新方法,并在構造函數中注冊到主題對象中。在 Main
類中,創建了主題對象和兩個觀察者對象,然后改變主題的狀態,觀察者對象會收到通知并更新。
使用場景
-
對象間存在一對多關系,當一個對象的改變需要同時改變其他對象的時候。
-
當一個抽象模型有兩個方面,其中一個方面依賴于另一方面時。
優缺點
優點:
-
降低了目標與觀察者之間的耦合關系,兩者之間是抽象耦合關系。
-
被觀察者發送通知,所有注冊的觀察者都會收到信息【可以實現廣播機制】
缺點:
-
如果觀察者非常多的話,那么所有的觀察者收到被觀察者發送的通知會耗時
-
如果被觀察者有循環依賴的話,那么被觀察者發送通知會使觀察者循環調用,會導致系統崩潰