柴毛毛大話設計模式——開發常用的設計模式梳理
寫在最前
- 本文是筆者的一點經驗總結,主要介紹幾種在Web開發中使用頻率較高的設計模式。
- 本文篇幅較長,建議各位同學挑選感興趣的設計模式閱讀。
- 在閱讀的同時,也麻煩各位大佬多多分享!有你們的肯定,才有我繼續分享的動力
- 如需轉載,請與我聯絡!
人工智慧看面相
- 最近忙裡偷閒,對人工智慧看面相進行了一些優化,歡迎各位大佬體驗!
- 體驗後懇請各位大佬分享朋友圈!
基礎學習:UML四種關係
耦合度大小關係
泛化 = 實現 > 組合 > 聚合 > 關聯 > 依賴
依賴(Dependency)
- 一個人(Person)可以買車(car)和房子(House),那麼就可以稱:Person類**依賴於**Car類和House類
- 這裡注意與下面的關聯關係區分:Person類裡並沒有使用Car和House型別的屬性,Car和House的例項是以參量的方式傳入到buy()方法中。
- 依賴關係在Java語言中體現為局域變數、方法的形參,或者對靜態方法的呼叫。
關聯(Association)
- 它使一個類知道另一個類的屬性和方法。
- 關聯可以是雙向的,也可以是單向的。
- 在Java語言中,關聯關係一般使用成員變數來實現。
聚合(Aggregation)
- 聚合是關聯關係的一種,是強的關聯關係。
- 聚合是整體和個體之間的關係,但個體可以脫離整體而存在。
- 例如,汽車類與引擎類、輪胎類,以及其它的零件類之間的關係便整體和個體的關係。
- 與關聯關係一樣,聚合關係也是通過成員變數實現的。但是關聯關係所涉及的兩個類是處在同一層次上的,而在聚合關係中,兩個類是處在不平等層次上的,一個代表整體,另一個代表部分。
組合(Composition)
- 組合是關聯關係的一種,是比聚合關係強的關係,也以成員變數的形式出現。
- 在某一個時刻,部分物件只能和一個整體物件發生組合關係,由後者排他地負責生命週期。
- 部分和整體的生命週期一樣。
- 整體可以將部分傳遞給另一個物件,這時候該部分的生命週期由新整體控制,然後舊整體可以死亡。
策略模式
什麼是策略模式
一個類中的一些行為,可能會隨著系統的迭代而發生變化。為了使得該類滿足開放-封閉原則(即:具備可擴充套件性 或 彈性),我們需要將這些未來會發生動態變化的行為從該類中剝離出來,並通過預測未來業務發展的方式,為這些行為抽象出共有的特徵,封裝在抽象類或介面中,並通過它們的實現類提供具體的行為。原本類中需要持有該抽象類/介面的引用。在使用時,將某一個具體的實現類物件注入給該類所持有的介面/抽象類的引用。
類圖描述
如果類A中有兩個行為X和Y會隨著業務的發展而變化,那麼,我們需要將這兩個行為從類A中剝離出來,並形成各自的繼承體系(策略體系)。每個繼承體系(策略體系)的頂層父類/介面中定義共有行為的抽象函式,每個子類/實現類中定義該策略體系具體的實現。
其中,每一個被抽象出來的繼承體系被稱為一個策略體系,每個具體的實現類被稱為策略。
此時,策略體系已經構建完成,接下來需要改造類A。 在類A中增加所需策略體系的頂層父類/介面,並向外暴露一個共有的函式action給呼叫者使用。
在Spring專案中,策略類和類A之間的依賴關係可以通過依賴注入來完成。
到此為止,策略模式已經構建完成,下面我們來看優缺點分析。
策略模式的優點
1. 滿足開放封閉原則
如果類A需要更換一種策略的時候,只需修改Spring的XML配置檔案即可,其餘所有的程式碼均不需要修改。
比如,將類A的策略X_1更換成X_2的方法如下:
<bean id="a" class="類A">
<!-- 將原本策略實現類X_1修改為策略實現類X_2即可 -->
<property name="策略介面X" class="策略實現類X_2" />
</bean>
此外,如果需要新增一種策略,只需要為策略介面X新增一個新的實現類即可,並覆蓋其中的commonAction函式。然後按照上面的方式修改XML檔案即可。
在這個過程中,在保持原有Java程式碼不發生變化的前提下,擴充套件性了新的功能,從而滿足開放封閉原則。
2. 可方便地建立具有不同策略的物件
如果我們需要根據不同的策略建立多種類A的物件,那麼使用策略模式就能很容易地實現這一點。
比如,我們要建立三個A類的物件,a、b、c。其中,a使用策略X_1和Y_1,b使用策略X_2和Y_2,c使用策略X_3和Y_3。 要建立這三個物件,我們只需在XML中作如下配置即可:
<bean id="a" class="類A">
<property name="策略介面X" class="策略實現類X_1" />
<property name="策略介面Y" class="策略實現類Y_1" />
</bean>
<bean id="b" class="類A">
<property name="策略介面X" class="策略實現類X_2" />
<property name="策略介面Y" class="策略實現類Y_2" />
</bean>
<bean id="c" class="類A">
<property name="策略介面X" class="策略實現類X_3" />
<property name="策略介面Y" class="策略實現類Y_3" />
</bean>
答疑
問:如何實現部分繼承?也就是類Son1只繼承Father的一部分功能,Son2繼承Father的另一部分功能。
這是設計上的缺陷,當出現這種情況時,應當將父類再次拆分成2個子類,保證任何一個父類的行為和特徵均是該繼承體系中共有的!
問:隨著需求的變化,父類中需要增加共有行為時怎麼辦?這就破壞了“開放封閉原則”。
這並未破壞“開放封閉原則”!在系統迭代更新的過程中,修改原有的程式碼是在所難免的,這並不違背“開放封閉原則”。 “開放封閉原則”要求我們:當系統在迭代過程中,第一次出現某一型別的需求時,是允許修改的;在此時,應該對系統進行修改,並進行合理地設計,以保證對該型別需求的再次修改具備可擴充套件性。當再一次出現該型別的需求時,就不應該修改原有程式碼,只允許通過擴充套件來滿足需求。
觀察者模式
觀察者模式是什麼
如果出現如下場景需求時,就需要使用觀察者模式。
如果存在一系列類,他們都需要向指定類獲取指定的資料,當獲取到資料後需要觸發相應的業務邏輯。這種場景就可以用觀察者模式來實現。
在觀察者模式中,存在兩種角色,分別是:觀察者和被觀察者。被觀察者即為資料提供者。他們呈多對一的關係。
類圖描述
- 被觀察者是資料提供方,觀察者是資料獲取方
- 一個普通的類,如果要成為觀察者,獲取指定的資料,一共需要如下幾步:
- 首先,需要實現Observer介面,並實現update函式;
- 然後,在該函式中定義獲取資料後的業務邏輯;
- update(Observable, Object)一共有兩個引數:
- Observable:被觀察者物件(資料提供方)
- Object:資料本身
- 最後,通過呼叫 被觀察者 的addObservable()或者通過Spring的XML配置檔案完成觀察者向被觀察者的注入。此時,該觀察者物件就會被新增進 被觀察者 的List中。
- 呼叫者才是真正的資料提供方。當呼叫者需要廣播最新資料時,只需呼叫 被觀察者 的notidyObservers()函式,該函式會遍歷List集合,並依次呼叫每個Observer的update函式,從而完成資料的傳送,並觸發每個Observer收到資料後的業務邏輯。
兩種註冊觀察者的方式
將Observer註冊進Observable中有如下兩種方式:
1. 執行前,通過Spring XML
在系統執行前,如果觀察者數量可以確定,並在執行過程中不會發生變化,那麼就可以在XML中完成List<Observer>
物件的注入,這種方式程式碼將會比較簡潔。
- 配置好所有 觀察者 bean
<!-- 建立observerA -->
<bean name="observerA" class="ObservserA">
</bean>
<!-- 建立observerB-->
<bean name="observerB" class="ObservserB">
</bean>
- 配置好 被觀察者 bean,並將所有觀察者bean注入給被觀察者bean
<!-- 建立observable -->
<bean name="observable" class="Observable">
<property name="observerList">
<list>
<ref bean="observerA" />
<ref bean="observerB" />
</list>
</property>
</bean>
2. 執行中,通過addObserver()函式
在Spring初始化的時候,通過addObserver()
函式將所有Observer物件
注入Observable
的observerList
中。
@Component
public class InitConfiguration implements ApplicationListener<ContextRefreshedEvent>{
@Override
public void onApplicationEvent(ContextRefreshedEvent arg0) {
if(event.getApplicationContext().getParent() == null){
Observable observable = (Observable)event.getApplicationContext().getBean("observable");
ObserverA observerA = (ObserverA)event.getApplicationContext().getBean("observerA");
ObserverB observerB = (ObserverB)event.getApplicationContext().getBean("observerB");
observable.setObserverList(Arrays.asList(observerA, observerB));
}
}
}
建議使用第一種方式初始化所有的觀察者,此外,被觀察者仍然需要提供
addObserver()
函式供系統在執行期間動態地新增、刪除觀察者物件。
JDK提供的觀察者模式工具包
JDK已經提供了觀察者模式的工具包,包括Observable類
和Observer介面
。若要實現觀察者模式,直接使用這兩個工具包即可。
裝飾模式
何時使用
- 需要增強一個物件中某些函式的功能。
- 需要動態地給一個物件增加功能,這些功能可以再動態地撤銷。
- 需要增加 由一些基本功能排列組合 而產生的大量功能,從而使繼承體系大爆炸。
類圖描述
在裝飾模式中的各個角色有:
- 抽象構件(Component)角色:給出一個抽象介面,以規範準備接收附加責任的物件。
- 具體構件(Concrete Component)角色:定義一個將要接收附加責任的類。
- 裝飾(Decorator)角色:持有一個構件(Component)物件的例項,並定義一個與抽象構件介面一致的介面。
- 具體裝飾(Concrete Decorator)角色:負責給構件物件”貼上”附加的責任。
Decorator中包含Component的成員變數,每個Concrete Decorator實現類均需要實現operation()函式,該函式大致過程如下:
class ConcreteDecorator {
private Component component;
返回型別 operation(){
// 執行上一層的operation(),並獲取返回結果
返回結果 = component.operation();
// 拿到返回結果後,再做額外的處理
處理返回結果
return 返回結果;
}
}
使用裝飾類的過程如下:
// 準備好所有裝飾類
DecoratorA decoratorA = new DecoratorA();
DecoratorB decoratorB = new DecoratorB();
DecoratorC decoratorC = new DecoratorC();
// 準備好 被裝飾的類
Component component = new Component();
// 組裝裝飾類
decoratorC.setComponent(decoratorB);
decoratorB.setComponent(decoratorA);
decoratorA.setComponent(component);
// 執行
decoratorC.operation();
執行過程如下:
優點
- Decorator模式與繼承關係的目的都是要擴充套件物件的功能,但是Decorator可以提供比繼承更多的靈活性。繼承通過覆蓋的方式重寫需要擴充套件的函式,當然也可以通過
super.xxx()
獲取原本的功能,然後在該功能基礎上擴充套件新功能,但它只能增加某一項功能;如果要通過繼承實現增加多種功能,那麼需要多層繼承多個類來實現;而Decorator模式可以在原有功能的基礎上通過組合來增加新功能,這些新功能已經被封裝成一個個獨立的裝飾類,在執行期間通過搭積木的方式選擇裝飾類拼湊即可。 - 通過使用不同的具體裝飾類以及這些裝飾類的排列組合,設計師可以創造出很多不同行為的組合。
缺點
- 這種比繼承更加靈活機動的特性,也同時意味著更加多的複雜性。
- 裝飾模式會導致設計中出現許多小類,如果過度使用,會使程式變得很複雜。
- 裝飾模式是針對抽象元件(Component)型別程式設計。但是,如果你要針對具體元件程式設計時,就應該重新思考你的應用架構,以及裝飾者是否合適。當然也可以改變Component介面,增加新的公開的行為,實現“半透明”的裝飾者模式。在實際專案中要做出最佳選擇。
設計原則
- 多用組合,少用繼承。 利用繼承設計子類的行為,是在編譯時靜態決定的,而且所有的子類都會繼承到相同的行為。然而,如果能夠利用組合的做法擴充套件物件的行為,就可以在執行時動態地進行擴充套件。
單例模式
Java中單例(Singleton)模式是一種廣泛使用的設計模式。單例模式的主要作用是保證在Java程式中,某個類只有一個例項存在。一些管理器和控制器常被設計成單例模式。
單例模式有很多好處,它能夠避免例項物件的重複建立,不僅可以減少每次建立物件的時間開銷,還可以節約記憶體空間;能夠避免由於操作多個例項導致的邏輯錯誤。如果一個物件有可能貫穿整個應用程式,而且起到了全域性統一管理控制的作用,那麼單例模式也許是一個值得考慮的選擇。
單例模式有很多種寫法,大部分寫法都或多或少有一些不足。下面將分別對這幾種寫法進行介紹。
餓漢模式
public class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton newInstance(){
return instance;
}
}
- 類的建構函式定義為private,保證其他類不能例項化此類;
- 然後提供了一個靜態例項並返回給呼叫者;
- 餓漢模式在類載入的時候就對例項進行建立,例項在整個程式週期都存在
- 優點:只在類載入的時候建立一次例項,不會存在多個執行緒建立多個例項的情況,避免了多執行緒同步的問題。
- 缺點:即使這個單例沒有用到也會被建立,而且在類載入之後就被建立,記憶體就被浪費了。
- 適用場景:這種實現方式適合單例佔用記憶體比較小,在初始化時就會被用到的情況。但是,如果單例佔用的記憶體比較大,或單例只是在某個特定場景下才會用到,使用餓漢模式就不合適了,這時候就需要用到懶漢模式進行延遲載入。
懶漢模式(存線上程安全性問題)
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton newInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
- 懶漢模式中單例是在需要的時候才去建立的,如果單例已經建立,再次呼叫獲取介面將不會重新建立新的物件,而是直接返回之前建立的物件。
- 如果某個單例使用的次數少,並且建立單例消耗的資源較多,那麼就需要實現單例的按需建立,這個時候使用懶漢模式就是一個不錯的選擇。
- 但是這裡的懶漢模式並沒有考慮執行緒安全問題,在多個執行緒可能會併發呼叫它的getInstance()方法,導致建立多個例項,因此需要加鎖解決執行緒同步問題,實現如下。
懶漢模式(執行緒安全,但效率低)
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton newInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
加鎖的懶漢模式看起來即解決了執行緒併發問題,又實現了延遲載入,然而它存在著效能問題,依然不夠完美。synchronized修飾的同步方法比一般方法要慢很多,如果多次呼叫getInstance(),累積的效能損耗就比較大了。
懶漢模式(執行緒安全,效率高)
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
這種方式比上一種方式只多加了一行程式碼,那就是在synchronized之上又加了一層判斷if (instance == null)
。這樣當單例建立完畢後,不用每次都進入同步程式碼塊,從而能提升效率。當然,除了初始化單例物件的執行緒ThreadA外,可能還存在少數執行緒,在ThreadA建立完單例後,剛釋放鎖的時候進入同步程式碼塊,但此時有第二道if (instance == null)
判斷,因此也就避免了建立多個物件。而且進入同步程式碼塊的執行緒相對較少。
靜態內部類(懶漢+無鎖)
public class Singleton{
private static class SingletonHolder{
public static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton newInstance(){
return SingletonHolder.instance;
}
}
這種方式同樣利用了類載入機制來保證只建立一個instance例項。它與餓漢模式一樣,也是利用了類載入機制,因此不存在多執行緒併發的問題。不一樣的是,它是在內部類裡面去建立物件例項。這樣的話,只要應用中不使用內部類,JVM就不會去載入這個單例類,也就不會建立單例物件,從而實現懶漢式的延遲載入。也就是說這種方式可以同時保證延遲載入和執行緒安全。
列舉
public enum Singleton{
instance;
public void whateverMethod(){}
}
上面提到的四種實現單例的方式都有共同的缺點:
- 需要額外的工作來實現序列化,否則每次反序列化一個序列化的物件時都會建立一個新的例項。
- 可以使用反射強行呼叫私有構造器(如果要避免這種情況,可以修改構造器,讓它在建立第二個例項的時候拋異常)。
而列舉類很好的解決了這兩個問題,使用列舉除了執行緒安全和防止反射呼叫構造器之外,還提供了自動序列化機制,防止反序列化的時候建立新的物件。因此,《Effective Java》作者推薦使用的方法。不過,在實際工作中,很少看見有人這麼寫。
模板方法模式
定義
在父類中定義演算法的流程,而演算法的某些無法確定的細節,通過抽象函式的形式,在子類中去實現。
也可以理解為,一套演算法的某些步驟可能隨著業務的發展而改變,那麼我們可以將確定的步驟在父類中實現,而可變的步驟作為抽象函式讓其在子類中實現。
類圖描述
- 在模板方法模式中,父類是一個抽象類,演算法的每一步都被封裝成一個函式,
templateMethod
函式將所有演算法步驟串聯起來。 - 對於不變的步驟,用
private
修飾,防止子類重寫; - 對於可變的步驟,用
abstract protected
修飾,必須要求子類重寫; - 子類重寫完所有抽象函式後,呼叫
templateMethod
即可執行演算法。
外觀模式
外觀模式這種思想在專案中普遍存在,也極其容易理解,大家一定用過,只是沒有上升到理論的層面。這裡對這種思想進行介紹。
外觀模式他遮蔽了系統功能實現的複雜性,向客戶端提供一套極其簡單的介面。客戶端只需要知道介面提供什麼功能,如何呼叫就行了,不需要管這些介面背後是如何實現的。從而使得客戶端和系統之間的耦合度大大降低,客戶端只需跟一套簡單的Facade介面打交道即可。
介面卡模式
定義
作為一個基金交易平臺,需要提供一套介面規範,供各個基金公司接入。然而,各個基金公司的介面各不相同,沒有辦法直接和平臺介面對接。此時,各個基金公司需要自行實現一個介面卡,介面卡完成不同介面的轉換工作,使得基金公司的介面和平臺提供的介面對接上。
三種介面卡
介面卡模式有三種實現方式,下面都以基金交易平臺的例子來解釋。
- 基金公司的交易介面:
/**
* 基金公司的交易介面
*/
class FundCompanyTrade{
/**
* 買入函式
*/
public void mairu() {
// ……
}
/**
* 賣出函式
*/
public void maichu() {
// ……
}
}
- 基金交易平臺的交易介面
/**
* 基金交易平臺的交易介面
*/
interface FundPlatformTrade {
// 買入介面
void buy();
// 賣出介面
void sell();
}
- 基金交易平臺均通過如下程式碼呼叫各個基金公司的交易介面:
class Client {
@Autowired
private FundPlatformTrade fundPlatformTrade;
/**
* 買入基金
*/
public void buy() {
fundPlatformTrade.buy();
}
/**
* 賣出基金
*/
public void sell() {
fundPlatformTrade.sell();
}
}
方式1:類介面卡
通過繼承來實現介面的轉換。
- 基金交易介面卡:
class Adapter extends FundCompanyTrade implements FundPlatformTrade {
void buy() {
mairu();
}
void sell(){
maichu();
}
}
介面卡Adapter
繼承了FundCompanyTrade
,因此擁有了FundCompanyTrade
買入和賣出的能力;介面卡Adapter
又實現了FundPlatformTrade
,因此需要實現其中的買入和賣出介面,這個過程便完成了基金公司交易介面向基金平臺交易介面的轉換。
使用時,只需將Adapter
通過Spring注入給Client類
的fundPlatformTrade成員變數
即可:
<!-- 宣告Adapter物件 -->
<bean name="adapter" class="Adapter">
</bean>
<!-- 將adapter注入給Client -->
<bean class="Client">
<property name="fundPlatformTrade" ref="adapter" />
</bean>
方式2:物件介面卡
通過組合來實現介面的轉換。
- 基金交易介面卡:
class Adapter implements FundPlatformTrade {
@Autowired
private FundCompanyTrade fundCompanyTrade;
void buy() {
fundCompanyTrade.mairu();
}
void sell(){
fundCompanyTrade.maichu();
}
}
這種方式中,介面卡Adapter
並未繼承FundCompanyTrade
,而是將該物件作為成員變數注入進來,一樣可以達到同樣的效果。
方式3:介面介面卡
當存在這樣一個介面,其中定義了N多的方法,而我們現在卻只想使用其中的一個到幾個方法,如果我們直接實現介面,那麼我們要對所有的方法進行實現,哪怕我們僅僅是對不需要的方法進行置空(只寫一對大括號,不做具體方法實現)也會導致這個類變得臃腫,呼叫也不方便,這時我們可以使用一個抽象類作為中介軟體,即介面卡,用這個抽象類實現介面,而在抽象類中所有的方法都進行置空,那麼我們在建立抽象類的繼承類,而且重寫我們需要使用的那幾個方法即可。
- 目標介面:A
public interface A {
void a();
void b();
void c();
void d();
void e();
void f();
}
- 介面卡:Adapter 實現所有函式,將所有函式先置空。
public abstract class Adapter implements A {
public void a(){
throw new UnsupportedOperationException("物件不支援這個功能");
}
public void b(){
throw new UnsupportedOperationException("物件不支援這個功能");
}
public void c(){
throw new UnsupportedOperationException("物件不支援這個功能");
}
public void d(){
throw new UnsupportedOperationException("物件不支援這個功能");
}
public void e(){
throw new UnsupportedOperationException("物件不支援這個功能");
}
public void f(){
throw new UnsupportedOperationException("物件不支援這個功能");
}
}
- 實現類:Ashili 繼承介面卡類,選擇性地重寫相應函式。
public class Ashili extends Adapter {
public void a(){
System.out.println("實現A方法被呼叫");
}
public void d(){
System.out.println("實現d方法被呼叫");
}
}
迭代器模式
定義
迭代器模式用於在無需瞭解容器內部細節的情況下,實現容器的迭代。
容器用於儲存資料,而容器的儲存結構種類繁多。在不使用介面卡模式的情況下,如果要迭代容器中的元素,就需要充分理解容器的儲存結構。儲存結構不同,導致了不同容器的迭代方式都不一樣。這無疑增加了我們使用容器的成本。
而迭代器模式提出了一種迭代容器元素的新思路,迭代器規定了一組迭代容器的介面,作為容器使用者,只需會用這套迭代器即可。容器本身需要實現這套迭代器介面,並實現其中的迭代函式。也就是容器提供方在提供容器的同時,還需要提供迭代器的實現。因為容器本身是瞭解自己的儲存結構的,由它來實現迭代函式非常合適。而我們作為容器的使用者,只需知道怎麼用迭代器即可,無需瞭解容器內部的儲存結構。
類圖描述
在迭代器模式中,一共有兩種角色:迭代器 和 容器
- 迭代器 Iterator:封裝了迭代容器的介面
- 容器 Container:儲存元素的東西
- 容器若要具備迭代的能力,就必須擁有
getIterator()
函式,該函式將會返回一個迭代器物件
- 每個容器都有屬於自己的
迭代器內部類
,該內部類實現了Iterator
介面,並實現了其中用於迭代的兩個函式hasNext()
和next()
-
boolean hasNext()
:用於判斷當前容器是否還有尚未迭代完的元素 -
Object next()
:用於獲取下一個元素
- 容器若要具備迭代的能力,就必須擁有
程式碼實現
- 迭代器介面:
public interface Iterator {
public boolean hasNext();
public Object next();
}
- 容器介面:
public interface Iterator {
public boolean hasNext();
public Object next();
}
- 具體的容器(必須實現Container介面):
public class NameRepository implements Container {
public String names[] = {"Robert" , "John" ,"Julie" , "Lora"};
@Override
public Iterator getIterator() {
return new NameIterator();
}
private class NameIterator implements Iterator {
int index;
@Override
public boolean hasNext() {
if(index < names.length){
return true;
}
return false;
}
@Override
public Object next() {
if(this.hasNext()){
return names[index++];
}
return null;
}
}
}
- 具體的容器實現了
Container
介面,並實現了其中的getIterator()
函式,該函式用於返回該容器的迭代器物件。 - 容器內部需要實現自己的迭代器內部類,該內部類實現
Iterator
介面,並實現了其中的hasNext()
和next()
函式。
當容器和容器的迭代器建立完畢後,接下來就輪到使用者使用了,使用就非常簡單了:
public class IteratorPatternDemo {
public static void main(String[] args) {
NameRepository namesRepository = new NameRepository();
for(Iterator iter = namesRepository.getIterator(); iter.hasNext();){
String name = (String)iter.next();
System.out.println("Name : " + name);
}
}
}
- 對於使用者而言,只要知道
Iterator
介面,就能夠迭代所有不同種類的容器了。
組合模式
定義
組合模式定義了樹形結構的物理儲存方式。
現實世界中樹形結構的東西,在程式碼實現中,都可以用組合模式來表示。
比如:多級選單、公司的組織結構等等。
下面就以多級選單為例,介紹組合模式。
例子
假設我們要實現一個多級選單,並實現多級選單的增刪改查操作。選單如下:
一級選單A
二級選單A_1
三級選單A_1_1
三級選單A_1_2
三級選單A_1_3
二級選單A_2
一級選單B
二級選單B_1
二級選單B_2
二級選單B_3
二級選單B_4
三級選單B_4_1
三級選單B_4_2
三級選單B_4_3
一級選單C
二級選單C_1
二級選單C_2
二級選單C_3
選單的特點如下:
- 深度不限,可以有無限級選單
- 每層選單數量不限
類圖描述
-
Item
表示樹中的節點; -
Item
中包含兩個成員變數:-
parent
:指向當前節點的父節點 -
childList
:當前節點的子節點列表
-
- 這種
Item
中又包含Item
的關係就構成了組合模式。
注意:迴圈引用
在構建樹的過程中,可能會出現迴圈引用,從而在遍歷樹的時候可能就會出現死迴圈。因此,我們需要在新增節點的時候避免迴圈引用的出現。
我們可以在Item
中再新增一個List
成員變數,用於記錄根節點到當前節點的路徑。該路徑可以用每個節點的ID表示。一旦新加入的節點ID已經出現在當前路徑中的時候,就說明出現了迴圈引用,此時應該給出提示。
狀態模式
使用場景
如果一個函式中出現大量的、**複雜的**if-else判斷,這時候就要考慮使用狀態模式了。
因為大量的if-else中往往包含了大量的業務邏輯,很有可能會隨著業務的發展而變化。如果將這些業務邏輯都寫死在一個類中,那麼當業務邏輯發生變化的時候就需要修改這個類,從而違反了開放封閉原則。而狀態模式就能很好地解決這一問題。
狀態模式將每一個判斷分支都封裝成一個獨立的類,每一個判斷分支成為一種“狀態”,因此每一個獨立的類就成為一個“狀態類”。並且由一個全域性狀態管理者Context
來維護當前的狀態。
類圖描述
- 在狀態模式中,每一個判斷分支被成為一種狀態,每一種狀態,都會被封裝成一個單獨的狀態類;
- 所有的狀態類都有一個共同的介面——
State
-
State
介面中有一個doAction函式
,每個狀態類的狀態處理邏輯均在該函式中完成;必須將Context
物件作為doAction函式
的引數傳入。該函式的結構如下:
class StateA implements State{
public void doAction(Context context){
if (滿足條件) {
// 執行相應的業務邏輯
}
else {
// 設定下一跳狀態
context.setState(new StateB());
// 執行下一跳狀態
context.doCurState();
}
}
}
- 每個狀態類的
doAction函式
中都有且僅有一對if-else,if中填寫滿足條件時的業務邏輯,而else中填寫不滿足條件時的業務邏輯。 - else中的程式碼都一樣,有且僅有這兩步:
- 首先將context的state設為下一個狀態物件;
- 然後呼叫context的
doCurState()
執行;
-
Context類
其實就是原本包含那個巨大、複雜的if-else的類。該類中持有了State物件,表示當前要執行的狀態物件。 -
Context類
必須要有一個doCurState
函式,該函式的程式碼都一樣:state.doAction()
- 開啟狀態判斷過程的程式碼如下:
// 準備好第一個狀態
StateA stateA = new StateA();
// 設定第一個狀態
context.setState(stateA);
// 開始執行
context.doCurState();
優點
狀態模式將原本在一個類中的龐大的if-else拆分成一個個獨立的狀態類。原本這個包含龐大if-else的類成為Context
,包含了當前的狀態。Context
只需要知道起始狀態類即可,不需要知道其他狀態類的存在。也就是Context
只與第一個狀態類發生耦合。而每一個狀態類只和下一個狀態類發生耦合,從而形成一條狀態判斷鏈。狀態類之間的耦合通過Spring XML檔案配置。這樣,當判斷邏輯發生變化的時候,只需要新增狀態類,並通過修改XML的方式將新的狀態類插入到判斷邏輯中。從而滿足了開放封閉原則。
代理模式
代理模式
代理模式是在不改變目標類和使用者的前提下,擴充套件該類的功能。
代理模式中存在『目標物件』和『代理物件』,它們必須實現相同的介面。使用者直接使用代理物件,而代理物件會將使用者的請求交給目標物件處理。代理物件可以對使用者的請求增加額外的處理。
Java動態代理的使用
- 首先你得擁有一個目標物件,該物件必須要實現一個介面:
public interface Subject
{
public void doSomething();
}
public class RealSubject implements Subject
{
public void doSomething()
{
System.out.println( "call doSomething()" );
}
}
- 其次,為目標物件增加額外的邏輯:
- 自定義一個類,並實現InvocationHandler介面;
- 實現invoke函式,並將需要增加的邏輯寫在該函式中;
public class ProxyHandler implements InvocationHandler
{
private Object proxied;
public ProxyHandler( Object proxied )
{
this.proxied = proxied;
}
public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable
{
//在轉調具體目標物件之前,可以執行一些功能處理
//轉調具體目標物件的方法
return method.invoke( proxied, args);
//在轉調具體目標物件之後,可以執行一些功能處理
}
}
- 建立代理物件,呼叫者直接使用該物件即可:
RealSubject real = new RealSubject();
Subject proxySubject = (Subject)Proxy.newProxyInstance(Subject.class.getClassLoader(),
new Class[]{Subject.class},
new ProxyHandler(real));
proxySubject.doSomething();