不學無數——組合模式

不學無數的程式設計師發表於2018-09-12

組合模式

在DebugMybatis的原始碼時,在DynamicSqlSource.getBoundSql動態獲取sql的時候,Debug會發現相同的方法但是進去的實現類卻不相同,不明白為什麼會這樣,於是上網查了資料說是運用了組合的設計模式。

1. 資料結構

聊組合模式為什麼會聊到資料結構呢?看到最後你應該就會明白了

相信大家都知道資料結構這門學科,在資料結構中有樹這樣的概念,樹中會有根節點、葉子節點等等。樹狀的結構在現實生活中應用廣泛,例如我們熟知的XML格式就是一個樹形的結構

說個簡單的例子,在我們身邊常見的,公司的人事管理就是一個典型的樹形結構。

普遍的公司組織架構

根據這個樹形結構,我們可以抽象出來兩種不同性質的點:

  • 有分支的點

    1. 根節點
    2. 樹枝節點
  • 無分支的點:葉子節點

因此按照我們的思路走下去的,那麼可以簡單的抽象出三個介面。

資料結構類圖

這是最直接能夠想到的類圖表示資訊,但是這個類圖資訊目前表示是有些問題的,如果你已經看出來這個類圖的缺陷的話,那麼這一小部分就可以一目十行跳讀過去了。首先我們先寫出三個介面的程式碼:

--根節點
interface IRoot{
	 //得到總經理的資訊
    String getInfo();
    //根節點下新增節點,例如總經理下面新增研發部經理
    void add(IBranch branch);
    //根節點下新增葉子節點,比如新增祕書
    void add(ILeaf leaf);
    //遍歷手下所有人的資訊
    List getSubordinateInfo();
}
--樹枝節點,資訊同上
interface IBranch{
    String getInfo();
    void add(IBranch branch);
    void add(ILeaf leaf);
    List getSubordinateInfo();
}
--葉子節點,因為葉子節點已經是最底層的了,所以不能增加任何資訊,只能獲得自身的資訊
interface ILeaf{
    String getInfo();
}

複製程式碼

然後看下IRoot的實現類

class Root implements IRoot{
	 //儲存根節點下的子節點資訊
    private List subordinateList=new ArrayList();
    //節點名稱
    private String name;
    //節點的薪資
    private Integer salary;
    //節點的職位
    private String position;

    public Root(String name, Integer salary, String position) {
        this.name = name;
        this.salary = salary;
        this.position = position;
    }

    @Override
    public String getInfo() {
        String info = "";
        info = "名稱: "+this.name;
        info = info + " 職位是: "+ this.position;
        info = info + " 薪水是: "+ this.salary;
        return null;
    }
    //增加樹枝節點
    @Override
    public void add(IBranch branch) {
        subordinateList.add(branch);
    }
	//增加子節點
    @Override
    public void add(ILeaf leaf) {
        subordinateList.add(leaf);
    }
	//得到下級的所有資訊
    @Override
    public List getSubordinateInfo() {
        return this.subordinateList;
    }
}

複製程式碼

樹枝節點Branch和葉子節點Leaf的實現和Root的實現方式一樣,這裡就不一一展示了。然後我們所有的節點資訊都寫完了,最後我們的工作就是進行組裝成一個樹狀結構並且遍歷這棵樹。程式碼如下

public static void main(String[] args) {
        //根節點
        IRoot ceo = new Root("王大麻子",100000,"總經理");
        //部門經理,樹枝節點
        IBranch developCeo = new Branch("劉大瘸子",50000,"研發部經理");
        IBranch saleCeo = new Branch("馬二愣子",50000,"銷售部經理");
        IBranch finaceCeo = new Branch("趙三駝子",50000,"財務部經理");
        //組長,樹枝節點
        IBranch developOne = new Branch("吳大棒槌",20000,"研發一組組長");
        IBranch developTwo = new Branch("鄭老六",20000,"研發二組組長");
        //員工,葉子節點
        ILeaf a = new Leaf("開發人員A",1000,"開發");
        ILeaf b = new Leaf("開發人員B",1000,"開發");
        ILeaf c = new Leaf("開發人員C",1000,"開發");
        ILeaf d = new Leaf("開發人員D",1000,"開發");
        ILeaf e = new Leaf("開發人員E",1000,"開發");
        ILeaf f = new Leaf("開發人員F",1000,"開發");
        ILeaf g = new Leaf("銷售人員G",1000,"銷售");
        ILeaf h = new Leaf("銷售人員H",1000,"銷售");
        ILeaf i = new Leaf("財務人員I",1000,"財務");
        ILeaf j = new Leaf("祕書J",1000,"祕書");
        //進行組裝這個組織架構
        //總經理下的三大得力干將
        ceo.add(developCeo);
        ceo.add(saleCeo);
        ceo.add(finaceCeo);
        //總經理下的祕書
        ceo.add(j);
        //研發部經理下的組長
        developCeo.add(developOne);
        developCeo.add(developTwo);
        //銷售部經理下的員工
        saleCeo.add(g);
        saleCeo.add(h);
        //財務部經理下的員工
        finaceCeo.add(i);
        //研發一組下的員工
        developOne.add(a);
        developOne.add(b);
        developOne.add(c);
        //研發二組下的員工
        developTwo.add(d);
        developTwo.add(e);
        developTwo.add(f);
        System.out.println(ceo.getInfo());
        //遍歷總經理下的所有資訊
        getAllSubordinateInfo(ceo.getSubordinateInfo());
    }

    public static void getAllSubordinateInfo(List subordinateList){
        for (int i = 0; i < subordinateList.size(); i++) {
            Object object = subordinateList.get(i);
            if ( object instanceof ILeaf){
                ILeaf leaf = (ILeaf) object;
                System.out.println(leaf.getInfo());
            }
            else {
                IBranch branch = (IBranch) object;
                System.out.println(branch.getInfo());
                //遞迴呼叫
                getAllSubordinateInfo(branch.getSubordinateInfo());
            }
        }
    }

複製程式碼

這樣我們就得到了我們想要的樹形結構,列印資訊如下

名稱: 王大麻子 職位是: 總經理 薪水是: 100000
名稱: 劉大瘸子 職位是: 研發部經理 薪水是: 50000
名稱: 吳大棒槌 職位是: 研發一組組長 薪水是: 20000
名稱: 開發人員A 職位是: 開發 薪水是: 1000
名稱: 開發人員B 職位是: 開發 薪水是: 1000
名稱: 開發人員C 職位是: 開發 薪水是: 1000
名稱: 鄭老六 職位是: 研發二組組長 薪水是: 20000
名稱: 開發人員D 職位是: 開發 薪水是: 1000
名稱: 開發人員E 職位是: 開發 薪水是: 1000
名稱: 開發人員F 職位是: 開發 薪水是: 1000
名稱: 馬二愣子 職位是: 銷售部經理 薪水是: 50000
名稱: 銷售人員G 職位是: 銷售 薪水是: 1000
名稱: 銷售人員H 職位是: 銷售 薪水是: 1000
名稱: 趙三駝子 職位是: 財務部經理 薪水是: 50000
名稱: 財務人員I 職位是: 財務 薪水是: 1000
名稱: 祕書J 職位是: 祕書 薪水是: 1000

複製程式碼

此時我們會發現,我們有一大坨的程式碼都是公用的,例如每個類中都有getInfo()方法,我們為什麼不把它抽象出來呢,還有為什麼要分根節點和樹枝節點呢,根節點本質上也適合樹枝節點是一樣的。此時我們就能將之前的介面抽象成如下的。

簡化的類圖

介面資訊如下

interface Info{
    String getInfo();
}

interface ILeafNew extends Info{

}

interface IBranchNew extends Info{
    void add(Info info);
    List getSubordinateInfo();
}

複製程式碼

其中BranchNew如下

class BranchNew implements IBranchNew{
    private List subordinateList=new ArrayList();
    private String name;
    private Integer salary;
    private String position;

    public BranchNew(String name, Integer salary, String position) {
        this.name = name;
        this.salary = salary;
        this.position = position;
    }

    @Override
    public String getInfo() {
        String info = "";
        info = "名稱: "+this.name;
        info = info + " 職位是: "+ this.position;
        info = info + " 薪水是: "+ this.salary;
        return info;
    }
	//此處將之前的兩個add方法合成了一個,因為葉子節點和樹枝節點都實現了一樣的介面
    @Override
    public void add(Info info) {
        subordinateList.add(info);
    }

    @Override
    public List getSubordinateInfo() {
        return this.subordinateList;
    }
}

複製程式碼

其中LeafNew如下

class LeafNew implements ILeafNew{
    private String name;
    private Integer salary;
    private String position;

    public LeafNew(String name, Integer salary, String position) {
        this.name = name;
        this.salary = salary;
        this.position = position;
    }

    @Override
    public String getInfo() {
        String info = "";
        info = "名稱: "+this.name;
        info = info + " 職位是: "+ this.position;
        info = info + " 薪水是: "+ this.salary;
        return info;
    }
}

複製程式碼

此時我們經過上面的優化以後還會覺得有些冗雜,因為在LeafNewBranchNew中還有有一模一樣的程式碼。即兩個類中都有重複的getInfo()方法,實現方式也一樣,此時我們完全可以將其抽象出來。類圖表示如下

再次精簡的類圖

看見這個圖,似乎已經是最完美的了,因為減少了很多的工作量,介面也沒了,改成了抽象類。省了很多的程式碼。具體看程式碼如下

首先看一下抽象類抽象出來的公共東西

abstract class Info{
    private String name;
    private Integer salary;
    private String position;

    public Info(String name, Integer salary, String position) {
        this.name = name;
        this.salary = salary;
        this.position = position;
    }
    public String getInfo() {
        String info = "";
        info = "名稱: "+this.name;
        info = info + " 職位是: "+ this.position;
        info = info + " 薪水是: "+ this.salary;
        return info;
    }
}

複製程式碼

抽象類的下面的兩個子類

class BranchNew extends Info{
    private List<Info> subordinateList=new ArrayList();

    public BranchNew(String name, Integer salary, String position) {
       super(name,salary,position);
    }

    //此處將之前的兩個add方法合成了一個,因為葉子節點和樹枝節點都實現了一樣的介面
    public void add(Info info) {
        subordinateList.add(info);
    }

    public List getSubordinateInfo() {
        return this.subordinateList;
    }
}

class LeafNew extends Info{
    public LeafNew(String name, Integer salary, String position) {
       super(name,salary,position);
    }
}

複製程式碼

而此時在建立樹形結構的時候如下,和之前建立的沒多大的差別。

public static void main(String[] args) {
        BranchNew ceo = new BranchNew("王大麻子",100000,"總經理");
        //部門經理,樹枝節點
        BranchNew developCeo = new BranchNew("劉大瘸子",50000,"研發部經理");
        BranchNew saleCeo = new BranchNew("馬二愣子",50000,"銷售部經理");
        BranchNew finaceCeo = new BranchNew("趙三駝子",50000,"財務部經理");
        //組長,樹枝節點
        BranchNew developOne = new BranchNew("吳大棒槌",20000,"研發一組組長");
        BranchNew developTwo = new BranchNew("鄭老六",20000,"研發二組組長");
        //員工,葉子節點
        LeafNew a = new LeafNew("開發人員A",1000,"開發");
        LeafNew b = new LeafNew("開發人員B",1000,"開發");
        LeafNew c = new LeafNew("開發人員C",1000,"開發");
        LeafNew d = new LeafNew("開發人員D",1000,"開發");
        LeafNew e = new LeafNew("開發人員E",1000,"開發");
        LeafNew f = new LeafNew("開發人員F",1000,"開發");
        LeafNew g = new LeafNew("銷售人員G",1000,"銷售");
        LeafNew h = new LeafNew("銷售人員H",1000,"銷售");
        LeafNew i = new LeafNew("財務人員I",1000,"財務");
        LeafNew j = new LeafNew("祕書J",1000,"祕書");
        //進行組裝這個組織架構
        //總經理下的三大得力干將
        ceo.add(developCeo);
        ceo.add(saleCeo);
        ceo.add(finaceCeo);
        //總經理下的祕書
        ceo.add(j);
        //研發部經理下的組長
        developCeo.add(developOne);
        developCeo.add(developTwo);
        //銷售部經理下的員工
        saleCeo.add(g);
        saleCeo.add(h);
        //財務部經理下的員工
        finaceCeo.add(i);
        //研發一組下的員工
        developOne.add(a);
        developOne.add(b);
        developOne.add(c);
        //研發二組下的員工
        developTwo.add(d);
        developTwo.add(e);
        developTwo.add(f);
        System.out.println(ceo.getInfo());
        getAllList(ceo);
    }

複製程式碼

遍歷的程式碼稍微有些變化

public static void getAllList(BranchNew branchNew){
    List<Info> subordinateInfo = branchNew.getSubordinateInfo();
    for (Info info:subordinateInfo){
        if (info instanceof LeafNew){
            System.out.println(info.getInfo());
        }else {
            System.out.println(info.getInfo());
            getAllList((BranchNew) info);
        }
    }
}

複製程式碼

此時發現執行結果和之前的結果一模一樣,這就是組合模式

2. 什麼是組合模式

在剛才的資料結構中我們用程式碼實現了樹形結構。這個就是組合模式。組合模式主要是用來描述部分與整體的關係。

將物件組合成樹形結構以表示“部分-整體”的層次結構,使得使用者對單個物件和組合物件的使用具有一致性

2.1 組合模式的組成

其實我們在上面已經實現了一個組合模式,組合模式的組合就是資料結構中樹形結構的組成並且將其程式碼簡化,抽象出來樹枝節點和葉子節點的公共部分形成抽象類或者介面,並且通過呼叫此抽象類或者介面將組合物件和簡單物件進行一致的處理。

組合模式的類圖

其中組合模式涉及到了三個角色

  • Component:抽象構件,定義了參加組合物件的共有方法和屬性。當然也可以定義為介面
  • Leaf:葉子節點構件,組合模式中最小的遍歷單位
  • Composite:樹枝節點構件,與葉子節點構成一個樹形結構

接下來我們可以寫出實際的組合模式程式碼示例,首先可以先看抽象的構建,它是組合模式的精髓所在

public abstract class Component{
	//無論是個體還是整體都是共享此程式碼的
	public void doSomething(){
	//具體的業務邏輯程式碼
	}
}

複製程式碼

Composite

class Composite extends Component{
    List<Component> list = new ArrayList<>();
    void add(Component component){
        list.add(component);
    }
    void remove(Component component){
        list.remove(component);
    }
    List<Component> getChild(){
        return list;
    }
}

複製程式碼

通用Leaf類可以重寫父類的方法。

通過建立場景類模擬建立樹狀的資料結構,並且通過遞迴的方式遍歷整個樹

public static void main(String[] args) {
    Composite root = new Composite();
    root.doSomething();
    LeafM leafM = new LeafM();
    Composite branch = new Composite();
    root.add(branch);
    branch.add(leafM);
}
//通過遞迴遍歷樹
public static void display(Composite composite){
    for (Component component : composite.getChild()){
        if (component instanceof LeafM){
            component.doSomething();
        }else {
            display((Composite) component);
        }
    }
}

複製程式碼

2.2 透明組合模式

組合模式分為兩種,一種是安全模式,一種是透明模式。我們上面講的是安全模式,那麼透明模式是什麼呢?可以看下透明模式的類圖。

透明模式類圖

通過類圖的對比我們便可知道,透明模式是將方法都放在抽象類中或者介面中。透明模式下的葉子節點和樹枝節點都會有相同的結構,通過判斷是否他下面還有子節點可以知道是葉子節點還是樹枝節點。

3. MyBatis中的組合模式應用

此時我們學完了組合模式以後就知道了在Mybatis中動態組裝Sql中用到了組合模式,那麼Mybatis是如何應用的呢。比如下面的一段Sql。

<select id="queryAllDown" resultType="map" parameterType="String">
    select * from 表名 where  cpt_pro=#{cpt}
    <if test="cpt!=''">
    and cpt_pro=#{cpt}
    </if>
</select>

複製程式碼

Mybatis在進行XML解析的時候會解析兩個標籤,一個是select一個是if,然後通過SqlNode進行解析標籤中的內容,下面是SqlNode中的實現類

SqlNode中的實現類

這些類就構成了SqlNode樹形結構中的各個節點。所有的子節點都是同一類節點,可以遞迴的向下執行。例如StaticTextSqlNode是最底層的節點,因此它直接將Sql拼接到sqlBuilder中。

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }

複製程式碼

而如果是碰到了if標籤,那麼可以看IfSqlNode,在IfSqlNode中會先做表示式的判斷,如果通過的話,那麼進行呼叫遞迴解析。如果不通過就直接跳過。

@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
  contents.apply(context);
  return true;
}
return false;
}
複製程式碼

因此Mybatis就是通過組合模式以一致的方式處理個別物件或者是帶有標籤的物件。

4. 參考文章

相關文章