設計模式 | 組合模式及典型應用

本文的主要內容:html

  • 介紹組合模式
  • 示例
  • 組合模式總結
  • 源碼分析組合模式的典型應用
    • java.awt中的組合模式
    • Java集合中的組合模式
    • Mybatis SqlNode中的組合模式

更多內容可訪問個人我的博客:laijianfeng.org java

關注【小旋鋒】微信公衆號

推薦閱讀

設計模式 | 簡單工廠模式及典型應用
設計模式 | 工廠方法模式及典型應用
設計模式 | 抽象工廠模式及典型應用
設計模式 | 建造者模式及典型應用
設計模式 | 原型模式及典型應用
設計模式 | 外觀模式及典型應用
設計模式 | 裝飾者模式及典型應用
設計模式 | 適配器模式及典型應用
設計模式 | 享元模式及典型應用sql

組合模式

樹形結構不論在生活中或者是開發中都是一種很是常見的結構,一個容器對象(如文件夾)下能夠存放多種不一樣的葉子對象或者容器對象,容器對象與葉子對象之間屬性差異可能很是大。編程

因爲容器對象和葉子對象在功能上的區別,在使用這些對象的代碼中必須有區別地對待容器對象和葉子對象,而實際上大多數狀況下咱們但願一致地處理它們,由於對於這些對象的區別對待將會使得程序很是複雜。segmentfault

一個簡化的Linux目錄樹

組合模式爲解決此類問題而誕生,它可讓葉子對象和容器對象的使用具備一致性設計模式

組合模式(Composite Pattern):組合多個對象造成樹形結構以表示具備 "總體—部分" 關係的層次結構。組合模式對單個對象(即葉子對象)和組合對象(即容器對象)的使用具備一致性,組合模式又能夠稱爲 "總體—部分"(Part-Whole) 模式,它是一種對象結構型模式。安全

因爲在軟件開發中存在大量的樹形結構,所以組合模式是一種使用頻率較高的結構型設計模式,Java SE中的AWT和Swing包的設計就基於組合模式。bash

除此之外,在XML解析、組織結構樹處理、文件系統設計等領域,組合模式都獲得了普遍應用。微信

角色

Component(抽象構件):它能夠是接口或抽象類,爲葉子構件和容器構件對象聲明接口,在該角色中能夠包含全部子類共有行爲的聲明和實現。在抽象構件中定義了訪問及管理它的子構件的方法,如增長子構件、刪除子構件、獲取子構件等。mybatis

Leaf(葉子構件):它在組合結構中表示葉子節點對象,葉子節點沒有子節點,它實現了在抽象構件中定義的行爲。對於那些訪問及管理子構件的方法,能夠經過異常等方式進行處理。

Composite(容器構件):它在組合結構中表示容器節點對象,容器節點包含子節點,其子節點能夠是葉子節點,也能夠是容器節點,它提供一個集合用於存儲子節點,實現了在抽象構件中定義的行爲,包括那些訪問及管理子構件的方法,在其業務方法中能夠遞歸調用其子節點的業務方法。

組合模式的關鍵是定義了一個抽象構件類,它既能夠表明葉子,又能夠表明容器,而客戶端針對該抽象構件類進行編程,無須知道它到底表示的是葉子仍是容器,能夠對其進行統一處理。同時容器對象與抽象構件類之間還創建一個聚合關聯關係,在容器對象中既能夠包含葉子,也能夠包含容器,以此實現遞歸組合,造成一個樹形結構。

示例

咱們來實現一個簡單的目錄樹,有文件夾和文件兩種類型,首先須要一個抽象構件類,聲明瞭文件夾類和文件類須要的方法

public abstract class Component {

    public String getName() {
        throw new UnsupportedOperationException("不支持獲取名稱操做");
    }

    public void add(Component component) {
        throw new UnsupportedOperationException("不支持添加操做");
    }

    public void remove(Component component) {
        throw new UnsupportedOperationException("不支持刪除操做");
    }

    public void print() {
        throw new UnsupportedOperationException("不支持打印操做");
    }

    public String getContent() {
        throw new UnsupportedOperationException("不支持獲取內容操做");
    }
}
複製代碼

實現一個文件夾類 Folder,繼承 Component,定義一個 List<Component> 類型的componentList屬性,用來存儲該文件夾下的文件和子文件夾,並實現 getName、add、remove、print等方法

public class Folder extends Component {
    private String name;
    private List<Component> componentList = new ArrayList<Component>();

    public Folder(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void add(Component component) {
        this.componentList.add(component);
    }

    @Override
    public void remove(Component component) {
        this.componentList.remove(component);
    }

    @Override
    public void print() {
        System.out.println(this.getName());
        for (Component component : this.componentList) {
            component.print();
        }
    }
}
複製代碼

文件類 File,繼承Component父類,實現 getName、print、getContent等方法

public class File extends Component {
    private String name;
    private String content;

    public File(String name, String content) {
        this.name = name;
        this.content = content;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void print() {
        System.out.println(this.getName());
    }

    @Override
    public String getContent() {
        return this.content;
    }
}
複製代碼

咱們來測試一下

public class Test {
    public static void main(String[] args) {
        Folder DSFolder = new Folder("設計模式資料");
        File note1 = new File("組合模式筆記.md", "組合模式組合多個對象造成樹形結構以表示具備 \"總體—部分\" 關係的層次結構");
        File note2 = new File("工廠方法模式.md", "工廠方法模式定義一個用於建立對象的接口,讓子類決定將哪個類實例化。");
        DSFolder.add(note1);
        DSFolder.add(note2);

        Folder codeFolder = new Folder("樣例代碼");
        File readme = new File("README.md", "# 設計模式示例代碼項目");
        Folder srcFolder = new Folder("src");
        File code1 = new File("組合模式示例.java", "這是組合模式的示例代碼");

        srcFolder.add(code1);
        codeFolder.add(readme);
        codeFolder.add(srcFolder);
        DSFolder.add(codeFolder);

        DSFolder.print();
    }
}
複製代碼

輸出結果

設計模式資料
組合模式筆記.md
工廠方法模式.md
樣例代碼
README.md
src
組合模式示例.java
複製代碼

輸出正常,不過有個小問題,從輸出看不出它們的層級結構,爲了體現出它們之間的層級關係,咱們須要改造一下 Folder 類,增長一個 level 屬性,並修改 print 方法

public class Folder extends Component {
    private String name;
    private List<Component> componentList = new ArrayList<Component>();
    public Integer level;

    public Folder(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void add(Component component) {
        this.componentList.add(component);
    }

    @Override
    public void remove(Component component) {
        this.componentList.remove(component);
    }

    @Override
    public void print() {
        System.out.println(this.getName());
        if (this.level == null) {
            this.level = 1;
        }
        String prefix = "";
        for (int i = 0; i < this.level; i++) {
            prefix += "\t- ";
        }
        for (Component component : this.componentList) {
            if (component instanceof Folder){
                ((Folder)component).level = this.level + 1;
            }
            System.out.print(prefix);
            component.print();
        }
        this.level = null;
    }
}
複製代碼

如今的輸出就有相應的層級結構了

設計模式資料
	- 組合模式筆記.md
	- 工廠方法模式.md
	- 樣例代碼
	- 	- README.md
	- 	- src
	- 	- 	- 組合模式示例.java
複製代碼

咱們能夠畫出它們之間的類圖

示例.組合模式類圖

在這裏父類 Component 是一個抽象構件類,Folder 類是一個容器構件類,File 是一個葉子構件類,Folder 和 File 繼承了 Component,Folder 與 Component 又是聚合關係

透明與安全

在使用組合模式時,根據抽象構件類的定義形式,咱們可將組合模式分爲透明組合模式和安 全組合模式兩種形式。

透明組合模式

透明組合模式中,抽象構件角色中聲明瞭全部用於管理成員對象的方法,譬如在示例中 Component 聲明瞭 addremove 方法,這樣作的好處是確保全部的構件類都有相同的接口。透明組合模式也是組合模式的標準形式。

透明組合模式的缺點是不夠安全,由於葉子對象和容器對象在本質上是有區別的,葉子對象不可能有下一個層次的對象,即不可能包含成員對象,所以爲其提供 add()remove() 等方法是沒有意義的,這在編譯階段不會出錯,但在運行階段若是調用這些方法可能會出錯(若是沒有提供相應的錯誤處理代碼)

安全組合模式

在安全組合模式中,在抽象構件角色中沒有聲明任何用於管理成員對象的方法,而是在容器構件 Composite 類中聲明並實現這些方法。

安全組合模式模式圖

安全組合模式的缺點是不夠透明,由於葉子構件和容器構件具備不一樣的方法,且容器構件中那些用於管理成員對象的方法沒有在抽象構件類中定義,所以客戶端不能徹底針對抽象編程,必須有區別地對待葉子構件和容器構件。

在實際應用中 java.awtswing 中的組合模式即爲安全組合模式。

組合模式總結

組合模式的主要優勢以下:

  • 組合模式能夠清楚地定義分層次的複雜對象,表示對象的所有或部分層次,它讓客戶端忽略了層次的差別,方便對整個層次結構進行控制。
  • 客戶端能夠一致地使用一個組合結構或其中單個對象,沒必要關心處理的是單個對象仍是整個組合結構,簡化了客戶端代碼。
  • 在組合模式中增長新的容器構件和葉子構件都很方便,無須對現有類庫進行任何修改,符合「開閉原則」。
  • 組合模式爲樹形結構的面向對象實現提供了一種靈活的解決方案,經過葉子對象和容器對象的遞歸組合,能夠造成複雜的樹形結構,但對樹形結構的控制卻很是簡單。

組合模式的主要缺點以下:

  • 使得設計更加複雜,客戶端須要花更多時間理清類之間的層次關係。
  • 在增長新構件時很難對容器中的構件類型進行限制。

適用場景

  • 在具備總體和部分的層次結構中,但願經過一種方式忽略總體與部分的差別,客戶端能夠一致地對待它們。
  • 在一個使用面嚮對象語言開發的系統中須要處理一個樹形結構。
  • 在一個系統中可以分離出葉子對象和容器對象,並且它們的類型不固定,須要增長一些新的類型。

源碼分析組合模式的典型應用

java.awt中的組合模式

Java GUI分兩種:

  • AWT(Abstract Window Toolkit):抽象窗口工具集,是第一代的Java GUI組件。繪製依賴於底層的操做系統。基本的AWT庫處理用戶界面元素的方法是把這些元素的建立和行爲委託給每一個目標平臺上(Windows、 Unix、 Macintosh等)的本地GUI工具進行處理。

  • Swing,不依賴於底層細節,是輕量級的組件。如今可能是基於Swing來開發。

咱們來看一個AWT的簡單示例:

注意:爲了正常顯示中文,須要在IDEA中的 Edit Configurations -> VM Options 中設置參數 -Dfile.encoding=GB18030

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

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);
    }
}
複製代碼

運行後窗體顯示以下

示例.AWT繪製窗體

咱們在Frame容器中添加了三個不一樣的構件 ButtonLabelTextField,還添加了一個 Panel 容器,Panel 容器中又添加了 ButtonLabelTextField 三個構件,爲何容器 FramePanel 能夠添加類型不一樣的構件和容器呢?

咱們先來看下AWT Component的類圖

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 和 基本組件如 ButtonLabelTextField 等都是 Component 的子類,因此能夠很清楚的看到這裏應用了組合模式

Component 類中封裝了組件通用的方法和屬性,如圖形的組件對象、大小、顯示位置、前景色和背景色、邊界、可見性等,所以許多組件類也就繼承了 Component 類的成員方法和成員變量,相應的成員方法包括:

&emsp;&emsp;&emsp;getComponentAt(int x, int y)
&emsp;&emsp;&emsp;getFont()
&emsp;&emsp;&emsp;getForeground()
&emsp;&emsp;&emsp;getName()
&emsp;&emsp;&emsp;getSize()
&emsp;&emsp;&emsp;paint(Graphics g)
&emsp;&emsp;&emsp;repaint()
&emsp;&emsp;&emsp;update()
&emsp;&emsp;&emsp;setVisible(boolean b)
&emsp;&emsp;&emsp;setSize(Dimension d)
&emsp;&emsp;&emsp;setName(String name)
複製代碼

Java集合中的組合模式

HashMap 提供 putAll 的方法,能夠將另外一個 Map 對象放入本身的存儲空間中,若是有相同的 key 值則會覆蓋以前的 key 值所對應的 value 值

public class Test {
    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 LinkedMap();
        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 實現,並造成樹形結構。

SQLNode 的類圖以下所示

Mybatis SqlNode 類圖

須要先了解 DynamicContext 類的做用:主要用於記錄解析動態SQL語句以後產生的SQL語句片斷,能夠認爲它是一個用於記錄動態SQL語句解析結果的容器

抽象構件爲 SqlNode 接口,源碼以下

public interface SqlNode {
  boolean apply(DynamicContext context);
}
複製代碼

applySQLNode 接口中定義的惟一方法,該方法會根據用戶傳入的實參,參數解析該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 :會根據子節點的解析結果,添加或刪除相應的前綴或後綴。
  • WhereSqlNodeSetSqlNode 都繼承了 TrimSqlNode
  • ForeachSqlNode:對應 <foreach> 標籤,對集合進行迭代
  • 動態SQL中的 <choose><when><otherwise> 分別解析成 ChooseSqlNodeIfSqlNodeMixedSqlNode

綜上,SqlNode 接口有多個實現類,每一個實現類對應一個動態SQL節點,其中 SqlNode 扮演抽象構件角色,MixedSqlNode 扮演容器構件角色,其它通常是葉子構件角色

參考:
劉偉:設計模式Java版
慕課網java設計模式精講 Debug 方式+內存分析
Java AWT基礎及佈局管理
【java源碼一帶一路系列】之HashMap.putAll()
徐郡明:Mybatis技術內幕 3.2 SqlNode&SqlSource
Mybatis 3.4.7 文檔:動態 SQL

相關文章
相關標籤/搜索