Jump to content
Mike Torrettinni

How to manage defined list values

Recommended Posts

Not sure how to frame the question title, so I hope it makes sense what I'm trying to ask:

 

I have many examples of defined values and I use one of the 2 approaches below. Is there a better approach, or it is what it is:

 

For example: I have Projects that are defined with 3 values: enum, internal_id (default value from external data) and caption (to show user).

 

So, here is 1st approach that I use:

type
  TProjectType = (ptMain, ptSub, ptExternalDev, ptInternalDev);

const
  cProjDefValues: array[TProjectType] of string = ('main_proj', 'sub_proj', 'dev_ext', 'dev_int');
  cProjCaptions: array[TProjectType] of string = ('Main Project', 'Sub Project', 'External Dev Project', 'Internal Dev Project');

And then I just use TProjectType to retrieve DefValue or Caption, whatever I need from consts.

I like this approach, but the problem is for example where I have 20+ enums - in this case the code because a long messy, not quickly readable, since each has to have 20+ values.


And here is another example that I sometimes use, 2nd example:

type
  TProjectType = (ptMain, ptSub, ptExternalDev, ptInternalDev);

TProject = record
    ProjectType: TProjectType;
    DefValue: string;
    Caption: string;
    constructor New(const aType: TProjectType; const aDefValue, aCaption: string);
  end;

 Projects: TList<TProject>;

constructor TProject.New(const aType: TProjectType; const aDefValue: string; const aCaption: string);
begin
  ProjectType := aType;
  DefValue    := aDefValue;
  Caption     := aCaption;
end;

procedure TForm7.FormCreate(Sender: TObject);
begin
  Projects := TList<TProject>.Create;
  Projects.Add(TProject.New(ptMain,        'main_proj', 'Project'));
  Projects.Add(TProject.New(ptSub,         'sub_proj',  'Sub Project'));
  Projects.Add(TProject.New(ptExternalDev, 'dev_ext',   'External Dev Project'));
  Projects.Add(TProject.New(ptInternalDev, 'dev_int',   'Internal Dev Project'));
end;


I like this approach, because I have all definitions in single method, even if I have 100+ records, it's still in 1 method. But I need to run this method at the start, while 1st example just exists, without creating any data.

 

So, what I'm looking for is any kind of comment, advice what is better, is there a third way (even better than these example).

 

 

 

Share this post


Link to post

The first approach ensures a single caption and defvalue for each value of tprojecttype due to the array structure.

 

With the list approach you could add multiple entries for one value of tprojecttype. You could replace the TList with a TDictionary. This way you can easily access the strings attached to the specific value of TProjectType.

 

A third solution that I prefer is the use of a record helper for TProjectType. Then you can use ptMain.Caption and ptSub.DefValue. The additional strings are closely connected to TProjectType.

Edited by Henk Post
  • Thanks 1

Share this post


Link to post

I prefer the 1st. But keep in mind that most of hard-coded constants could require changing to run-time assignments if you want multi-language support

  • Thanks 1

Share this post


Link to post

I prefer record helpers, combined with the const arrays.  I really wish I could make generic record helpers for arrays, though 😕

 

type
  TProjectType = (ptMain, ptSub, ptExternalDev, ptInternalDev);
  TProjectTypeHelper = record helper for TProjectType
  private
    const
      cProjDefValues: array[TProjectType] of string = ('main_proj', 'sub_proj', 'dev_ext', 'dev_int');
      cProjCaptions: array[TProjectType] of string = ('Main Project', 'Sub Project', 'External Dev Project', 'Internal Dev Project');
  public
    class function DefValueOf(const aProjectType: TProjectType): string; static;
    class function CaptionOf(const aProjectType: TProjectType): string; static;
    function DefValue: string;
    function Caption: string;
  end;

implementation

{ TProjectTypeHelper }

function TProjectTypeHelper.Caption: string;
begin
  Result := CaptionOf(Self);
end;

class function TProjectTypeHelper.CaptionOf(const aProjectType: TProjectType): string;
begin
  Result := cProjCaptions[aProjectType];
end;

function TProjectTypeHelper.DefValue: string;
begin
  Result := DefValueOf(Self);
end;

class function TProjectTypeHelper.DefValueOf(const aProjectType: TProjectType): string;
begin
  Result := cProjDefValues[aProjectType];
end;


The class functions could also be case statements - but the array[type] ensures you have values for all.  The weak spot here is if you insert a value in the constant list, or reorder values - without doing the same for the strings.

 

Or you can use class vars - which would make it easier to load up different languages at runtime, if so needed.

 

type
  TProjectType = (ptMain, ptSub, ptExternalDev, ptInternalDev);
  TProjectTypeHelper = record helper for TProjectType
  private
  class var
    cProjDefValues: array[TProjectType] of string;
    cProjCaptions: array[TProjectType] of string;
  public
    class procedure InitHelper; static;
    class function DefValueOf(const aProjectType: TProjectType): string; static;
    class function CaptionOf(const aProjectType: TProjectType): string; static;
    function DefValue: string;
    function Caption: string;
  end;

implementation

{ TProjectTypeHelper }

function TProjectTypeHelper.Caption: string;
begin
  Result := CaptionOf(Self);
end;

class function TProjectTypeHelper.CaptionOf(const aProjectType: TProjectType): string;
begin
  Result := cProjCaptions[aProjectType];
end;

function TProjectTypeHelper.DefValue: string;
begin
  Result := DefValueOf(Self);
end;

class function TProjectTypeHelper.DefValueOf(const aProjectType: TProjectType): string;
begin
  Result := cProjDefValues[aProjectType];
end;

class procedure TProjectTypeHelper.InitHelper;
begin
  cProjDefValues[ptMain] :=         'main_proj';
  cProjCaptions [ptMain] :=         'Main Project';

  cProjDefValues[ptSub] :=          'sub_proj';
  cProjCaptions [ptSub] :=          'Sub Project';

  cProjDefValues[ptExternalDev] :=  'dev_ext';
  cProjCaptions [ptExternalDev] :=  'External Dev Project';

  cProjDefValues[ptInternalDev] :=  'dev_int';
  cProjCaptions [ptInternalDev] :=  'Internal Dev Project';
end;

 

 

  • Thanks 1

Share this post


Link to post
30 minutes ago, Lars Fosdal said:

I prefer record helpers, combined with the const arrays.  I really wish I could make generic record helpers for arrays, though 😕

So, you use record helpers instead of:

 

cProjCaptions[ptMain]

?

Share this post


Link to post

Both.  See updated post.

 

Let me correct myself...

With a record helper, the methods of the helper become part of the helped type.

 

begin 
  TProjectType.InitClass; // only for the second example
  var prj: TProjectType = ptMain;
  writeln(prj.Caption);
  writeln(ptMain.Caption)
  writeln(TProjectType.CaptionOf(ptMain))
end;

all output the same string.


Populating a combobox can be done like this

  for var prj := Low(TProjectType) to High(TProjectType)
   do ComboBox1.AddItem(prj.Caption, Pointer(Ord(prj))); 

and later, if you like such ugly hacks 😉

  if ComboBox1.ItemIndex >= 0
  then begin
    var selectedprj := TProjectType(Integer(ComboBox1.Items[ComboBox1.ItemIndex]));
  end;

 

  • Thanks 1

Share this post


Link to post

For the latter example:

 

begin
  TProjectType.InitHelper;
  var prj: TProjectType := ptExternalDev;
  Writeln(prj.Caption);
end;

will output 

External Dev Project

Share this post


Link to post
17 minutes ago, Lars Fosdal said:

For the latter example:

 


begin
  TProjectType.InitHelper;
  var prj: TProjectType := ptExternalDev;
  Writeln(prj.Caption);
end;

will output 


External Dev Project

Aha, I see. InitHelper initializes Caption, right? From const of names.., OR is Caption a function that return const_of_names[ptExternalDev]?

Share this post


Link to post
22 minutes ago, Lars Fosdal said:

@David Heffernan - Do you scan the rtti attributes once in a class init method/first use, or do you fetch the attributes at each use?

I use a cache, a dictionary keyed on the type info pointer, iirc

  • Thanks 1

Share this post


Link to post
14 minutes ago, Mike Torrettinni said:

Aha, I see. InitHelper initializes Caption, right? From const of names.., OR is Caption a function that return const_of_names[ptExternalDev]?

InitHelper initializes the class var arrays for Caption and DevTitle (for the second example using class vars).  You only need to call this once for the application, so you could do f.x. it in the unit init section.

For your second question, see the implementation of TProjectTypeHelper.Caption.

 

Why the extra class function CaptionOf? A habit of mine, due to the rule of only one class helper for a type in scope at a time. 

Share this post


Link to post
19 minutes ago, David Heffernan said:

I use a cache, a dictionary keyed on the type info pointer, iirc

How do you associate the string attribute(s) with each enum value?

Share this post


Link to post

A dictionary is nice, but in older versions -- I work a lot in D2007 -- then I make use of enums and const arrays. Defining them can be tedious, but maintenance is simple, and associated code is clean and easy.

 

One thing to note on defining const arrays in the private section of a class: In D2007, I have found that defining consts in the class breaks the Ctrl-Shift-Up/Dn navigation between interface and implementation on members below the const declaration. I therefore put const declarations in the implementation section of the unit.

Share this post


Link to post
1 hour ago, Lars Fosdal said:

How do you associate the string attribute(s) with each enum value?

[Names('foo', 'bar')]
TMyEnum = (foo, bar);

It's kinda flaky of course because of the limitations on attribute constructor arguments being true constants.

 

Later on I can write

 

Enum.Name(someEnum)

 

Edited by David Heffernan
  • Thanks 1

Share this post


Link to post

Last question before I do some testing with new approaches:

 

Would you reconsider your chosen approach if you have 100+ type of enums? If you have so much different data, that you need to repeat your implementation for 100+ times... would it make sense to still use your approach, or in such case you would (or have you) use something else?

 

Share this post


Link to post

For such big numbers you can have all the data in any convenient form (CSV, JSON, XML) and write a tool that would generate a unit from this data.

Share this post


Link to post
14 hours ago, Mike Torrettinni said:

Last question before I do some testing with new approaches:

 

Would you reconsider your chosen approach if you have 100+ type of enums? If you have so much different data, that you need to repeat your implementation for 100+ times... would it make sense to still use your approach, or in such case you would (or have you) use something else?

 

There is no definitive answer to that as YMMV. 

 

We do literally have 100+ types of enums, and we implemented record helpers for each one, simply to ensure that all enums had the same capabilities. AsString, FromString, Name, and in some cases more texts, or biz.logic associated with the enum.

We actually did the last push for this model this autumn, to get rid of the last couple of dozens of standalone TypeNameToStr functions.

 

We also introduced app wide translations (Norwegian, Swedish, English) using Sisulizer.

It turned out that using attributes for naming was a bit of a challenge, since you can't use a resourcestring as an argument to an attribute - go figure.

 

resourcestring
  sName = 'Name';

type
  AttrNameAttribute = class(TCustomAttribute)
    constructor Create(const aName: String);
  end;

type
  TSomeType = class
  private
    FaName: string;
    procedure SetaName(const Value: string);
  public
    [AttrName(sName)] // <-- [dcc32 Error] : E2026 Constant expression expected
    property aName: string read FaName write SetaName;
  end;

 

We ended up setting names explicitly in code instead.

  • Thanks 1

Share this post


Link to post

Come to think of it, it could have been nice to be able to enforce the helper implementations with an interface. or even better, be able to reuse an interface implementation for handling attribute naming.

Also, this: 

/SendsLetterToCompilerSanta

Share this post


Link to post

OK, it makes sense. I'm trying to find easiest solution to manage, but it's hard when in the middle of the progress I find out about different or better approach.

Share this post


Link to post
3 hours ago, Fr0sT.Brutal said:

For such big numbers you can have all the data in any convenient form (CSV, JSON, XML) and write a tool that would generate a unit from this data.

Thank you, interesting idea, especially if they have very similar or even same structure.

Share this post


Link to post
13 minutes ago, Mike Torrettinni said:

OK, it makes sense. I'm trying to find easiest solution to manage, but it's hard when in the middle of the progress I find out about different or better approach.

Well, you can define lots of overloaded functions like EnumDefValue, EnumCaption etc for now and implement them in any form. Later, if you find a structure you like more, you won't have to change the interface, only the internal implementation. Or, if you decide to switch to enum helpers, perform replace in files with regexp-s ( EnumDefValue\((.+?)\) => \1.DefValue )

  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×