組合模式的應用
組合模式介紹
組合模式(Composite Pattern) 的定義是:將對象組合成樹形結構以表示整體和部分的層次結構。組合模式可以讓用戶統一對待單個對象和對象的組合。
比如:Windows操作系統中的目錄結構,通過tree命令實現樹形結構展示。
在上圖中包含了文件夾和文件兩類不同元素,其中在文件夾中可以包含文件,還可以繼續包含子文件夾。子文件夾中可以放入文件,也可以放入子文件夾。 文件夾形成了一種容器結構(樹形結構),遞歸結構。
盡管文件夾和文件是不同類型的對象,它們有一個共性,就是都可以被放入文件夾中。文件和文件夾可以被當做是同一種對象看待。
組合模式其實就是將一組對象(文件夾和文件)組織成樹形結構,以表示一種“部分-整體”的層次結構(目錄與子目錄的嵌套結構)。組合模式讓客戶端可以統一處理單個對象(文件)和組合對象(文件夾)的邏輯(遞歸遍歷)。
組合模式更像是一種數據結構和算法的抽象,數據可以表示成樹這種數據結構,業務需求可以通過在樹上的遞歸遍歷算法來實現。
組合模式主要包含三種角色:
-
抽象根節點(Component):定義系統各層次對象的共有方法和屬性,可以預先定義一些默認行為和屬性。
- 包含所有子類共有行為的聲明和實現。在抽象根節點中定義了訪問及管理子構件的方法,如增加子節點、刪除子節點、獲取子節點等。
-
樹枝節點(Composite):定義樹枝節點的行為,存儲子節點,組合樹枝節點和葉子節點形成一個樹形結構。
- 樹枝節點可以包含樹枝節點,也可以包含葉子節點。它其中有一個集合可以用于存儲子節點,包含在抽象根節點中定義的行為。業務方法中可以遞歸調用其子節點的業務方法。
-
葉子節點(Leaf):葉子節點對象,其下再無分支,是系統層次遍歷的最小單位。
- 葉子節點沒有子節點,包含在抽象根節點中定義的行為。
組合模式示例
程序功能:列出某一目錄下所有的文件和文件夾。
類圖如下:
我們按照下圖的表示,進行文件和文件夾的構建。
Entry類: 抽象類,用來定義File類和Directory類的共性內容
/*** Entry抽象類,表示目錄條目(文件+文件夾)的抽象類*/
public abstract class Entry {public abstract String getName(); // 獲取文件名public abstract int getSize(); // 獲取文件大小// 添加文件夾或文件public abstract Entry add(Entry entry);// 顯示指定目錄下的所有信息public abstract void printList(String prefix);@Overridepublic String toString() {return getName() + "(" + getSize() + ")";}
}
File類: 葉子節點,表示文件
/*** File類 表示文件*/
public class File extends Entry {private String name; // 文件名private int size; // 文件大小public File(String name, int size) {this.name = name;this.size = size;}@Overridepublic String getName() {return name;}@Overridepublic int getSize() {return size;}@Overridepublic Entry add(Entry entry) {return null; // 葉子節點不能添加子節點}@Overridepublic void printList(String prefix) {System.out.println(prefix + "/" + this);}
}
Directory類: 樹枝節點,表示文件夾
/*** Directory表示文件夾*/
public class Directory extends Entry {private String name; // 文件夾名private ArrayList<Entry> directory = new ArrayList<>(); // 文件夾與文件的集合public Directory(String name) {this.name = name;}@Overridepublic String getName() {return this.name;}/*** 獲取文件大小* 1.如果entry對象是File類型,則調用getSize方法獲取文件大小* 2.如果entry對象是Directory類型,會繼續調用子文件夾的getSize方法,形成遞歸調用.*/@Overridepublic int getSize() {int size = 0;for (Entry entry : directory) {size += entry.getSize();}return size;}@Overridepublic Entry add(Entry entry) {directory.add(entry);return this;}@Overridepublic void printList(String prefix) {System.out.println(prefix + "/" + this);for (Entry entry : directory) {entry.printList(prefix + "/" + name);}}
}
測試代碼
public class Client {public static void main(String[] args) {// 根節點Directory rootDir = new Directory("root");// 樹枝節點Directory binDir = new Directory("bin");// 向bin目錄中添加葉子節點binDir.add(new File("vi", 10000));binDir.add(new File("test", 20000));Directory tmpDir = new Directory("tmp");Directory usrDir = new Directory("usr");Directory mysqlDir = new Directory("mysql");mysqlDir.add(new File("my.cnf", 30));mysqlDir.add(new File("test.db", 25000));usrDir.add(mysqlDir);rootDir.add(binDir);rootDir.add(tmpDir);rootDir.add(mysqlDir);rootDir.printList("");}
}
組合模式優點
- 組合模式可以清楚地定義分層次的復雜對象,表示對象的全部或部分層次,它讓客戶端忽略了層次的差異,方便對整個層次結構進行控制。
- 在組合模式中增加新的樹枝節點和葉子節點都很方便,無須對現有類庫進行任何修改,符合“開閉原則”。
- 組合模式為樹形結構的面向對象實現提供了一種靈活的解決方案,通過葉子節點和樹枝節點的遞歸組合,可以形成復雜的樹形結構,但對樹形結構的控制卻非常簡單。
組合模式缺點
- 使用組合模式的前提在于,你的業務場景必須能夠表示成樹形結構。所以,組合模式的應用場景也比較局限,它并不是一種很常用的設計模式。
組合模式使用場景分析
- 處理一個樹形結構,比如,公司人員組織架構、訂單信息等;
- 跨越多個層次結構聚合數據,比如,統計文件夾下文件總數;
- 統一處理一個結構中的多個對象,比如,遍歷文件夾下所有 XML 類型文件內容。
MyBatis中的應用
MyBatis支持動態SQL的強大功能,比如下面的這個SQL:
<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">UPDATE users<trim prefix="SET" prefixOverrides=","><if test="name != null and name != ''">name = #{name}</if><if test="age != null and age != ''">, age = #{age}</if><if test="birthday != null and birthday != ''">, birthday = #{birthday}</if></trim>where id = ${id}
</update>
在這里面使用到了trim、if等動態標簽,我們可以根據實際的業務需求,動態地拼裝SQL語句。這些動態SQL標簽在MyBatis中被解析后會被轉換為不同的SQL節點樹結構,通過組合模式將這些SQL節點組織在一起,最后生成完整的SQL語句。
MyBatis中用組合模式的例子有MixedSqlNode
、TrimSqlNode
、ChooseSqlNode
、IfSqlNode
、WhereSqlNode
等。
我們通過查看MixedSqlNode
類的源碼,可以看到組合模式的應用。
public class MixedSqlNode implements SqlNode {private final List<SqlNode> contents;public MixedSqlNode(List<SqlNode> contents) {this.contents = contents;}@Overridepublic boolean apply(DynamicContext context) {for (SqlNode sqlNode : contents) {sqlNode.apply(context);}return true;}
}
在MixedSqlNode
類中,包含了一個List<SqlNode>
集合contents
,它可以包含SqlNode
類型的對象,如IfSqlNode
、TrimSqlNode
、WhereSqlNode
等。這些節點可以組成一個復雜的樹形結構,通過遞歸調用它們的apply
方法,可以逐步生成完整的SQL語句。
例如,IfSqlNode
類的源碼:
public class IfSqlNode implements SqlNode {private final ExpressionEvaluator evaluator;private final String test;private final SqlNode contents;public IfSqlNode(SqlNode contents, String test) {this.test = test;this.contents = contents;this.evaluator = new ExpressionEvaluator();}@Overridepublic boolean apply(DynamicContext context) {if (evaluator.evaluateBoolean(test, context.getBindings())) {contents.apply(context);return true;}return false;}
}
在IfSqlNode
中,它包含一個SqlNode
類型的contents
,表示如果條件滿足時需要執行的SQL節點。通過調用contents.apply(context)
,可以將子節點的內容拼接到當前的SQL上下文中。
總結
組合模式在處理復雜結構時非常有用,尤其是樹形結構和遞歸處理的場景。它可以通過統一接口處理單個對象和組合對象,簡化了代碼的實現和維護。在MyBatis中,組合模式被廣泛應用于動態SQL的生成過程中,通過不同類型的SQL節點組織成樹形結構,遞歸地生成最終的SQL語句。
通過以上內容,我們了解了組合模式的定義、結構、優缺點以及在實際中的應用,特別是在MyBatis中的具體實現。掌握組合模式可以幫助我們更好地設計和實現復雜結構的代碼,提高代碼的可維護性和擴展性。