类的递归和组合模式

                                                                  类的递归和组合模式

     递归算法是大家非常熟悉的一种算法。如著名的菲波拉契数列:输入数列的前两项,以后的每一项都是它的前两项之和。现在我们要求该数列的第N(N>0)位的数是多少。对于这个问题,我们可以很轻松的给出下面的算法:
 public int getResult(int first,int second,int number)
 {
   if(number==1) return first;
   else if(number==2) return second;
   else if(number>2) return getResult(first,second,number-2)+getResult(first,second,number-1);
   //输入有误,返回错误代码
   else return –1;
 }
      这就是一个典型的递归算法。这种算法的一个好处是客户端在使用该方法时,不管是N为1或2或者以上,都使用同一个方法,简化了客户端的调用。
      关于递归的话题,在这里我们也不想多说,我们却有兴趣将这个问题扩展开来说:我们知道,类其实是数据和对数据的算法放在一起的封装,既然算法有递归的问题,如上面所示;那么数据是不是也有递归的问题呢?如果答案是肯定的的话,那么该算法和数据加在一起不就是类了,那么类也应该有递归的问题了?
      真的是这样吗?
      我们来看这样一个实际的问题:
      我们会经常去买一些光盘,如果买得多的话,就会需要一个光盘盒。假设商家需要这么一个售卖系统,卖给客户商品需要打印商品信息和应付款项。
      这里我们分析一下,需要两个类:光盘类和光盘盒类。每一类都有相同的两个方法,一个打印商品信息、一个给出应付款项。
      然后我们在客户端调用如下:
      如果客户买光盘,则调用光盘类的两个方法;
      如果客户买光盘盒,则调用光盘盒类的两个方法;
      如果客户买装有光盘的光盘盒,则调用两者之和。
      以上是我们的一个常规的思路。对于这样的客户端调用,我们会发现它的很多弱点:第一是不符合依赖颠倒原则,客户端强烈的依赖两个具体的类,这样,如果商家再增加一种新的商品,则需要修改客户端;第二是将对商品的判断放在了客户端,不符合单一职责原则。
      但是我们看看客户端对两个类的调用方法,就会发现它很符合递归算法的一般逻辑,只不过这里不是简单方法,而是类。那么我们对于这样的类,也会不会有所谓的递归算法呢?
      为什么需要递归算法?我们先不考虑太多,递归算法有一个明显的好处就是客户端调用的接口是统一的,而不需要做if…else…判断,这简化了客户端的调用。
      这里所谓的类的递归,其实就是组合模式。下面我们来看看组合模式是怎么来解决上面的问题的:
      首先我们来写一个接口,这是绝大多数的模式需要做的,它是为了满足面向对象的基本原则的依赖颠倒原则,而该原则的目的是为了使用多态性:
 public interface Equipment
 {
   public void description();
   public double getCount();
 }
      这个接口有两个目的:对商品进行描述和取得应付款项。
      接着我们来看光盘类的实现:
 public class Disk implements Equipment {
  public void description() {
   System.out.println("disk...");
  }
  public double getCount() {
   return 1.2;
  }
 }
      这个类很简单,没有什么好说的。下面来看光盘盒类:
 import java.util.ArrayList;
 import java.util.List;
 
 public class Classic implements Equipment {
  private List disks = new ArrayList();
  public void add(Equipment equipment)
  {
   disks.add(equipment);
  }
  public void description() {
   //先给出对盒部分的描述
   System.out.println("classic...");
   //然后是对盘部分的描述,两者加起来就是对带盘的光盘盒的描述
   for(int i=0;i<disks.size();i++)
   {
    ((Equipment)disks.get(i)).description();
   }
 
  }
  public double getCount() {
   //先是盒的价钱
   double count = 2.8;
   //再加上盘的价钱
   for(int i=0;i<disks.size();i++)
   {
    count+=((Equipment)disks.get(i)).getCount();
   }
   return count;
  }
 }
      这个类比较复杂一些,其实是一个递归算法:首先是数据的递归,一个光盘盒除了它自己以外,还有一些光盘,表现在数据里用了一个类型为List的变量disks来存储。方法add是用来往盒子里加光盘。然后的两个方法都是递归算法,首先是算它自己的那部分,然后是加上光盘的部分,这是一个典型的递归算法。下面我们来看客户端:
 equipment.description();
 System.out.println(equipment.getCount());
      这就是一个简单的客户端调用,如果equipment = new Disk();的话,计算的是光盘;如果equipment = new Classic();的话,计算的是光盘盒;如果是带光盘的盒,如:Disk d1 = new Disk();Disk d2 = new Disk();Classic equipment = new Classic();equipment.add(d1);equipment.add(d2);那么计算的是它们所有。
      你看,一个类的递归通过对数据和方法的递归就完成了。
      在模式里,我们把这种所谓的类的递归称为:组合模式。
      所谓的组合模式,就是对软件过程中的一种“整体-部分”的解决的一个解决方案。如上面的商品就是有这种关系:
      工具箱
            ——光盘盒
                      ——光盘
      光盘可以放在光盘盒里,这里带光盘的光盘盒就是一个整体,光盘和光盘盒各是部分;而光盘盒又可以放在工具箱里,装有物品的工具箱又是一个整体,其中的光盘、盒和箱都是部分。
      组合模式的解决思路是:让整体和部分都实现相同的接口,而整体类是由组成它的各部分组合而成。这样,客户端只需调用接口的方法就行。客户端无需关心它使用的那个类是整体还是部分,由于都实现相同的接口,通过多态的后期绑定,如果调用的类是部分的话,直接调用部分类的方法;如果是整体类,则通过递归,一直到达部分得出计算结果。
      我们后过头去看看上面的例子:Disk类实现了Equipment接口,它是一个部分类,也是递归的尽头,当客户端调用一个整体类的时候,需要一直递归到这个类才结束。Classic类是一个整体类,也实现了Equipment接口,是为了对整体和部分使用多态,在客户端调用的时候不需要关心那个类是整体类还是部分类。既然Classic类是一个整体类,是Disk类的组合,我们用了一个List对象来存储这些部分,在各方法中的计算是对各部分的计算之和。
      我们再来看组合模式的优点:第一是简化了客户端的调用,客户端不管是整体还是部分都调用统一的接口;第二如果又增加了新的整体或部分,只需要增加相应的类,对客户端无需做任何修改。
      例如,在上例中,我们再增加一个工具箱类:
 public class Cabinet implements Equipment
 {
  private List disks = new ArrayList();
  public void add(Equipment equipment)
  {
   disks.add(equipment);
  }
  public void description() {
   System.out.println("cabinet...");
   for(int i=0;i<disks.size();i++)
   {
    ((Equipment)disks.get(i)).description();
   }
 
  }
  public double getCount() {
   double count = 3.0;
   for(int i=0;i<disks.size();i++)
   {
    count+=((Equipment)disks.get(i)).getCount();
   }
   return count;
     }
 }
      我们来看客户端的调用:
 Disk disk = new Disk();
 Classic clc = new Classic();
 clc.add(disk);
 Cabinet cbt = new Cabinet();
 Cbt.add(clc);
      如果觉的还不够,我们可以再放一个光盘类在工具箱内:
 Disk d1 = new Disk();
 Cbt.add(d1);
      最后计算:
 cbt.description();
 System.out.println(cbt.count());
      这就是一个完整的组合模式的解决过程。如果大家对Classic类和Cabinet类的很多代码重复感到不满意,可以使用模板方法模式对这两个类进行改造,如下:
 //我们对两个整体类再进行一次抽象
 public abstract class Whole implements Equipment
 {
   //部分
   private List parts = new ArrayList();
   public void add(Equipment e)
   {
    this.parts.add(e);
 }
 //一部分的描述延迟到子类中实现
 public abstract void partDescription();
 //整体的描述
 public void description()
 {
  //部分描述
  partDescription();
  //另外部分描述
 for(int I=0;I<parts.size();I++)
 {
   ((Equipment)parts.get(i)).description();
 }
 }
 //一部分的款项延迟到子类中计算
 public abstract double getPartCount();
 //整体的应收款项的计算
 public double getCount() {
   double count = this.getPartCount();
   for(int i=0;i<this.parts.size();i++)
   {
    count+=((Equipment)this.parts.get(i)).getCount();
   }
   return count;
  }
 }
      这样的话,我们可以对光盘盒类改造为:
 public class Classic extends Whole {
  public void partDescription()
 {
   System.out.println("classic............");
  }
  public double getPartCount() {
   return 2.9;
  }
 }
      对于工具箱类也可以做同样的修改,这里不再给出。
      我们来看客户端的调用情况:
Equipment disk1 = new Disk();
   Equipment disk2 = new Disk();
   Whole whole = new Classic1();
   whole.add(disk1);
   whole.add(disk2);
   whole.description();
      现在来总结一下组合模式。组合模式是一种解决有类似“整体—部分”结构的问题的一种设计模式。组合模式和Decorator模式一样,都是在利用类的组合来解决问题。只不过Decorator模式解决的是类的继承使得类的数量过多的问题,而组合模式来解决的是一种“整体和部分”的问题。
      对组合模式而言,首先是对整体和部分设计一个相同的接口,再让部分类实现接口,然后整体类也实现该接口,并且由前面已实现的部分来组合成该类,这种组合可能是一对一的关系、也可以是一对多的关系。
      最后我们以一个例子来作为结束语:这个例子是一个二叉树的例子,我们用组合模式来对二叉树进行左序遍历、右序遍历、中序遍历和计算深度。
      首先我们来看共同接口:
 public interface TreeComponent {
  public void first();
  public void middle();
  public void rear();
  public int deepth();
 }
      然后是叶子类,实现了该接口:
 public class Leaf implements TreeComponent {
  private String leaf;
  public Leaf(String leaf)
  {
   this.leaf = leaf;
  }
  public void first() {
   System.out.println(leaf);
  }
  public void middle() {
   System.out.println(leaf);
  }
  public void rear() {
   System.out.println(leaf);
  }
  public int deepth()
  {
   return 1;
  }
 }
      最后我们来看树类:
 //首先是实现TreeComponet接口
 public class Tree implements TreeComponent {
  private String treeTop;
 //左右叶子,两个被组合对象
  private TreeComponent left;
  private TreeComponent right;
  public Tree(String treeTop,TreeComponent left,TreeComponent right)
  {
   this.treeTop = treeTop;
   this.left = left;
   this.right = right;
  }
  public void first() {
   //先访问自己
   System.out.println(this.treeTop);
   //再访问左边子树
   if(this.left!=null)
   {
    this.left.first();
   }
   //最后是右边子树
   if(this.right!=null)
   {
    this.right.first();
   }
 
  }
  public void middle() {
   if(this.left!=null)
   {
    this.left.middle();
   }
   System.out.println(this.treeTop);
   if(this.right!=null)
   {
    this.right.middle();
   }
 
  }
  public void rear() {
   if(this.left!=null)
   {
    this.left.rear();
   }
   if(this.right!=null)
   {
    this.right.rear();
   }
   System.out.println(this.treeTop);
 
  }
  public int deepth()
  {
   int ld = 0;
   int rd = 0;
   if(this.left!=null)
   {
    ld = this.left.deepth();
   }
   if(this.right!=null)
   {
    rd = this.right.deepth();
   }
   return (ld>rd?ld:rd)+1;
  }
 }
阅读更多
个人分类: 模式
上一篇对象的相等与比较
下一篇AJAX会取代桌面系统吗?(译文)
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭