tags:

views:

766

answers:

7

I application that uses strings for different status an item can be during its life.

ie

OPEN, ACTIVE, CLOSED, DELETE,

and so on, at the moment they are all hard coded into code like so

MyVar := 'OPEN';

I am working on changing this as it can be a maintenance problem, so I want to change them all to a constants, I was going to do it like so

MyVar := STATUS_OPEN;

but I would like to group them together into one data structure like so

MyVar := TStatus.Open;

What is the best way to do this in delphi 2007?

I know I can make a record for this, but how do I populate it with the values, so that it is available to all objects in the system without then having to create a variable and populating the values each time?

Ideal I would like to have one central place for the data structure and values, and have them easily accessible (like TStatus.Open) without having to assign it to a variable or creating an object each time I use it.

I am sure there is a simple solution that i am just missing. any ideas?

+1  A: 

You could have a global variable of a record type. The record type must have a field for each status you have.

You can populate the record in the Initialize section of any unit, calling a procedure or function declared in that unit. You could have a single unit to specifically do this work, something like StatusTypes.pas.

In the interface section you could declare something like:

type 
  TStatus = record
    OPEN: string;
  end;

var
  Status: TStatus;
eKek0
+9  A: 

See http://edn.embarcadero.com/article/34324 ("New Delphi language features since Delphi 7".

A class constant would do nicely. From that link above:

type
    TClassWithConstant = class
      public 
        const SomeConst = 'This is a class constant';
    end;


 procedure TForm1.FormCreate(Sender: TObject);
 begin
   ShowMessage(TClassWithConstant.SomeConst);
 end;
Jim
I don't see how this is different from a normal const. If it is the namespace you are after, qualify with unit name
Marco van de Voort
+1 the class const is a clean and OOP way to keep related information together
mjustin
Oops - muat have overlooked this answer. This is very similar to the record approach I suggested (sorry!)
David Taylor
+11  A: 

As Jim mentioned you could use class constants or an enumerated type:

type
  TItemStatus = (isOpen, isActive, isClosed);
const
  ItemStatusStrings: array[TItemStatus] of string = ('Open', 'Active', 'Closed');
TOndrej
+3  A: 

This is something that you can do in all versions of Delphi:

type
  TStatus = class
    class function Active: string;
    class function Open: string;
    ...
  end;

  class function TStatus.Active: string;
  begin
    Result := 'ACTIVE';
  end;

  class function TStatus.Open: string;
  begin
    Result := 'OPEN';
  end;

You can use this exactly as you want:

MyVar := TStatus.Open;

there is exactly one place to change the strings, and there is only code involved, no runtime instantiation. New features of recent Delphi versions aren't always necessary to do stuff like that...

mghie
Agreed. Just for completeness' sake, there's neither code NOR runtime instantiation involved in the class const approach.
Jim
Indeed. I agree that yours is a good solution, and it allows for the "MyVar := TStatus.Open;" assignment as well. That part of my answer was in response to the question, speaking of "having to assign it to a variable or creating an object". There was never a need to do this.
mghie
+4  A: 

Or you could combine @Jim and @TOndrej

TGlobalConsts = class
type
  TItemStatus = (isOpen, isActive, isClosed);
const
  ItemStatusStrings: array[TItemStatus] of string = ('Open', 'Active', 'Closed');
end;

Although you could make it a class or a record really. Although if it is a class you can add the class functions like @mghie suggested, and instead just get the values from the const array. Personally I prefer having all the strings in a const array in the interface instead of peppered in the function bodies in the implementation.

class function TGlobalConsts.Active: string;
begin
  Result := ItemStatusStrings[itsActive];
end;

class function TGlobalConsts.Open: string;
begin
  Result := ItemStatusStrings[itsOpen];
end;

There are a lot of ways to do it, that is for sure.

Jim McKeeth
+5  A: 

I personally have used TOndrej's approach extensively in several large mission critical data processing platforms. The benefit of enumerations is that they can be easily passed around within an application, are very compact (an ordinal type), work perfectly in case statements and are completely type safe. The later point is important for maintenance since deleting or changing enumeration values will cause a compile error (a good think IMHO).

Some gotcha's to look out for with this approach:

  • Changing the declared order of enum values will foo bar the enum->string lookup array.

  • If you use the enum in case statements (a nice feature) be sure to take into account new values being added. I usually add an else to the case and throw an exception on unknown values. Much better than falling through the case.

If you are very concerned about the first gotcha, you can use a record in the lookup array and include the enum value in each record and validate the ordering from the unit initialization. This has saved my bacon many times in mission critical systems where maintenance is frequent. This approach can also be used to add additional meta data to each value (a very handy feature).

Best of luck!

unit Unit1;

interface

type
  TItemStatusEnum = (isOpen, isActive, isClosed);

  TItemStatusConst = class
    class function EnumToString(EnumValue : TItemStatusEnum): string;
    property OPEN: string index isOpen read EnumToString;
    property CLOSED: string index isClosed read EnumToString;
    property ACTIVE: string index isActive read EnumToString;
  end;

var
  ItemStatusConst : TItemStatusConst;

implementation

uses
  SysUtils;

type
  TItemStatusRec = record
    Enum  : TItemStatusEnum;
    Value : string;
  end;

const
  ITEM_STATUS_LOOKUP : array[TItemStatusEnum] of TItemStatusRec =
    ((Enum: isOpen;   Value: 'OPEN'),
     (Enum: isActive; Value: 'ACTIVE'),
     (Enum: isClosed; Value: 'CLOSED'));

procedure ValidateStatusLookupOrder;
  var
    Status : TItemStatusEnum;
  begin
    for Status := low(Status) to high(Status) do
      if (ITEM_STATUS_LOOKUP[Status].Enum <> Status) then
        raise Exception.Create('ITEM_STATUS_LOOKUP values out of order!');
  end;

class function TItemStatusConst.EnumToString(EnumValue: TItemStatusEnum): string;
  begin
    Result := ITEM_STATUS_LOOKUP[EnumValue].Value;
  end;

initialization
  ValidateStatusLookupOrder;
end.

Update:

Thanks for the critique - you are absolutely correct in that I was solving what I perceived as the problem underlying the question. Below is a much simpler approach if you can live with the constraint that this must ONLY work in Delphi 2007 or later versions (might work in D2006?). Hard to shed those nagging thoughts of backward compatibility ;)

type
  ItemStatusConst = record
    const OPEN   = 'OPEN';
    const ACTIVE = 'ACTIVE';
    const CLOSED = 'CLOSED';
  end;

This approach is simple and has a similar feel to what one might do in a Java or .Net application. Usage semantics are as expected and will work with code completion. A big plus is that the constants are scoped at a record level so there is no risk of clashes with other definitions as happens with unit scoped types.

Sample usage:

    ShowMessage(ItemStatusConst.ACTIVE);
    ShowMessage(ItemStatusConst.CLOSED);

Just for fun, I have also revised my earlier approach to directly address the original question with a similar outcome. This time I made use of "simulated class properties" (see here) with a property indexer. This is clearly more complex than the record approach, but retains the ability to work with enum values, sets, strings AND can also be extended to implement additional meta data features where required. I believe this works for Delphi versions going as far back as Delphi 5 IIRC.

An example of where I used this technique to great effect was a parsing framework I created in Delphi to handle the full IBM AFPDS print data stream grammar. This flexibility is why I love working in Delphi - it is like a Swiss army knife ;)

David Taylor
I agree very much with all you wrote, however I don't really see how this pertains to the question? You present a nice solution to a problem that the OP doesn't even have unless he uses an enumeration. Enumerations are nice, but for replacing hard-coded strings with a common constant they are not really necessary.
mghie
+2  A: 
Marco van de Voort
Yes, using RTTI to map enum values to names is very effective, especially if you have a standard naming *convention* for enum values e.g. you can strip off an enum prefix string in code. Question - does this approach cause memory leak reports in FastMM? I have not checked, but it seems it might.
David Taylor
No memleaks that I know off. It is also all normal string operations, so I don't see where the memleak could happen.
Marco van de Voort
The leakage I am talking about is a perceived leak detected by FastMM when it is configured to report memory leakage (see comments in FastMM4Options.ini). The issue is that any allocations remaining when FastMM ticks and ties memory at shutdown are reported as leaks. Memory allocated in unit initialization blocks are a prime candidate as are threads other than the main app thread. Indy 10 is notorious for this type of leakage. Please see FastMM RegisterExpectedMemoryLeak functions if you are interested.
David Taylor
Yes I know that kind of leak. The FPC variant of mem detection lists this count even, and subtracts it in the definitive report. But it is inherited to doing any dynamic allocation in the initialization, and I don't consider that a problem of the code, but of the way the memmanager is plugged. The code doesn't leak, the memmanager can't simply determine it.
Marco van de Voort