设计模式之组合模式

一、引子

在大学的数据结构这门课上,树是最重要的章节之一。还记得树是怎么定义的吗?树(Tree)是n(n≥0)个结点的有限集T,T为空时称为空树,否则它满足如下两个条件:

(1)    有且仅有一个特定的称为根(Root)的结点;

(2)   其余的结点可分为m(m≥0)个互不相交的子集Tl,T2,…,Tm,其中每个子集本身又是一棵树,并称其为根的子树(SubTree)。

上面给出的递归定义刻画了树的固有特性:一棵非空树是由若干棵子树构成的,而子树又可由若干棵更小的子树构成。而这里的子树可以是叶子也可以是分支。

今天要学习的组合模式就是和树型结构以及递归有关系。

 

二、定义与结构

组合(Composite)模式的其它翻译名称也很多,比如合成模式、树模式等等。在《设计模式》一书中给出的定义是:将对象以树形结构组织起来,以达成“部分-整体”的层次结构,使得客户端对单个对象和组合对象的使用具有一致性。

从定义中可以得到使用组合模式的环境为:在设计中想表示对象的“部分-整体”层次结构;希望用户忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象。

看下组合模式的组成。

1)       抽象构件角色Component:它为组合中的对象声明接口,也可以为共有接口实现缺省行为。

2)       树叶构件角色Leaf:在组合中表示叶节点对象——没有子节点,实现抽象构件角色声明的接口。

3)       树枝构件角色Composite:在组合中表示分支节点对象——有子节点,实现抽象构件角色声明的接口;存储子部件。

下图为组合模式的类图表示。

 

如图所示:一个Composite实例可以像一个简单的Leaf实例一样,可以把它传递给任何使用Component的方法或者对象,并且它表现的就像是一个Leaf一样。

可以看出来,使用组合模式使得这个设计结构非常灵活,在下面的例子中会得到进一步的印证。

      

三、安全性与透明性

组合模式中必须提供对子对象的管理方法,不然无法完成对子对象的添加删除等等操作,也就失去了灵活性和扩展性。但是管理方法是在Component中就声明还是在Composite中声明呢?

一种方式是在Component里面声明所有的用来管理子类对象的方法,以达到Component接口的最大化(如下图所示)。目的就是为了使客户看来在接口层次上树叶和分支没有区别——透明性。但树叶是不存在子类的,因此Component声明的一些方法对于树叶来说是不适用的。这样也就带来了一些安全性问题。

 

另一种方式就是只在Composite里面声明所有的用来管理子类对象的方法(如下图所示)。这样就避免了上一种方式的安全性问题,但是由于叶子和分支有不同的接口,所以又失去了透明性。

    


    《设计模式》一书认为:在这一模式中,相对于安全性,我们比较强调透明性。对于第一种方式中叶子节点内不需要的方法可以使用空处理或者异常报告的方式来解决。

 

四、举例

这里以JUnit中的组合模式的应用为例(JUnit入门)。

JUnit是一个单元测试框架,按照此框架下的规范来编写测试代码,就可以使单元测试自动化。为了达到“自动化”的目的,JUnit中定义了两个概念:TestCase和TestSuite。TestCase是对一个类或者jsp等等编写的测试类;而TestSuite是一个不同TestCase的集合,当然这个集合里面也可以包含TestSuite元素,这样运行一个TestSuite会将其包含的TestCase全部运行。

然而在真实运行测试程序的时候,是不需要关心这个类是TestCase还是TestSuite,我们只关心测试运行结果如何。这就是为什么JUnit使用组合模式的原因。

JUnit为了采用组合模式将TestCase和TestSuite统一起来,创建了一个Test接口来扮演抽象构件角色,这样原来的TestCase扮演组合模式中树叶构件角色,而TestSuite扮演组合模式中的树枝构件角色。下面将这三个类的有关代码分析如下:

 

//Test接口——抽象构件角色

public interface Test {
       /**
        * Counts the number of test cases that will be run by this test.
        */
       public abstract int countTestCases();
       /**
        * Runs a test and collects its result in a TestResult instance.
        */
       public abstract void run(TestResult result);
}

//TestSuite类的部分有关源码——Composite角色,它实现了接口Test

public class TestSuite implements Test {
//用了较老的Vector来保存添加的test
       private Vector fTests= new Vector(10);
       private String fName;
       …… 
/**
        * Adds a test to the suite.
        */
       public void addTest(Test test) {          
//注意这里的参数是Test类型的。这就意味着TestCase和TestSuite以及以后实现Test接口的任何类都可以被添加进来
              fTests.addElement(test);
       }
       ……
       /**
        * Counts the number of test cases that will be run by this test.
        */
       public int countTestCases() {
              int count= 0;
              for (Enumeration e= tests(); e.hasMoreElements(); ) {
                     Test test= (Test)e.nextElement();
                     count= count + test.countTestCases();
              }
              return count;
       }
       /**
        * Runs the tests and collects their result in a TestResult.
        */
       public void run(TestResult result) {
              for (Enumeration e= tests(); e.hasMoreElements(); ) {
                    if (result.shouldStop() )
                           break;
                     Test test= (Test)e.nextElement();
                           //关键在这个方法上面
                     runTest(test, result);
              }
       }
            //这个方法里面就是递归的调用了,至于你的Test到底是什么类型的只有在运行的时候得知
            public void runTest(Test test, TestResult result) {
                   test.run(result);
            }
……
}

//TestCase的部分有关源码——Leaf角色,你编写的测试类就是继承自它

public abstract class TestCase extends Assert implements Test {
       ……
       /**
        * Counts the number of test cases executed by run(TestResult result).
        */
       public int countTestCases() {
              return 1;
       }
/**
        * Runs the test case and collects the results in TestResult.
        */
       public void run(TestResult result) {
              result.run(this);
       }
……
}
       可以看出这是一个偏重安全性的组合模式。因此在使用TestCase和TestSuite时,不能使用Test来代替。

 

五、优缺点

从上面的举例中可以看到,组合模式有以下优点:

1)         使客户端调用简单,客户端可以一致的使用组合结构或其中单个对象,用户就不必关心自己处理的是单个对象还是整个组合结构,这就简化了客户端代码。

2)       更容易在组合体内加入对象部件. 客户端不必因为加入了新的对象部件而更改代码。这一点符合开闭原则的要求,对系统的二次开发和功能扩展很有利!

当然组合模式也少不了缺点:组合模式不容易限制组合中的构件。

应用组合模式的会员卡消费:

        那么我们就根据我们会员卡的消费,来模拟一下组合模式的实现吧!let's go!

        首先:

               1.我们的部件有,总店,分店,加盟店!

               2.我们的部件共有的行为是:刷会员卡

               3.部件之间的层次关系,也就是店面的层次关系是,总店下有分店、分店下可以拥有加盟店。

        有了我们这几个必要条件后,我的要求就是目前店面搞活动当我在总店刷卡后,就可以累积相当于在所有下级店面刷卡的积分总额,设计的代码如下:

 

  1. import java.util.ArrayList;  
  2. import java.util.List;  
  3.   
  4. public class PayDemo {  
  5.   
  6.     public abstract class Market {  
  7.         String name;  
  8.   
  9.         public abstract void add(Market m);  
  10.   
  11.         public abstract void remove(Market m);  
  12.   
  13.         public abstract void PayByCard();  
  14.     }  
  15.   
  16.     // 分店 下面可以有加盟店  
  17.     public class MarketBranch extends Market {  
  18.         // 加盟店列表  
  19.         List<Market> list = new ArrayList<PayDemo.Market>();  
  20.   
  21.         public MarketBranch(String s) {  
  22.             this.name = s;  
  23.         }  
  24.   
  25.         @Override  
  26.         public void add(Market m) {  
  27.             // TODO Auto-generated method stub  
  28.             list.add(m);  
  29.         }  
  30.   
  31.         @Override  
  32.         public void remove(Market m) {  
  33.             // TODO Auto-generated method stub  
  34.             list.remove(m);  
  35.         }  
  36.   
  37.         // 消费之后,该分店下的加盟店自动累加积分  
  38.         @Override  
  39.         public void PayByCard() {  
  40.             // TODO Auto-generated method stub  
  41.             System.out.println(name + "消费,积分已累加入该会员卡");  
  42.             for (Market m : list) {  
  43.                 m.PayByCard();  
  44.             }  
  45.         }  
  46.     }  
  47.   
  48.     // 加盟店 下面不在有分店和加盟店,最底层  
  49.     public class MarketJoin extends Market {  
  50.         public MarketJoin(String s) {  
  51.             this.name = s;  
  52.   
  53.         }  
  54.   
  55.         @Override  
  56.         public void add(Market m) {  
  57.             // TODO Auto-generated method stub  
  58.   
  59.         }  
  60.   
  61.         @Override  
  62.         public void remove(Market m) {  
  63.             // TODO Auto-generated method stub  
  64.   
  65.         }  
  66.   
  67.         @Override  
  68.         public void PayByCard() {  
  69.             // TODO Auto-generated method stub  
  70.             System.out.println(name + "消费,积分已累加入该会员卡");  
  71.         }  
  72.     }  
  73.   
  74.     public static void main(String[] args) {  
  75.         PayDemo demo = new PayDemo();  
  76.           
  77.         MarketBranch rootBranch = demo.new MarketBranch("总店");  
  78.         MarketBranch qhdBranch = demo.new MarketBranch("秦皇岛分店");  
  79.         MarketJoin hgqJoin = demo.new MarketJoin("秦皇岛分店一海港区加盟店");  
  80.         MarketJoin btlJoin = demo.new MarketJoin("秦皇岛分店二白塔岭加盟店");  
  81.           
  82.         qhdBranch.add(hgqJoin);  
  83.         qhdBranch.add(btlJoin);  
  84.         rootBranch.add(qhdBranch);  
  85.         rootBranch.PayByCard();  
  86.     }  
  87. }  


运行结果如下:

 

  这样在累积所有子店面积分的时候,就不需要去关心子店面的个数了,也不用关系是否是叶子节点还是组合节点了,也就是说不管是总店刷卡,还是加盟店刷卡,都可以正确有效的计算出活动积分。

      3.什么情况下使用组合模式

       引用大话设计模式的片段:“当发现需求中是体现部分与整体层次结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑组合模式了。”

总结

组合模式是一个应用非常广泛的设计模式,在前面已经介绍过的解释器模式、享元模式中都是用到了组合模式。它本身比较简单但是很有内涵,掌握了它对你的开发设计有很大的帮助。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值