點(diǎn)贊再看,養(yǎng)成習(xí)慣,公眾號(hào)搜一搜【一角錢(qián)技術(shù)】關(guān)注更多原創(chuàng)技術(shù)文章。本文 GitHub org_hejianhui/JavaStudy 已收錄丝格,有我的系列文章。
前言
- 23種設(shè)計(jì)模式速記
- 單例(singleton)模式
- 工廠方法(factory method)模式
- 抽象工廠(abstract factory)模式
- 建造者/構(gòu)建器(builder)模式
- 原型(prototype)模式
- 享元(flyweight)模式
- 外觀(facade)模式
- 適配器(adapter)模式
- 裝飾(decorator)模式
- 觀察者(observer)模式
- 策略(strategy)模式
- 橋接(bridge)模式
- 模版方法(template method)模式
- 責(zé)任鏈(chain of responsibility)模式
- 持續(xù)更新中......
23種設(shè)計(jì)模式快速記憶的請(qǐng)看上面第一篇镜雨,本篇和大家一起來(lái)學(xué)習(xí)組合模式相關(guān)內(nèi)容囱嫩。
模式定義
將對(duì)象組合成樹(shù)形結(jié)構(gòu)以表示“部分-整體”的層次結(jié)構(gòu)恃疯。組合模式使得用戶對(duì)單個(gè)對(duì)象和組合對(duì)象的使用具有一致性。
組合模式分為透明式的組合模式和安全式的組合模式墨闲。
透明方式
在該方式中今妄,由于抽象構(gòu)件聲明了所有子類中的全部方法,所以客戶端無(wú)須區(qū)別樹(shù)葉對(duì)象和樹(shù)枝對(duì)象鸳碧,對(duì)客戶端來(lái)說(shuō)是透明的盾鳞。但其缺點(diǎn)是:樹(shù)葉構(gòu)件本來(lái)沒(méi)有 Add()、Remove() 及 GetChild() 方法瞻离,卻要實(shí)現(xiàn)它們(空實(shí)現(xiàn)或拋異常)腾仅,這樣會(huì)帶來(lái)一些安全性問(wèn)題。
安全方式
在該方式中套利,將管理子構(gòu)件的方法移到樹(shù)枝構(gòu)件中推励,抽象構(gòu)件和樹(shù)葉構(gòu)件沒(méi)有對(duì)子對(duì)象的管理方法,這樣就避免了上一種方式的安全性問(wèn)題肉迫,但由于葉子和分支有不同的接口验辞,客戶端在調(diào)用時(shí)要知道樹(shù)葉對(duì)象和樹(shù)枝對(duì)象的存在,所以失去了透明性喊衫。
解決的問(wèn)題
它在我們樹(shù)型結(jié)構(gòu)的問(wèn)題中跌造,模糊了簡(jiǎn)單元素和復(fù)雜元素的概念,客戶端程序可以像處理簡(jiǎn)單元素一樣來(lái)處理復(fù)雜元素格侯,從而使得客戶程序與復(fù)雜元素的內(nèi)部結(jié)構(gòu)解耦鼻听。
模式組成
組成(角色) | 作用 |
---|---|
抽象構(gòu)件(Component)角色 | 它的主要作用是為樹(shù)葉構(gòu)件和樹(shù)枝構(gòu)件聲明公共接口,并實(shí)現(xiàn)它們的默認(rèn)行為联四。在透明式的組合模式中抽象構(gòu)件還聲明訪問(wèn)和管理子類的接口撑碴;在安全式的組合模式中不聲明訪問(wèn)和管理子類的接口,管理工作由樹(shù)枝構(gòu)件完成朝墩。 |
樹(shù)葉構(gòu)件(Leaf)角色 | 是組合中的葉節(jié)點(diǎn)對(duì)象醉拓,它沒(méi)有子節(jié)點(diǎn),用于實(shí)現(xiàn)抽象構(gòu)件角色中 聲明的公共接口收苏。 |
樹(shù)枝構(gòu)件(Composite)角色 | 是組合中的分支節(jié)點(diǎn)對(duì)象亿卤,它有子節(jié)點(diǎn)。它實(shí)現(xiàn)了抽象構(gòu)件角色中聲明的接口鹿霸,它的主要作用是存儲(chǔ)和管理子部件排吴,通常包含 Add()、Remove()懦鼠、GetChild() 等方法钻哩。 |
模式實(shí)現(xiàn)
假如要訪問(wèn)集合 c0={leaf1,{leaf2,leaf3}} 中的元素屹堰,其對(duì)應(yīng)的樹(shù)狀圖如下:
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();
}
}
//抽象構(gòu)件
interface Component {
void add(Component c);
void remove(Component c);
Component getChild(int i);
void operation();
}
//樹(shù)葉構(gòu)件
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("樹(shù)葉" + name + ":被訪問(wèn)!");
}
}
//樹(shù)枝構(gòu)件
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();
}
}
}
輸出結(jié)果如下:
樹(shù)葉1:被訪問(wèn)街氢!
樹(shù)葉2:被訪問(wèn)扯键!
樹(shù)葉3:被訪問(wèn)!
實(shí)例說(shuō)明
實(shí)例概況
用組合模式實(shí)現(xiàn)當(dāng)用戶在商店購(gòu)物后珊肃,顯示其所選商品信息荣刑,并計(jì)算所選商品總價(jià)的功能。
說(shuō)明:隔壁老王到南京旅游伦乔,在超市購(gòu)物
用 1 個(gè)紅色小袋子裝了 2 包南京特產(chǎn)(單價(jià) 7.9 元)厉亏、1 張南京地圖(單價(jià) 9.9 元);
用 1 個(gè)白色小袋子裝了 2 包香藉(單價(jià) 68 元)和 3 包紅茶(單價(jià) 180 元)烈和;
用 1 個(gè)中袋子裝了前面的紅色小袋子和 1 個(gè)雨花石吊墜(單價(jià) 380 元)叶堆;
用 1 個(gè)大袋子裝了前面的中袋子、白色小袋子和 1 雙李寧牌運(yùn)動(dòng)鞋(單價(jià) 198 元)斥杜。
最后“大袋子”中的內(nèi)容有如下,現(xiàn)在要求編程顯示隔壁老王放在大袋子中的所有商品信息并計(jì)算要支付的總價(jià)沥匈。
使用步驟
可按安全組合模式設(shè)計(jì)蔗喂,其結(jié)構(gòu)圖如下:
步驟1:定義抽象構(gòu)件(Component)角色:物品
interface Articles {
float calculation(); //計(jì)算
void show();
}
步驟2:定義樹(shù)葉構(gòu)件(Leaf)角色:商品
class Goods implements Articles {
private String name; //名字
private int quantity; //數(shù)量
private float unitPrice; //單價(jià)
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 + "(數(shù)量:" + quantity + ",單價(jià):" + unitPrice + "元)");
}
}
步驟3:定義樹(shù)枝構(gòu)件(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:計(jì)算支付總價(jià)
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("南京特產(chǎn)", 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("李寧牌運(yùn)動(dòng)鞋", 1, 198);
BigBag.add(sp);
BigBag.add(smallWhiteBag);
BigBag.add(mediumBag);
System.out.println("隔壁老王選購(gòu)的商品有:");
BigBag.show();
s = BigBag.calculation();
System.out.println("要支付的總價(jià)是:" + s + "元");
}
}
輸出結(jié)果
隔壁老王選購(gòu)的商品有:
李寧牌運(yùn)動(dòng)鞋(數(shù)量:1高帖,單價(jià):198.0元)
香菇(數(shù)量:2缰儿,單價(jià):68.0元)
紅茶(數(shù)量:3,單價(jià):180.0元)
雨花石吊墜(數(shù)量:1散址,單價(jià):380.0元)
南京特產(chǎn)(數(shù)量:2乖阵,單價(jià):7.9元)
南京地圖(數(shù)量:1,單價(jià):9.9元)
要支付的總價(jià)是:1279.7元
優(yōu)點(diǎn)
- 組合模式使得客戶端代碼可以一致地處理單個(gè)對(duì)象和組合對(duì)象预麸,無(wú)須關(guān)心自己處理的是單個(gè)對(duì)象瞪浸,還是組合對(duì)象,這簡(jiǎn)化了客戶端代碼吏祸;
- 更容易在組合體內(nèi)加入新的對(duì)象对蒲,客戶端不會(huì)因?yàn)榧尤肓诵碌膶?duì)象而更改源代碼,滿足“開(kāi)閉原則”贡翘;
缺點(diǎn)
- 設(shè)計(jì)較復(fù)雜蹈矮,客戶端需要花更多時(shí)間理清類之間的層次關(guān)系;
- 不容易限制容器中的構(gòu)件鸣驱;
- 不容易用繼承的方法來(lái)增加構(gòu)件的新功能泛鸟;
應(yīng)用場(chǎng)景
- 在需要表示一個(gè)對(duì)象整體與部分的層次結(jié)構(gòu)的場(chǎng)合。
- 要求對(duì)用戶隱藏組合對(duì)象與單個(gè)對(duì)象的不同踊东,用戶可以用統(tǒng)一的接口使用組合結(jié)構(gòu)中的所有對(duì)象的場(chǎng)合北滥。
部分刚操、整體場(chǎng)景,如樹(shù)形菜單碑韵,文件赡茸、文件夾的管理。
源碼中的應(yīng)用
- java.awt中的組合模式
- Java集合中的組合模式
- Mybatis SqlNode中的組合模式
java.awt中的組合模式
Java GUI分兩種:
- AWT(Abstract Window Toolkit):抽象窗口工具集祝闻,是第一代的Java GUI組件占卧。繪制依賴于底層的操作系統(tǒng)×基本的AWT庫(kù)處理用戶界面元素的方法是把這些元素的創(chuàng)建和行為委托給每個(gè)目標(biāo)平臺(tái)上(Windows华蜒、 Unix、 Macintosh等)的本地GUI工具進(jìn)行處理豁遭。
- Swing叭喜,不依賴于底層細(xì)節(jié),是輕量級(jí)的組件”托唬現(xiàn)在多是基于Swing來(lái)開(kāi)發(fā)捂蕴。
我們來(lái)看一個(gè)AWT簡(jiǎn)單案例:
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");
// 定義三個(gè)構(gòu)件,添加到Frame中去
Button button = new Button("按鈕 A");
Label label = new Label("這是一個(gè) AWT Label!");
TextField textField = new TextField("這是一個(gè) AWT TextField!");
frame.add(button, BorderLayout.EAST);
frame.add(label, BorderLayout.SOUTH);
frame.add(textField, BorderLayout.NORTH);
// 定義一個(gè) Panel闪幽,在Panel中添加三個(gè)構(gòu)件啥辨,然后再把Panel添加到Frame中去
Panel panel = new Panel();
panel.setBackground(Color.pink);
Label lable1 = new Label("用戶名");
TextField textField1 = new TextField("請(qǐng)輸入用戶名:", 20);
Button button1 = new Button("確定");
panel.add(lable1);
panel.add(textField1);
panel.add(button1);
frame.add(panel, BorderLayout.CENTER);
// 設(shè)置Frame的屬性
frame.setSize(500, 300);
frame.setBackground(Color.orange);
// 設(shè)置點(diǎn)擊關(guān)閉事件
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
frame.setVisible(true);
}
}
運(yùn)行后窗體顯示如下:
我們?cè)贔rame容器中添加了三個(gè)不同的構(gòu)件 Button
、Label
盯腌、TextField
溉知,還添加了一個(gè) Panel
容器,Panel
容器中又添加了 Button
腕够、Label
级乍、TextField
三個(gè)構(gòu)件,為什么容器 Frame
和 Panel
可以添加類型不同的構(gòu)件和容器呢帚湘?
AWT Component的類圖
GUI組件根據(jù)作用可以分為兩種:基本組件和容器組件玫荣。
- 基本組件又稱構(gòu)件,諸如按鈕客们、文本框之類的圖形界面元素崇决。
- 容器是一種比較特殊的組件,可以容納其他組件底挫,容器如窗口恒傻、對(duì)話框等。所有的容器類都是
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
內(nèi)部定義了一個(gè)集合用于存儲(chǔ) Component
對(duì)象建邓,而容器組件 Container
和 基本組件如 Button
盈厘、Label
、TextField
等都是 Component
的子類官边,所以可以很清楚的看到這里應(yīng)用了組合模式沸手。
Component
類中封裝了組件通用的方法和屬性外遇,如圖形的組件對(duì)象、大小契吉、顯示位置跳仿、前景色和背景色、邊界捐晶、可見(jiàn)性等菲语,因此許多組件類也就繼承了 Component
類的成員方法和成員變量,相應(yīng)的成員方法包括:
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
的方法惑灵,可以將另一個(gè) Map
對(duì)象放入自己的存儲(chǔ)空間中山上,如果有相同的 key 值則會(huì)覆蓋之前的 key 值所對(duì)應(yīng)的 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);
}
}
輸出結(jié)果如下:
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
接收的參數(shù)為父類 Map
類型英支,所以 HashMap
是一個(gè)容器類佩憾,Map
的子類為葉子類,當(dāng)然如果 Map
的其他子類也實(shí)現(xiàn)了 putAll
方法干花,那么它們都既是容器類妄帘,又都是葉子類。
同理池凄,ArrayList
中的 addAll(Collection<? extends E> c)
方法也是一個(gè)組合模式的應(yīng)用寄摆。
Mybatis SqlNode中的組合模式
MyBatis 的強(qiáng)大特性之一便是它的動(dòng)態(tài)SQL,其通過(guò) if
, choose
, when
, otherwise
, trim
, where
, set
, foreach
標(biāo)簽修赞,可組合成非常靈活的SQL語(yǔ)句,從而提高開(kāi)發(fā)人員的效率桑阶。
動(dòng)態(tài)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>
動(dòng)態(tài)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>
動(dòng)態(tài)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>
動(dòng)態(tài)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在處理動(dòng)態(tài)SQL節(jié)點(diǎn)時(shí)柏副,應(yīng)用到了組合設(shè)計(jì)模式,Mybatis會(huì)將映射配置文件中定義的動(dòng)態(tài)SQL節(jié)點(diǎn)蚣录、文本節(jié)點(diǎn)等解析成對(duì)應(yīng)的 SqlNode 實(shí)現(xiàn)割择,并形成樹(shù)形結(jié)構(gòu)。
需要先了解 DynamicContext
類的作用:主要用于記錄解析動(dòng)態(tài)SQL語(yǔ)句之后產(chǎn)生的SQL語(yǔ)句片段萎河,可以認(rèn)為它是一個(gè)用于記錄動(dòng)態(tài)SQL語(yǔ)句解析結(jié)果的容器荔泳。
抽象構(gòu)件為 SqlNode
接口,源碼如下
public interface SqlNode {
boolean apply(DynamicContext context);
}
apply
是 SQLNode
接口中定義的唯一方法虐杯,該方法會(huì)根據(jù)用戶傳入的實(shí)參玛歌,參數(shù)解析該SQLNode所記錄的動(dòng)態(tài)SQL節(jié)點(diǎn),并調(diào)用 DynamicContext.appendSql()
方法將解析后的SQL片段追加到 DynamicContext.sqlBuilder
中保存擎椰,當(dāng)SQL節(jié)點(diǎn)下所有的 SqlNode
完成解析后支子,我們就可以從 DynamicContext
中獲取一條動(dòng)態(tài)生產(chǎn)的、完整的SQL語(yǔ)句
然后來(lái)看 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
維護(hù)了一個(gè) List<SqlNode>
類型的列表达舒,用于存儲(chǔ) SqlNode
對(duì)象值朋,apply
方法通過(guò) for循環(huán)
遍歷 contents 并調(diào)用其中對(duì)象的 apply
方法叹侄,這里跟我們的示例中的 Folder
類中的 print
方法非常類似,很明顯 MixedSqlNode
扮演了容器構(gòu)件角色昨登。
對(duì)于其他SqlNode子類的功能趾代,稍微概括如下:
-
TextSqlNode
:表示包含${}
占位符的動(dòng)態(tài)SQL節(jié)點(diǎn),其 apply 方法會(huì)使用GenericTokenParser
解析${}
占位符丰辣,并直接替換成用戶給定的實(shí)際參數(shù)值 -
IfSqlNode
:對(duì)應(yīng)的是動(dòng)態(tài)SQL節(jié)點(diǎn)<If>
節(jié)點(diǎn)撒强,其 apply 方法首先通過(guò)ExpressionEvaluator.evaluateBoolean()
方法檢測(cè)其 test 表達(dá)式是否為 true,然后根據(jù) test 表達(dá)式的結(jié)果糯俗,決定是否執(zhí)行其子節(jié)點(diǎn)的 apply() 方法 -
TrimSqlNode
:會(huì)根據(jù)子節(jié)點(diǎn)的解析結(jié)果尿褪,添加或刪除相應(yīng)的前綴或后綴。 -
WhereSqlNode
和SetSqlNode
都繼承了TrimSqlNode
-
ForeachSqlNode
:對(duì)應(yīng)<foreach>
標(biāo)簽得湘,對(duì)集合進(jìn)行迭代 - 動(dòng)態(tài)SQL中的
<choose>
杖玲、<when>
、<otherwise>
分別解析成ChooseSqlNode
淘正、IfSqlNode
摆马、MixedSqlNode
綜上,SqlNode
接口有多個(gè)實(shí)現(xiàn)類鸿吆,每個(gè)實(shí)現(xiàn)類對(duì)應(yīng)一個(gè)動(dòng)態(tài)SQL節(jié)點(diǎn)囤采,其中 SqlNode
扮演抽象構(gòu)件角色,MixedSqlNode
扮演容器構(gòu)件角色惩淳,其它一般是葉子構(gòu)件角色蕉毯。
PS:以上代碼提交在 Github :https://github.com/Niuh-Study/niuh-designpatterns.git
文章持續(xù)更新,可以公眾號(hào)搜一搜「 一角錢(qián)技術(shù) 」第一時(shí)間閱讀思犁, 本文 GitHub org_hejianhui/JavaStudy 已經(jīng)收錄代虾,歡迎 Star。