3

I have a procedure for sorting nodes in a node tree (VirtualTreeView) All memory leaks, extracted from FMM4 report, are stored in objects of a class TMemoryLeakList(these are the list I want to sort), which are stored in a list of lists called TGroupedMemoryLeakList, and both TMLL and TGMLL extend TObjectList. If I want to keep the functionality of being able to chose between ascending and descending sort order and choosing between sorting by one of four different data types, I 'have to' implement EIGHT different comparison methods (4 sort types * 2 sort directions) which I pass on to the main sorting method, because my TMLL list extends TObjectList. The main sorting method look like this.

The values for the fields fSortType and fSortDirection are acquired from the GUI comboboxes. One of those eight generic comparison functions looks like this. The seven remaining are copy/pasted variations of this one.

Is there any rational way to refactor this huge amount of copy paste code and still keep the functionality of choosing a specific sort type and direction?

programstinator
  • 1,354
  • 3
  • 12
  • 31
  • 2
    In modern Delphi you can call `SortList` instead of `Sort` and pass in a `reference to` compare function. That will accept methods of objects, or anonymous procs. And that way you can get your state into the compare function. Without being tempted to use global variables. Or you can use `TObjectList` and pass to the `Sort` function an `IComparer`. Again you can pass the state in. What Delphi are you targeting? Either of these solutions are, in my view, better than anything using `Contnrs.TObjectList`. – David Heffernan Oct 01 '12 at 15:25
  • I am using rad studio 2007, so I don't really know if it qualifies as modern Delphi. I will try your suggestion, once I understand it :) – programstinator Oct 01 '12 at 16:54
  • 1
    No, that's ancient Delphi. No dice this way. Class helper could make life easier, but you've probably got what you need now. – David Heffernan Oct 01 '12 at 18:12
  • I recommend using SortList as well vs Sort, it seems to be 3 times faster in my tests. – hikari Dec 12 '12 at 04:40

3 Answers3

5

Nice question about refactoring, but I dislike the answer you presumably are looking for. There is nothing wrong with a few extra lines of code, or a few extra routines. Especially the latter in which case naming actively assist in more readability.

My advice would be: leave the design as you have, but shorten the code:

function CompareSizeAsc(Item1, Item2: Pointer): Integer;
begin
  Result := TMemoryLeak(Item2).Size - TMemoryLeak(Item1).Size;
end;

function CompareSizeDesc(Item1, Item2: Pointer): Integer;
begin
  Result := TMemoryLeak(Item1).Size - TMemoryLeak(Item2).Size;
end;

function CompareClassNameAsc(Item1, Item2: Pointer): Integer;
begin
  Result := CompareStr(TMemoryLeak(Item1).ClassName,
    TMemoryLeak(Item2).ClassName);
end;

procedure TMemoryLeakList.Sort;
begin
  case FSortDirection of
    sdAsc:
      case FSortType of
        stSize: inherited Sort(CompareSizeAsc);
        stClassName: inherited Sort(CompareClassNameAsc);
        stCallStackSize: inherited Sort(CompareCallStackSizeAsc);
        stId: inherited Sort(CompareIdAsc);
      end;
    sdDesc:
      case FSortType of
        stSize: inherited Sort(CompareSizeDesc);
        stClassName: inherited Sort(CompareClassNameDesc);
        stCallStackSize: inherited Sort(CompareCallStackSizeDesc);
        stId: inherited Sort(CompareIdDesc);
      end;
  end;
end;

You can't get it much smaller then this ánd preserve the same level of readability.

Of course, you could rewrite the Sort routine as suggested by Arioch 'The:

procedure TMemoryLeakList.Sort;
const
  Compares: array[TSortDirection, TSortType] of TListSortCompare =
    ((CompareSizeAsc, CompareClassNameAsc, CompareCallStackSizeAsc,
    CompareIdAsc), (CompareSizeDesc, CompareClassNameDesc,
    CompareCallStackSizeDesc, CompareIdDesc));
begin
  inherited Sort(Compares[FSortDirection, FSortType]);
end;

But then: why not rewrite the QuickSort routine to eliminate the need for separate compare routines?

Alternatively, you could add ownership to TMemoryLeak in which case you have a reference to the owning list and its sort direction and sort type, for use within óne single compare routine.

Community
  • 1
  • 1
NGLN
  • 43,011
  • 8
  • 105
  • 200
3

Use function pointers.

var comparator1, comparator2: function (Item1, Item2: Pointer): Integer;

function sortComplex (Item1, Item2: Pointer): Integer;
begin
  Result := comparator1(Item1, Item2);
  if 0 = Result then   Result := comparator2(Item1, Item2);
end;

Then you GUI elements should behave like

 case ListSortType.ItemIndex of
    itemBySzie : comparator1 := sortBySizeProcAsc;
....
 end;

 DoNewSort;

PS: make sure that you correctly specify those pointers even before user 1st click any GUI element;

PPS: you can rearrange this even further like

 type t_criteria = (bySize, byName,...);
      t_comparators = array[t_criteria] of array [boolean {Descending?}]
                      of function (Item1, Item2: Pointer): Integer;

 const comparator1table: t_comparators = 
       ( {bySize} ( {false} sortBySizeProcAsc, {true} sortBySizeProcDesc),
         {byName} ( {false} sortByNameProcAsc, ...

Then you would fill working pointers from that array constants

Arioch 'The
  • 15,799
  • 35
  • 62
0

This is my solution. Apart from completely rewriting the two procedures I also added two 'static' variables to my TMemoryLeakList class, and removed the former instance variables of the same name. This way, they are globally accessible to the Sort function.

TMemoryLeakList=class(TObjectList)
class var fSortType      :TMlSortType;
class var fSortDirection :TMLSortDirection;
...
end

procedure TMemoryLeakList.Sort;
begin
  inherited sort(sortBySomethingSomething);
end;

function sortBySomethingSomething(Item1, Item2: Pointer): Integer;
var
 a, b : string;
 ret : Integer;
begin
  ret := 1;
  if(TMemoryLeakList.fSortDirection = sdAsc) then
     ret := -1;
  case TMemoryLeakList.fSortType of stSize:
  begin
    a := IntToStr(TMemoryLeak(Item1).Size);
    b := IntToStr(TmemoryLeak(Item2).Size);
  end;
  end;
  case TMemoryLeakList.fSortType of stClassName:
  begin
    a := TMemoryLeak(Item1).ClassName;
    b := TMemoryLeak(Item2).ClassName;
  end;
  end;
  case TMemoryLeakList.fSortType of stID:
  begin
    a := IntToStr(TMemoryLeak(Item1).ID);
    b := IntToStr(TMemoryLeak(Item2).ID);
  end;
  end;
  case TMemoryLeakList.fSortType of stCallStackSize:
  begin
    a := IntToStr(TMemoryLeak(Item1).CallStack.Count);
    b := IntToStr(TMemoryLeak(Item2).CallStack.Count);
  end;
  end;
  //...jos tu
  if a=b then
    Result:=0
  else if a>b then
    Result:=-1*ret
  else if a<b then
    Result:=1*ret;
end;

I would like to rewrite this solution so as to use instance bounded variables fSortType,fSortDirection in TMemoryLeakList, but it seems impossible to pass a member function to an inherited sort function (from TObjectList), or is it?

programstinator
  • 1,354
  • 3
  • 12
  • 31
  • Sorry, but this makes things much more worse. First, you convert all integer type values to strings, and then compare those strings agains each other. Conversion and string comparison are inefficient, especially in these kind of loops where you should want things to narrow as down as possible. To answer you question: indeed, it is impossible to add a third argument to a `TListSortCompare` routine. – NGLN Oct 01 '12 at 16:26
  • @NGLN This won't work as intended, say for `a := "123"` and `b := "4"`, desc sort would yield `(b,a)` instead of `(a,b)`. I have a question for you @NGLN. What if `a` and `b` were of object type? Would the type be determined at runtime so as to call the suited operators `<,>,=`? Would it call for a (implicit) conversion then? P.S Delphi is aloof to me, so pardon me for assuming things here. – nullpotent Oct 01 '12 at 16:40
  • @iccthedral If 'a' and 'b' were to be of TObject type, the compiler would give a "Operator not applicable to this operand type" message, so there would be no implicit conversion, I'm afraid. – programstinator Oct 01 '12 at 17:06
  • I guess it could work with some kind of variant record, an equivalent to `union` in `C` language. Is there such a thing in Delphi? – nullpotent Oct 01 '12 at 17:10
  • @iccthedral it does - it is called "variant record". In Pascal union and struct are not distinct concepts. Well, frankly i doubt that in C union was back in 1974. Though dunno. – Arioch 'The Oct 02 '12 at 06:10
  • @Goran - i dislike such code in speed-critical paths. A chain of IFs would hamper the performance (aside from conversions that already was emntioned). Read about branch prediction, speculative execution and CPU pipeline flash. I definitely prefer to cache all the ifs so that code would be linear. That is why i proposed that array after all, you do all preparations once and then only call that one function time and again. I so dislike IFs that i wish Delphi has "procedure/function of Self" feature :-) – Arioch 'The Oct 02 '12 at 06:13
  • 1
    @Arioch'The Wouldn't branch prediction do well in this case? Only one branch is executed during the sort procedure. Btw, your solution is great. – nullpotent Oct 02 '12 at 09:18