不学无数—组合模式

组合模式

Posted by 不学无数 on September 12, 2018

组合模式

在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. 参考文章