代码重构实践 —— 代码改进的一个案例分析

类别:软件工程 点击:0 评论:0 推荐:

      前几天,我接手了一个使用 DELPHI 开发的项目,其中,较迫切的任务是需要解决原有几处代码的性能问题。其中,有一处代码较为典型,因此,特地将其详细问题、解决思路与相关想法整理出来,供大家参考讨论。
      在设计进行数据处理工作的代码时,我们常常会发现有这样的需要:将一些数据按树状的样式显示到屏幕上,方便用户查看或其他操作。举例说:我们有可能想在TREEVIEW控件中显示地区,然后将隶属该区的供应商加入到对应的地区中;或者又是显示客户,然后将隶属于该客户的订单号加入到对应的客户中(这是简要的流程,在应用的时候,会有各种变化,受到其他的业务规则影响——而往往又是这些东西迷惑了人们)。 
      在上述的项目中也碰到这样的情况,原先的代码按下面的思路进行设计:首先,将“地区”数据查询到一个数据对象中,并通过迭代运算将该部分数据加到树图上;然后,分别查询各个“地区”的“供应商”数据,再将该部分数据分别放到“地区”节点之下。下面是其主要的代码:
      adqArea.SQL.TEXT := 'SELECT Name FROM Area ORDER BY Name'; // 查询“地区”的SQL语句
      adqArea.Open;      // 打开“地区”记录集
      while not adqArea.EOF do     // 遍历“地区”数据
      begin
          …… // 将当前记录的“地区”写到树图控件上;
          adqProvider.SQL.TEXT  := 'SELECT Provider FROM Provider ORDER BY Provider WHERE AreaName = ' + QuotedStr(adqArea.FieldByName('Name').AsString); // 查询符合当前“地区”条件的“供应商”的SQL语句。 
          adqProvider.Open;     // 打开“供应商”记录集
          while not adqProvider.EOF do
          begin
              …… // 将当前记录的“供应商”加到树图控件中相对应的“地区”节点下;
              adqProvider.Next;
          end;
          adqArea.Next;
      end;

      这种代码与我们平时考虑问题的方式相近,我们会很自然而然地想起这样做。不过,它存在着非常明显的性能缺陷,在类似这样的代码中,数据查询的次数 = “地区”的个数 + 1。不消说,这样在网络查询的时候,服务器与网络都需要进行多次的计算或传输工作,花费很多的时间资源或带宽资源;此外还有,数据对象需要多次进行属性的设置与进行打开、关闭等操作,也有一定程度上的资源消耗。因此,结果最显然不过了,屏幕显示的速度肯定不会快,换句说,性能将会达不到实际应用的速度指标要求。
      深入分析该应用的要求来看,其实这是将客户数据以从属于地区的形式在树状视图上排列显示、又或者是将订单数据以从属于客户的形式排列到树状视图中的应用。一句话,就是将按物理顺序排列的数据进行加工,按某种逻辑顺序拼成一棵树。大家知道,因为数据输入顺序的原因,在数据库的物理空间中,“供应商”数据的位置都会是以“地区”作“犬牙交错”状的顺序进行排列,象下表所示的一样:
  
      -----------------------------------------------------------------------
    物理位置顺序                  供应商                  地区
                1                               张三              中国
                2                               李四              美国
                3                               王五              中国
                4                               郑六              新加坡
                5                               李七              中国
                …
                …
                …

      而在我们的应用中,要求的是将相同“地区”的“供应商”组织起来以树图的方式进行显示和操作,归结起来,这是一个数据组织方式的应用问题。它让我联想起“空间”与“时间”的概念来,如果能够针对上面代码中多次查询的缺陷进行优化,利用速度快的内存存储需要的数据,减少查询的次数,就可以提高应用时候的性能了。按这个思路出发,有以下三个方面问题需要考虑或解决:首先,将所有符合条件的数据一次性地查询到内存中的数据对象去;其次,这些数据应该按逻辑顺序进行组织排列,以方便下一步的运算;第三、在遍历数据对象进行运算时,通过比较当前行与上一行数据中的标志值,检测到逻辑顺序变化,并以此作控制,分别进行树图中父节点与子节点的绘制工作。所以,我就设计了下述的主要代码(为突出主题,忽略了一些数据检测和错误处理的代码):
      设定一个变量 PreviousAreaName,用以记录当前数据之前一行数据的“地区”名称;
      adqProvider.SQL.TEXT  := 'SELECT Provider, Area.Name FROM Provider RIGHT JION Area ON AreaName = Area.Name ORDER BY Area.Name';  // 查询所有的地区及包含的供应商数据。
      adqProvider.Open;
      PreviousAreaName := '';
      while not adqProvider.EOF do
      begin
          if PreviousAreaName <> adqProvider.FieldByName('Name').AsString then
          begin
              …… // 将当前记录的“地区”加到树的节点中去作为新的父节点
              PreviousAreaName = adqProvider.FieldByName('Name').AsString;
          end;
          if not adqProvider.FieldByName('Provider').IsNull then  // 由于 RIGHT JION 的缘故,某些地区的“供应商”的值可能为空值;
              …… // 将当前记录的“供应商”加到树图控件中的当前“地区”父节点下;
          adqProvider.Next;
      end;
 
      这些代码做到了上述考虑的三个方面的要求,让人高兴的是,该部分功能程序的显示速度有了数量级的提升,由原来11秒的时间提升到1秒的时间。因此说,通过深入分析程序流程,重新设计了程序的处理方式,我们实现了优化性能的目标。总结起来,关键就在于新的程序能够将需要的数据一次性地加载到本地内存里进行运算,大大地减少了较慢的网络传输速度影响。
      进一步地看,这种解决方式与“预先加载数据”的数据处理方式类似,即将数据从速度较慢的媒介中一次性地加载到速度较快的内存中,进行操作,从而达到提高响应速度的目的。该方式普遍存在于各种的数据应用环境中,象 WINDOWS 系统在启动的时候将系统图标库与注册表数据加载到内存中,因此它能够让应用程序可以在极短的时间内就可以取得需要的系统图标或注册表数据。
      推广开来,在数据处理中的程序设计中,我们都可以借鉴这种批量获取数据的处理方式。当然,不是所有情况都适合使用“批量加载数据”的处理方式,如果数据量特别大的时候,我们还是有必要再将该部分的数据细分成几个小的部分,至于如何处理就要根据实际情况而定了。
      以上这些是当前重构工作、代码改进的成果,主要是从优化性能的角度出发来考虑的;今后,我们可以从OOP的角度、接口等程序架构方面继续作一些改进工作。
      有道是功夫在于一个“勤”字,要设计出性能良好的代码,就要勤于研究与改进现成的代码,不断地积累良好的经验。本文在这方面作一个小的尝试,抛砖引玉,期望各位同道也能多谈谈自己一些好的设计经验或心得,相互促进共同提高。

QQ:       272568028
E-MAIL:[email protected]
欢迎朋友们联络,深入研究与探讨。

本文地址:http://com.8s8s.com/it/it36695.htm