設計模式之組合模式
本文通過老王和小王探討書房、書架、各類書的管理問題,引出結構型設計模式家族中的一個重要成員——組合模式,本文會給予兩種組合模式的典型程式碼實現,為了加深理解會在第三部分應用中介紹組合模式在原始碼中的實際運用,最後總結該設計模式學習後的一些思考。
讀者可以拉取完整程式碼到本地進行學習,實現程式碼均測試通過後上傳到 碼雲 。
一、引出問題
上篇文章 中老王給小王買車以後,小王對老王感激涕零,看著老王凌亂的書房,小王提出要幫助老王整理整理他的書架。
小王開始了他的分析。老王平時博覽群書,中文、英文、梵文...每個語種佔滿了書架,而每個語種中又分經濟學、計算機學、社會學等等類目。這是典型的分層次結構,將語種比作是圖書的子類,類目是語種的子類結構劃分。
將圖書、語種、類目都看做是組織結構,他們之間沒有繼承關係,而是一個樹形結構,可以更好的實現管理操作。
二、概念與使用
實際上,小王提出來的設計思路正是結構型設計模式中的組合模式,我們首先看一下組合模式的相關概念, 組合模式 (Composite Pattern),又叫部分整體模式,它建立了物件組的樹形結構,將物件組合成樹狀結構以表示“ 整體-部分” 的層次關係。組合模式依據 樹形結構 來組合物件,用來表示部分以及整體層次。
組合模式使得使用者對單個物件和組合物件的訪問具有一致性,即:組合能讓客戶以一致的方式處理個別對象以及組合物件。
用大白話解釋也就是,在實際應用中將所有圖書依照樹形模式進行組合,老王尋找書籍時,無論是訪問某一類書還是某一個語種的書,使用同樣的姿勢即可,保證了訪問的一致性。
在該模式中應該是有三個角色:
1、 Root :這是組合中物件宣告介面,在適當情況下,實現所有類共有的介面預設行為,用於訪問和管理Root 子部件, Root 可以是抽象類或者介面。
2、 Branches :非葉子節點用於儲存子部件,在Root介面中實現了 子部件的相關操作。
2、 Leaf : 在組合中表示葉子節點,葉子節點沒有子節點。
小王分析的頭頭是道,老王提出來了他的疑問。
當我按語種查詢還是按類目查詢是使用的方法有時候是不一樣的,如果你把所有方法都定義在Root中,在語種或者類目中實現中是無意義的,而且這違背了介面隔離原則。
小王覺得說的對也不對,如果我改成不在Root中定義,那麼我在客戶端呼叫的時候就需要判斷是枝還是葉了,增加了繁雜的邏輯判斷,而且相比另外一種變得不透明瞭,依賴倒置原則也沒有遵守。
兩種方式似乎都有缺陷,小王陷入了糾結不知道該如何取捨,老王提出了他的一些見解,沒有任何一個設計模式是完全沒有缺點的,兩種都有各自的好處,在實際的運用中根據條件進行取捨,而正確選擇的前提就是要對所有的設計模式充分的把握。
上面兩種就對應組合模式中的兩個大分類、①透明組合模式、安全組合模式。
①透明組合模式把所有的公共方法都定義在Root中,這樣做的好處就是客戶端無需分辨是葉子節點(Leaf)和樹枝節點(Branches),他們具備完全一致的介面;缺點是葉子節點(Leaf)會繼承得到一些它所不需要(管理子類操作的方法)的方法,這與設計模式介面隔離原則相違背。
②安全組合模式的好處是介面定義職責清晰,符合設計模式單一職責原則和介面隔離原則;缺點是客戶需要區分樹枝節點(Branches)和葉子節點(Leaf),這樣才能正確處理各個層次的操作,客戶端依賴抽象(Root),違背了依賴倒置原則。
我們把兩種的方式實現,讀者對比他們之間的區別。
安全模式
Root(根節點):
/** * @author tcy * @Date 08-08-2022 */ public abstract class RootBook { protected String name; public RootBook(String name) { this.name = name; } public abstract String operation(); }
Branches(樹枝節點)
/** * @author tcy * @Date 08-08-2022 */ public class BranchesLanguages extends RootBook { private List<RootBook> roots; public BranchesLanguages(String name) { super(name); this.roots = new ArrayList<RootBook>(); } public String operation() { StringBuilder builder = new StringBuilder(this.name); for (RootBook component : this.roots) { builder.append("\n"); builder.append(component.operation()); } return builder.toString(); } public boolean addChild(RootBook component) { return this.roots.add(component); } public boolean removeChild(RootBook component) { return this.roots.remove(component); } public RootBook getChild(int index) { return this.roots.get(index); } }
Leaf(葉子節點)
/** * @author tcy * @Date 08-08-2022 */ public class LeafClassify extends RootBook { public LeafClassify(String name) { super(name); } @Override public String operation() { return this.name; } }
客戶端:
/** * @author tcy * @Date 08-08-2022 */ public class Client { public static void main(String[] args) { System.out.println("安全組合模式..."); // 來一個根節點 BranchesLanguages BranchesRoot = new BranchesLanguages("root/書"); // 來一個樹枝節點 BranchesLanguages branchA = new BranchesLanguages("------branchA/英語"); BranchesLanguages branchB = new BranchesLanguages("------branchB/中文"); // 來一個葉子節點 RootBook leafA = new LeafClassify("------leafA/經濟學"); RootBook leafB = new LeafClassify("------leafB/計算機學"); RootBook leafC = new LeafClassify("------leafC/法學"); BranchesRoot.addChild(branchA); BranchesRoot.addChild(leafC); branchA.addChild(leafA); branchA.addChild(branchB); branchB.addChild(leafB); String result = BranchesRoot.operation(); System.out.println(result); } }
透明模式
Root(根節點):
/** * @author tcy * @Date 08-08-2022 */ public abstract class RootBook { protected String name; public RootBook(String name) { this.name = name; } public abstract String operation(); public boolean addChild(RootBook component) { throw new UnsupportedOperationException("addChild not supported!"); } public boolean removeChild(RootBook component) { throw new UnsupportedOperationException("removeChild not supported!"); } public RootBook getChild(int index) { throw new UnsupportedOperationException("getChild not supported!"); } }
Branches(樹枝節點)
/** * @author tcy * @Date 08-08-2022 */ public class BranchesLanguages extends RootBook { private List<RootBook> roots; public BranchesLanguages(String name) { super(name); this.roots = new ArrayList<RootBook>(); } public String operation() { StringBuilder builder = new StringBuilder(this.name); for (RootBook component : this.roots) { builder.append("\n"); builder.append(component.operation()); } return builder.toString(); } @Override public boolean addChild(RootBook component) { return this.roots.add(component); } @Override public boolean removeChild(RootBook component) { return this.roots.remove(component); } @Override public RootBook getChild(int index) { return this.roots.get(index); } }
Leaf(葉子節點)
/** * @author tcy * @Date 08-08-2022 */ public class LeafClassify extends RootBook { public LeafClassify(String name) { super(name); } @Override public String operation() { return this.name; } }
客戶端:
/** * @author tcy * @Date 08-08-2022 */ public class Client { public static void main(String[] args) { System.out.println("透明組合模式..."); // 來一個根節點 RootBook BranchesRoot = new BranchesLanguages("root/書"); // 來一個樹枝節點 RootBook branchA = new BranchesLanguages("------branchA/英語"); RootBook branchB = new BranchesLanguages("------branchB/漢語"); // 來一個葉子節點 RootBook leafA = new LeafClassify("------leafA/計算機學"); RootBook leafB = new LeafClassify("------leafB/法學"); RootBook leafC = new LeafClassify("------leafC/社會學"); BranchesRoot.addChild(branchA); BranchesRoot.addChild(leafC); branchA.addChild(leafA); branchA.addChild(branchB); branchB.addChild(leafB); String result = BranchesRoot.operation(); System.out.println(result); } }
使用組合模式的兩種實現方法,這樣就對老王的書架改造工程就完成了,對憑空捏造出來的需求有些讀者看完想必還是雲裡霧裡。我們結合JDK的原始碼和一些開發常用框架,再次深入原始碼對組合模式的使用。
三、應用
通過查詢資料可知,組合模式在Jdk中的應用主要是集合類HashMap和Mybtis中的SqlNode。
我們分別看其實現。
1、jdk中HashMap的運用
在HashMap中有一個父類AbstractMap和一個子類Node。如下圖
我們看下原始碼:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, 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節點就是葉子節點。
Node是HashMap中的一個內部類,HashMap的儲存節點指的正是Node,讀者可以重點看這個類的實現。
在這個例項中,HashMap就是樹枝節點,Node就是葉節點,Map就是根節點。
2、Mybtis中的SqlNode
SqlNode是一個介面,主要功能就是構造SQL語句。
public interface SqlNode { boolean apply(DynamicContext context); }
SqlNode有一大堆的實現類,我們看其中的MixedSqlNode。
public class MixedSqlNode implements SqlNode { private final List<SqlNode> contents; public MixedSqlNode(List<SqlNode> contents) { this.contents = contents; } @Override public boolean apply(DynamicContext context) { contents.forEach(node -> node.apply(context)); return true; } }
SqlNode就充當組合模式中的Root,而他的眾多子類作用就在於拼接各種型別的SQL,在組合模式的角色中相當於樹枝節點。其中在TrimSqlNode 中有一個子類WhereSqlNode就充當組合模式中的樹葉節點。
這兩個都屬於組合模式中的典型例子,讀者體會下使用這種模式的好處,和如果不使用組合模式應該怎樣實現。
通過這兩個例子我們應該可以看到,設計模式的使用中並不是完全遵循各自的角色,更多的是設計模式中的一些變種,讀者不深入原始碼並不能瞭解到該模式的實現細節。讀者需要做的就是儘可能的熟悉設計模式,在自己開發過程中可以“擇優錄取”。
四、總結
到這裡組合模式也就介紹完了,這種模式的優缺點都非常的明顯,優點就在於清楚的定義分層次的結構,在呼叫時忽略他們之間的差異,方便對整個層次進行控制,但是組合模式會違反依賴倒置原則。
理解是一回事,在實際應用中能正確的使用它就是另外一回事了。
讀者要對每種設計模式都能做到心中有數,當我們在實際程式設計中,在潛意識裡有各個設計模式的大體輪廓,參考代入進各種設計模式中,對於簡化開發和易於維護性有沒有好的幫助,選擇一個最優的設計模式。
推薦讀者,參考 軟體設計七大原則 認真閱讀往期的文章,認真體會。
建立型設計模式:
結構型設計模式:
- 記一次批量更新整型型別的列 → 探究 UPDATE 的使用細節
- 編碼中的Adapter,不僅是一種設計模式,更是一種架構理念與解決方案
- 執行緒池底層原理詳解與原始碼分析
- 30分鐘掌握 Webpack
- 線性迴歸大結局(嶺(Ridge)、 Lasso迴歸原理、公式推導),你想要的這裡都有
- Django 之路由層
- 【前端必會】webpack loader 到底是什麼
- day42-反射01
- 中心化決議管理——雲端分析
- HashMap底層原理及jdk1.8原始碼解讀
- 詳解JS中 call 方法的實現
- 列印 Logger 日誌時,需不需要再封裝一下工具類?
- 初識設計模式 - 代理模式
- 設計模式---享元模式
- 密碼學奇妙之旅、01 CFB密文反饋模式、AES標準、Golang程式碼
- [ML從入門到入門] 支援向量機:從SVM的推導過程到SMO的收斂性討論
- 從應用訪問Pod元資料-DownwardApi的應用
- Springboot之 Mybatis 多資料來源實現
- Java 泛型程式設計
- CAS核心思想、底層實現