[HNOI2011]卡農(容斥/DP)
1995 年,GoF(Gang of Four,四人組/四人幫)合作出版了《設計模式:可複用面向物件軟體的基礎》一書,共收錄了 23 種設計模式,從此樹立了軟體設計模式領域的里程碑,人稱「GoF設計模式」。
1. 軟體設計模式的概念
軟體設計模式(Software Design Pattern),又稱設計模式,是一套被反覆使用、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。它描述了在軟體設計過程中的一些不斷重複發生的問題,以及該問題的解決方案。也就是說,它是解決特定問題的一系列套路,是前輩們的程式碼設計經驗的總結,具有一定的普遍性,可以反覆使用。其目的是為了提高程式碼的可重用性、程式碼的可讀性和程式碼的可靠性。
2. 學習設計模式的意義
設計模式的本質是面向物件設計原則的實際運用,是對類的封裝性、繼承性和多型性以及類的關聯關係和組合關係的充分理解。正確使用設計模式具有以下優點。
- 可以提高程式設計師的思維能力、程式設計能力和設計能力。
- 使程式設計更加標準化、程式碼編制更加工程化,使軟體開發效率大大提高,從而縮短軟體的開發週期。
- 使設計的程式碼可重用性高、可讀性強、可靠性高、靈活性好、可維護性強。
當然,軟體設計模式只是一個引導。在具體的軟體幵發中,必須根據設計的應用系統的特點和要求來恰當選擇。對於簡單的程式開發,苛能寫一個簡單的演算法要比引入某種設計模式更加容易。但對大專案的開發或者框架設計,用設計模式來組織程式碼顯然更好。
1. 根據目的來分
根據模式是用來完成什麼工作來劃分,這種方式可分為建立型模式、結構型模式和行為型模式3 種。
- 建立型模式:用於描述“怎樣建立物件”,它的主要特點是“將物件的建立與使用分離”。GoF 中提供了單例、原型、工廠方法、抽象工廠、建造者等 5 種建立型模式。
- 結構型模式:用於描述如何將類或物件按某種佈局組成更大的結構,GoF 中提供了代理、介面卡、橋接、裝飾、外觀、享元、組合等 7 種結構型模式。
- 行為型模式:用於描述類或物件之間怎樣相互協作共同完成單個物件都無法單獨完成的任務,以及怎樣分配職責。GoF 中提供了模板方法、策略、命令、職責鏈、狀態、觀察者、中介者、迭代器、訪問者、備忘錄、直譯器等 11 種行為型模式。
3. GoF的23種設計模式的功能
前面說明了 GoF 的 23 種設計模式的分類,現在對各個模式的功能進行介紹。
- 單例(Singleton)模式:某個類只能生成一個例項,該類提供了一個全域性訪問點供外部獲取該例項,其拓展是有限多例模式。
- 原型(Prototype)模式:將一個物件作為原型,通過對其進行復制而克隆出多個和原型類似的新例項。
- 工廠方法(Factory Method)模式:定義一個用於建立產品的介面,由子類決定生產什麼產品。
- 抽象工廠(AbstractFactory)模式:提供一個建立產品族的介面,其每個子類可以生產一系列相關的產品。
- 建造者(Builder)模式:將一個複雜物件分解成多個相對簡單的部分,然後根據不同需要分別建立它們,最後構建成該複雜物件。
- 代理(Proxy)模式:為某物件提供一種代理以控制對該物件的訪問。即客戶端通過代理間接地訪問該物件,從而限制、增強或修改該物件的一些特性。
- 介面卡(Adapter)模式:將一個類的介面轉換成客戶希望的另外一個介面,使得原本由於介面不相容而不能一起工作的那些類能一起工作。
- 橋接(Bridge)模式:將抽象與實現分離,使它們可以獨立變化。它是用組合關係代替繼承關係來實現,從而降低了抽象和實現這兩個可變維度的耦合度。
- 裝飾(Decorator)模式:動態的給物件增加一些職責,即增加其額外的功能。
- 外觀(Facade)模式:為多個複雜的子系統提供一個一致的介面,使這些子系統更加容易被訪問。
- 享元(Flyweight)模式:運用共享技術來有效地支援大量細粒度物件的複用。
- 組合(Composite)模式:將物件組合成樹狀層次結構,使使用者對單個物件和組合物件具有一致的訪問性。
- 模板方法(TemplateMethod)模式:定義一個操作中的演算法骨架,而將演算法的一些步驟延遲到子類中,使得子類可以不改變該演算法結構的情況下重定義該演算法的某些特定步驟。
- 策略(Strategy)模式:定義了一系列演算法,並將每個演算法封裝起來,使它們可以相互替換,且演算法的改變不會影響使用演算法的客戶。
- 命令(Command)模式:將一個請求封裝為一個物件,使發出請求的責任和執行請求的責任分割開。
- 職責鏈(Chain of Responsibility)模式:把請求從鏈中的一個物件傳到下一個物件,直到請求被響應為止。通過這種方式去除物件之間的耦合。
- 狀態(State)模式:允許一個物件在其內部狀態發生改變時改變其行為能力。
- 觀察者(Observer)模式:多個物件間存在一對多關係,當一個物件發生改變時,把這種改變通知給其他多個物件,從而影響其他物件的行為。
- 中介者(Mediator)模式:定義一箇中介物件來簡化原有物件之間的互動關係,降低系統中物件間的耦合度,使原有物件之間不必相互瞭解。
- 迭代器(Iterator)模式:提供一種方法來順序訪問聚合物件中的一系列資料,而不暴露聚合物件的內部表示。
- 訪問者(Visitor)模式:在不改變集合元素的前提下,為一個集合中的每個元素提供多種訪問方式,即每個元素有多個訪問者物件訪問。
- 備忘錄(Memento)模式:在不破壞封裝性的前提下,獲取並儲存一個物件的內部狀態,以便以後恢復它。
- 直譯器(Interpreter)模式:提供如何定義語言的文法,以及對語言句子的解釋方法,即直譯器。
UML類圖,類設計關係:
UML中的類圖及類圖之間的關係 (biancheng.net)
類之間的關係強度:依賴《關聯《聚合《組合《泛化=實現。
面向物件的設計原則
在軟體開發中,為了提高軟體系統的可維護性和可複用性,增加軟體的可擴充套件性和靈活性,程式設計師要儘量根據 7 條原則來開發程式,從而提高軟體開發效率、節約軟體開發成本和維護成本。我們將在下面的幾節中依次來介紹這 7 條原則,本節首先介紹開閉原則。
開閉原則的含義是:當應用的需求改變時,在不修改軟體實體的原始碼或者二進位制程式碼的前提下,可以擴充套件模組的功能,使其滿足新的需求。
開閉原則的實現方法
可以通過“抽象約束、封裝變化”來實現開閉原則,即通過介面或者抽象類為軟體實體定義一個相對穩定的抽象層,而將相同的可變因素封裝在相同的具體實現類中。
下面以 Windows 的桌面主題為例介紹開閉原則的應用。
【例1】Windows 的桌面主題設計。
分析:Windows 的主題是桌面背景圖片、視窗顏色和聲音等元素的組合。使用者可以根據自己的喜愛更換自己的桌面主題,也可以從網上下載新的主題。這些主題有共同的特點,可以為其定義一個抽象類(Abstract Subject),而每個具體的主題(Specific Subject)是其子類。使用者窗體可以根據需要選擇或者增加新的主題,而不需要修改原始碼,所以它是滿足開閉原則的,其類圖如圖 1 所示。
里氏替換原則主要闡述了有關繼承的一些原則,也就是什麼時候應該使用繼承,什麼時候不應該使用繼承,以及其中蘊含的原理。里氏替換原是繼承複用的基礎,它反映了基類與子類之間的關係,是對開閉原則的補充,是對實現抽象化的具體步驟的規範。
里氏替換原則的實現方法
里氏替換原則通俗來講就是:子類可以擴充套件父類的功能,但不能改變父類原有的功能。也就是說:子類繼承父類時,除新增新的方法完成新增功能外,儘量不要重寫父類的方法。
根據上述理解,對里氏替換原則的定義可以總結如下:
- 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法
- 子類中可以增加自己特有的方法
- 當子類的方法過載父類的方法時,方法的前置條件(即方法的輸入引數)要比父類的方法更寬鬆
- 當子類的方法實現父類的方法時(重寫/過載或實現抽象方法),方法的後置條件(即方法的的輸出/返回值)要比父類的方法更嚴格或相等
【例1】里氏替換原則在“幾維鳥不是鳥”例項中的應用。
分析:鳥一般都會飛行,如燕子的飛行速度大概是每小時 120 千米。但是紐西蘭的幾維鳥由於翅膀退化無法飛行。假如要設計一個例項,計算這兩種鳥飛行 300 千米要花費的時間。顯然,拿燕子來測試這段程式碼,結果正確,能計算出所需要的時間;但拿幾維鳥來測試,結果會發生“除零異常”或是“無窮大”,明顯不符合預期,
程式執行錯誤的原因是:幾維鳥類重寫了鳥類的 setSpeed(double speed) 方法,這違背了里氏替換原則。正確的做法是:取消幾維鳥原來的繼承關係,定義鳥和幾維鳥的更一般的父類,如動物類,它們都有奔跑的能力。幾維鳥的飛行速度雖然為 0,但奔跑速度不為 0,可以計算出其奔跑 300 千米所要花費的時間。
依賴倒置原則的原始定義為:高層模組不應該依賴低層模組,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象(High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions)。其核心思想是:要面向介面程式設計,不要面向實現程式設計。
依賴倒置原則的實現方法
依賴倒置原則的目的是通過要面向介面的程式設計來降低類間的耦合性,所以我們在實際程式設計中只要遵循以下4點,就能在專案中滿足這個規則。
- 每個類儘量提供介面或抽象類,或者兩者都具備。
- 變數的宣告型別儘量是介面或者是抽象類。
- 任何類都不應該從具體類派生。
- 使用繼承時儘量遵循里氏替換原則。
單一職責原則的定義
單一職責原則(Single Responsibility Principle,SRP)又稱單一功能原則,由羅伯特·C.馬丁(Robert C. Martin)於《敏捷軟體開發:原則、模式和實踐》一書中提出的。這裡的職責是指類變化的原因,單一職責原則規定一個類應該有且僅有一個引起它變化的原因,否則類應該被拆分(There should never be more than one reason for a class to change)。
該原則提出物件不應該承擔太多職責,如果一個物件承擔了太多的職責,至少存在以下兩個缺點:
- 一個職責的變化可能會削弱或者抑制這個類實現其他職責的能力;
- 當客戶端需要該物件的某一個職責時,不得不將其他不需要的職責全都包含進來,從而造成冗餘程式碼或程式碼的浪費。
單一職責原則的優點
單一職責原則的核心就是控制類的粒度大小、將物件解耦、提高其內聚性。如果遵循單一職責原則將有以下優點。
- 降低類的複雜度。一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單得多。
- 提高類的可讀性。複雜性降低,自然其可讀性會提高。
- 提高系統的可維護性。可讀性提高,那自然更容易維護了。
- 變更引起的風險降低。變更是必然的,如果單一職責原則遵守得好,當修改一個功能時,可以顯著降低對其他功能的影響。
單一職責原則的實現方法
單一職責原則是最簡單但又最難運用的原則,需要設計人員發現類的不同職責並將其分離,再封裝到不同的類或模組中。而發現類的多重職責需要設計人員具有較強的分析設計能力和相關重構經驗。下面以大學學生工作管理程式為例介紹單一職責原則的應用。
介面隔離原則的定義
介面隔離原則(Interface Segregation Principle,ISP)要求程式設計師儘量將臃腫龐大的介面拆分成更小的和更具體的介面,讓介面中只包含客戶感興趣的方法。
以上兩個定義的含義是:要為各個類建立它們需要的專用介面,而不要試圖去建立一個很龐大的介面供所有依賴它的類去呼叫。
介面隔離原則和單一職責都是為了提高類的內聚性、降低它們之間的耦合性,體現了封裝的思想,但兩者是不同的:
- 單一職責原則注重的是職責,而介面隔離原則注重的是對介面依賴的隔離。
- 單一職責原則主要是約束類,它針對的是程式中的實現和細節;介面隔離原則主要約束介面,主要針對抽象和程式整體框架的構建。
介面隔離原則的優點
介面隔離原則是為了約束介面、降低類對介面的依賴性,遵循介面隔離原則有以下 5 個優點。
- 將臃腫龐大的介面分解為多個粒度小的介面,可以預防外來變更的擴散,提高系統的靈活性和可維護性。
- 介面隔離提高了系統的內聚性,減少了對外互動,降低了系統的耦合性。
- 如果介面的粒度大小定義合理,能夠保證系統的穩定性;但是,如果定義過小,則會造成介面數量過多,使設計複雜化;如果定義太大,靈活性降低,無法提供定製服務,給整體專案帶來無法預料的風險。
- 使用多個專門的介面還能夠體現物件的層次,因為可以通過介面的繼承,實現對總介面的定義。
- 能減少專案工程中的程式碼冗餘。過大的大接口裡面通常放置許多不用的方法,當實現這個介面的時候,被迫設計冗餘的程式碼
介面隔離原則的實現方法
在具體應用介面隔離原則時,應該根據以下幾個規則來衡量。
- 介面儘量小,但是要有限度。一個介面只服務於一個子模組或業務邏輯。
- 為依賴介面的類定製服務。只提供呼叫者需要的方法,遮蔽不需要的方法。
- 瞭解環境,拒絕盲從。每個專案或產品都有選定的環境因素,環境不同,介面拆分的標準就不同深入瞭解業務邏輯。
- 提高內聚,減少對外互動。使介面用最少的方法去完成最多的事情。
【例1】學生成績管理程式。
分析:學生成績管理程式一般包含插入成績、刪除成績、修改成績、計算總分、計算均分、列印成績資訊、査詢成績資訊等功能,如果將這些功能全部放到一個介面中顯然不太合理,正確的做法是將它們分別放在輸入模組、統計模組和列印模組等 3 個模組中,其類圖如圖 1 所示。
迪米特法則的優點
迪米特法則要求限制軟體實體之間通訊的寬度和深度,正確使用迪米特法則將有以下兩個優點。- 降低了類之間的耦合度,提高了模組的相對獨立性。
- 由於親合度降低,從而提高了類的可複用率和系統的擴充套件性。
迪米特法則的實現方法
從迪米特法則的定義和特點可知,它強調以下兩點:- 從依賴者的角度來說,只依賴應該依賴的物件。
- 從被依賴者的角度說,只暴露應該暴露的方法。
所以,在運用迪米特法則時要注意以下 6 點。
- 在類的劃分上,應該建立弱耦合的類。類與類之間的耦合越弱,就越有利於實現可複用的目標。
- 在類的結構設計上,儘量降低類成員的訪問許可權。
- 在類的設計上,優先考慮將一個類設定成不變類。
- 在對其他類的引用上,將引用其他物件的次數降到最低。
- 不暴露類的屬性成員,而應該提供相應的訪問器(set 和 get 方法)。
- 謹慎使用序列化(Serializable)功能。
分析:明星由於全身心投入藝術,所以許多日常事務由經紀人負責處理,如與粉絲的見面會,與媒體公司的業務洽淡等。這裡的經紀人是明星的朋友,而粉絲和媒體公司是陌生人,所以適合使用迪米特法則,其類圖如圖 1 所示。
合成複用原則的定義
合成複用原則(Composite Reuse Principle,CRP)又叫組合/聚合複用原則(Composition/Aggregate Reuse Principle,CARP)。它要求在軟體複用時,要儘量先使用組合或者聚合等關聯關係來實現,其次才考慮使用繼承關係來實現。
如果要使用繼承關係,則必須嚴格遵循里氏替換原則。合成複用原則同里氏替換原則相輔相成的,兩者都是開閉原則的具體實現規範。
合成複用原則的重要性
通常類的複用分為繼承複用和合成複用兩種,繼承複用雖然有簡單和易實現的優點,但它也存在以下缺點。
- 繼承複用破壞了類的封裝性。因為繼承會將父類的實現細節暴露給子類,父類對子類是透明的,所以這種複用又稱為“白箱”複用。
- 子類與父類的耦合度高。父類的實現的任何改變都會導致子類的實現發生變化,這不利於類的擴充套件與維護。
- 它限制了複用的靈活性。從父類繼承而來的實現是靜態的,在編譯時已經定義,所以在執行時不可能發生變化。
採用組合或聚合複用時,可以將已有物件納入新物件中,使之成為新物件的一部分,新物件可以呼叫已有物件的功能,它有以下優點。
- 它維持了類的封裝性。因為成分物件的內部細節是新物件看不見的,所以這種複用又稱為“黑箱”複用。
- 新舊類之間的耦合度低。這種複用所需的依賴較少,新物件存取成分物件的唯一方法是通過成分物件的介面。
- 複用的靈活性高。這種複用可以在執行時動態進行,新物件可以動態地引用與成分物件型別相同的物件。
【例1】汽車分類管理程式。
分析:汽車按“動力源”劃分可分為汽油汽車、電動汽車等;按“顏色”劃分可分為白色汽車、黑色汽車和紅色汽車等。如果同時考慮這兩種分類,其組合就很多。圖 1 所示是用繼承關係實現的汽車分類的類圖。
從圖 1 可以看出用繼承關係實現會產生很多子類,而且增加新的“動力源”或者增加新的“顏色”都要修改原始碼,這違背了開閉原則,顯然不可取。但如果改用組合關係實現就能很好地解決以上問題,其類圖如圖 2 所示。
這 7 種設計原則是軟體設計模式必須儘量遵循的原則,各種原則要求的側重點不同。其中,開閉原則是總綱,它告訴我們要對擴充套件開放,對修改關閉;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向介面程式設計;單一職責原則告訴我們實現類要職責單一;介面隔離原則告訴我們在設計介面的時候要精簡單一;迪米特法則告訴我們要降低耦合度;合成複用原則告訴我們要優先使用組合或者聚合關係複用,少用繼承關係複用。
建立型模式的主要關注點是“怎樣建立物件?”,它的主要特點是“將物件的建立與使用分離”。這樣可以降低系統的耦合度,使用者不需要關注物件的建立細節,物件的建立由相關的工廠來完成。就像我們去商場購買商品時,不需要知道商品是怎麼生產出來一樣,因為它們由專門的廠商生產。
建立型模式分為以下幾種。
- 單例(Singleton)模式:某個類只能生成一個例項,該類提供了一個全域性訪問點供外部獲取該例項,其拓展是有限多例模式。
- 原型(Prototype)模式:將一個物件作為原型,通過對其進行復制而克隆出多個和原型類似的新例項。
- 工廠方法(FactoryMethod)模式:定義一個用於建立產品的介面,由子類決定生產什麼產品。
- 抽象工廠(AbstractFactory)模式:提供一個建立產品族的介面,其每個子類可以生產一系列相關的產品。
- 建造者(Builder)模式:將一個複雜物件分解成多個相對簡單的部分,然後根據不同需要分別建立它們,最後構建成該複雜物件。
單例模式的定義與特點
單例模式在現實生活中的應用也非常廣泛,例如公司 CEO、部門經理等都屬於單例模型。J2EE 標準中的ServletContext 和 ServletContextConfig、Spring框架應用中的 ApplicationContext、資料庫中的連線池等也都是單例模式。
單例模式有 3 個特點:
- 單例類只有一個例項物件;
- 該單例物件必須由單例類自行建立;
- 單例類對外提供一個訪問該單例的全域性訪問點。
單例模式的優點和缺點
單例模式的優點:
- 單例模式可以保證記憶體裡只有一個例項,減少了記憶體的開銷。
- 可以避免對資源的多重佔用。
- 單例模式設定全域性訪問點,可以優化和共享資源的訪問。
單例模式的缺點:
- 單例模式一般沒有介面,擴充套件困難。如果要擴充套件,則除了修改原來的程式碼,沒有第二種途徑,違背開閉原則。
- 在併發測試中,單例模式不利於程式碼除錯。在除錯過程中,如果單例中的程式碼沒有執行完,也不能模擬生成一個新的物件。
- 單例模式的功能程式碼通常寫在一個類中,如果功能設計不合理,則很容易違背單一職責原則。
單例模式的應用場景
對於Java來說,單例模式可以保證在一個 JVM 中只存在單一例項。單例模式的應用場景主要有以下幾個方面。
- 需要頻繁建立的一些類,使用單例可以降低系統的記憶體壓力,減少 GC。
- 某類只要求生成一個物件的時候,如一個班中的班長、每個人的身份證號等。
- 某些類建立例項時佔用資源較多,或例項化耗時較長,且經常使用。
- 某類需要頻繁例項化,而建立的物件又頻繁被銷燬的時候,如多執行緒的執行緒池、網路連線池等。
- 頻繁訪問資料庫或檔案的物件。
- 對於一些控制硬體級別的操作,或者從系統上來講應當是單一控制邏輯的操作,如果有多個例項,則系統會完全亂套。
- 當物件需要被共享的場合。由於單例模式只允許建立一個物件,共享該物件可以節省記憶體,並加快物件訪問速度。如 Web 中的配置物件、資料庫的連線池等。
1. 單例模式的結構
單例模式的主要角色如下。
- 單例類:包含一個例項且能自行建立這個例項的類。
- 訪問類:使用單例的類。
2. 單例模式的實現
Singleton 模式通常有兩種實現形式。
第 1 種:懶漢式單例
該模式的特點是類載入時沒有生成單例,只有當第一次呼叫 getlnstance 方法時才去建立這個單例。程式碼如下:
public class LazySingleton { private static volatile LazySingleton instance=null; //保證 instance 在所有執行緒中同步 private LazySingleton(){} //private 避免類在外部被例項化 public static synchronized LazySingleton getInstance() { //getInstance 方法前加同步 if(instance==null) { instance=new LazySingleton(); } return instance; } }
注意:如果編寫的是多執行緒程式,則不要刪除上例程式碼中的關鍵字 volatile 和 synchronized,否則將存線上程非安全的問題。如果不刪除這兩個關鍵字就能保證執行緒安全,但是每次訪問時都要同步,會影響效能,且消耗更多的資源,這是懶漢式單例的缺點。
第 2 種:餓漢式單例
該模式的特點是類一旦載入就建立一個單例,保證在呼叫 getInstance 方法之前單例已經存在了。
public class HungrySingleton { private static final HungrySingleton instance=new HungrySingleton(); private HungrySingleton(){} public static HungrySingleton getInstance() { return instance; } }
餓漢式單例在類建立的同時就已經建立好一個靜態的物件供系統使用,以後不再改變,所以是執行緒安全的,可以直接用於多執行緒而不會出現問題。
單例模式的擴充套件
單例模式可擴充套件為有限的多例(Multitcm)模式,這種模式可生成有限個例項並儲存在 ArrayList 中,客戶需要時可隨機獲取,其結構圖如圖 5 所示。
原型模式的定義與特點
原型(Prototype)模式的定義如下:用一個已經建立的例項作為原型,通過複製該原型物件來建立一個和原型相同或相似的新物件。在這裡,原型例項指定了要建立的物件的種類。用這種方式建立物件非常高效,根本無須知道物件建立的細節。例如,Windows 作業系統的安裝通常較耗時,如果複製就快了很多。在生活中複製的例子非常多,這裡不一一列舉了。原型模式的優點:
- Java自帶的原型模式基於記憶體二進位制流的複製,在效能上比直接 new 一個物件更加優良。
- 可以使用深克隆方式儲存物件的狀態,使用原型模式將物件複製一份,並將其狀態儲存起來,簡化了建立物件的過程,以便在需要的時候使用(例如恢復到歷史某一狀態),可輔助實現撤銷操作。
原型模式的缺點:
- 需要為每一個類都配置一個 clone 方法
- clone 方法位於類的內部,當對已有類進行改造的時候,需要修改程式碼,違背了開閉原則。
- 當實現深克隆時,需要編寫較為複雜的程式碼,而且當物件之間存在多重巢狀引用時,為了實現深克隆,每一層物件對應的類都必須支援深克隆,實現起來會比較麻煩。因此,深克隆、淺克隆需要運用得當。
原型模式的應用場景
原型模式通常適用於以下場景。- 物件之間相同或相似,即只是個別的幾個屬性不同的時候。
- 建立物件成本較大,例如初始化時間長,佔用CPU太多,或者佔用網路資源太多等,需要優化資源。
- 建立一個物件需要繁瑣的資料準備或訪問許可權等,需要提高效能或者提高安全性。
- 系統中大量使用該類物件,且各個呼叫者都需要給它的屬性重新賦值。
在Spring中,原型模式應用的非常廣泛,例如 scope='prototype'、JSON.parseObject() 等都是原型模式的具體應用。
簡單工廠模式
在日常開發中,凡是需要生成複雜物件的地方,都可以嘗試考慮使用工廠模式來代替。
注意:上述複雜物件指的是類的建構函式引數過多等對類的構造有影響的情況,因為類的構造過於複雜,如果直接在其他業務類內使用,則兩者的耦合過重,後續業務更改,就需要在任何引用該類的原始碼內進行更改,光是查詢所有依賴就很消耗時間了,更別說要一個一個修改了。
工廠模式的定義:定義一個建立產品物件的工廠介面,將產品物件的實際建立工作推遲到具體子工廠類當中。這滿足建立型模式中所要求的“建立與使用相分離”的特點。
按實際業務場景劃分,工廠模式有 3 種不同的實現方式,分別是簡單工廠模式、工廠方法模式和抽象工廠模式。
我們把被建立的物件稱為“產品”,把建立產品的物件稱為“工廠”。如果要建立的產品不多,只要一個工廠類就可以完成,這種模式叫“簡單工廠模式”。
在簡單工廠模式中建立例項的方法通常為靜態(static)方法,因此簡單工廠模式(Simple Factory Pattern)又叫作靜態工廠方法模式(Static Factory Method Pattern)。
簡單來說,簡單工廠模式有一個具體的工廠類,可以生成多個不同的產品,屬於建立型設計模式。簡單工廠模式不在 GoF 23 種設計模式之列。
簡單工廠模式每增加一個產品就要增加一個具體產品類和一個對應的具體工廠類,這增加了系統的複雜度,違背了“開閉原則”。
“工廠方法模式”是對簡單工廠模式的進一步抽象化,其好處是可以使系統在不修改原來程式碼的情況下引進新的產品,即滿足開閉原則。
優點和缺點
優點:
- 工廠類包含必要的邏輯判斷,可以決定在什麼時候建立哪一個產品的例項。客戶端可以免除直接建立產品物件的職責,很方便的創建出相應的產品。工廠和產品的職責區分明確。
- 客戶端無需知道所建立具體產品的類名,只需知道引數即可。
- 也可以引入配置檔案,在不修改客戶端程式碼的情況下更換和新增新的具體產品類。
缺點:
- 簡單工廠模式的工廠類單一,負責所有產品的建立,職責過重,一旦異常,整個系統將受影響。且工廠類程式碼會非常臃腫,違背高聚合原則。
- 使用簡單工廠模式會增加系統中類的個數(引入新的工廠類),增加系統的複雜度和理解難度
- 系統擴充套件困難,一旦增加新產品不得不修改工廠邏輯,在產品型別較多時,可能造成邏輯過於複雜
- 簡單工廠模式使用了 static 工廠方法,造成工廠角色無法形成基於繼承的等級結構。
應用場景
對於產品種類相對較少的情況,考慮使用簡單工廠模式。使用簡單工廠模式的客戶端只需要傳入工廠類的引數,不需要關心如何建立物件的邏輯,可以很方便地建立所需產品。
模式的結構與實現
簡單工廠模式的主要角色如下:
- 簡單工廠(SimpleFactory):是簡單工廠模式的核心,負責實現建立所有例項的內部邏輯。工廠類的建立產品類的方法可以被外界直接呼叫,建立所需的產品物件。
- 抽象產品(Product):是簡單工廠建立的所有物件的父類,負責描述所有例項共有的公共介面。
- 具體產品(ConcreteProduct):是簡單工廠模式的建立目標。
public class Client { public static void main(String[] args) { } //抽象產品 public interface Product { void show(); } //具體產品:ProductA static class ConcreteProduct1 implements Product { public void show() { System.out.println("具體產品1顯示..."); } } //具體產品:ProductB static class ConcreteProduct2 implements Product { public void show() { System.out.println("具體產品2顯示..."); } } final class Const { static final int PRODUCT_A = 0; static final int PRODUCT_B = 1; static final int PRODUCT_C = 2; } static class SimpleFactory { public static Product makeProduct(int kind) { switch (kind) { case Const.PRODUCT_A: return new ConcreteProduct1(); case Const.PRODUCT_B: return new ConcreteProduct2(); } return null; } } }
工廠模式的結構與實現
工廠方法模式由抽象工廠、具體工廠、抽象產品和具體產品等4個要素構成。本節來分析其基本結構和實現方法。
1. 模式的結構
工廠方法模式的主要角色如下。
- 抽象工廠(Abstract Factory):提供了建立產品的介面,呼叫者通過它訪問具體工廠的工廠方法 newProduct() 來建立產品。
- 具體工廠(ConcreteFactory):主要是實現抽象工廠中的抽象方法,完成具體產品的建立。
- 抽象產品(Product):定義了產品的規範,描述了產品的主要特性和功能。
- 具體產品(ConcreteProduct):實現了抽象產品角色所定義的介面,由具體工廠來建立,它同具體工廠之間一一對應。
模式的定義與特點
抽象工廠(AbstractFactory)模式的定義:是一種為訪問類提供一個建立一組相關或相互依賴物件的介面,且訪問類無須指定所要產品的具體類就能得到同族的不同等級的產品的模式結構。
抽象工廠模式是工廠方法模式的升級版本,工廠方法模式只生產一個等級的產品,而抽象工廠模式可生產多個等級的產品。
使用抽象工廠模式一般要滿足以下條件。
- 系統中有多個產品族,每個具體工廠建立同一族但屬於不同等級結構的產品。
- 系統一次只可能消費其中某一族產品,即同族的產品一起使用。
抽象工廠模式除了具有工廠方法模式的優點外,其他主要優點如下。
- 可以在類的內部對產品族中相關聯的多等級產品共同管理,而不必專門引入多個新的類來進行管理。
- 當需要產品族時,抽象工廠可以保證客戶端始終只使用同一個產品的產品組。
- 抽象工廠增強了程式的可擴充套件性,當增加一個新的產品族時,不需要修改原始碼,滿足開閉原則。
其缺點是:當產品族中需要增加一個新的產品時,所有的工廠類都需要進行修改。增加了系統的抽象性和理解難度。
模式的結構與實現
抽象工廠模式同工廠方法模式一樣,也是由抽象工廠、具體工廠、抽象產品和具體產品等 4 個要素構成,但抽象工廠中方法個數不同,抽象產品的個數也不同。現在我們來分析其基本結構和實現方法。
1. 模式的結構
抽象工廠模式的主要角色如下。
- 抽象工廠(Abstract Factory):提供了建立產品的介面,它包含多個建立產品的方法 newProduct(),可以建立多個不同等級的產品。
- 具體工廠(Concrete Factory):主要是實現抽象工廠中的多個抽象方法,完成具體產品的建立。
- 抽象產品(Product):定義了產品的規範,描述了產品的主要特性和功能,抽象工廠模式有多個抽象產品。
- 具體產品(ConcreteProduct):實現了抽象產品角色所定義的介面,由具體工廠來建立,它同具體工廠之間是多對一的關係。
抽象工廠模式的結構圖如圖 2 所示。
模式的定義與特點
建造者(Builder)模式的定義:指將一個複雜物件的構造與它的表示分離,使同樣的構建過程可以建立不同的表示,這樣的設計模式被稱為建造者模式。它是將一個複雜的物件分解為多個簡單的物件,然後一步一步構建而成。它將變與不變相分離,即產品的組成部分是不變的,但每一部分是可以靈活選擇的。
該模式的主要優點如下:
- 封裝性好,構建和表示分離。
- 擴充套件性好,各個具體的建造者相互獨立,有利於系統的解耦。
- 客戶端不必知道產品內部組成的細節,建造者可以對建立過程逐步細化,而不對其它模組產生任何影響,便於控制細節風險。
其缺點如下:
- 產品的組成部分必須相同,這限制了其使用範圍。
- 如果產品的內部變化複雜,如果產品內部發生變化,則建造者也要同步修改,後期維護成本較大。
模式的結構與實現
建造者(Builder)模式由產品、抽象建造者、具體建造者、指揮者等 4 個要素構成,現在我們來分析其基本結構和實現方法。
1. 模式的結構
建造者(Builder)模式的主要角色如下。
- 產品角色(Product):它是包含多個組成部件的複雜物件,由具體建造者來建立其各個零部件。
- 抽象建造者(Builder):它是一個包含建立產品各個子部件的抽象方法的介面,通常還包含一個返回複雜產品的方法 getResult()。
- 具體建造者(Concrete Builder):實現 Builder 介面,完成複雜產品的各個部件的具體建立方法。
- 指揮者(Director):它呼叫建造者物件中的部件構造與裝配方法完成複雜物件的建立,在指揮者中不涉及具體產品的資訊。
2. 模式的實現
圖 1 給出了建造者(Builder)模式的主要結構,其相關類的程式碼如下。
(1) 產品角色:包含多個組成部件的複雜物件。
- class Product
- {
- private String partA;
- private String partB;
- private String partC;
- public void setPartA(String partA)
- {
- this.partA=partA;
- }
- public void setPartB(String partB)
- {
- this.partB=partB;
- }
- public void setPartC(String partC)
- {
- this.partC=partC;
- }
- public void show()
- {
- //顯示產品的特性
- }
- }
(2) 抽象建造者:包含建立產品各個子部件的抽象方法。
- abstract class Builder
- {
- //建立產品物件
- protected Product product=new Product();
- public abstract void buildPartA();
- public abstract void buildPartB();
- public abstract void buildPartC();
- //返回產品物件
- public Product getResult()
- {
- return product;
- }
- }
(3) 具體建造者:實現了抽象建造者介面。
- public class ConcreteBuilder extends Builder
- {
- public void buildPartA()
- {
- product.setPartA("建造 PartA");
- }
- public void buildPartB()
- {
- product.setPartB("建造 PartB");
- }
- public void buildPartC()
- {
- product.setPartC("建造 PartC");
- }
- }
(4) 指揮者:呼叫建造者中的方法完成複雜物件的建立。
- class Director
- {
- private Builder builder;
- public Director(Builder builder)
- {
- this.builder=builder;
- }
- //產品構建與組裝方法
- public Product construct()
- {
- builder.buildPartA();
- builder.buildPartB();
- builder.buildPartC();
- return builder.getResult();
- }
- }
(5) 客戶類。
純文字複製
- public class Client
- {
- public static void main(String[] args)
- {
- Builder builder=new ConcreteBuilder();
- Director director=new Director(builder);
- Product product=director.construct();
- product.show();
- }
- }
建造者模式和工廠模式的區別
通過前面的學習,我們已經瞭解了建造者模式,那麼它和工廠模式有什麼區別呢?
- 建造者模式更加註重方法的呼叫順序,工廠模式注重建立物件。
- 建立物件的力度不同,建造者模式建立複雜的物件,由各種複雜的部件組成,工廠模式創建出來的物件都一樣
- 關注重點不一樣,工廠模式只需要把物件創建出來就可以了,而建造者模式不僅要創建出物件,還要知道物件由哪些部件組成。
- 建造者模式根據建造過程中的順序不一樣,最終物件部件組成也不一樣。
結構型模式描述如何將類或物件按某種佈局組成更大的結構。它分為類結構型模式和物件結構型模式,前者採用繼承機制來組織介面和類,後者釆用組合或聚合來組合物件。
由於組合關係或聚合關係比繼承關係耦合度低,滿足“合成複用原則”,所以物件結構型模式比類結構型模式具有更大的靈活性。
結構型模式分為以下 7 種:
- 代理(Proxy)模式:為某物件提供一種代理以控制對該物件的訪問。即客戶端通過代理間接地訪問該物件,從而限制、增強或修改該物件的一些特性。
- 介面卡(Adapter)模式:將一個類的介面轉換成客戶希望的另外一個介面,使得原本由於介面不相容而不能一起工作的那些類能一起工作。
- 橋接(Bridge)模式:將抽象與實現分離,使它們可以獨立變化。它是用組合關係代替繼承關係來實現的,從而降低了抽象和實現這兩個可變維度的耦合度。
- 裝飾(Decorator)模式:動態地給物件增加一些職責,即增加其額外的功能。
- 外觀(Facade)模式:為多個複雜的子系統提供一個一致的介面,使這些子系統更加容易被訪問。
- 享元(Flyweight)模式:運用共享技術來有效地支援大量細粒度物件的複用。
- 組合(Composite)模式:將物件組合成樹狀層次結構,使使用者對單個物件和組合物件具有一致的訪問性。
代理模式的定義與特點
代理模式的定義:由於某些原因需要給某物件提供一個代理以控制對該物件的訪問。這時,訪問物件不適合或者不能直接引用目標物件,代理物件作為訪問物件和目標物件之間的中介。
代理模式的主要優點有:
- 代理模式在客戶端與目標物件之間起到一箇中介作用和保護目標物件的作用;
- 代理物件可以擴充套件目標物件的功能;
- 代理模式能將客戶端與目標物件分離,在一定程度上降低了系統的耦合度,增加了程式的可擴充套件性
其主要缺點是:
- 代理模式會造成系統設計中類的數量增加
- 在客戶端和目標物件之間增加一個代理物件,會造成請求處理速度變慢;
- 增加了系統的複雜度;
代理模式的結構與實現
代理模式的結構比較簡單,主要是通過定義一個繼承抽象主題的代理來包含真實主題,從而實現對真實主題的訪問,下面來分析其基本結構和實現方法。
1. 模式的結構
代理模式的主要角色如下。
- 抽象主題(Subject)類:通過介面或抽象類宣告真實主題和代理物件實現的業務方法。
- 真實主題(Real Subject)類:實現了抽象主題中的具體業務,是代理物件所代表的真實物件,是最終要引用的物件。
- 代理(Proxy)類:提供了與真實主題相同的介面,其內部含有對真實主題的引用,它可以訪問、控制或擴充套件真實主題的功能。
其結構圖如圖 1 所示。
根據代理的建立時期,代理模式分為靜態代理和動態代理。
- 靜態:由程式設計師建立代理類或特定工具自動生成原始碼再對其編譯,在程式執行前代理類的 .class 檔案就已經存在了。
- 動態:在程式執行時,運用反射機制動態建立而成
代理模式的擴充套件
在前面介紹的代理模式中,代理類中包含了對真實主題的引用,這種方式存在兩個缺點。
- 真實主題與代理主題一一對應,增加真實主題也要增加代理。
- 設計代理以前真實主題必須事先存在,不太靈活。採用動態代理模式可以解決以上問題,如SpringAOP,其結構圖如圖 4 所示。
模式的定義與特點
介面卡模式(Adapter)的定義如下:將一個類的介面轉換成客戶希望的另外一個介面,使得原本由於介面不相容而不能一起工作的那些類能一起工作。介面卡模式分為類結構型模式和物件結構型模式兩種,前者類之間的耦合度比後者高,且要求程式設計師瞭解現有元件庫中的相關元件的內部結構,所以應用相對較少些。
該模式的主要優點如下。
- 客戶端通過介面卡可以透明地呼叫目標介面。
- 複用了現存的類,程式設計師不需要修改原有程式碼而重用現有的適配者類。
- 將目標類和適配者類解耦,解決了目標類和適配者類介面不一致的問題。
- 在很多業務場景中符合開閉原則。
其缺點是:
- 介面卡編寫過程需要結合業務場景全面考慮,可能會增加系統的複雜性。
- 增加程式碼閱讀難度,降低程式碼可讀性,過多使用介面卡會使系統程式碼變得凌亂。
package adapter; //目標介面 interface Target { public void request(); } //適配者介面 class Adaptee { public void specificRequest() { System.out.println("適配者中的業務程式碼被呼叫!"); } } //類介面卡類 class ClassAdapter extends Adaptee implements Target { public void request() { specificRequest(); } } //客戶端程式碼 public class ClassAdapterTest { public static void main(String[] args) { System.out.println("類介面卡模式測試:"); Target target = new ClassAdapter(); target.request(); } }
橋接模式的定義與特點
橋接(Bridge)模式的定義如下:將抽象與實現分離,使它們可以獨立變化。它是用組合關係代替繼承關係來實現,從而降低了抽象和實現這兩個可變維度的耦合度。
通過上面的講解,我們能很好的感覺到橋接模式遵循了里氏替換原則和依賴倒置原則,最終實現了開閉原則,對修改關閉,對擴充套件開放。這裡將橋接模式的優缺點總結如下。
橋接(Bridge)模式的優點是:
- 抽象與實現分離,擴充套件能力強
- 符合開閉原則
- 符合合成複用原則
- 其實現細節對客戶透明
缺點是:由於聚合關係建立在抽象層,要求開發者針對抽象化進行設計與程式設計,能正確地識別出系統中兩個獨立變化的維度,這增加了系統的理解與設計難度。