用OO方法解一道算术题

板桥里人 http://www.jdon.com 2005/12/26(转载请保留)

  本篇主要为说明使用面向对象的分析和设计方法可以帮助更快地认识事物,更快地排除编程设计过程一个个拦路虎。

  这是在开发Jdon Framework 1.4批量查询过程碰到的一个小案例。J2EE系统中避免不了批量分页查询,就象一个孩子满房间找东西一样,这样的频繁查找是对数据库的折腾,这里有一篇文章试图通过存储过程等数据库操作实现批量查询的案例:海量数据库的查询优化及分页算法方案,但是在设计上这是一个错误的方向,向数据库要性能要潜力的余地已经很小了。

  我以前讲了,面向数据库的设计编程方法已经过去,其中一点是,我们将使用缓存来替代大部分的数据库直接操作,将对数据库的折腾带来的重负载转移到中间J2EE服务器上,而中间服务器可以方便地实现集群拓展。

  那么按照这个思路,由于批量查询条件是各种各样,似乎很难将结果进行缓存,但是,在实际操作中,用户的操作总是按照我们系统提供的导引进行操作,用户又可以随意对任何一个操作进行反复进行,通过缓存至少我们可以降低这部分反复操作对数据库负载开销。

  我们根据查询条件读取数据库时,采取固定块的读取方法,每个块可以为200个数据,也就是说,我们就每次都读取200个数据,然后将这个数据块放在缓存中供下次查询。

  我们将查询结果以分页的方式显示给用户,但是这个分页的结果可能是随机的,例如有可能每页显示20个或每页显示100个或300个等等,这些分页的结果是从固定数据块中获取的。

  一般分页查询条件有两个参数:startIndex:本页在数据块中的开始位置,因为每页开始不一定和数据块的开始是吻合的;还有一个参数是count:就是当前页面显示多少行数据。

好了,算术题来了

  我们需要每次比较查询条件的startIndex和count和数据块,有可能当前这个查询的count超过固定数据块的长度,那么就要启动下一个数据块获取。

  其他场景还有:我们是通过PageIterator(Iterator一个子类)这个对象推送到表现层的,在PageIterator中保存的应该是符合查询条件的所有数据块总和,但是PageIterator的startIndex开始应该是查询条件的startIndex,而count则可能是查询条件的count,也可能是数据块的实际长度,因为符合查询条件的数据集合可能达不到数据块的固定长度,比如固定长度是200,但是满足查询条件的数据只有7个,而查询条件的count可能是任意数值,因此PageIterator的count必须返回一个正确的数值给表现层。

  还有一个场景是:我们必须查询数据库获得数据块,同时将数据块以一个主键Key值保存到缓存中,那么缓存的这个Key值如何实现?Key值的粒度越细;缓存的利用率就越低。

  我开始以为非常简单,不就是查询条件和缓存数据块以及数据块数据块在start/count上比较嘛,也许早就有一种算法来解决这个问题,只是我不知道而已,但是我现在也不可能去google搜索算法大全。

  谈到算法,我想表达一种观点:学习软件的人都要学习算法,其实这是被诱导欺骗了,算法其实就是数学,算法的发明有赖于数学的突破,所以,算法本身没什么东西,但是它将软件诱导服从于数学了,结果,执著于算法概念搞软件的人最后发现,软件其实类似CAD绘图工具,真正创造性思想来自算法后面的数学。最后产生软件比数学低人一等的想法,相当一段时间软件被用作计算的工具,其实这些都是极大地对软件认识不足以及不尊重,软件自身本身就是和数学平等一样的,都是人类表达世界的媒介,特别是面向对象技术和SOA的发展更加表达这种观点,软件最大的追求是什么?

  现在我们回到主题上来,如果我们认为这个算术题仅仅是个算法问题,我们只要穷尽其所有情况就可以,但是深入进去时,会发现可变的条件太多了,可以说每当你发现一个规律后,发现它又是有前提的,这个前提可能还依赖于你的规律,又搞出先有鸡还是先有蛋的老问题出来,这是编程设计中经常会碰到的拦路虎。

  因为不重视这个算术题,以为很简单,所以就按照直觉直接开始编程设计,有如下代码:

  首先,我们设计数据块的缓存Key,它肯定是有查询条件语句组成,比如String sqlquery, Collection queryParams表示前者是查询SQL语句,后面参数是查询参数值,数据块缓存Key是由这两个参数组成,既然是数据块又必须有一个起点,因此我们还需要数据块起点作为它的Key值,它的起点如何计算,公式如下:

int blockID = start / count;
int blockStart = blockID * count;

  得出的blockStart是一个数据块起点,这是动态的值。

  然后,我们设计一个方法从数据库读取然后保存到缓存中:

private List getBlockKeys(QueryConditonDatakey qcdk) {
  logger.debug(" start=" + qcdk.getStart() );
  List keys = blockCacheManager.getBlockKeysFromCache(qcdk);
  if ((keys == null) || (!cacheEnable)) {
    keys = blockQueryJDBC.fetchDatas(qcdk);
    blockCacheManager.saveBlockKeys(qcdk, keys);
  }
  return keys;
}

  这样获得一个以List表示的数据块结果。

  现在,我们要根据这个结果,和查询条件进行比较,得出一个PageIterator结果出来,根据直觉得出如下(当然你的直觉可能比我更加精确):

private List test(QueryConditonDatakey qcdk , int count){
  List keys = getBlockKeys(qcdk);
  int thisDataBlockLength = keys.size() + qcdk.getBlockStart() - qcdk.getStart();
  int currentBlocklength = count;
  if (thisDataBlockLength == QueryConditonDatakey.BLOCK_SIZE) {
    if (count > thisDataBlockLength) {
      //必须满足查询块长度
      //再创建一个DataBlock和CacheBlock 进入循环
      keys.addAll(test(qcdk, count));
    }
  }
  return keys;
}

  可惜这个方法试图做两件事情:获得符合查询条件的数据块,并且获得真实的count,从而能够创建PageIterator。但是开始显得力不从心了。

  我们再看看如何生成PageIterator的代码:

public PageIterator getPageIterator(String sqlqueryAllCount, String sqlquery, Collection queryParams, int startIndex, int count) {
  logger.debug("enter getPageIterator ..");
  QueryConditonDatakey qcdk = new QueryConditonDatakey(sqlquery, queryParams,                     startIndex);

  List keys = test(qcdk, count);//获得符合查询条件的数据块

  int dataBlockLength = keys.size();
  int thisDataBlockLength = keys.size() + qcdk.getBlockStart() - startIndex;

  int currentBlocklength = count;
  if (thisDataBlockLength < 200) {
    if (count > thisDataBlockLength){
      currentBlocklength = thisDataBlockLength;
    }
  }
  int endIndex = startIndex + currentBlocklength;
  int allCount = getDatasAllCount(queryParams, sqlqueryAllCount);//符合条件所有
  logger.debug(" allcount=" + allCount + " keys.length=" + keys.size());
  logger.debug(" startIndex=" + startIndex + " endIndex = " + endIndex);
  return new PageIterator(allCount, keys.toArray(), startIndex, endIndex);
}

  我们觉得应该将test方法内容拉进getPageIterator了,否则有些计算是重复的,而且无法把握两个方法中同样的值是否肯定一致。但是编程直觉告诉我们,将这么多代码放进入一个方法肯定是不对的。

  至少我当时是陷入了多种矛盾和重复中去,而且这段代码虽然可以编译,但是测试下来是有BUG的,因此这段代码肯定是有BUG,或者说不正确的,但是如何保证正确呢?我知道必须持续走下去,我的钻牛角尖顶真的劲头是不小的,但是在这里我被我自己挡住了,说句体外话:牛劲很大的人适合搞软件,有两种可能:被自己拦住了,不是别人,才考虑选择另外一条道路了;但是可怕的是第二种:牛角越钻越尖,变成钻牛角尖,最后觉得自己都变傻了,年纪一大就吃力,只好改行。

  现在,我碰到这样问题,当我在一个问题上原本计划一两个小时都没有完成时,我知道必须“斩仓止损”了,需要换另外一种思维来对待它。

  既然这个问题花费我一段时间,当然也可能我比较笨,或者当初上小学时没有做尽天下所有的算术题,但是和我相似的人应该有一些,怎么办呢?

当你关注它时,它就是一个对象

  这是我使用面向对象分析方法的一个触发点,这道算术题花费我不少时间,该对它特别关注了,在这个算术题中,到底存在哪些对象呢?

  我找到了,我相信你也会找到,数据块Block是一个对象,其实我们费老鼻子劲就是获得一个满足当前页面的Block数据块,而且还有在这个数据块范围内的查询起点和实际页面显示个数。

  另外,这个数据块在不同阶段还表现不同,在缓存里有一个数据块,称为缓存数据块;在数据库中有一个数据块叫数据库数据块;客户端查询条件组合的数据块;根据前两者计算出的当前数据块。

  数据块代码如下:


public class Block {

  private int start;
  private int count;

  private List list;

  public Block(int start, int count) {
    super();
    this.start = start;
    this.count = count;
  }

  public int getCount() {
    return count;
  }

  public void setCount(int count) {
    this.count = count;
  }

  ......
}

  当然,这个数据块对象是最终简化后的结果,其中有反复随着认识深入逐步修改的结果。

  通过将数据块作为一个对象来看待以后,我们思考就更加可以符合实际情况,说白了,就更容易写流水帐了:

1.创建一个数据库的数据块dataBaseBlock

2.创建缓存的数据块cacheBlock

3.创建客户端查询条件的数据块clientBlock,当然这时只有start和count,list有待计算。

4.创建当前页面可用的数据块,这是我们的结果值,这个数据块的start和count需要分别计算,list可能是一个cacheBlock/dataBaseBlock,也可能是多个cacheBlock/dataBaseBlock,需要计算。

  最后实现的功能代码可见开源Jdon Framework1.4(需要专门测试近期会公布)。

  通过本篇文章记录真实实战开发中的思考过程,目的在于让大家了解,面向对象的方法不是一种技术;而是一种思考方式,也是可以解脱我们程序员痛苦编程的一种值得尝试的方法。

  另外,我也想说明的是:OO方法不只是表现在重整Refactoring,也就是说先使用直觉将结果不顾一切地输出,然后再考虑使用OO来整理。如果你一开始使用OO方法,而不是直觉,那么一切本身也许就很流畅,如同庖丁解牛一样。

  最后,所幸的是,不是所有的程序员都必须解这个题,因为你只要直接使用Jdon框架就可以了。将不能把握的、或者不能取得统一解决方案的解决办法放入框架,这样能够大大降低项目延期或失败的风险。我想这点对于所有聪明的人都会明白的。

相关文章:

面向对象与领域建模

J2SE等基础的重要性?

更多OO思维专题

更多算法专题

讨论