代码重构——之获得封装性DELPHI编码实例

    技术2022-05-11  134

    代码重构——之获得封装性DELPHI编码实例

     

    代码重构是获得结构良好的方法,通过重构,我们在保持功能不变的情况下,改善代码的质量,提高代码的复用程度。下面是一个获得改善代码质量和获得封装性的一个具体的例子。(例子使用DELPHI

    代码功能:

           给数据集设(TClientDataSet)置过滤器,用户可以在一个TComboBox中选择要过滤的字段,然后在一个Tedit框中输入要过滤的值。如图一:

    最常见的做法就是在TComboBoxItems属性中硬码写入我们数据集中的字段名称,然后在代码中加入一大堆case或者if语句在判断用户选择的字段来给数据集设置过滤器。

    ……

          case ComboBox1.ItemIndex of

    0:

                 ClientDataSet.Filtered := False;

            ClientDataSet.Filter := ' F_CODE = ''' + Edit2.Text + '''';

                  ClientDataSet.Filtered := True;

    1:

                 ClientDataSet.Filtered := False;

            ClientDataSet.Filter := ' F_CHINESE_NAME = ''' + Edit2.Text + '''';

                  ClientDataSet.Filtered := True;

    ……       

    end;                

    或者用

    ….…

           if ComboBox1.Text = '物料编码' then

        begin

                  ClientDataSet.Filtered := False;

            ClientDataSet.Filter := ' F_CODE = ''' + Edit2.Text + '''';

                  ClientDataSet.Filtered := True;

    end

    else if ComboBox1.Text = '名称' then

    begin

    ClientDataSet.Filtered := False;

            ClientDataSet.Filter := ' F_CHINESE_NAME = ''' + Edit2.Text + '''';

                  ClientDataSet.Filtered := True;

     

    end

    ……

    这样的代码通过硬码同样也实现了这个给数据集设置过滤器的功能,满足了需求,但是上面这段代码是不灵活的。如果数据集的字段很多就要求编码人员一个一个字段录入在Items中,而且在写case必须核对好顺序,不然设置的过滤器就是错误的也就很容易由开发人员引入BUG。用if语句时也一样维护一个大量的if同样是痛苦的,而且不支持需求变化,当用户要求改变数据集字段的中文显示名称时必须也要记住更改TComboBox. Items中的硬码数据,如果一旦忘记就会引入BUG

     

    于是我在第一次重构中,尝试动态的加载TComboBox. Items中的数据,同时为了实现加载后用户选择时实现对照。我在这个查询FORM中加了一个 私有FFields: array[0..20, 0..2] of string; 字段来保存数据集中的字段信息数据。同时实现了一个加载数据的过程:

     

    procedure TFrmSPARealStorageQuery.GetQueryFields;

    var

      i, iFieldsCount: Integer;

    begin

      iFieldsCount := 0;

      with DBGride1.DataSource.DataSet do

      begin

        for i := 0 to Fields.Count - 1 do

          if Fields[i].Visible then

          begin

            FFields[iFieldsCount, 0] := Fields[i].FieldName;

            FFields[iFieldsCount, 1] := Fields[i].DisplayLabel;

            Inc(iFieldsCount);

          end;

        ComboBox1.Items.Clear;

        for i := 0 to iFieldsCount - 1 do

          ComboBox1.Items.Add(FFields[i, 1]);

      end;

    end;

     

    这样就实现了在运行时动态加载字段信息。这样我的过滤器设置就变成了这样的。

     

    if ComboBox1.Text <> '' then

    begin

    ClientDataSet.Filtered := False;

        ClientDataSet.Filter := FFields[ComboBox1.ItemIndex, 0] +  '''' + Edit2.Text + '''';

           ClientDataSet.Filtered := True;

    end;

     

    本方法无疑增加了代码的灵活性,同时增加了代码的复用度,因为代码很好的隔离了变化的数据。因此只要在另一个也是要实现这种的功能的FORM中增加私有字段FFields: array[0..20, 0..2] of string 和使用上面的动态加载数据集字段过程,就可以说方便的实现了重用。但是这种重用并不是很好的,因为我们没有实现很好的封装性。导致在你的程序中到处散落有重复的代码(你常常会通过COPY来获得这个函数的重用,因为上面的代码是没有好的封装性)。如果有一天你要修改数据装载函数你就必须到处去找那里拷贝了该函数——你也得修改散落在其他地方的代码。于是我进行了再一次的重构,并对代码进行了进一步的封装。

    代码如下:

    unit uDataSetFieldsInfo;

    // Description:单元包括 TDataSetFieldsInfo 类,该类封装了获得数据集子段信息。

    // 并提供了在combobox列表显示字段显示信息和获得对应子段名称的方法接口

    // Created : wuchhao

    // Date : 2003.5

     

    interface

    uses Classes, DBClient, StdCtrls;

    type

      TDataSetFieldsInfo = class

      private

        FFieldsList: TStrings;

      public

        constructor Create;

        destructor Destroy; override;

        procedure GetDataSetFields(Source: TClientDataSet);

        procedure ShowFieldsInfo(Target: TComboBox);

        function GetFieldsNameByDisplayLabel(DisplayLabel: string): string;

      end;

     

    implementation

     

    { TDataSetFieldsInfo }

    constructor TDataSetFieldsInfo.Create;

    begin

      FFieldsList := TStringList.Create;

    end;

     

    destructor TDataSetFieldsInfo.Destroy;

    begin

      FFieldsList.Free;

      inherited;

    end;

     

    procedure TDataSetFieldsInfo.GetDataSetFields(Source: TClientDataSet);

    var

      i: Integer;

    begin

      FFieldsList.Clear;

      with Source do

      begin

        for i := 0 to Fields.Count - 1 do

          if Fields[i].Visible then

          begin

            FFieldsList.Add(Fields[i].DisplayLabel);

            FFieldsList.Add(Fields[i].FieldName);

          end;

      end;

    end;

     

    function TDataSetFieldsInfo.GetFieldsNameByDisplayLabel(

      DisplayLabel: string): string;

    var

      index: Integer;

    begin

      Result := '';

      index := FFieldsList.IndexOf(DisplayLabel);

      if index <> -1 then

        Result := FFieldsList.Strings[index+1]  ;

    end;

     

    procedure TDataSetFieldsInfo.ShowFieldsInfo(Target: TComboBox);

    var

      i: Integer;

    begin

      Target.Items.Clear;

      i:=0;

      while i <  FFieldsList.Count do

      begin

        Target.Items.Add(FFieldsList.Strings[i]);

        i:= i+ 2;

      end;

    end;

    end.

     

    单元uDataSetFieldsInfo 封装了与实现本文所述功能相关的数据和方法,把它们封装在一个类里面,从而实现了面向对象设计里面的 Open - Close 原则。类变成了一个黑盒,于是就可方便的重用(black-box reuse),而不必担心代码的重复。同时因为封装了与功能相关的信息,类的职责定义明确(单职责),并有了足够合适的粒度和好的封装性。TdataSetFieldsInfo 很好的把组合框与变化的数据隔离开来,最终提高了代码的复用程度,同时减少了FORM类的职责和 magic number硬编码的量。下面是新的代码:

    首先在FORM中声明TdataSetFieldsInfo类的一个引用。

    ……

    FORM创建的时候调用:

    FFieldsInfo := TDataSetFieldsInfo.Create;

    FFieldsInfo.GetDataSetFields(cdMaster);

    FFieldsInfo.ShowFieldsInfo(ComboBox1);

    这时候我的过滤器设置就变成了:

    if ComboBox1.Text <> '' then

    begin

    ClientDataSet.Filtered := False;

        ClientDataSet.Filter := FFieldsInfo.GetFieldsNameByDisplayLabel(ComboBox1.Text) +  '''' + Edit2.Text + '''';

           ClientDataSet.Filtered := True;

    end;

     

    通过调用FfieldsInfo对象的接口过程来获得对应的子段名称。

     

    本文是一个重构代码的简单例子,我想上面我实现的这个类还可以有很多种写法和更好的算法。这里只是提供一种关于重构代码的思路,为提高我们的编写代码质量和它的可维护性、扩展性,探讨OOD编程方式上的思路。

     

     

    参考资料:

    3.《Refactoring: Improving the Design of Existing Code Martin Fowler 1999

    4.《设计模式》机械工业出版社 2000    

     


    最新回复(0)