• Java 设计模式——组合模式


    概述

    有时我们可能会被要求处理一个层级结构明显的对象,比如上下级的公司员工、比如层级嵌套的文件夹,还有丰富多彩的美食菜单。可是,我们可能要屡试不爽地编写深度搜索代码、要小心翼翼地编写递归逻辑。现在你可以忘掉这些,学习一些新的技能,让你秒刷副本。当然,这句有些夸张,你可以忽略。只是它单纯地表达我对本文要说的这个模式的喜欢(也有可能只是因为我工作中恰好遇到这个问题)。


    版权说明

    著作权归作者所有。
    商业转载请联系作者获得授权,非商业转载请注明出处。
    本文作者:Q-WHai
    发表日期: 2016年5月17日
    本文链接:http://blog.csdn.net/lemon_tree12138/article/details/51437883
    来源:CSDN
    更多内容:分类 >> 设计模式


    目录


    组合模式

    定义

    将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

    情境分析

    看到组合的模式的定义,或许你就知道了这里的组合与我们平常讨论的“继承-组合”并不是同一个概念。因为在学习组合模式之前,我就是把这两个弄得有点混淆,所以一直没有认真地学习过它,以为组合也就是那么回事了嘛。可是,当我开始真的学习它的时候,才知道,这是两回事。
    定义上说的是部分与整体的层次结构,可能就这前半句还不能说明什么,不过我们可以从后半句中找到突破点,那就是单个对象与组合对象。也就是在组合对象中组合了一个部分的集合,这个集合中的元素也就是单个元素所具有的对象类型。
    当然,你不要理解成组合模式里也可以只有这个集合了。如果是这样,那么一个 List 完全可以可以搞定,又何必搞出个模式来突显逼格呢?
    现在我们就来“举个栗子”,对,是栗子。哈哈~
    我现在是一家中式餐厅的老板了。在我的店里你可以享用早餐、中餐、晚餐。早餐就简单点,只提供包子、油条跟豆浆吧(是的,这个不是主业)。午餐会丰盛一些,在午餐里你们选择北方菜或是南方菜。北方菜主要有:锅包肉、猪肉炖粉条、辣子鸡、干炸里脊、糖醋里脊、板栗烧鸡、地三鲜、红烧肉、回锅肉、 口水鸡、宫保鸡丁、可乐鸡翅;南方菜主要有:炒米粉、南瓜饼、南焖羊肉、蒸排骨、肉片炒青椒、水果皮萨、北炒鱼香茄子、糯米糍团、芥菜煲。晚餐上可以吃得精致一些,主要有地道小吃和甜点。地道小吃:肉夹馍、羊肉泡馍、乌冬面、章鱼小丸子、葱油饼、老婆饼;甜点:冰淇淋、鲜奶蛋糕还有蜜汁藕。
    把上面的描述转换成图片菜单就像下面这样的:

    这里写图片描述

    当然,这里并不够齐全,只是为了照顾图片的大小(当然也不可否认是博主偷懒了),只绘制了其中的一部分,不过足够我们说明问题了。

    一般组合模式

    模式分析

    在一般组合模式里,我们只做了一件事,那就是模糊了简单元素与复杂元素。怎么说明这一点呢?针对上面举的例子来说,我们的每一种菜都是一个简单元素,而每一种菜系(南方/北方/小说/甜点)或是餐饮的类型(早餐/午餐/晚餐)都是一个复杂元素,因为这里又包含若干的简单元素。我们把菜系定义为 Menu,而每一道具体的菜则定义成 MenuItem。这样我们就可以绘制出一般组合模式的类图,如下:

    这里写图片描述

    这里面我们的菜单(Menu)和菜单项(MenuItem)都继承自一个 MenuComponent。MenuComponent 是一个抽象的类。当 Menu 和 MenuItem 继承自同一个类时,我们就可以实现模糊简单元素与复杂元素了,因为我们可以按照处理 MenuComponent 的方式处理 Menu 和 MenuItem。这里我们只是模糊了这两个的分界,却不能真的等同看待。很简单,上面的的 MenuItem 最起码有一个价格的属性、而 Menu 就不存在这个属性;Menu 可以有一个 add MenuComponent 的方法,而 MenuItem 则不可能会有。
    说到这里,可能你会说组合模式并不完美。是的,我也这么觉得。它让这件事情模糊了,让 MenuComponent 的使用产生了歧义。比如我们在使用它的时候,根本不知道它的某一个方法是可以正常使用。比如,一个 MenuComponent(可实际的类型可能是 Menu,而我们不知道),这时可能会调用它的 getPrice() 方法,这是有问题的,逻辑上是走不通的。那么,我们就必须要为 Menu 的这个方法抛出一个异常(异常是一个心机 boy,我们都不太喜欢它)。当然,也可以在外部使用 instanceof 关键字处理。可是,这样的处理总让我有一种非面向对象的处理过程,所以还是抛出异常吧。
    另外,Menu 中组合了 MenuItem,从这一点来看,倒是有几分“继承-组合”的意味。

    逻辑实现

    先来看看抽象类,这个是基础:
    MenuComponent.java

    public abstract class MenuComponent {
    
        public String getName() {
            throw new UnsupportedOperationException("暂不支持此操作");
        }
    
        public String getDescription() {
            throw new UnsupportedOperationException("暂不支持此操作");
        }
    
        public double getPrice() {
            throw new UnsupportedOperationException("暂不支持此操作");
        }
    
        public boolean isVegetarian() {
            throw new UnsupportedOperationException("暂不支持此操作");
        }
    
        public void print() {
            throw new UnsupportedOperationException("暂不支持此操作");
        }
    
        public void add(MenuComponent menuComponent) {
            throw new UnsupportedOperationException("暂不支持此操作");
        }
    
        public void remove(MenuComponent menuComponent) {
            throw new UnsupportedOperationException("暂不支持此操作");
        }
    
        public MenuComponent getChild(int childIndex) {
            throw new UnsupportedOperationException("暂不支持此操作");
        }
    }

    而在 Menu 的具体类中,虽然是继承了 MenuComponent,可是它的抽象方法又不能全部重写。原因上面也说了,这里不赘述了。可是,由于 Java 语法的客观存在,所以这里我们抛出了一个异常。
    Menu.java

    public class Menu extends MenuComponent {
    
        private String name = null;
        private String desc = null;
        private List<MenuComponent> menuComponents = null;
    
        public Menu(String _name, String _desc) {
            name = _name;
            desc = _desc;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        @Override
        public String getDescription() {
            return desc;
        }
    
        @Override
        public void print() {
            System.out.println("
    Menu: { " + name + ", " + desc + " }");
            if (menuComponents == null) {
                return;
            }
            System.out.println("-------------------------");
    
            for (MenuComponent menuComponent : menuComponents) {
                menuComponent.print();
            }
        }
    
        @Override
        public void add(MenuComponent menuComponent) {
            if (menuComponents == null) {
                menuComponents = new ArrayList<MenuComponent>();
            }
            menuComponents.add(menuComponent);
        }
    
        @Override
        public MenuComponent getChild(int childIndex) {
            if (menuComponents == null || menuComponents.size() <= childIndex) {
                return null;
            }
            return menuComponents.get(childIndex);
        }
    }

    基于上面对 Menu 类的说明,这里的 MenuItem 类的实现过程也是一样:只重写能够重写的部分,不能重写的地方抛出一个异常等待上层处理。
    MenuItem.java

    public class MenuItem extends MenuComponent {
    
        private String name = null;
        private String desc = null;
        private boolean vegetarian = false;
        private double price = 0.0d;
    
        public MenuItem(String _name, String _desc, boolean _vegetarian, double _price) {
            this.name = _name;
            this.desc = _desc;
            this.vegetarian = _vegetarian;
            this.price = _price;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        @Override
        public String getDescription() {
            return desc;
        }
    
        @Override
        public double getPrice() {
            return price;
        }
    
        @Override
        public boolean isVegetarian() {
            return vegetarian;
        }
    
        @Override
        public void print() {
            System.out.println("MenuItem: { " + name + ", " + desc + ", " + vegetarian + ", " + price + " }");
        }
    }

    上面的代码是整个一般组合模式的关键部分,这是需要注意的是它们的 print() 方法。对于 MenuItem 的 print() 来说,是很常规的打印,而 Menu 的打印则需要作处理。因为我们正常的理解里一个菜单因为会包含很多菜单项,所以,这里我们就把当前菜单下的所以菜单打印一遍。不过,这也不是什么难事,因为在每份 Menu 中都有一个 MenuItem 的列表。好了,问题解决。详情参见上面的代码部分。

    模式小结

    从上面的例子也可以看出,组合模式在解决有层级关系时,有着得天独厚的优势。思路清晰、代码优雅。唯一的不足是我们要针对不同的情况抛出相应的异常。

    组合与迭代

    对于组合模式息息相关的另一种模式——迭代模式,它在组合模式中可以说有着重要的地位。在上面的代码中,有点编程逻辑的人应该都可以发现,它们的 print() 方法是对象内部的操作。也就是说,如果我想要通过一个 Menu 操作一个 MenuItem 就必须在 Menu 内部进行实现。这是不现实的,因为需求变化的速度,可能隔了几秒连它的亲妈也不认识了。正因为如此,所以我们就必须想办法从外部拿到 Menu 中的 MenuItem。
    比如现在我想知道餐厅里所有的素食有哪些,如果我们不去改动原有代码,那么就可以添加一个外部的迭代逻辑。

    深搜的试水之行

    或许你又会说,这里根本不需要使用迭代,用一次深搜就 OK 了。是的没错,而且对于一个数据结构基本功还可以的同学,可以马上写出一个深搜的解决方案。这里给出我的深搜方案:

    public void showVegetarMenu(MenuComponent menu) {
            List<MenuComponent> visited = new ArrayList<>();
            showVegetarMenu(menu, visited);
        }
    
        private void showVegetarMenu(MenuComponent menu, List<MenuComponent> visited) {
            if (visited.contains(menu)) {
                return;
            }
    
            if (menu instanceof MenuItem) {
                if (menu.isVegetarian()) {
                    System.out.println(menu);
                }
                return;
            }
    
            List<MenuComponent> children = ((Menu) menu).getChildren();
            for (int i = 0; i < children.size(); i++) {
                showVegetarMenu(children.get(i), visited);
            }
        }
    素食菜单(深搜)
    MenuItem: { 包子, bun, true, 1.5 }
    MenuItem: { 油条, fritters, true, 1.2 }
    MenuItem: { 豆浆, milk, true, 2.0 }
    MenuItem: { 炒米粉, Fried noodles, true, 8.0 }
    MenuItem: { 冰淇淋, ice cream, true, 5.0 }

    结果不出意外。能够使用深搜,已然是逼格满满了。不过,使用深搜让我有一种面向过程编程的感觉,不够优雅。下面就让我用迭代器来实现一次华丽的逆转吧。

    迭代的逆转

    首先我们为 MenuComponent 添加一个 createIterator() 方法。就像下面这样:
    MenuComponent.java

    public abstract class MenuComponent {
    
        ( ... 省略重复的 N 条  ... )
    
        public abstract Iterator<MenuComponent> createIterator();
    }

    由于这里添加的是一个抽象的方法,那么在 Menu 和 MenuItem 中就要必须重写这个 createIterator() 方法。
    Menu.java

    public class Menu extends MenuComponent {
    
        private CompositeIterator iterator = null;
    
        ( ... 省略重复的 N 条  ... )
    
        @Override
        public Iterator<MenuComponent> createIterator() {
            if (iterator == null) {
                iterator = new CompositeIterator(menuComponents.iterator());
            }
    
            return iterator;
        }
    }

    MenuItem.java

    public class MenuItem extends MenuComponent {
    
        ( ... 省略重复的 N 条  ... )
    
        @Override
        public Iterator<MenuComponent> createIterator() {
            return new NullIterator();
        }
    }

    在上面两段代码中提到了两个迭代器类:CompositeIterator、NullIterator。这里有参照书本上的逻辑,不过也有改动,因为书本的迭代器没有通用性,下面会对这一点进行说明的。
    CompositeIterator.java

    public class CompositeIterator implements Iterator<MenuComponent> {
    
        private Stack<Iterator> stack = new Stack<>();
    
        public CompositeIterator(Iterator iterator) {
            stack.push(iterator);
        }
    
        @Override
        public boolean hasNext() {
            if (stack.empty()) {
                return false;
            }
    
            Iterator iterator = stack.peek();
            if (!iterator.hasNext()) {
                stack.pop();
                return hasNext();
            }
    
            return true;
        }
    
        @Override
        public MenuComponent next() {
            if (hasNext()) {
                Iterator iterator = stack.peek();
                MenuComponent component = (MenuComponent) iterator.next();
                if (component instanceof Menu) {
                    Iterator menuIterator = component.createIterator();
                    if (!stack.contains(menuIterator)) {
                        stack.push(menuIterator);
                    }
                }
                return component;
            }
    
            return null;
        }
    
        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    这里的栈结构使用得很巧妙,因为这个栈的使用让我想到在 LeetCode 上的一道算法题,也是使用栈来实现,而且比一般的算法复杂度低很多,如果我不犯懒的话,应该会写那一篇博客的。咳咳,扯远了,回到正题。有关于栈的使用是一些数据结构和 Java api 的基础,这里不多说什么了。还有这里的 hasNext() 和 next() 方法,这里要求你对数据结构和 Java api(主要是 Stack 这一块)比较熟悉。所以,如果你看到这个地方有什么不太理解的,可以留言,也可以自行复习一下这两块内容。

    NullIterator.java

    public class NullIterator implements Iterator<MenuComponent> {
    
        @Override
        public boolean hasNext() {
            return false;
        }
    
        @Override
        public MenuComponent next() {
            return null;
        }
    
        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    因为每个菜单项都不可能什么子菜单项,也就不存在什么迭代器了,所以在 MenuItem 中就可以返回一个 Null 的迭代器。当然,这是理想的做法。你也可以直接返回 null,只是这样一来,在上层就要多一次判空处理,相比较而言,这样的实现更优雅。
    程序的结果自然不出所料:

    素食菜单(迭代)
    MenuItem: { 包子, bun, true, 1.5 }
    MenuItem: { 油条, fritters, true, 1.2 }
    MenuItem: { 豆浆, milk, true, 2.0 }
    MenuItem: { 炒米粉, Fried noodles, true, 8.0 }
    MenuItem: { 冰淇淋, ice cream, true, 5.0 }

    只是,如果你只采用书本上的迭代器来实现,就会出现多级菜单下的菜单项被 show 了 N 遍。而你只能一脸懵逼。


    Ref

    • 《Head First 设计模式》

    GitHub 源码下载


  • 相关阅读:
    scala言语基础学习七
    scala言语基础学习六
    scala言语基础学习五
    scala言语基础学习四
    scala言语基础学习三(面向对象编程)
    scala言语基础学习三
    scala言语基础学习二
    scala言语基础学习
    并发编程实战的阅读(锁的重入)
    数据库必会必知 之 SQL四种语言:DDL DML DCL TCL(转)
  • 原文地址:https://www.cnblogs.com/fengju/p/6335988.html
Copyright © 2020-2023  润新知