欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-17 13:36:42
...
《编程机制探析》第八章 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之前,我们必须做好相应的知识储备。下一章,我们讲解线程相关的种种概念和模型。
相关标签: 设计模式