《编程机制探析》第八章 Compositor Pattern

《编程机制探析》第八章 Compositor Pattern

在程序设计过程中,设计模式并不一定是单独使用的,很多情况下,我们可能同时组合应用多个设计模式,从而构建成一个更复杂的设计模式。当然,这样构建出来的设计模式,通常已经失去了通用性。
在前面的章节中,我们用sort排序算法作为例子,讲解了最简单的Visitor(Functor)Pattern。那个排序的例子可以进一步扩展,引入更多的设计模式——比如,Compositor Pattern(组合模式)。
在扩展排序例子之前,我们先花一点时间,了解一下关系数据库的相关知识。
关系数据库是现在应用最为广泛的一类数据库,其优点在于概念简单,模型简单,便于操作。从概念模型上来讲,关系数据库就是一堆二维数据表的集合。什么叫做二维数据表?就是纵横排列的一群数据。
你随便打开电脑上一个电子表格程序,比如,Excel等,你看到的表格全都是二维的。为什么?因为你的电脑屏幕就是一个平面,上面最多只能显示二维的信息。至于超出的维度,比如三维、四维什么的,也只能映射到二维的平面上进行显示。
由于关系数据库里面的表格全都是二维的,有时候,关系数据库也被叫做二维关系数据库,关系数据表也被叫做二维关系数据表。别看关系数据表只有二维,但是,多个关系数据表关联起来,能够表达任意多维度的数据关系。从数学模型上来说,关系数据库与其他结构更加复杂的数据库,在表达能力上是一样的。
关系数据库是应用开发中非常重要的组成部分,是应用开发人员必须掌握的一门技术。不过,普及关系数据库知识,并不是本书的内容。关于更多的关系数据库的基本概念和模型,请读者自行补充。
关系数据库由于模型概念简单,操作起来也甚为方便。关系数据库有一门标准的查询语言,叫做SQL,是Structured Query Language(结构化查询语言)的缩写。
SQL是比普通高级编程语言——如Java、Python、Ruby等——更为接近自然语言的一门高级语言,其语法更接近于英语。SQL中最主要的语句就是select语句。通过这个语句,我们可以从关系数据库中查询符合条件的相关数据,并对查询出来的数据结果进行排序。一条带有排序语句的select语句看起来大概是这个样子的:
Select * from … where … order by field1 asc, field2 asc, field3 desc
其中,asc是ascending的缩写,表示从小到大(升序)排序;desc是descending的缩写,表示从大到小(降序)排序。上述语句的排序要求就是——按照field1的升序,field2的升序,field3的降序排序。需要注意的是,这三个排序条件是优先级的。即先按field1的升序排序,如果几个元素的field1相同,这几个元素再根据field2的升序排序。后面的排序条件以此类推,当几个元素的field2相同,这几个元素再根据field3的降序排序。
我们来看一看,如何用sort算法框架和Comparator回调来实现这么复杂的排序。上一章的例子中,Record类中恰好有field1、field2、field3三个字段。我们可以构造一个Comparator的实现类,实现如下的逻辑:先比较field1,如果不相等,那么返回比较结果;如果field1相等,那么比较field2,如果不相等,那么返回比较结果;如果field2相等,那么比较field3,并返回比较结果。
这样的解决方案,看起来很直观,也很简单。但是,这就是最终的解决方案了吗?如果我们有更多的排序条件组合呢?Record类有三个字段,每个字段都可以按照升序降序来排列,根据排列组合原理,可能的排序条件组合多达几十种:
(1)按field1排序。(2)按field2排序。(3)按field3排序。(4)按field1,field2排序。(5)按field1升序,按field2降序排序…...
如果我们为每一种条件组合都写一个Comparator的实现类,那么,我们将需要几十个这样的类。这还是只有三个字段的情况下,如果字段个数继续增长,排序条件的个数将呈幂级数的增长。显然,为每一种条件组合写一个Comparator是不现实的。我们必须为这种组合条件排序找到一种通用的解决方法。我们来所有分析条件组合中变化的部分和不变的部分。
上面所有的排序条件中,不变的部分有3部分:(1)A.field1和B.field1的比较,(2)A.field2和B.field2的比较,(3) A.field3和B.field3的比较。
变化的部分有两部分:(1)升序和降序,(2)这三种比较条件的任意组合排列。
根据这段分析,我们引入两个类,ReverseComparator类和CompositeComparator类。ReverseComparator类用来解决字段的升序、降序问题。
CompositeComparator类用来解决字段的组合排列问题。
请注意,下面的Java代码是可以编译运行的代码。我用的是Java标准开发包中java.util包的Collections中的sort(List, Comparator)过程作为算法框架。相应的,我实现的Comparator也是Java标准开发包中的java.util.Comparator接口。
ReverseComparator的代码如下:
import java.util.Comparator;

public class ReverseComparator implements Comparator{
  /** the original comparator*/
  private Comparator originalComparator = null;

  /** constructor takes a comparator as parameter */
  public ReverseComparator(Comparator comparator){
originalComparator = comparator;
  }

  /** reverse the result of the original comparator */
  public int compare(Object o1, Object o2){
    return - originalComparator.compare(o1, o2);
  }
}

CompositeComparator的代码如下:
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;

public class CompositeComparator implements Comparator{
  /** in the condition list, comparators' priority decrease from head to tail */
  private List comparators = new LinkedList();

  /** get the comparators, you can manipulate it as need.*/
  public List getComparators(){
return comparators;
}

  /** add a batch of comparators to the condition list */
  public void addComparators(Comparator[] comparatorArray){
    if(comparatorArray == null){
      return;
    }

    for(int i = 0; i < comparatorArray.length; i++){
      comparators.add(comparatorArray[i]);
    }
  }

  /** compare by the priority */
  public int compare(Object o1, Object o2){
    for(Iterator iterator = comparators.iterator(); iterator.hasNext();){
      Comparator comparator = (Comparator)iterator.next();

      int result = comparator.compare(o1, o2);
        if(result != 0)
          return result;
       else
     return 0;
} // end for
}
这两段代码有点长,我很抱歉。值得庆幸的是,这两段代码不算太长,还不至于占据太多的篇幅。
除了这两个包含类之外,我们还需要三个基本类——Field1Comaprator、Field2Comaprator、Field3Comaprator,用来做field1、field2、field3的比较。这三个类的功能一目了然,就不再写出来了。
利用这三个基本类和两个包含类,共五个类,我们就可以任意进行条件组合了。下面举例说明,如何组合这些Comparator实现不同的排序条件。
例1:order by field1 asc, field2 asc
CompoiComparator myComparator = new CompoiComparator();
myComparator. addComparators(
new Comparator[]{new Field1Comaprator (),  new Field2Comaprator ()};
);

// records is a list of Record
Collections.sort(records,  myComparator);

例2:order by field1 desc, field2 asc
CompoiComparator myComparator = new CompoiComparator();
myComparator. addComparators(
          new Comparator[]{
new ReverseComparator(new Field1Comaprator ()), 
new Field2Comaprator ()
};
);

// records is a list of Record
Collections.sort(records,  myComparator);

其余的条件组合,可以类推。在上述的例子中,我们又引入了两个简单的Design Pattern——Proxy Pattern(代理模式)和Composite Pattern(组合模式)。
ReverseComparator只包含一个Comparator对象,并保持了Comparator的接口方法。这种模式叫做Proxy Pattern(代理模式,有时候也叫做Delegate Pattern),其实就是把内部对象包装了一下,并执行了一些自己的操作。在这里,ReverseComparator就是把内部对象的比较方法给反转了一下。另外有一种Design Pattern叫做Decorator Pattern,其作用与Proxy Pattern类似,只不过是用继承来实现的。前面讲过,用继承来实现重用,实际上就固定了被重用的类,失去了多态性。我们不推荐。而且,Proxy Pattern完全能够做Decorator Pattern的工作,因此,我们不需要考虑Decorator Pattern,只需要知道Proxy Pattern这个简单、常用、又非常重要的的设计模式就行了。
CompositeComparator是Composite Pattern,能够把一堆基本的简单的功能块结合在一起,构成一个接口相同、功能却复杂得多的功能块。
Composite Pattern能够在各种条件组合的场景中使用,排序条件组合只不过是冰山一角。Java标准开发包中有一个java.io.File类,定义了很多文件操作方法。其中,有一个方法叫做listFile,能够根据不同的过滤条件选出符合条件的文件列表。listFile这个方法接受一个FileFilter接口类型的对象作为过滤条件。
这个功能有点象我们在文件管理器用通配符来搜索相应的文件。比如,我们想搜索JPG图片文件,我们就在文件搜索框中输入*.JPG。除此之外,我们还可以根据大小、时间等信息来搜索文件。
对于File类的listFile方法,我们可以定制不同的FileFilter,实现不同的过滤条件,比如文件时间在某个范围内;文件后缀名,文件名符合某种模式; 是目录,还是文件,等等。
我们可以应用上述的解决方法,实现灵活的过滤条件组合——用一个CompositeFilter类任意组合过滤条件,用一个ReverseFilter类作为排除条件来过滤掉那些我们不想要的文件。
CompositeFilter类的代码
import java.io.FileFilter;
import java.io.File;

import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;

public class CompositeFilter implements FileFilter {

  /** in the filter list, every condition should be met. */
  private List filters = new LinkedList();

  /** get the filters, you can manipulate it as need.*/
  public List getFilters(){
    return filters;
  }

  /** add a batch of filters to the condition list */
  public void addFilters(FileFilter[] filterArray){
    if(filterArray == null){
      return;
    }

    for(int i = 0; i < filterArray.length; i++){
      filters.add(filterArray[i]);
    }
  }

  /** must meet all the filter condition */
  public boolean accept(File pathname) {
    for(Iterator iterator = filters.iterator(); iterator.hasNext();){
      FileFilter filter = (FileFilter)iterator.next();

      boolean result = filter.accept(pathname);

      // if any condition can not be met, return false.
      if(result == false){
        return false;
      }
    }

    // all conditions are met, return true.
    return true;
  }
}

ReverseFilter类的代码
import java.io.FileFilter;
import java.io.File;

public class ReverseFilter implements FileFilter {
  /** the original filter*/
  private FileFilter originalFilter = null;

  /** constructor takes a filter as parameter */
  public ReverseFilter(FileFilter filter){
    originalFilter = filter;
  }

  /** must meet all the filter condition */
  public boolean accept(File pathname) {
      return !originalFilter.accept(pathname);
  }
}

关于这两个类的组合用法,请读者自己尝试,这里不再赘述。
讲到这里,不知读者有没有注意到一个问题。在本书前面的章节中,我曾经强调过,在学习命令式语言时,任何时候,都不要忘记内存模型的概念。在描述C、C++语言特性的时候,我用了很多篇幅来描述复合结构和对象的内存映射结构。而我在用Java语言描述Design Pattern的时候,却有意无意地忽略了这个问题。这是为什么呢?难道Java语言高端到这种程度,都不需要考虑内存模型了吗?
不,不是这样的。下面我就要来讲述Java对象的内存映射问题。之所以推迟到现在来讲,是因为之前我们还没有足够复杂的对象关系的例子。现在,我们已经接触到足够多的、结构足够复杂的Java对象。我们接触到了包含类、组合类、列表类以及数组结构。可以说,Java对象的大部分结构,我们都已经遇到了。
关于Java对象的内存结构,我们应该注意什么呢?Java对象的内存结构,和C++对象有什么区别呢?
在C++语言中,对象有可能分配在内存堆中,也有可能分配在运行栈中。当对象分配在内存堆中的时候,程序员必须自己负责对象内存的释放。当对象分配在运行栈上时,当前过程执行完毕之后,对象内存就会自动释放。因此,在C++中,对象的内存分配情况是比较复杂的。
在Java语言中,情况就简单得多。所有的对象都是在内存堆中分配的。而且,Java虚拟机负责对象的内存自动回收,程序员根本就不用考虑内存释放回收问题。对于C#、Python、Ruby等支持内存自动回收的语言来说,情况也是如此。
在前面的例子中,我们看到,一个Java对象中可以包含另一个Java对象(一个Java类中包含的成员变量的类型是另一个Java类)。这种包含关系,在内存模型中是如何映射的呢?
Java语言中有一个Object Reference(对象引用)的概念。在Java虚拟机中,所有的对象实例都有一个唯一的虚拟机内部的内存地址。这个内存地址,就是Object Reference。当一个对象包含另一个对象的时候,实际上,只是一个对象内部记录了另一个对象的内存地址(即Object Reference)而已。
我们可以想象一下,在一个布满了格子的巨大的内存架上,有两个对象分别占据了两块内存。其中一个对象的内存格中有一个格子记录另一个对象的内存地址。这就形成了一个表面上的包含关系。实际上,这两个对象完全是分立的,最多只能说是一种引用(Reference)关系。从这个意义上来说,Object Reference这个名词是非常贴切的。
Java对象数组也是如此,Java对象数组是一排连续内存格。每一个格子里面都放着一个内存地址,该内存地址记录着某个Java对象在Java虚拟机中的内存地址。
那么,是否存在真正意义上的内存结构的包含呢?是的,存在。比如,C语言的structure类型,C++的class类型,都能够真实的内存包含结构。但是,在Java中,我们只能做到“引用”。这种“引用”关系很有点像关系数据库里面的数据表之间的关系。所有的数据表都只能依靠关联数据来“引用”另一个表,而无法真正地“嵌套包含”另一个数据表。
在有些资料中,用C语言的指针概念来比拟Java的Object Reference概念。这种比拟不能说错。但是,我是坚决摒弃C语言指针概念的。所以,在本书中,我一律用内存地址这个概念来表述Object Reference。
前面讲述了Visitor、Proxy、Composite等常见的、重要的、基本的设计模式。另外,还有一个极其重要的、极其常见的设计模式,我们还没有讲到。那个设计模式就是Iterator Pattern。
实际上,我们在前面的代码例子中已经接触到Iterator了。我写CompositeComparator的时候,用数组来放置多个Comparator对象。但是,我在写CompositeFilter类的代码时,刻意用了LinkedList这个类。当我们需要遍历List数据结构是,我们就必须用到Iterator。还记得那段CompositeFilter类中的accept方法中的代码吗?
for(Iterator iterator = filters.iterator(); iterator.hasNext();)
上述代码中的Iterator的概念和用法,不仅在Java语言中极为常见,在其他的高级语言中也极为普遍。
注:上述代码中的Iterator用法极为冗长繁琐,在比Java更高级的语言中,在Java语言的高级版本,都针对这种用法进行了简化。这里采用这种冗长写法,是为了更清楚地表示出Iterator接口的具体方法。
Iterator Pattern的重要性,怎么强调也不为过。这也是本书重点讲解的Design Pattern之一。
但是,Iterator Pattern的实现,可不像前面讲述的那些基本Design Pattern那么简单。Iterator Pattern的实现,涉及到复杂的机理和相关知识。在讲述Iterator Pattern之前,我们必须做好相应的知识储备。下一章,我们讲解线程相关的种种概念和模型。

你可能感兴趣的:(设计模式)