點贊再看,養成習慣,公眾號搜一搜【一角錢技術】關注更多原創技術文章。本文 GitHub org_hejianhui/JavaStudy 已收錄,有我的系列文章。
前言
- 23種設計模式速記
- 單例(singleton)模式
- 工廠方法(factory method)模式
- 抽象工廠(abstract factory)模式
- 建造者/構建器(builder)模式
- 原型(prototype)模式
- 享元(flyweight)模式
- 外觀(facade)模式
- 適配器(adapter)模式
- 裝飾(decorator)模式
- 觀察者(observer)模式
- 策略(strategy)模式
- 橋接(bridge)模式
- 模版方法(template method)模式
- 責任鏈(chain of responsibility)模式
- 持續更新中......
23種設計模式快速記憶的請看上面第一篇,本篇和大家一起來學習組合模式相關內容。
模式定義
將對象組合成樹形結構以表示“部分-整體”的層次結構。組合模式使得用戶對單個對象和組合對象的使用具有一致性。
組合模式分為透明式的組合模式和安全式的組合模式。
透明方式
在該方式中,由于抽象構件聲明了所有子類中的全部方法,所以客戶端無須區別樹葉對象和樹枝對象,對客戶端來說是透明的。但其缺點是:樹葉構件本來沒有 Add()、Remove() 及 GetChild() 方法,卻要實現它們(空實現或拋異常),這樣會帶來一些安全性問題。
安全方式
在該方式中,將管理子構件的方法移到樹枝構件中,抽象構件和樹葉構件沒有對子對象的管理方法,這樣就避免了上一種方式的安全性問題,但由于葉子和分支有不同的接口,客戶端在調用時要知道樹葉對象和樹枝對象的存在,所以失去了透明性。
解決的問題
它在我們樹型結構的問題中,模糊了簡單元素和復雜元素的概念,客戶端程序可以像處理簡單元素一樣來處理復雜元素,從而使得客戶程序與復雜元素的內部結構解耦。
模式組成
組成(角色) | 作用 |
---|---|
抽象構件(Component)角色 | 它的主要作用是為樹葉構件和樹枝構件聲明公共接口,并實現它們的默認行為。在透明式的組合模式中抽象構件還聲明訪問和管理子類的接口;在安全式的組合模式中不聲明訪問和管理子類的接口,管理工作由樹枝構件完成。 |
樹葉構件(Leaf)角色 | 是組合中的葉節點對象,它沒有子節點,用于實現抽象構件角色中 聲明的公共接口。 |
樹枝構件(Composite)角色 | 是組合中的分支節點對象,它有子節點。它實現了抽象構件角色中聲明的接口,它的主要作用是存儲和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。 |
模式實現
假如要訪問集合 c0={leaf1,{leaf2,leaf3}} 中的元素,其對應的樹狀圖如下:
package com.niuh.designpattern.composite.v1;
import java.util.ArrayList;
/**
* 組合模式
*/
public class CompositePattern {
public static void main(String[] args) {
Component c0 = new Composite();
Component c1 = new Composite();
Component leaf1 = new Leaf("1");
Component leaf2 = new Leaf("2");
Component leaf3 = new Leaf("3");
c0.add(leaf1);
c0.add(c1);
c1.add(leaf2);
c1.add(leaf3);
c0.operation();
}
}
//抽象構件
interface Component {
void add(Component c);
void remove(Component c);
Component getChild(int i);
void operation();
}
//樹葉構件
class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
public void add(Component c) {
}
public void remove(Component c) {
}
public Component getChild(int i) {
return null;
}
public void operation() {
System.out.println("樹葉" + name + ":被訪問!");
}
}
//樹枝構件
class Composite implements Component {
private ArrayList<Component> children = new ArrayList<Component>();
public void add(Component c) {
children.add(c);
}
public void remove(Component c) {
children.remove(c);
}
public Component getChild(int i) {
return children.get(i);
}
public void operation() {
for (Object obj : children) {
((Component) obj).operation();
}
}
}
輸出結果如下:
樹葉1:被訪問!
樹葉2:被訪問!
樹葉3:被訪問!
實例說明
實例概況
用組合模式實現當用戶在商店購物后,顯示其所選商品信息,并計算所選商品總價的功能。
說明:隔壁老王到南京旅游,在超市購物
用 1 個紅色小袋子裝了 2 包南京特產(單價 7.9 元)、1 張南京地圖(單價 9.9 元);
用 1 個白色小袋子裝了 2 包香藉(單價 68 元)和 3 包紅茶(單價 180 元);
用 1 個中袋子裝了前面的紅色小袋子和 1 個雨花石吊墜(單價 380 元);
用 1 個大袋子裝了前面的中袋子、白色小袋子和 1 雙李寧牌運動鞋(單價 198 元)。
最后“大袋子”中的內容有如下,現在要求編程顯示隔壁老王放在大袋子中的所有商品信息并計算要支付的總價。
使用步驟
可按安全組合模式設計,其結構圖如下:
步驟1:定義抽象構件(Component)角色:物品
interface Articles {
float calculation(); //計算
void show();
}
步驟2:定義樹葉構件(Leaf)角色:商品
class Goods implements Articles {
private String name; //名字
private int quantity; //數量
private float unitPrice; //單價
public Goods(String name, int quantity, float unitPrice) {
this.name = name;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public float calculation() {
return quantity * unitPrice;
}
public void show() {
System.out.println(name + "(數量:" + quantity + ",單價:" + unitPrice + "元)");
}
}
步驟3:定義樹枝構件(Composite)角色:袋子
class Bags implements Articles {
private String name; //名字
private ArrayList<Articles> bags = new ArrayList<Articles>();
public Bags(String name) {
this.name = name;
}
public void add(Articles c) {
bags.add(c);
}
public void remove(Articles c) {
bags.remove(c);
}
public Articles getChild(int i) {
return bags.get(i);
}
public float calculation() {
float s = 0;
for (Object obj : bags) {
s += ((Articles) obj).calculation();
}
return s;
}
public void show() {
for (Object obj : bags) {
((Articles) obj).show();
}
}
}
步驟4:計算支付總價
public class CompositePattern {
public static void main(String[] args) {
float s = 0;
Bags BigBag, mediumBag, smallRedBag, smallWhiteBag;
Goods sp;
BigBag = new Bags("大袋子");
mediumBag = new Bags("中袋子");
smallRedBag = new Bags("紅色小袋子");
smallWhiteBag = new Bags("白色小袋子");
sp = new Goods("南京特產", 2, 7.9f);
smallRedBag.add(sp);
sp = new Goods("南京地圖", 1, 9.9f);
smallRedBag.add(sp);
sp = new Goods("香菇", 2, 68);
smallWhiteBag.add(sp);
sp = new Goods("紅茶", 3, 180);
smallWhiteBag.add(sp);
sp = new Goods("雨花石吊墜", 1, 380);
mediumBag.add(sp);
mediumBag.add(smallRedBag);
sp = new Goods("李寧牌運動鞋", 1, 198);
BigBag.add(sp);
BigBag.add(smallWhiteBag);
BigBag.add(mediumBag);
System.out.println("隔壁老王選購的商品有:");
BigBag.show();
s = BigBag.calculation();
System.out.println("要支付的總價是:" + s + "元");
}
}
輸出結果
隔壁老王選購的商品有:
李寧牌運動鞋(數量:1,單價:198.0元)
香菇(數量:2,單價:68.0元)
紅茶(數量:3,單價:180.0元)
雨花石吊墜(數量:1,單價:380.0元)
南京特產(數量:2,單價:7.9元)
南京地圖(數量:1,單價:9.9元)
要支付的總價是:1279.7元
優點
- 組合模式使得客戶端代碼可以一致地處理單個對象和組合對象,無須關心自己處理的是單個對象,還是組合對象,這簡化了客戶端代碼;
- 更容易在組合體內加入新的對象,客戶端不會因為加入了新的對象而更改源代碼,滿足“開閉原則”;
缺點
- 設計較復雜,客戶端需要花更多時間理清類之間的層次關系;
- 不容易限制容器中的構件;
- 不容易用繼承的方法來增加構件的新功能;
應用場景
- 在需要表示一個對象整體與部分的層次結構的場合。
- 要求對用戶隱藏組合對象與單個對象的不同,用戶可以用統一的接口使用組合結構中的所有對象的場合。
部分、整體場景,如樹形菜單,文件、文件夾的管理。
源碼中的應用
- java.awt中的組合模式
- Java集合中的組合模式
- Mybatis SqlNode中的組合模式
java.awt中的組合模式
Java GUI分兩種:
- AWT(Abstract Window Toolkit):抽象窗口工具集,是第一代的Java GUI組件。繪制依賴于底層的操作系統。基本的AWT庫處理用戶界面元素的方法是把這些元素的創建和行為委托給每個目標平臺上(Windows、 Unix、 Macintosh等)的本地GUI工具進行處理。
- Swing,不依賴于底層細節,是輕量級的組件。現在多是基于Swing來開發。
我們來看一個AWT簡單案例:
package com.niuh.designpattern.composite.v3;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
* java.awt中的組合模式
*/
public class MyFrame extends Frame {
public MyFrame(String title) {
super(title);
}
public static void main(String[] args) {
MyFrame frame = new MyFrame("組合模式之 Frame");
// 定義三個構件,添加到Frame中去
Button button = new Button("按鈕 A");
Label label = new Label("這是一個 AWT Label!");
TextField textField = new TextField("這是一個 AWT TextField!");
frame.add(button, BorderLayout.EAST);
frame.add(label, BorderLayout.SOUTH);
frame.add(textField, BorderLayout.NORTH);
// 定義一個 Panel,在Panel中添加三個構件,然后再把Panel添加到Frame中去
Panel panel = new Panel();
panel.setBackground(Color.pink);
Label lable1 = new Label("用戶名");
TextField textField1 = new TextField("請輸入用戶名:", 20);
Button button1 = new Button("確定");
panel.add(lable1);
panel.add(textField1);
panel.add(button1);
frame.add(panel, BorderLayout.CENTER);
// 設置Frame的屬性
frame.setSize(500, 300);
frame.setBackground(Color.orange);
// 設置點擊關閉事件
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
frame.setVisible(true);
}
}
運行后窗體顯示如下:
我們在Frame容器中添加了三個不同的構件 Button
、Label
、TextField
,還添加了一個 Panel
容器,Panel
容器中又添加了 Button
、Label
、TextField
三個構件,為什么容器 Frame
和 Panel
可以添加類型不同的構件和容器呢?
AWT Component的類圖
GUI組件根據作用可以分為兩種:基本組件和容器組件。
- 基本組件又稱構件,諸如按鈕、文本框之類的圖形界面元素。
- 容器是一種比較特殊的組件,可以容納其他組件,容器如窗口、對話框等。所有的容器類都是
java.awt.Container
的直接或間接子類
容器父類 Container
的部分代碼如下:
public class Container extends Component {
/**
* The components in this container.
* @see #add
* @see #getComponents
*/
private java.util.List<Component> component = new ArrayList<>();
public Component add(Component comp) {
addImpl(comp, null, -1);
return comp;
}
// 省略...
}
容器父類 Container
內部定義了一個集合用于存儲 Component
對象,而容器組件 Container
和 基本組件如 Button
、Label
、TextField
等都是 Component
的子類,所以可以很清楚的看到這里應用了組合模式。
Component
類中封裝了組件通用的方法和屬性,如圖形的組件對象、大小、顯示位置、前景色和背景色、邊界、可見性等,因此許多組件類也就繼承了 Component
類的成員方法和成員變量,相應的成員方法包括:
getComponentAt(int x, int y)
getFont()
getForeground()
getName()
getSize()
paint(Graphics g)
repaint()
update()
setVisible(boolean b)
setSize(Dimension d)
setName(String name)
Java集合中的組合模式
HashMap
提供 putAll
的方法,可以將另一個 Map
對象放入自己的存儲空間中,如果有相同的 key 值則會覆蓋之前的 key 值所對應的 value 值。
package com.niuh.designpattern.composite.v3;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class HashMapTest {
public static void main(String[] args) {
Map<String, Integer> map1 = new HashMap<String, Integer>();
map1.put("aa", 1);
map1.put("bb", 2);
map1.put("cc", 3);
System.out.println("map1: " + map1);
Map<String, Integer> map2 = new LinkedHashMap<>();
map2.put("cc", 4);
map2.put("dd", 5);
System.out.println("map2: " + map2);
map1.putAll(map2);
System.out.println("map1.putAll(map2): " + map1);
}
}
輸出結果如下:
map1: {aa=1, bb=2, cc=3}
map2: {cc=4, dd=5}
map1.putAll(map2): {aa=1, bb=2, cc=4, dd=5}
查看 putAll
源碼
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
putAll
接收的參數為父類 Map
類型,所以 HashMap
是一個容器類,Map
的子類為葉子類,當然如果 Map
的其他子類也實現了 putAll
方法,那么它們都既是容器類,又都是葉子類。
同理,ArrayList
中的 addAll(Collection<? extends E> c)
方法也是一個組合模式的應用。
Mybatis SqlNode中的組合模式
MyBatis 的強大特性之一便是它的動態SQL,其通過 if
, choose
, when
, otherwise
, trim
, where
, set
, foreach
標簽,可組合成非常靈活的SQL語句,從而提高開發人員的效率。
動態SQL – IF
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
動態SQL – choose, when, otherwise
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
動態SQL – where
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
動態SQL – foreach
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT * FROM POST P WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
Mybatis在處理動態SQL節點時,應用到了組合設計模式,Mybatis會將映射配置文件中定義的動態SQL節點、文本節點等解析成對應的 SqlNode 實現,并形成樹形結構。
需要先了解 DynamicContext
類的作用:主要用于記錄解析動態SQL語句之后產生的SQL語句片段,可以認為它是一個用于記錄動態SQL語句解析結果的容器。
抽象構件為 SqlNode
接口,源碼如下
public interface SqlNode {
boolean apply(DynamicContext context);
}
apply
是 SQLNode
接口中定義的唯一方法,該方法會根據用戶傳入的實參,參數解析該SQLNode所記錄的動態SQL節點,并調用 DynamicContext.appendSql()
方法將解析后的SQL片段追加到 DynamicContext.sqlBuilder
中保存,當SQL節點下所有的 SqlNode
完成解析后,我們就可以從 DynamicContext
中獲取一條動態生產的、完整的SQL語句
然后來看 MixedSqlNode
類的源碼
public class MixedSqlNode implements SqlNode {
private List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
MixedSqlNode
維護了一個 List<SqlNode>
類型的列表,用于存儲 SqlNode
對象,apply
方法通過 for循環
遍歷 contents 并調用其中對象的 apply
方法,這里跟我們的示例中的 Folder
類中的 print
方法非常類似,很明顯 MixedSqlNode
扮演了容器構件角色。
對于其他SqlNode子類的功能,稍微概括如下:
-
TextSqlNode
:表示包含${}
占位符的動態SQL節點,其 apply 方法會使用GenericTokenParser
解析${}
占位符,并直接替換成用戶給定的實際參數值 -
IfSqlNode
:對應的是動態SQL節點<If>
節點,其 apply 方法首先通過ExpressionEvaluator.evaluateBoolean()
方法檢測其 test 表達式是否為 true,然后根據 test 表達式的結果,決定是否執行其子節點的 apply() 方法 -
TrimSqlNode
:會根據子節點的解析結果,添加或刪除相應的前綴或后綴。 -
WhereSqlNode
和SetSqlNode
都繼承了TrimSqlNode
-
ForeachSqlNode
:對應<foreach>
標簽,對集合進行迭代 - 動態SQL中的
<choose>
、<when>
、<otherwise>
分別解析成ChooseSqlNode
、IfSqlNode
、MixedSqlNode
綜上,SqlNode
接口有多個實現類,每個實現類對應一個動態SQL節點,其中 SqlNode
扮演抽象構件角色,MixedSqlNode
扮演容器構件角色,其它一般是葉子構件角色。
PS:以上代碼提交在 Github :https://github.com/Niuh-Study/niuh-designpatterns.git
文章持續更新,可以公眾號搜一搜「 一角錢技術 」第一時間閱讀, 本文 GitHub org_hejianhui/JavaStudy 已經收錄,歡迎 Star。