Back
Featured image of post 设计模式-组合模式

设计模式-组合模式

一、模式简介

今天学习的是组合模式,通过前面在迭代器中举的例子我们可以用迭代器来遍历某个菜单中的所有菜单项,那如果现在加入了一个子菜单(甜点菜单)作为早餐菜单的一个菜单项,再使用迭代器遍历元素时就会发现因为某个菜单中的菜单项类型不同,我们现有的迭代器就无法完成菜单中元素的遍历打印,因此我们引入了组合模式。组合模式:允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象及对象组合。

二、模式详解

我们上面提出了当某个菜单中含有子菜单时,例如早餐菜单中含有一个甜点菜单作为早餐菜单中的一个菜单项,正好构成了一个对象的树形结构,这时我们无法用原有的迭代器进行遍历元素进行打印。通过前面组合模式的概念我们发现,正好可以用组合模式来解决这一问题。下面我们来看一下组合模式的类图:

  • compent为组合中的对象定义一个接口,不管是组合还是叶节点。
  • composite 支持add、remove、getChild等操作组合方法,并且支持自身行为及和叶节点共有行为
  • leaf 叶节点只实现其支持的行为。

下面我们来改造一下我们的菜单使其支持,打印出菜单项和子菜单:

  • MenuComponent
/**
 * 菜单组件
 *
 */
public abstract class MenuComponent {
    /**
     *上面是操作组件方法
     *
     * */
    public void add(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }
    public void remove(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }
    public void getChild(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }
    /**
     * 下面是一些组件本身的方法
     *
     * */
    public String getName(){
        throw new UnsupportedOperationException();
    }
    public String getDescription(){
        throw new UnsupportedOperationException();
    }
    public String getPrice(){
        throw new UnsupportedOperationException();
    }
    public boolean isVegetarian(){
        throw new UnsupportedOperationException();
    }
    public void  print(){
        throw new UnsupportedOperationException();
    }
}
  • 菜单项
/**
 * 菜单项
 *
 */
@Data
@AllArgsConstructor
public class MenuItem extends MenuComponent{
    /**
     * 名字
     */
    private String name;
    /**
     * 描述
     */
    private String description;
    /**
     * 价格
     */
    double price;
    public void  print(){
        System.out.println("我是"+getName());
        if (isVegetarian()){
            System.out.println("v");
        }
        System.out.println(" "+getPrice());
        System.out.println("描述"+getDescription());
    }

}
  • 早餐菜单
/**
 * 早餐菜单
 *
 */
@Data
public class BreakfastMenu extends MenuComponent {
    ArrayList menuItems;
    String name;
    String description;
    public BreakfastMenu(){
        menuItems=new ArrayList<>();
    }
    public void addMenuItems(String name,String description,double price) {
        com.example.test.demo.iterator.MenuItem menuItem=new MenuItem(name,description,price);
        menuItems.add(menuItem);
    }

    /**
     * 这里获取菜单的方法可以不用了
     *
     *
     */
    public ArrayList getMenuItems(){
        return menuItems;
    }

    public void print(){
        System.out.println("名称"+getName());
        System.out.println("描述"+getDescription());
        //我们使用了迭代器,他遍历所有菜单组件,再遍历的过程中,可能遇到菜单项或者菜单,因为它们都实现了print(),我们调用print()方法即可
        Iterator iterator = menuItems.iterator();
        while (iterator.hasNext()){
            MenuComponent next = (MenuComponent) iterator.next();
            next.print();
        }
    }
}

这里我们使用菜单间的组合,利用print()方法能够让我们在打印时不区分菜单和菜单项进行打印(处理整体/部分间的公共行为)。看到这里你可能会发现我们只是在对象内部提供了print()方法在内部使用了迭代器,并没有像迭代器模式那样为服务员提供迭代器进行元素遍历。别急,这只是为了让我们熟悉一下组合模式的结构,下面我们来通过组合模式生成复合迭代器:

  • 菜单组件
/**
 * 菜单组件
 *
 */
public abstract class MenuComponent {
    /**
     *上面是操作组件方法
     *
     * */
    public void add(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }
    public void remove(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }
    public void getChild(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }
    /**
     * 下面是一些组件本身的方法
     *
     * */
    public String getName(){
        throw new UnsupportedOperationException();
    }
    public String getDescription(){
        throw new UnsupportedOperationException();
    }
    public String getPrice(){
        throw new UnsupportedOperationException();
    }
    public boolean isVegetarian(){
        throw new UnsupportedOperationException();
    }
    public void  print(){
        throw new UnsupportedOperationException();
    }
    public Iterator createIterator(){
        throw new UnsupportedOperationException();
    }
}

我们想要生成迭代器因此在组件中加入createIterator()方法

  • 早餐菜单 在早餐菜单中加入生成组合迭代器的方法
/**
 * 早餐菜单
 *
 * @author 张鹏
 * @date 2022/03/13
 */
@Data
public class BreakfastMenu extends MenuComponent {
    ArrayList menuItems;
    String name;
    String description;
    public BreakfastMenu(){
        menuItems=new ArrayList<>();
    }
    public void addMenuItems(String name,String description,double price) {
        com.example.test.demo.iterator.MenuItem menuItem=new MenuItem(name,description,price);
        menuItems.add(menuItem);
    }

    /**
     * 这里获取菜单的方法可以不用了
     *
     *
     */
    public ArrayList getMenuItems(){
        return menuItems;
    }

    public void print(){
        System.out.println("名称"+getName());
        System.out.println("描述"+getDescription());
        //我们使用了迭代器,他遍历所有菜单组件,再遍历的过程中,可能遇到菜单项或者菜单,因为它们都实现了print(),我们调用print()方法即可
        Iterator iterator = menuItems.iterator();
        while (iterator.hasNext()){
            MenuComponent next = (MenuComponent) iterator.next();
            next.print();
        }
    }
    public Iterator createIterator(){
        return new CompositeIterator(menuItems.iterator());
    }
}
  • 复合迭代器

使用复合迭代器解决遍历菜单和菜单项的问题

/**
 * 复合迭代器
 *
 */
public class CompositeIterator implements Iterator {
    Deque stack=new LinkedList();
    public CompositeIterator(Iterator iterator){
        stack.push(iterator);
    }
    //想要知道是否还有下一个元素,我们检查栈是否被清空,如果已经清空了就表示没有下一个元素
    //否则我们就从栈中去除迭代器,看看是否还有下一个元素,如果他没有我们弹出递归去执行
    @Override
    public boolean hasNext() {
        if (stack.isEmpty()){
            return false;
        }else {
            Iterator iterator = (Iterator) stack.pop();
            if (!iterator.hasNext()){
                stack.pop();
                return hasNext();
            }else {
                return true;
            }
        }
    }

    @Override
    public Object next() {
        if (hasNext()){
            Iterator iterator = (Iterator) stack.peek();
            MenuComponent next = (MenuComponent) iterator.next();
            //看查当前元素是否是菜单项
            if (next instanceof BreakfastMenu){
                stack.push(next.createIterator());
            }
            return next;
        }else {
            return null;
        }
    }
}

有了复合迭代器后我们就可以用迭代器遍历菜单和菜单项

三、适用场景

  • 如果你需要实现树状对象结构,可以使用组合模式。

  • 如果你希望客户端代码以相同方式处理简单和复杂元素,可以使用该模式。