组合模式

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了组合模式相关的知识,希望对你有一定的参考价值。

定义:

将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户能以一致的方式处理个别对象和组合对象。

设计类图:

组合模式中的角色:

  • Component抽象组件:为组合中所有对象提供一个接口,不管是叶子对象还是组合对象。
  • Composite组合节点对象:实现了Component的所有操作,并且持有子节点对象。
  • Leaf叶节点对象:叶节点对象没有任何子节点,实现了Component中的某些操作。

组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。

使用组合结构,我们能把相同的操作应用在组合和个别对象上。换句活说,在大多数情况下,我们可以忽略对象组合和个别对象之问的差别。

示例代码:

public abstract class Component 
    protected String name;

    public Component(String name) 
        this.name = name;
    

    public abstract void operation();

   public void add(Component c) 
        throw new UnsupportedOperationException();
    

    public void remove(Component c) 
        throw new UnsupportedOperationException();
    

    public Component getChild(int i) 
        throw new UnsupportedOperationException();
    

    public List<Component> getChildren() 
        return null;
    
public class Composite extends Component 
    private List<Component> components = new ArrayList<>();

    public Composite(String name) 
        super(name);
    

    @Override
    public void operation() 
        System.out.println("组合节点"+name+"的操作");
        //调用所有子节点的操作
        for (Component component : components) 
             component.operation();
        
    

    @Override
    public void add(Component c) 
        components.add(c);
    

    @Override
    public void remove(Component c) 
        components.remove(c);
    

    @Override
    public Component getChild(int i) 
        return components.get(i);
    

    @Override
    public List<Component> getChildren() 
        return components;
    
public class Leaf extends Component 

    public Leaf(String name) 
        super(name);
    

    @Override
    public void operation() 
        System.out.println("叶节点"+name+"的操作");
    
public class Client 
    public static void main(String[] args) 
        //创建根节点对象
        Component component = new Composite("component");

        //创建两个组合节点对象
        Component composite1 = new Composite("composite1");
        Component composite2 = new Composite("composite2");

        //将两个组合节点对象添加到根节点
        component.add(composite1);
        component.add(composite2);

        //给第一个组合节点对象添加两个叶子节点
        Component leaf1 = new Leaf("leaf1");
        Component leaf2 = new Leaf("leaf2");
        composite1.add(leaf1);
        composite1.add(leaf2);

        //给第二个组合节点对象添加一个叶子节点和一个组合节点
        Component leaf3 = new Leaf("leaf3");
        Component composite3 = new Composite("composite3");
        composite2.add(leaf3);
        composite2.add(composite3);

        //给第二个组合节点下面的组合节点添加两个叶子节点
        Component leaf4 = new Leaf("leaf4");
        Component leaf5 = new Leaf("leaf5");
        composite3.add(leaf4);
        composite3.add(leaf5);

        //执行所有节点的操作
        component.operation();
    

输出结果:

  上述代码中,在组合节点对象Composite的operation()方法中除了执行自身的操作外,还调用了子节点的operation()方法,这样使得客户端可以透明的遍历所有的节点对象的操作,而不用关心操作的是叶子节点还是组合节点,将它们一视同仁。这看上去有点像二叉树的遍历,不过这里并不是二叉树,每个组合节点可以有若干个子节点,而这些子节点,如果是组合节点,则可以继续拥有子节点,如果是叶子节点,那么就终止了。

  叶子节点和组合节点可以有相同的操作,如上面代码中的operation()方法,但是叶子节点不具备add、remove以及getChild操作,如果你试图在叶子节点上调用这些方法就会抛出不支持的异常。组合节点可以添加子节点,因此组合节点实现了add、remove以及getChild等操作。组合节点持有一个节点的集合,在组合节点的operation()方法中通过遍历调用持有节点的operation()方法,就像是在递归遍历一样。通过这种方式Client客户端可以透明的访问节点对象,你可以在客户端中调用一个组合节点的operation()方法,也可以调用一个叶子节点的operation()方法,也就是说你根本不需要关心调用的是组合节点还是叶子节点,它们都可以进行相同的操作。

菜单的例子

  服务员需要打印菜单,如菜单的名称和价格,但是菜单既可以有子菜单组合,也可以有子菜单项,对于子菜单组合,它的下面又可能会有子菜单项,如饮料菜单和甜点菜单等会包含很多东西,而子菜单项就是一个具体的菜名,不会有子菜单了。现在要打印所有的菜单描述,我们用组合模式来实现这个功能:

实现代码:

/**
 * 抽象菜单组件
 */
public abstract class MenuComponent 

    public void add(MenuComponent menu) 
        throw new UnsupportedOperationException();
    

    public void remove(MenuComponent menu) 
        throw new UnsupportedOperationException();
    

    public MenuComponent getChild(int i) 
        throw new UnsupportedOperationException();
    

    public String getName() 
        throw new UnsupportedOperationException();
    

    public double getPrice() 
        throw new UnsupportedOperationException();
    

    public abstract void print();
/**
 * 菜单组件
 * 菜单组件有菜单名和子菜单,但没有价格,支持添加、删除和打印等操作
 */
public class Menu extends MenuComponent 
    private List<MenuComponent> menuList = new ArrayList<>();
    private String name;

    public Menu(String name) 
        this.name = name;
    

    @Override
    public void add(MenuComponent menu) 
        menuList.add(menu);
    

    @Override
    public void remove(MenuComponent menu) 
        menuList.remove(menu);
    

    @Override
    public MenuComponent getChild(int i) 
        return menuList.get(i);
    

    @Override
    public String getName() 
        return name;
    

    @Override
    public void print() 
        System.out.println("--------");
        System.out.println(getName());
        //打印所有子菜单
        for (MenuComponent menu : menuList) 
             menu.print();
        
        System.out.println("--------");
    
/**
 * 菜单项
 * 菜单项拥有名称和价格,可以打印,但是不支持添加、删除等操作
 */
public class MenuItem extends MenuComponent 
    private String name;
    private double price;

    public MenuItem(String name, double price) 
        this.name = name;
        this.price = price;
    

    @Override
    public String getName() 
        return name;
    

    @Override
    public double getPrice() 
        return price;
    

    @Override
    public void print() 
        System.out.println(getName() + " -- " + getPrice());
    
public class Client 
    public static void main(String[] args) 
        Menu menu = new Menu("所有菜单");

        Menu menu1 = new Menu("子菜单1");
        Menu menu2 = new Menu("子菜单2");
        Menu menu3 = new Menu("子菜单3");

        //给所有菜单添加三个子菜单
        menu.add(menu1);
        menu.add(menu2);
        menu.add(menu3);

        //给第二个菜单添加一个菜单项和一个子菜单
        menu2.add(new MenuItem("子菜单2--菜单项", 10.0));
        Menu menu4 = new Menu("子菜单2--子菜单");
        menu2.add(menu4);
        menu4.add(new MenuItem("子菜单2--子菜单--菜单项", 20.0));

        //打印所有菜单
        menu.print();
    

打印结果:

  在抽象菜单MenuComponent组件中,我们将一些操作默认抛出UnsupportedOperationException异常,如果子类支持该操作就重写实现该操作,如果子类不支持该操作,就不用管它。使用组合模式,打印菜单变得非常容易,而且更好的一点是,你现在可以拿任何一个子菜单来打印结果,而不用管它是具体的菜单项还是里面又包含了子菜单。如果不用组合模式,很难想象有一种方法能很方便的将所有的菜单打印出来。

菜单例子中,既要管理层次结构,又要执行打印操作,是否破坏了单一职责?

  严格来说,是的。我们可以这么说,组合模式以单一职责换取透明性。 什么是透明性?通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合和叶节点 一视同仁。也就是说一个元素究竟是组合还是叶节点,对客户是透明的。

  现在,我们在 MenuComponent 类中同时具有两种类型的操作. 因为客户有机会对一个元素做一些没有意义的操作(例如试图把菜单添加到菜单项),所以我们失去 一些‘安会性”。这是设计上的抉择;我们当然也可以采用另一种方向的设计,将责任区分开来放在不同的接口中。这么一来,设计上就比较安全,但我们也因此失去了透明性,客户的代码将必须使用条件语句和 instanceof 操作符处理不同类型的节点。

  所以, 这是一个很典型的折衷案例。尽管我们受到设计原则的指导,但是,我们总是需要观察某原则对我们的设计所造成的影响。有时候,我们会故意做一些看似违反原则的事情。然而,在某些例子中,这是观点的问题。比方说让管理孩子的操作(例如 add ( )、 remove( )、 getchild ( ) )出现在叶节点中,似乎很不恰当,但是换个视角来看,你可以把叶布点视为没有孩子的节点。

组合模式的扩展

子节点可以指向父节点

  组件可以有一个指向父节点的引用,以便在游走时更容易。而且,如果引用某个孩子,你想从树形结构中删除这个孩子,你会需要从父节点中去蒯除它。一旦孩子有了指向父亲的引用,这做起来就很容易。这样做也使得遍历操作可上可下,更加自由灵活。

使用缓存

  有时候,如果这个组合结构很复杂,或者遍历的代价太高,那么实现组合节点的缓存就很有帮助。比方说,如果你要不断地遍历一个组合,而且它的每一个子节点都需要进行某些计算,那你就应该使用缓存来临时保存结果,从而省去遍历的开支。

组合模式应用场景

  这个应用的地方也比较多,比如大多数系统的UI界面的导航菜单一般都是组合模式,再如android里面的xml布局都是用的组合模式。在选择是否应用组合模式时,要考虑设计上的抉择,到底是要透明性更多一点,还是安全性更多一点,需要做一个平衡。


参考:

以上是关于组合模式的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript设计模式与开发实践---读书笔记(10) 组合模式

设计模式15:组合模式

扎实基础_设计模式_结构型_组合模式

设计模式 组合模式

PHP设计模式—组合模式

结构型设计模式-组合模式