浅谈设计模式 - 组合模式(十二)

前言

组合模式是一种十分重要的设计模式,应用场景简直随处可见,各类菜单和目录等中央都能看到组合模式的影子,组合模式通常状况下是和树形构造相辅相成的,而树是软件设计外面十分重要的数据结构,这篇文章将介绍什么是组合模式。

什么是组合模式

容许你将对象组合到树形构造体现“整体局部”的构造,组合能让客户以统一的形式解决个别对象和对象组合,组合其实更像是对于对于各种独立组建的“统一性”,能够将一类类似的事物看为一个整体然而领有齐全不同的工作机制。

介绍

能够说将类似的物品造成一个汇合的模式就是组合模式,他能看两个类似的物品在一处进行完满的交融以及操作。当咱们须要 整体/局部的操作时候,就能够应用这种模式。

特点

  • 组合模式考究的是整体和局部之间的关系,整体能够蕴含局部,局部能够回溯到整体,相互蕴含
  • 组合模式能够让对象构造以“树”的模式蕴含关系。少数状况能够疏忽整体和个体之前的差异

优缺点

长处:

  • 组合模式能够帮忙对象和组合的对象厚此薄彼的看待

毛病:

  • 继承构造,批改抽象类违反凋谢敞开准则
  • 如果层次结构十分深,递归结构影响效率
  • 应用迭代器有可能造成并发遍历菜单的问题

组合模式以繁多职责的准则换取透明性?

组合模式毁坏了的繁多职责准则,组合了多个对象的办法,同时在办法外面做了多种操作,然而这样做却是能够让整个对象能够更加直观的理解整体和局部的个性,这是设计模式外面十分常见的操作。

组合模式的结构图

组合模式的结构图如下:

  • Component 组件:定义组件的接口,这里能够设计为抽象类,能够设计为接口,能够视为组件的“可能的公共行为”。
  • Leaf 叶子节点:用于示意原始对象,叶子节点只须要实现本人的非凡性能即可,比方菜单的菜单子项。
  • Composite 组件节点:定义组件行为,能够具备子节点。同时实现叶子节点的相干操作(继承同一个接口),能够视为一个分类的大类

理论利用场景

因为事实场景当中这样的设计模式构造是有树状构造转换而来的,所以组合模式的应用场景就是呈现树形构造的中央。比方:文件目录显示,多及目录出现等树形构造数据的操作。上面咱们就应用一个菜单的构造来理解一下组合模式的“模板”代码。

实战

模仿场景

组合模式是为树形结构设计的一种设计模式,案例参照一个菜单的治理性能作为模仿,咱们须要拿到不同的菜单分类,在菜单的分类外面,咱们有须要拿到不同的菜单项,咱们能够由任意的菜单项进入到不同的菜单分类,同时能够进入不同的叶子节点。

这次的代码案例是从网上找的例子:

形象组件

形象组件定义了组件的告诉接口,并实现了增删子组件及获取所有子组件的办法。同时重写了hashCodeequales办法(至于起因,请读者自行思考。如有疑难,请在评论区留言)。

package com.jasongj.organization;import java.util.ArrayList;import java.util.List;public abstract class Organization {  private List<Organization> childOrgs = new ArrayList<Organization>();  private String name;  public Organization(String name) {    this.name = name;  }  public String getName() {    return name;  }  public void addOrg(Organization org) {    childOrgs.add(org);  }  public void removeOrg(Organization org) {    childOrgs.remove(org);  }  public List<Organization> getAllOrgs() {    return childOrgs;  }  public abstract void inform(String info);  @Override  public int hashCode(){    return this.name.hashCode();  }    @Override  public boolean equals(Object org){    if(!(org instanceof Organization)) {      return false;    }    return this.name.equals(((Organization) org).name);  }}

简略组件(部门)

简略组件在告诉办法中只负责对接管到音讯作出响应。

package com.jasongj.organization;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class Department extends Organization{    public Department(String name) {    super(name);  }  private static Logger LOGGER = LoggerFactory.getLogger(Department.class);    public void inform(String info){    LOGGER.info("{}-{}", info, getName());  }}

复合组件(公司)

复合组件在本身对音讯作出响应后,还须告诉其下所有子组件

package com.jasongj.organization;import java.util.List;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class Company extends Organization{    private static Logger LOGGER = LoggerFactory.getLogger(Company.class);    public Company(String name) {    super(name);  }  public void inform(String info){    LOGGER.info("{}-{}", info, getName());    List<Organization> allOrgs = getAllOrgs();    allOrgs.forEach(org -> org.inform(info+"-"));  }}

awt的组合模式

组合模式因为应用了同样的接口,会让叶子节点实现一些不必要的性能,此时个别能够应用一个空对象或者应用更为激进的应用抛出异样的模式。

awt这种老掉牙的货色就不多介绍,java的gui其实就是应用了组合模式,上面是一部分的案例代码:

 //创立组件    public MethodsTank() {        //创立组件等        jm = new JMenu("我的菜单(G)");        jmb = new JMenuBar();        jl1 = new JMenuItem("开始新游戏(F)");        jl2 = new JMenuItem("完结游戏");        jl3 = new JMenuItem("从新开始(R)");        jl4 = new JMenuItem("存盘退出");        jl5 = new JMenuItem("回到上次游戏");        draw = new DrawTank();        ses = new selectIsSallup();        //设置快捷键形式        jm.setMnemonic('G');        jl1.setMnemonic('f');        jl3.setMnemonic('r');        jl4.setMnemonic('q');        jl5.setMnemonic('w');        //开启闪动线程        new Thread(ses).start();        //先运行开始画面        this.addTank();    }    public void addTank() {        //增加菜单栏目        jm.add(jl1);        jm.add(jl2);        jm.add(jl3);        jm.add(jl4);        jm.add(jl5);        jmb.add(jm);        //运行选关界面        this.add(ses);        //对于子菜单增加事件        jl1.addActionListener(this);        jl1.setActionCommand("newgame");        jl2.addActionListener(this);        jl2.setActionCommand("gameexit");        jl3.addActionListener(this);        jl3.setActionCommand("restart");        //设置窗体的一些根本属性        this.setTitle("我的坦克大战");        this.setBounds(600, 350, width, height);        //增加菜单栏的形式        this.setJMenuBar(jmb);        this.setDefaultCloseOperation(this.EXIT_ON_CLOSE);        this.setVisible(true);    }

总结

组合模式精华在于“破而后立”,他尽管违反了设计准则,然而通过更加优雅的模式,实现了将繁多的对象由局部变为一个整体。

而组合模式也常常和适配器模式搭配应用,本文的案例只是一个简略的套板,对于组合模式的理论使用场景其实更常见的状况是对于菜单和菜单子项的内容。

结语

组合模式很多状况下可能并不是非常用的上,更多的时候是和其余的设计模式搭配,组合模式咱们须要关注的是“整体-局部”的交融对立即可。

参考资料:

这里有一篇讲的更好的材料,在组合模式的根底上给了一个品质稍高的案例代码:

实战组合模式「营销差异化人群发券,决策树引擎搭建场景」

源码剖析组合模式的典型利用

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);    }}复制代码

运行后窗体显示如下

咱们在Frame容器中增加了三个不同的构件 ButtonLabelTextField,还增加了一个 Panel 容器,Panel 容器中又增加了 ButtonLabelTextField 三个构件,为什么容器 FramePanel 能够增加类型不同的构件和容器呢?

咱们先来看下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 的类图如下所示

须要先理解 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 表演容器构件角色,其它个别是叶子构件角色