10.享元模式
10.1.課程目標
1、掌握享元模式和組合模式的應用場景。
2、瞭解享元模式的內部狀態和外部狀態。
3、掌握組合模式的透明寫法和安全寫法。
4、享元模式和組合模式的的優缺點。
10.2.內容定位
適合有項目開發經驗的人群。
10.3.享元模式定義
面向對象技術可以很好地解決一些靈活性或可擴展性問題,但在很多情況下需要在系統中增加類和 對象的個數。當對象數量太多時,將導致運行代價過高,帶來性能下降等問題。享元模式正是為解決這 一類問題而誕生的。
享元模式(Flyweight Pattern)又稱為輕量級模式,是對象池的一種實現。類似於線程池,線程池 可以避免不停的創建和銷燬多個對象,消耗性能。提供了減少對象數量從而改善應用所需的對象結構的 方式。其宗旨是共享細粒度對象,將多個對同一對象的訪問集中起來,不必為每個訪問者創建一個單獨 的對象,以此來降低內存的消耗,屬於結構型模式。
原文:Use sharing to support large numbers of fine-grained objects efficiently.
解釋:使用共享對象可有效地支持大量的細粒度的對象。
享元模式把一個對象的狀態分成內部狀態和外部狀態,內部狀態即是不變的,外部狀態是變化的;然後通過共享不變的部分,達到減少對象數量並節約內存的目的。
享元模式模式的本質是緩存共享對象,降低內存消耗。
首先我們來看享元模式的通用UML類圖:
從類圖上看,享元模式有三個參與角色:
抽象享元角色(Flyweight):享元對象抽象基類或者接口,同時定義出對象的外部狀態和內部狀態 的接口或實現;
具體享元角色(ConcreteFlyweight):實現抽象角色定義的業務。該角色的內部狀態處理應該與 環境無關,不能出現會有一個操作改變內部狀態,同時修改了外部狀態;
享元工廠(FlyweightFactory):負責管理享元對象池和創建享元對象。
10.4.享元模式的應用場景
當系統中多處需要同一組信息時,可以把這些信息封裝到一個對象中,然後對該對象進行緩存,這 樣,一個對象就可以提供給多處需要使用的地方,避免大量同一對象的多次創建,消耗大量內存空間。
享元模式其實就是工廠模式的一個改進機制,享元模式 同樣要求創建一個或一組對象,並且就是通 過工廠方法生成對象的,只不過享元模式中為工廠方法增加了緩存這一功能。主要總結為以下應用場景:
1、常常應用於系統底層的開發,以便解決系統的性能問題。
2、系統有大量相似對象、需要緩衝池的場景。
在生活中的享元模式也很常見,比如各中介機構的房源共享,再比如全國社保聯網。
10.5.使用享元模式實現共享池業務
下面我們舉個例子,我們每年春節為了搶到一張回家的火車票都要大費周折,進而出現了很多刷票 軟件,刷票軟件會將我們填寫的信息緩存起來,然後定時檢查餘票信息。搶票的時候,我們肯定是要查 詢下有沒有我們需要的票信息,這裡我們假設一張火車的信息包含:出發站,目的站,價格,座位類別。 現在要求編寫一個查詢火車票查詢偽代碼,可以通過出發站,目的站查到相關票的信息。
比如要求通過出發站,目的站查詢火車票的相關信息,那麼我們只需構建出火車票類對象,然後提 供一個查詢出發站,目的站的接口給到客戶進行查詢即可,具體代碼如下。
創建ITicket接口:
<code> public interface ITicket {
void showInfo(String bunk);
}/<code>
然後,創建TrainTicket接口:
<code> public class TrainTicket implements ITicket {
private String from;
private String to;
private int price;
public TrainTicket(String from, String to) {
this.from = from;
this.to = to;
}
public void showInfo(String bunk) {
this.price = new Random().nextInt(500);
System.out.println(String.format("%s->%s:%s價格:%s 元", this.from, this.to, bunk, this.price));
}
}/<code>
最後創建TicketFactory 類:
<code> class TicketFactory {
public static ITicket queryTicket(String from, String to) {
return new TrainTicket(from, to);
}
}/<code>
編寫客戶端代碼:
<code> public class Test {
public static void main(String[] args) {
ITicket ticket = TicketFactory.queryTicket("北京西", "長沙");
ticket.showInfo("硬座");
ticket = TicketFactory.queryTicket("北京西", "長沙");
ticket.showInfo("軟座");
ticket = TicketFactory.queryTicket("北京西", "長沙");
ticket.showInfo("硬臥");
}
}/<code>
分析上面的代碼,我們發現客戶端進行查詢時,系統通過TicketFactory 直接創建一個火車票對象, 但是這樣做的話,當某個瞬間如果有大量的用戶請求同一張票的信息時,系統就會創建出大量該火車票 對象,系統內存壓力驟增。而其實更好的做法應該是緩存該票對象,然後複用提供給其他查詢請求,這 樣一個對象就足以支撐數以千計的查詢請求,對內存完全無壓力,使用享元模式可以很好地解決這個問 題。
我們繼續優化代碼,只需在TicketFactory 類中進行更改,增加緩存機制:
<code> class TicketFactory {
private static Map<string> sTicketPool = new ConcurrentHashMap<string>();
public static ITicket queryTicket(String from, String to) {
String key = from + "->" + to;
if (TicketFactory.sTicketPool.containsKey(key)) {
System.out.println("使用緩存:" + key);
return TicketFactory.sTicketPool.get(key);
}
System.out.println("首次查詢,創建對象: " + key);
ITicket ticket = new TrainTicket(from, to);
TicketFactory.sTicketPool.put(key, ticket);
return ticket;
}
}/<string>/<string>/<code>
運行結果如下:
<code> 首次查詢,創建對象: 北京西->長沙
北京西->長沙:硬座價格:285 元
使用緩存:北京西->長沙
北京西->長沙:軟座價格:202 元
使用緩存:北京西->長沙
北京西->長沙:硬臥價格:12 元/<code>
可以看到,除了第一次查詢創建對象後,後續查詢相同車次票信息都是使用緩存對象,無需創建新 對象了。來看一下類結構圖:
其中 ITicket就是抽象享元角色,TrainTicket 就是具體享元角色,TicketFactory 就是享元工廠。有 些小夥伴一定會有疑惑了,這不就是註冊式單例模式嗎?對,這就是註冊式單例模式。雖然,結構上很 像,但是享元模式的重點在結構上,而不是在創建對象上。後面看看享元模式在 JDK源碼中的一個應用, 大家應該就能徹底清除明白了。
再比如,我們經常使用的數據庫連接池,因為我們使用 Connection對象時主要性能消耗在建立連 接和關閉連接的時候,為了提高Connection在調用時的性能,我們和將Connection對象在調用前創建好緩存起來,用的時候從緩存中取值,用完再放回去,達到資源重複利用的目的。來看下面的代碼:
<code> public class ConnectionPool {
private Vector<connection> pool;
private String url = "jdbc:mysql://localhost:3306/test";
private String username = "root";
private String password = "root";
private String driverClassName = "com.mysql.jdbc.Driver";
private int poolSize = 100;
public ConnectionPool() {
pool = new Vector<connection>(poolSize);
try {
Class.forName(driverClassName);
for (int i = 0; i < poolSize; i++) {
Connection conn = DriverManager.getConnection(url, username, password);
pool.add(conn);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized Connection getConnection() {
if (pool.size() > 0) {
Connection conn = pool.get(0);
pool.remove(conn);
return conn;
}
return null;
}
public synchronized void release(Connection conn) {
pool.add(conn);
}
}/<connection>/<connection>/<code>
這樣的連接池,普遍應用於開源框架,有效提升底層的運行性能。
10.6.享元模式在源碼中的應用
1、String中的享元模式
Java中將 String類定義為final(不可改變的),JVM中字符串一般保存在字符串常量池中,java 會確保一個字符串在常量池中只有一個拷貝,這個字符串常量池在 JDK6.0以前是位於常量池中,位於 永久代,而在JDK7.0中,JVM將其從永久代拿出來放置於堆中。
我們做一個測試:
<code> public class StringTest {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = "he" + "llo";
String s4 = "hel" + "lo";
String s5 = "hello";
String s6 = s5.intern();
String s7 = "h";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//true
System.out.println(s1 == s4);//false
System.out.println(s1 == s9);//false
System.out.println(s4 == s5);//false
System.out.println(s1 == s6);//true
}
}/<code>
String 類的 final 修飾的,以字面量的形式創建 String 變量時,JVM 會在編譯期間就把該字面量 "hello"放到字符串常量池中,由 Java程序啟動的時候就已經加載到內存中了。這個字符串常量池的特 點就是有且只有一份相同的字面量,如果有其它相同的字面量,JVM則返回這個字面量的引用,如果沒 有相同的字面量,則在字符串常量池創建這個字面量並返回它的引用。
由於 s2 指向的字面量"hello"在常量池中已經存在了(s1 先於s2),於是JVM就返回這個字面量 綁定的引用,所以s1==s2。
s3 中字面量的拼接其實就是"hello",JVM在編譯期間就已經對它進行優化,所以 s1 和 s3 也是相 等的。
s4 中的 new String("lo")生成了兩個對象,lo,new String("lo"),lo 存在字符串常量池,new String("lo")存在堆中,String s4 = "hel" + new String("lo")實質上是兩個對象的相加,編譯器不會進 行優化,相加的結果存在堆中,而s1存在字符串常量池中,當然不相等。s1==s9的原理一樣。
s4==s5兩個相加的結果都在堆中,不用說,肯定不相等。
s1==s6 中,s5.intern()方法能使一個位於堆中的字符串在運行期間動態地加入到字符串常量池中 (字符串常量池的內容是程序啟動的時候就已經加載好了),如果字符串常量池中有該對象對應的字面 量,則返回該字面量在字符串常量池中的引用,否則,創建複製一份該字面量到字符串常量池並返回它 的引用。因此s1==s6輸出true。
2、Integer中的享元模式
再舉例一個大家都非常熟悉的對象Integer,也用到了享元模式,其中暗藏玄機,我們來看個例子:
<code>public class IntegerTest {
public static void main(String[] args) {
Integer a = Integer.valueOf(100);
Integer b = 100;
Integer c = Integer.valueOf(1000);
Integer d = 1000;
System.out.println("a==b:" + (a == b));
System.out.println("c==d:" + (c == d));
}
}/<code>
大家猜猜看它的運行結果是什麼?我們跑完程序之後才發現總有些不對,得到了一個意向不到的結 果,其運行結果如下:
<code>a==b:true
c==d:false/<code>
之所以得到這樣的結果,是因為Integer用到的享元模式,我們來看Integer的源碼:
<code>public final class Integer extends Number implements Comparable<integer> {
...
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
...
}/<integer>/<code>
我們發現Integer源碼中的valueOf()方法做了一個條件判斷,如果目標值在-128到 127之間,則 直接從緩存中取值,否則新建對象。那JDK為何要這樣做呢?因為在-128到 127之間的數據在int範 圍內是使用最頻繁的,為了節省頻繁創建對象帶來的內存消耗,這裡就用到了享元模式,來提高性能。
3、Long中的享元模式
<code>public final class Long extends Number implements Comparable<long> {
...
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
private static class LongCache {
private LongCache(){}
static final Long cache[] = new Long[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
...
}/<long>/<code>
同理,Long 中也有緩存,不過不能指定緩存最大值。
4、Apache Commons Pool2中的享元模式
對象池化的基本思路是:將用過的對象保存起來,等下一次需要這種對象的時候,再拿出來重複使 用,從而在一定程度上減少頻繁創建對象所造成的開銷。用於充當保存對象的“容器”的對象,被稱為 “對象池”(Object Pool,或簡稱Pool)。
ApacheCommonsPool實現了對象池的功能。定義了對象的生成、銷燬、激活、鈍化等操作及其 狀態轉換,並提供幾個默認的對象池實現。有幾個重要的對象: PooledObject(池對象):用於封裝對象(如:線程、數據庫連接、TCP連接),將其包裹成可被池 管理的對象。
PooledObjectFactory(池對象工廠):定義了操作 PooledObject 實例生命週期的一些方法, PooledObjectFactory 必須實現線程安全。
ObjectPool (對象池):ObjectPool負責管理PooledObject,如:借出對象,返回對象,校驗對象, 有多少激活對象,有多少空閒對象。
<code>private final Map<identitywrapper>, PooledObject> allObjects; /<identitywrapper>/<code>
這裡我們就不分析其具體源碼了。
10.7.享元模式的內部狀態和外部狀態
享元模式的定義為我們提出了兩個要求:細粒度和共享對象。因為要求細粒度對象,所以不可避免 地會使對象數量多且性質相近,此時我們就將這些對象的信息分為兩個部分:內部狀態和外部狀態。
內部狀態指對象共享出來的信息,存儲在享元對象內部並且不會隨環境的改變而改變;外部狀態指 對象得以依賴的一個標記,是隨環境改變而改變的、不可共享的狀態。
比如,連接池中的連接對象,保存在連接對象中的用戶名、密碼、連接 url等信息,在創建對象的 時候就設置好了,不會隨環境的改變而改變,這些為內部狀態。而每個連接要回收利用時,我們需要給 它標記為可用狀態,這些為外部狀態。
10.8.享元模式的優缺點
優點:
1、減少對象的創建,降低內存中對象的數量,降低系統的內存,提高效率;
2、減少內存之外的其他資源佔用。
缺點:
1、關注內、外部狀態、關注線程安全問題;
2、使系統、程序的邏輯複雜化。
11.組合模式
11.1.組合模式定義
我們知道古代的皇帝想要管理國家,是不可能直接管理到具體每一個老百姓的,因此設置了很多機 構,比如說三省六部,這些機構下面又有很多小的組織。他們共同管理著這個國家。再比如說,一個大 公司,下面有很多小的部門,每一個部門下面又有很多個部門。說到底這就是組合模式。
組合模式(Composite Pattern)也稱為整體-部分(Part-Whole)模式,它的宗旨是通過將單個 對象(葉子節點)和組合對象(樹枝節點)用相同的接口進行表示,使得客戶對單個對象和組合對象的 使用具有一致性,屬於結構型模式。
原文:Compose objects into tree structures to represent part-whole hierarchies.Composite lets clients treat individual objects and compositions of objects uniformly.
解釋:將對象組合成樹形結構以表示 “部分-整體” 的層次結構,使得用戶對單個對象和組合對象的使用具有一致性。
組合關係與聚合關係的區別:
1、組合關係:在古代皇帝三宮六院,貴妃很多,但是每一個貴妃只屬於皇帝(具有相同的生命週期)。
2、聚合關係:一個老師有很多學生,但是每一個學生又屬於多個老師(具有不同的生命週期)。
組合模式一般用來描述整體與部分的關係,它將對象組織到樹形結構中,最頂層的節點稱為根節點, 根節點下面可以包含樹枝節點和葉子節點,樹枝節點下面又可以包含樹枝節點和葉子節點。如下圖所示:
由上圖可以看出,其實根節點和樹枝節點本質上是同一種數據類型,可以作為容器使用;而葉子節 點與樹枝節點在語義上不屬於同一種類型,但是在組合模式中,會把樹枝節點和葉子節點認為是同一種 數據類型(用同一接口定義),讓它們具備一致行為。這樣,在組合模式中,整個樹形結構中的對象都 是同一種類型,帶來的一個好處就是客戶無需辨別樹枝節點還是葉子節點,而是可以直接進行操作,給 客戶使用帶來極大的便利。
組合模式包含3個角色:
1、抽象根節點(Component):定義系統各層次對象的共有方法和屬性,可以預先定義一些默認 行為和屬性; 2、樹枝節點(Composite):定義樹枝節點的行為,存儲子節點,組合樹枝節點和葉子節點形成 一個樹形結構; 3、葉子節點(Leaf):葉子節點對象,其下再無分支,是系統層次遍歷的最小單位。
組合模式 在代碼具體實現上,有兩種不同的方式,分別是透明組合模式和安全組合模式。
11.2.組合模式的應用場景
當子系統與其內各個對象層次呈現樹形結構時,可以使用組合模式讓子系統內各個對象層次的行為 操作具備一致性。客戶端使用該子系統內任意一個層次對象時,無須進行區分,直接使用通用操作即可,為客戶端的使用帶來了便捷。
注:如果樹形結構系統不使用組合模式進行架構,那麼按照正常的思維邏輯,對該系統進行職責分析,按上文樹形結 構圖所示,該系統具備兩種對象層次類型:樹枝節點和葉子節點。那麼我們就需要構造兩種對應的類型,然後由於樹 枝節點具備容器功能,因此樹枝節點類內部需維護多個集合存儲其他對象層次(如:List<composite>,List<leaf>), 如果當前系統對象層次更復雜時,那麼樹枝節點內就又要增加對應的層次集合,這對樹枝節點的構建帶來了巨大的復 雜性,臃腫性以及不可擴展性。同時客戶端訪問該系統層次時,還需進行層次區分,這樣才能使用對應的行為,給客 戶端的使用也帶來了巨大的複雜性。而如果使用組合模式構建該系統,由於組合模式抽取了系統各個層次的共性行為, 具體層次只需按需實現所需行為即可,這樣子系統各個層次就都屬於同一種類型,所以樹枝節點只需維護一個集合 (List<component>)即可存儲系統所有層次內容,並且客戶端也無需區分該系統各個層次對象,對內系統架構簡潔優 雅,對外接口精簡易用。/<component>/<leaf>/<composite>
先對組合模式主要總結為以下應用場景:
1、希望客戶端可以忽略組合對象與單個對象的差異時;
2、對象層次具備整體和部分,呈樹形結構。
在我們生活中的組合模式也非常常見,比如樹形菜單,操作系統目錄結構,公司組織架構等。
11.3.透明組合模式的寫法
透明組合模式是把所有公共方法都定義在Component 中,這樣做的好處是客戶端無需分辨是葉子 節點(Leaf)和樹枝節點(Composite),它們具備完全一致的接口。其 UML 類圖如下所示:
來看一個例子,還是以咕泡的課程為例。這次我們來設計一個課程的關係結構。比如我們有Java入 門課程、人工智能課程、Java設計模式、源碼分析、軟技能等,而 Java設計模式、源碼分析、軟技能 又屬於 Java架構師系列課程包,每個課程的定價都不一樣。但是,這些課程不論怎麼組合,都有一些 共性,而且是整體和部分的關係,可以用組合模式來設計。先創建一個頂層的抽象組件 CourseComponent類:
<code>public abstract class CourseComponent {
public void addChild(CourseComponent catalogComponent){
throw new UnsupportedOperationException("不支持添加操作");
}
public void removeChild(CourseComponent catalogComponent){
throw new UnsupportedOperationException("不支持刪除操作");
}
public String getName(CourseComponent catalogComponent){
throw new UnsupportedOperationException("不支持獲取名稱操作");
}
public double getPrice(CourseComponent catalogComponent){
throw new UnsupportedOperationException("不支持獲取價格操作");
}
public void print(){
throw new UnsupportedOperationException("不支持打印操作");
}
}/<code>
把所有可能用到的方法都定義到這個最頂層的抽象類中,但是不寫任何邏輯處理的代碼,而是直接 拋異常。這裡,有些小夥伴會有疑惑,為什麼不用抽象方法?因為,用了抽象方法,其子類就必須實現, 這樣便體現不出各子類的細微差異。因此,子類繼承此抽象類後,只需要重寫有差異的方法覆蓋父類的 方法即可。下面我們分別創建課程類Course和課程包CoursePackage類。先創建Course 類:
<code>public class Course extends CourseComponent {
private String name;
private double price;
public Course(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String getName(CourseComponent catalogComponent) {
return this.name;
}
@Override
public double getPrice(CourseComponent catalogComponent) {
return this.price;
}
@Override
public void print() {
System.out.println(name + " (¥" + price + "元)");
}
}/<code>
再創建CoursePackage類:
<code>public class CoursePackage extends CourseComponent {
private List<coursecomponent> items = new ArrayList<coursecomponent>();
private String name;
private Integer level;
public CoursePackage(String name, Integer level) {
this.name = name;
this.level = level;
}
@Override
public void addChild(CourseComponent catalogComponent) {
items.add(catalogComponent);
}
@Override
public String getName(CourseComponent catalogComponent) {
return this.name;
}
@Override
public void removeChild(CourseComponent catalogComponent) {
items.remove(catalogComponent);
}
@Override
public void print() {
System.out.println(this.name);
for(CourseComponent catalogComponent : items){
//控制顯示格式
if(this.level != null){
for(int i = 0; i < this.level; i ++){
//打印空格控制格式
System.out.print(" ");
}
for(int i = 0; i < this.level; i ++){
//每一行開始打印一個+號
if(i == 0){ System.out.print("+"); }
System.out.print("-");
}
}
//打印標題
catalogComponent.print();
}
}
}/<coursecomponent>/<coursecomponent>/<code>
來看測試代碼:
<code>public class Test {
public static void main(String[] args) {
System.out.println("============透明組合模式===========");
CourseComponent javaBase = new Course("Java入門課程",8280);
CourseComponent ai = new Course("人工智能",5000);
CourseComponent packageCourse = new CoursePackage("Java架構師課程",2);
CourseComponent design = new Course("Java設計模式",1500);
CourseComponent source = new Course("源碼分析",2000);
CourseComponent softSkill = new Course("軟技能",3000);
packageCourse.addChild(design);
packageCourse.addChild(source);
packageCourse.addChild(softSkill);
CourseComponent catalog = new CoursePackage("課程主目錄",1);
catalog.addChild(javaBase);
catalog.addChild(ai);
catalog.addChild(packageCourse);
catalog.print();
}
}/<code>
運行結果:
<code>============透明組合模式===========
課程主目錄
+-Java入門課程 (¥8280.0元)
+-人工智能 (¥5000.0元)
+-Java架構師課程
+--Java設計模式 (¥1500.0元)
+--源碼分析 (¥2000.0元)
+--軟技能 (¥3000.0元)/<code>
透明組合模式把所有公共方法都定義在 Component 中,這樣做的好處是客戶端無需分辨是葉子 節點(Leaf)和樹枝節點(Composite),它們具備完全一致的接口;缺點是葉子節點(Leaf)會繼承 得到一些它所不需要(管理子類操作的方法)的方法,這與設計模式 接口隔離原則相違背。
為了讓大家更加透徹理解,下面我們來看安全組合模式的寫法。
11.4.安全組合模式的寫法
安全組合模式是隻規定系統各個層次的最基礎的一致行為,而把組合(樹節點)本身的方法(管理 子類對象的添加,刪除等)放到自身當中。其UML類圖如下所示:
再舉一個程序員更熟悉的例子。對於程序員來說,電腦是每天都要接觸的。電腦的文件系統其實就 是一個典型的樹形結構,目錄包含文件夾和文件,文件夾裡面又可以包含文件夾和文件···下面我們就用代碼來實現一個目錄系統。
文件系統有兩個大的層次:文件夾,文件。其中,文件夾能容納其他層次,為樹枝節點;文件為最 小單位,為葉子節點。由於目錄系統層次較少,且樹枝節點(文件夾)結構相對穩定,而文件其實可以 有很多類型,所以這裡我們選擇使用 安全組合模式 來實現目錄系統,可以避免為葉子類型(文件)引 入冗餘方法。先創建最頂層的抽象組件Directory 類:
<code>public abstract class Directory {
protected String name;
public Directory(String name) {
this.name = name;
}
public abstract void show();
}/<code>
然後分別創建File類和Folder類。先看File類:
<code>public class File extends Directory {
public File(String name) {
super(name);
}
@Override
public void show() {
System.out.println(this.name);
}
}/<code>
然後創建Folder類。
<code>public class Folder extends Directory {
private List<directory> dirs;
private Integer level;
public Folder(String name,Integer level) {
super(name);
this.level = level;
this.dirs = new ArrayList<directory>();
}
@Override
public void show() {
System.out.println(this.name);
for (Directory dir : this.dirs) {
//控制顯示格式
if(this.level != null){
for(int i = 0; i < this.level; i ++){
//打印空格控制格式
System.out.print(" ");
}
for(int i = 0; i < this.level; i ++){
//每一行開始打印一個+號
if(i == 0){ System.out.print("+"); }
System.out.print("-");
}
}
//打印名稱
dir.show();
}
}
public boolean add(Directory dir) {
return this.dirs.add(dir);
}
public boolean remove(Directory dir) {
return this.dirs.remove(dir);
}
public Directory get(int index) {
return this.dirs.get(index);
}
public void list(){
for (Directory dir : this.dirs) {
System.out.println(dir.name);
}
}
}/<directory>/<directory>/<code>
注意Folder類不僅覆蓋了頂層的show()方法,而且還增加了list()方法。看測試代碼:
<code>class Test {
public static void main(String[] args) {
System.out.println("============安全組合模式===========");
File qq = new File("QQ.exe");
File wx = new File("微信.exe");
Folder office = new Folder("辦公軟件",2);
File word = new File("Word.exe");
File ppt = new File("PowerPoint.exe");
File excel = new File("Excel.exe");
office.add(word);
office.add(ppt);
office.add(excel);
Folder wps = new Folder("金山軟件",3);
wps.add(new File("WPS.exe"));
office.add(wps);
Folder root = new Folder("根目錄",1);
root.add(qq);
root.add(wx);
root.add(office);
System.out.println("----------show()方法效果-----------");
root.show();
System.out.println("----------list()方法效果-----------");
root.list();
}
}/<code>
運行結果如下:
<code>============安全組合模式===========
----------show()方法效果-----------
根目錄
+-QQ.exe
+-微信.exe
+-辦公軟件
+--Word.exe
+--PowerPoint.exe
+--Excel.exe
+--金山軟件
+---WPS.exe
----------list()方法效果-----------
QQ.exe
微信.exe
辦公軟件
Disconnected from the target VM, address: '127.0.0.1:6205', transport: 'socket'
Process finished with exit code 0/<code>
安全組合模式的好處是接口定義職責清晰,符合設計模式 單一職責原則 和 接口隔離原則;缺點是 客戶需要區分樹枝節點(Composite)和葉子節點(Leaf),這樣才能正確處理各個層次的操作,客戶 端無法依賴抽象(Component),違背了設計模式依賴倒置原則。
11.5.組合模式在源碼中的應用
組合模式在源碼中應用也是非常廣泛的。首先我們來看一個非常熟悉的HashMap,他裡面有一個 putAll()方法:
<code>public class HashMapextends AbstractMap /<code>
implements Map, Cloneable, Serializable {
...
public void putAll(Map extends K, ? extends V> m) {
putMapEntries(m, true);
}
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
```
}
我們看到 putAll()方法傳入的是 Map 對象,Map 就是一個抽象構件(同時這個構件中只支持鍵值 對的存儲格式),而HashMap是一箇中間構件,HashMap 中的Node節點就是葉子節點。說到中間 構件就會有規定的存儲方式。HashMap中的存儲方式是一個靜態內部類的數組Node
<code> static class Nodeimplements Map.Entry /<code>{
final int hash;
final K key;
V value;
Nodenext;
Node(int hash, K key, V value, Nodenext) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry,?> e = (Map.Entry,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Nodee; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
同理,我們常用的ArrayList對象也有addAll()方法,其參數也是ArrayList 的父類Collection,來 看源代碼:
<code>public class ArrayListextends AbstractList /<code>
implements List, RandomAccess, Cloneable, java.io.Serializable
{
\t...
public boolean addAll(Collection extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
...
}
組合對象和被組合對象都應該有統一的接口實現或者統一的抽象父類。在這裡我再舉一個開源框架 中非常經典的案例,MyBatis 解析各種 Mapping 文件中的 SQL,設計了一個非常關鍵的類叫做 SqlNode,xml 中的每一個 Node 都會解析為一個 SqlNode對象,最後把所有的 SqlNode 拼裝到一 起就成了一條完整的SQL語句,它的頂層設計非常簡單。來看源代碼:
<code>public interface SqlNode {
boolean apply(DynamicContext context);
} /<code>
Apply()方法會根據傳入的參數 context,參數解析該 SqlNode 所記錄的 SQL 片段,並調用 DynamicContext.appendSql()方法將解析後的SQL片段追加到DynamicContext.的sqlBuilder中保存。當 SQL節點下的所有SqlNode完成解析後,可以通過DynamicContext.getSql()獲取一條完成的 SQL語句。對具體源碼實現感興趣的小夥伴可以去研究一下,我們這裡給大家展示一下類圖:
11.6.組合模式的優缺點
很多小夥伴肯定還有個疑問,既然組合模式會被分為兩種實現,那麼肯定是不同的場合某一種會更 加適合,也即具體情況具體分析。透明組合模式將公共接口封裝到抽象根節點(Component)中,那 麼系統所有節點就具備一致行為,所以如果當系統絕大多數層次具備相同的公共行為時,採用透明組合 模式也許會更好(代價:為剩下少數層次節點引入不需要的方法);而如果當系統各個層次差異性行為 較多或者樹節點層次相對穩定(健壯)時,採用安全組合模式
注:設計模式的出現並不是說我們要寫的代碼一定要遵循設計模式所要求的方方面面,這是不現實同時也是不可能的。 設計模式的出現,其實只是強調好的代碼所具備的一些特徵(六大設計原則),這些特徵對於項目開發是具備積極效 應的,但不是說我們每實現一個類就一定要全部滿足設計模式的要求,如果真的存在完全滿足設計模式的要求,反而 可能存在過度設計的嫌疑。同時,23 種設計模式,其實都是嚴格依循設計模式六大原則進行設計,只是不同的模式在 不同的場景中會更加適用。設計模式的理解應該重於意而不是形,真正編碼時,經常使用的是某種設計模式的變形體, 真正切合項目的模式才是正確的模式。
下面我們再來總結一下組合模式的優缺點。
優點:
1、清楚地定義分層次的複雜對象,表示對象的全部或部分層次
2、讓客戶端忽略了層次的差異,方便對整個層次結構進行控制
3、簡化客戶端代碼
4、符合開閉原則
缺點:
1、限制類型時會較為複雜
2、使設計變得更加抽象
11.7.作業
1、你還能舉出哪些關於享元模式的應用場景?
例如:TCP連接池,線程池,數據庫連接池,字符串常量池,包裝類常量池,JVM。
2、請用組合模式實現一個無限級擴展的樹(提示,可以引入xpath)
展示一個這種效果的樹
1.抽象根節點
<code>public abstract class Root {
protected String name;
public Root(String name) {
this.name = name;
}
public abstract void show();
}/<code>
2.樹枝節點
<code>public class Branch extends Root {
private List<root> roots;
private Integer level;
public Branch(String name, Integer level) {
super(name);
this.level = level;
this.roots = new ArrayList<root>();
}
@Override
public void show() {
System.out.println(this.name);
for (Root dir : this.roots) {
//控制顯示格式
if(this.level != null){
for(int i = 0; i < this.level; i ++){
//打印空格控制格式
System.out.print(" ");
}
for(int i = 0; i < this.level; i ++){
//每一行開始打印一個+號
if(i == 0){ System.out.print("+"); }
System.out.print("-");
}
}
//打印名稱
dir.show();
}
}
public boolean add(Root root) {
return this.roots.add(root);
}
public boolean remove(Root root) {
return this.roots.remove(root);
}
public Root get(int index) {
return this.roots.get(index);
}
public void list(){
for (Root root : this.roots) {
System.out.println(root.name);
}
}
}/<root>/<root>/<code>
3.葉子節點
<code> public class Leaf extends Root {
public Leaf(String name) {
super(name);
}
@Override
public void show() {
System.out.println(this.name);
}
}/<code>
4.測試代碼
<code> class Test {
public static void main(String[] args) {
// 根節點Root
Branch root = new Branch("Root",1);
// 分支1
Branch branch1 = new Branch("Branch1",2);
Leaf leaf1 = new Leaf("Leaf1");
Leaf leaf2 = new Leaf("Leaf2");
branch1.add(leaf1);
branch1.add(leaf2);
root.add(branch1);
// 葉子3
Leaf leaf3 = new Leaf("leaf3");
root.add(leaf3);
// 分支2
Branch branch2 = new Branch("Branch2",2);
Leaf leaf4 = new Leaf("Leaf4");
Leaf leaf5 = new Leaf("Leaf5");
branch2.add(leaf4);
branch2.add(leaf5);
// 分支2中加上分支3
Branch branch3 = new Branch("Branch3",3);
branch3.add(new Leaf("Leaf6"));
branch2.add(branch3);
root.add(branch2);
// 效果展示
root.show();
}
}/<code>
5.運行效果如下:
<code> Root
+-Branch1
+--Leaf1
+--Leaf2
+-leaf3
+-Branch2
+--Leaf4
+--Leaf5
+--Branch3
+---Leaf6/<code>
閱讀更多 我是阿喵醬 的文章