目錄
設計模式的意義
編寫軟件過程中,程序員面臨着來自耦合性,內聚性以及可維護性,可擴展性,複用性,靈活性等多方面的挑戰,設計模式是爲了讓程序(軟件),具有更好的
- 代碼複用性
- 可讀性
- 可擴展性
- 可靠性 (當我們增加新的功能後,對原來的功能沒有影響)
- 使程序呈現高內聚,低耦合的特性
1. 七大原則
設計模式原則,其實就是程序員在編程時,應當遵守的原則,也是各種設計模式的基礎
設計模式常用的七大原則有:
- 單一職責原則
- 接口隔離原則
- 依賴倒轉(倒置)原則
- 里氏替換原則
- 開閉原則
- 迪米特法則
- 合成複用原則
1.1 單一職責原則
大白話解釋:一個類就是一個最小的功能單位
描述
一個類應該只負責一項職責。
如類A負責兩個不同職責:職責1,職責2。 當職責1需求變更而改變A時,可能造成職責2執行錯誤,所以需要將類A的粒度分解爲 A1,A2。
實例
假設有一個“交通工具”類,他的作用只有一個,就是“運行交通工具”,假設它只有一個run方法,打印“交通工具 xx 在地上跑”這句話。
如果我們的交通工具只是車,這個類沒有問題,如果交通工具加上“飛機”、“船”,那麼“交通工具 飛機 在地上跑”、“交通工具 船 在地上跑”就不符合實際。
由於交通工具有多個,因此這個類不符合“單一職責原則”。
我們可以將其改爲3個類,“水上交通工具”、“空中交通工具”、“陸地交通工具”,分別對海陸空負責(單一職責)。
此外,由於這個類的功能比較單一,只有run方法,我們也可以在方法級別上實現單一職責原則,即爲該類創建“水上運行”、“空中運行”、“陸地運行”方法。
注意事項與細節
- 降低類的複雜度,一個類只負責一項職責。
- 提高類的可讀性,可維護性
- 降低變更引起的風險
- 通常情況下,我們應當遵守單一職責原則,只有邏輯足夠簡單,纔可以在代碼級違 反單一職責原則;
如果類中方法數量足夠少,可以在方法級別保持單一職責原則
1.2 接口隔離原則
大白話解釋:實現接口的所有類都應當覺得接口中沒有多餘的方法
Interface Segregation Principle
描述
客戶端不應該依賴它不需要的接口,即一個類對另一個類的依賴應該建立在最小的接口上。
實例
A通過調用B,需要操作1、2、3,
C通過調用D,需要操作1、4、5,
但是B、D都實現了1、2、3、4、5,顯然B、D都實現了多餘的方法。
若要符合“接口隔離原則”,只需要讓B、D實現必需的接口即可。
然而,實際中我們不一定能確定B是否真的不需要4、5方法,也不能確定D是否真的不需要2、3方法。
因此,不是說學好設計模式就萬事大吉的。
實際還得多方面考慮。
1.3 依賴倒轉(倒置)原則
大白話解釋:我們都是用接口聲明一個變量,而不是直接使用具體類(如聲明一個ArrayList,最左邊用的是List接口;聲明一個HashMap,最左邊用的是Map)
Dependence Inversion Principle
描述
- 高層模塊不應該依賴低層模塊,二者都應該依賴其抽象
- 抽象不應該依賴細節,細節應該依賴抽象
- 依賴倒轉(倒置)的中心思想是
面向接口編程
- 依賴倒轉原則是基於這樣的設計理念:相對於細節的多變性,抽象的東西要穩定的多。
以抽象爲基礎搭建的架構比以細節爲基礎的架構要穩定的多
。(在java中,抽象指的是接口或抽象類,細節就是具體的實現類) - 使用接口或抽象類的目的是制定好規範,而不涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成
就是一句話,前期設計應當從最基本、最核心的抽象入手,構建接口。
實例
用戶(User類)通過receive方法接收信息(Message類)。
如果用戶接受的消息包括Email、QQ、WeChat…等多種方式,那麼我們就需要在User類中寫多個receive重載函數分別接收不同的消息類。
然而,誰也不知道以後還會有什麼消息類,這就導致每次增加一個消息類,我們都得對User類進行修改。
如果Message不是類,而是一個接口,receive接收的是Message接口,就能解決問題。
只需要讓各種不同的消息類實現Message接口,receive就能夠接收他們;如果出現新的消息類,也只需要增加該消息類並實現Message接口即可,不需要對原有代碼進行更改。
依賴關係傳遞的方式
- 聲明接口傳遞:在普通方法參數中聲明接口
- 構造方法傳遞:在構造方法參數中聲明接口
- setter方法傳遞:類中有個接口成員,該成員通過setter聲明
注意事項和細節
- 低層模塊儘量都要有抽象類或接口,或者兩者都有,程序穩定性更好
- 變量的
聲明類型儘量是抽象類或接口
, 這樣我們的變量引用和實際對象間,就存在 一個緩衝層,利於程序擴展和優化 - 繼承時遵循里氏替換原則
1.4 里氏替換原則
大白話解釋:少繼承、別重寫父類方法
OO中的繼承,產生的問題
- 繼承包含這樣一層含義:父類中凡是已經實現好的方法,實際上是在設定規範和契約,雖然它不強制要求所有的子類必須遵循這些契約,但是如果子類對這些已經實現的方法任意修改,就會對整個繼承體系造成破壞。
- 繼承在給程序設計帶來便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加對象間的耦合性,如果一個類被其他的類所繼承, 則當這個類需要修改時,必須考慮到所有的子類,並且父類修改後,所有涉及到子類的功能都有可能產生故障
解決以上問題,考慮里氏替換原則。
描述
Liskov Substitution Principle
- 如果對每個類型爲T1的對象o1,都有類型爲T2的對象o2,使得以T1定義的所有程序P在所有的對象o1都代換成o2時,程序P的行爲沒有發生變化,那麼類型T2是類型T1 的子類型。換句話說,
所有引用基類的地方必須能透明地使用其子類的對象
。 - 在使用繼承時,遵循里氏替換原則,在子類中
儘量不要重寫父類的方法
- 里氏替換原則告訴我們,繼承實際上讓兩個類耦合性增強了,在適當的情況下,可以通過
聚合
,組合
,依賴
來解決問題。.
實例
類A的fun1是減法器,類B繼承了類A;但是類B不小心將fun1重寫成了加法器。
假設極端情況,類A就只有fun1方法,那麼類B繼承類A就沒有必要了,把A唯一的方法都重寫了。
實際編程中常常重寫父類的方法,但是整個繼承體系的複用性、穩定性較差。
一般可以這麼做:讓原來的父類A和子類B都繼承一個更通俗的基類,取消AB繼承關係,AB直接採用聚合、組合、依賴的關係實現方法調用。
比如上述實例,A和B都繼承一個基礎類Base(爲了保證一些基本方法),然後B中聲明一個成員類A,此時B可以寫一個方法調用A的專有方法即可。
1.5 開閉原則
大白話解釋:寫的系統易於擴展,不允許修改。
Open Closed Principle
描述
- 開閉原則是編程中最基礎、最重要的設計原則
- 一個軟件實體如類,模塊和函數應該
對擴展開放,對修改關閉
。用抽象構建框架,用實現擴展細節 - 當軟件需要變化時,儘量通過擴展軟件實體的行爲來實現變化,而不是通過修改已有的代碼來實現變化。
- 編程中遵循其它原則,以及使用設計模式的目的就是遵循開閉原則
1.6 迪米特法則
大白話解釋:減少類之間的依賴
描述
- 一個對象應該對其他對象保持最少的瞭解
- 類與類關係越密切,耦合度越大
迪米特法則(Demeter Principle)又叫最少知道原則,即一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類不管多麼複雜,都儘量將邏輯封裝在類的內部
。對外除了提供的public 方法,不泄露任何信息
迪米特法則還有個更簡單的定義:只與直接的朋友通信
直接的朋友:每個對象都會與其他對象有耦合關係,只要兩個對象之間有耦合關係, 我們就說這兩個對象之間是朋友關係。
耦合的方式很多,依賴,關聯,組合,聚合 等。其中,我們稱出現成員變量
,方法參數
,方法返回值
中的類
爲直接的朋友,而出現在局部變量中的類不是直接的朋友。也就是說,陌生的類最好不要以局部變量的形式出現在類的內部
。
此外,有時候我們也會引入了“陌生的朋友”而不自知。
比如較長的調用鏈,調用鏈中可能生成了多個陌生的類,這也是不被允許的。
迪米特法則的目的在於降低類之間的耦合度。
1.7 合成複用原則
大白話解釋:多用組合、聚合,少用繼承
描述
儘量使用合成/聚合的方式,而不是使用繼承。
如果僅僅是爲了讓B類使用A類的方法,就讓B繼承A,只是徒增耦合。
我們只需要在B中聚合一個A的對象,或者將A作爲B的某個方法參數。
小結
- 找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起。
- 針對接口編程,而不是針對實現編程。
- 爲了交互對象之間的松耦合設計而努力
2. UML類圖
3. 設計模式
設計模式分爲三種類型,共23種
- 創建型模式:單例模式、工廠模式、抽象工廠模式、原型模式、建造者模式。
- 結構型模式:適配器模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式。
- 行爲型模式:模板方法模式、命令模式、訪問者模式、迭代器模式、觀察者 模式、中介者模式、備忘錄模式、解釋器模式、狀態模式、策略模式、職責鏈模式(責任鏈模式)。
大白話解釋:
創建型模式:
- 單例模式:創建一個對象,使得整個系統中,這個對象有且僅有一個。
- 工廠模式:多個類的創建交給一個工廠,客戶端給出所需的類的類型,工廠返回該對象
- 抽象工廠模式:給每一類產品都創建一個工廠
- 原型模式:利用一個對象本體(稱爲原型),克隆出另一個
- 建造者模式:一個對象的創建過程比較複雜,通過一系列建造過程完成創建
結構型模式:
- 適配器模式:將類通過一個“轉換器”轉換某些屬性讓它變得適合使用
- 橋接模式:將類的某一可變屬性抽出來作爲類組合進去(幾何圖形有三種顏色,將三種顏色抽取出來作爲三個類)
- 裝飾模式:把類放進去加工一下,得到具有額外功能的類
- 組合模式:具有樹形結構的類羣,使用組合模式讓其變成一個樹
- 外觀模式:一大堆類的運行放在一個類中(外觀),一鍵調用
- 享元模式:實現元素共享
- 代理模式:爲了控制對象的訪問,加一個代理人
行爲型模式:
- 模板方法模式:把執行流程(算法)都放在一個方法中,形成一個模板
- 命令模式:將所有命令單獨抽出來作爲類來調用,而不是作爲方法
- 訪問者模式:A進入B中(B某個方法使用了A),調用A的方法訪問B(A的方法中又需要傳入B)
- 迭代器模式:遍歷類
- 觀察者模式:一對多關係時,這個一想要通知多個對象
- 中介者模式:讓客戶與多個類的溝通,都通過一箇中介類
- 備忘錄模式:記錄一個類的狀態
- 解釋器模式:解釋一串表達式
- 狀態模式:一個流程存在特別多的狀態,使用狀態模式
- 策略模式:把類的某一類方法(策略)抽出來作爲類,實現動態改變
- 職責鏈模式(責任鏈模式):攔截鏈,輪着一個攔一個
3.1單例模式
單例模式有八種實現方法(有一種是錯誤示範):
- 餓漢式(靜態常量)
- 餓漢式(靜態代碼塊)
- 懶漢式(線程不安全)
- 懶漢式(線程安全,同步方法)
- 懶漢式(線程安全,同步代碼塊)
- 雙重檢查
- 靜態內部類
- 枚舉
3.2抽象工廠模式
簡單工廠模式
AbstractProduct:抽象類產品,比如說披薩
ConcreteProduct:具體的產品,比如說中國披薩,美國披薩,巴西披薩等
SimpleFactory:用於創建披薩類,依賴於抽象披薩
FacrotyClient:工廠的使用者,通過FactoryClient,調用SimpleFactory生成不同的披薩。
3.3原型模式
原型模式(Prototype Pattern)是用於創建重複的對象
,同時又能保證性能。
Spring的bean.xml中配置的bean,scope可以選擇單例,也可以選擇prototype,即原型模式創建。
如果你的bean中有初始化信息,那麼通過prototype模式創建的bean,都會帶上這些初始化信息
原理:實現Cloneable接口,利用Object的clone,實現克隆
淺拷貝:使用clone克隆的對象,基本類型是值傳遞(新的對象),引用類型只是地址傳遞(依舊是指向舊的對象,如果此時對該對象修改,也會影響原有對象)。
深拷貝:
對於拷貝對象中的引用類型,也實現Cloneable接口,然後對其單獨處理。但是這種方式侷限性太大,不推薦,直接學習另一種方式,利用反序列化
原理:序列化之後,能夠保存所有“值信息”
反序列化得到的是一個新的對象。
public class PrototypePattern {
public static void main(String[] args) {
Sheep s1 = new Sheep("duoli","black");
Sheep friend = new Sheep("friend","red");
s1.setFriend(friend);
Sheep s2 = (Sheep) s1.deepClone();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1.getFriend()==s2.getFriend());
}
}
class Sheep implements Serializable {
private String name;
private String color;
// 深拷貝測試對象
private Sheep friend;
Sheep(String name,String color){
this.name=name;
this.color=color;
}
@Override
public String toString() {
return "Sheep{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", friend=" + friend +
'}';
}
Object deepClone() {
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
ByteArrayInputStream bis = null;
try {
// 序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
return (Sheep) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}finally {
try {
if (bos != null) {
bos.close();
}
if (oos != null) {
oos.close();
}
if (bis != null) {
bis.close();
}
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void setFriend(Sheep friend) {
this.friend = friend;
}
public Sheep getFriend() {
return friend;
}
}
- 創建新的對象比較複雜時,可以利用原型模式簡化對象的創建過程,同時也能夠提高效率
- 不用重新初始化對象,而是
動態地獲得對象運行時的狀態
- 如果原始對象發生變化(增加或者減少屬性),其它克隆對象的也會發生相應的變化,無需修改代碼
- 在實現深克隆的時候可能需要比較複雜的代碼
- 缺點:需要爲每一個類配備一個克隆方法,這對全新的類來說不是很難,但對已有的類進行改造時,需要修改其源代碼,違背了ocp原則
3.4建造者模式
BuilderPattern
蓋房子,需要逐步完成一系列工序才能得到最終的房子。
建造者模式,就是將產品(房子)與產品構造過程(一系列工序)進行解耦。
涉及角色:
產品:Product,比如一棟房子
抽象建造者:Builder,大家一直認爲施工隊該有的基本操作
具體建造者:ConcreteBuilder,實際開工的工人,只提供動作,指揮者讓幹嘛就幹嘛
指揮者:Director,工頭(隔離了老闆與工人直接接觸,負責控制房子的生產過程)
調用過程:
(客戶端)老闆告訴(指揮者)工頭,我要“這種”房子(指定建造者,
工人),工頭對這類房子的建造方式已經記住了,然後告訴工人開始搭建,最後工頭將房子交給老闆。
此處老闆指定建造者,而不是老闆定製(指定)房子,是因爲:
如果定製房子,老闆就得對房子的參數非常熟悉纔行
如果有新的建造者加入,就得更改客戶端代碼
package creationMode._4_builderPattern;
public class BuilderPattern {
public static void main(String[] args) {
// 老闆跟包工頭說要一個小平房
Director director = new Director(new ConcreteHouseBuilderOne());
House result1 = director.getResult();
System.out.println(result1);
// 老闆跟包工頭說要一棟高樓
director = new Director(new ConcreteHouseBuilderTwo());
House result2 = director.getResult();
System.out.println(result2);
}
}
// 產品:房子
class House{
private String name;
private Integer height;
House(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getHeight() {
return height;
}
public void setHeight(Integer height) {
this.height = height;
}
@Override
public String toString() {
return "House{" +
"name='" + name + '\'' +
", height=" + height +
'}';
}
}
// 抽象建造者
interface AbstractHouseBuilder{
void buildStepOne();
void buildStepTwo();
House getResule();
}
// 具體建造者:工人1號
class ConcreteHouseBuilderOne implements AbstractHouseBuilder{
private House house = new House();
@Override
public void buildStepOne() {
this.house.setName("小平房");
}
@Override
public void buildStepTwo() {
this.house.setHeight(10);
}
@Override
public House getResule() {
return house;
}
}
// 具體建造者:工人2號
class ConcreteHouseBuilderTwo implements AbstractHouseBuilder{
private House house = new House();
@Override
public void buildStepOne() {
this.house.setName("大高樓");
}
@Override
public void buildStepTwo() {
this.house.setHeight(100);
}
@Override
public House getResule() {
return house;
}
}
// 指揮者:包工頭
class Director{
private AbstractHouseBuilder builderOne;
Director(AbstractHouseBuilder builderOne){
this.builderOne = builderOne;
}
House getResult(){
builderOne.buildStepOne();
builderOne.buildStepTwo();
return builderOne.getResule();
}
}
StringBuilder使用的建造者模式
- Appendable 接口定義了多個append方法(抽象方法), 即Appendable 爲抽象建造者, 定義了抽象方法
- AbstractStringBuilder 實現了 Appendable 接口方法,這裏的AbstractStringBuilder 已經是建造者,只是不能實例化
- StringBuilder 即充當了指揮者角色,同時充當了具體的建造者,建造方法的 實現是由 AbstractStringBuilder 完成, 而StringBuilder 繼承了 AbstractStringBuilder
特點
- 客戶端(使用程序)不必知道產品內部組成的細節,將產品本身與產品的創建過程解耦,使得相同的創建過程可以創建不同的產品對象
- 每一個具體建造者都相對獨立,而與其他的具體建造者無關,因此可以很方便地替換具體建造者或增加新的具體建造者, 用戶使用不同的具體建造者即可得到不同 的產品對象
- 可以更加精細地控制產品的創建過程 。將複雜產品的創建步驟分解在不同的方法中,使得創建過程更加清晰,也更方便使用程序來控制創建過程
- 增加新的具體建造者無須修改原有類庫的代碼,指揮者類針對抽象建造者類編程, 系統擴展方便,符合 “開閉原則”
- 建造者模式所創建的產品一般具有較多的共同點,其組成部分相似,
如果產品之間的差異性很大,則不適合使用建造者模式
,因此其使用範圍受到一定的限制。 - 如果產品的內部變化複雜,可能會導致需要定義很多具體建造者類來實現這種變化, 導致系統變得很龐大,因此在這種情況下,要考慮是否選擇建造者模式.
抽象工廠模式VS建造者模式
:抽象工廠模式實現對產品家族的創建,一個產品家族是這樣的一系列產品:具有不同分類維度的產品組合,採用抽象工廠模式不需要關心構建過程
,只關心什麼產品由什麼工廠生產即可。而建造者模式則是要求按照指定的藍圖
建造產品,它的主要目的是通過完成一系列工序而產生一個新產品
3.5適配器模式
描述
- 適配器模式(
Adapter
Pattern)將某個類的接口轉換成客戶端期望的另一個接口表示,主的目的是兼容性
,讓原本因接口不匹配不能一起工作的兩個類可以協同工作。其別名爲包裝器(Wrapper
) - 適配器模式屬於結構型模式
- 主要分爲三類:
類適配器模式
、對象適配器模式
、接口適配器模式
工作原理
- 適配器模式:將一個類的接口轉換成另一種接口.讓原本接口不兼容的類可以兼 容
- 從用戶的角度看不到被適配者,是解耦的
- 用戶調用適配器轉化出來的目標接口方法,適配器再調用被適配者的相關接口方法
- 用戶收到反饋結果,感覺只是和目標接口交互,如圖
類適配器
Adapter類,通過繼承
src類,實現 dst 類接口,完成src->dst的適配。
實例
國家只提供了220V電壓,我們的手機需要5V電壓,因此需要一個適配器。
適配器繼承220V電壓(爲了拿到電壓),然後實現5V的接口(相當於適配器變壓標準)。
接着手機充電的時候,只需要遵循5V接口就行了。
然後用戶Client左拿適配器(充電器),右拿手機,就可以充電了。
注意事項
- Java是單繼承機制,所以類適配器需要繼承src類這一點算是一個缺點, 因爲這要求dst必須是接口,有一定侷限性;
- src類的方法在Adapter中都會暴露出來,也增加了使用的成本。
- 由於其繼承了src類,所以它可以根據需求重寫src類的方法,使得Adapter的靈活性增強了。
對象適配器
Adapter類,通過持有
src類,實現 dst 類接口, 完成src->dst的適配。
細節
- 對象適配器和類適配器其實算是同一種思想,只不過實現方式不同。 根據合成複用原則,使用組合替代繼承, 所以它解決了類適配器必須繼承src的 侷限性問題,也不再要求dst必須是接口。
- 使用成本更低,更靈活。
接口適配器
- 一些書籍稱爲:適配器模式(Default Adapter Pattern)或缺省適配器模式。
- 當不需要全部實現接口提供的方法時,可先設計一個抽象類實現接口,併爲該接口中每個方法提供一個默認實現(空方法),那麼該抽象類的子類可有選擇地覆蓋父類的某些方法來實現需求
- 適用於一個接口不想使用其所有的方法的情況。
實例
如上圖,接口提供了4個方法,如果我們直接實現接口,將不得不實現全部方法。
但是我們加一個適配器,實現接口中的全部方法(但是都是空方法,沒有寫具體實現),
然後A繼承適配器,就可以有選擇的重寫自己想要的方法。
Spring源碼適配器模式分析
實現邏輯:前端發起請求,DispatchServlet調用doDispatch()方法,調用controller方法。
爲什麼應用適配器模式:前端發起的請求有多種,可能需要HttpController進行處理,也可能是SimpleController或者AnnotationController進行處理,但是我們並不知道要用哪個;最簡單的方式就是在處理的時候用if-else對請求類別進行甄別,然後調用對應controller;但是,顯然這種模式違背了OCP原則。
適配器模式:
處理請求的控制器接口;
分別處理請求的三個具體控制器;
每個具體的控制器都有一個對應的適配器,這些適配器都遵循同一個適配器接口。
dispatchServlet拿到請求之後,通過適配器判斷控制器類型,並調用控制器方法。
模擬運行軌跡:
前端發起Http請求==》DispatcherServlet拿到請求,調用doDispatch()
》doDispatch()拿到該請求,交給HandlerAdapter適配器進行類別判斷》判斷結果爲Http請求,此時用HttpHandlerAdapter調用HTTPController中的方法,處理請求。
適配器模式注意事項
- 三種命名方式,是根據 src是以怎樣的形式給到Adapter(在Adapter裏的形式)來命名的。
- 類適配器:以類給到,在Adapter裏,就是將src當做類,
繼承
對象適配器:以對象給到,在Adapter裏,將src作爲一個對象,持有
接口適配器:以接口給到,在Adapter裏,將src作爲一個接口,實現
- Adapter模式最大的作用還是將原本不兼容的接口融合在一起工作。
- 實際開發中,實現起來不拘泥於上述三種經典形式
3.7橋接模式
考慮這麼一個問題:有“圓形”“正方形”“三角形”三個形狀,然後有“紅色”“綠色”“藍色”三種顏色,現在我們需要“紅色正方形”、“綠色正方形”、“藍色正方形”、“紅色三角形”、“綠色三角形”、“藍色三角形”、“紅色圓形”、“綠色圓形”、“藍色圓形”,需要幾個類?
如果是按x色x形分別建類,那麼顯然需要3*3個類。
這種實現快速簡單,但是卻難以擴展。比如我們現在多了一個綠色,那麼就得再建三個“綠色xx形”,形成類爆炸。
這就是理解橋接模式的方式——屬性維度。
將每一維,都單獨抽出來,然後通過組合聚合的形式放在一個橋接類中。
這個“抽象”與“實現”的區別,網上並沒有找到結論。
個人覺得,沒啥區別,抽象接口直接跟Client交互,並且組合實現類,因此,可以將重要的“屬性”作爲抽象。
或者說,抽象與實現是主從關係,實現屬於抽象,比如說“顏色屬於形狀,因此形狀是抽象,顏色是實現”。
此外,個人覺得這個二維維度可以擴展成多維。
3.8裝飾模式
定義
在不改變原有對象的基礎之上,將功能附加到對象上。提供了比繼承更有彈性的替代方案(擴展原有對象功能)
優點
- 擴展一個類的功能或者給一個類添加附加職責
- 給一個對象動態的添加功能,或動態撤銷功能。
類圖特點:
- 被裝飾者和裝飾器都實現同一個接口(抽象類),該接口中有被裝飾者的
可裝飾屬性
相關方法比如說用調味品裝飾咖啡,那麼咖啡的描述和價格就是可裝飾屬性,此時該頂級接口應當帶上描述和價格,只有這樣,才能在裝飾後對這些屬性進行動態修改。
3.9組合模式
常見這種形式:
- 一個用戶有多個訂單,一個訂單有多個訂單詳情,一個訂單詳情有多個商品
- 一個學校有多個學院,一個學院有多個系,一個繫有多個班,一個班有多個同學
這種模式,可以抽象爲一棵樹
。
學校是一個根節點,同學是葉子節點。
這就是組合模式。
- ClientApp是調用方,只跟Component發生關聯
- Component是抽象節點,包含根節點和葉子節點的所有默認行爲
- Composite是非葉子節點
- Leaf是葉子節點。
注意:
- Component作爲抽象類時,將
葉子節點也包含的行爲
作爲抽象方法,讓葉子也能實現;將葉子節點不包含的行爲
(如移除子節點和增加子節點)寫一個默認實現,拋出不支持調用的異常,讓其他子節點自己實現。 - 一般情況下,儘管都是非葉子節點,但是也可能實現不一致(比如增加一個學院和增加一個系,實際邏輯不一樣),因此不能只寫一個非葉子節點,可能存在多個很相似的非葉子節點
其他細節:
- 簡化客戶端操作。客戶端只需要面對一致的對象而
不用考慮整體部分或者節點葉子
的問題。 - 具有較強的擴展性。當我們要更改組合對象時,我們只需要調整內部的層次關係, 客戶端不用做出任何改動.
- 方便創建出複雜的層次結構。客戶端不用理會組合裏面的組成細節,容易添加節點 或者葉子從而創建出複雜的樹形結構
- 需要遍歷組織機構,或者處理的對象具有樹形結構時, 非常適合使用組合模式.
- 要求較高的抽象性,
如果節點和葉子有很多差異性的話,比如很多方法和屬性都不一樣,不適合使用組合模式
劃重點:樹形結構適合、能幫助客戶端忽略節點和葉子的差異性、如果節點和葉子本身存在較大差異則不適合使用組合模式(比如學校和學生的操作肯定存在較大差異,建議將學生、班級從這個樹狀結構排除,讓系作爲葉子節點)
3.10外觀模式
考慮:
家庭影院系統
- 幕布的開關
- 投影儀的開關,亮度調節,節目選擇
- 躺椅的準備與收起
看電影的步驟:
準備躺椅
打開幕布
打開投影儀,調節亮度,選擇節目
結束看電影的步驟:
關閉投影儀
關閉幕布
收起躺椅
如果是用戶直接面對躺椅、幕布、投影儀,那他就得一步一步操作(儘管這裏看起來不復雜),假設是一個子系統非常多的大系統,那麼這些一步一步操作就會非常複雜。
可以這麼做:
弄一個Facade類,組合
幕布、投影儀、躺椅,用戶只需要輸入一個電影名,這個影院系統就自動完成所有準備工作。
外觀模式的注意事項和細節
- 外觀模式對外屏蔽了子系統的細節,因此外觀模式降低了客戶端對子系統使用的複雜性
- 外觀模式對客戶端與子系統的耦合關係,讓子系統內部的模塊更易維護和擴展
- 通過合理的使用外觀模式,可以幫我們更好的劃分訪問的層次
- 當系統需要進行
分層設計
時,可以考慮使用Facade模式 - 在
維護一個遺留
的大型系統時,可能這個系統已經變得非常難以維護和擴展,此時可以考慮爲新系統開發一個Facade類,來提供遺留系統的比較清晰簡單的接口, 讓新系統與Facade類交互,提高複用性 - 不能過多的或者不合理的使用外觀模式,使用外觀模式好,還是直接調用模塊好。 要以
讓系統有層次,利於維護爲目的
。