Amazon.com Widgets Authoring Generics: A Simple First Step

Authoring Generics: A Simple First Step

By Nick at March 20, 2012 00:39
Filed Under: Delphi, Software Development

Consumer or Producer?

By now I suspect that most of you have heard of generics.  Perhaps you have started using them via the Generics.Collections unit, where you can find TList<T>TStack<T>, etc.  The Delphi Spring Framework contains an even more sophisticated set of collections and interfaces to those collections, including the very powerful IEnumerable<T>

These are all very useful classes, and our code is made much simpler, easier to maintain, and more type-safe because of them.  But using them merely makes us consumers of generics.  If we want to truly take advantage of generics, we need to become producers of generic classes.  It’s a big step to see the benefit of generic collections, but the truly big step is to start seeing opportunities for the use of generics in your own code.

A Contrived, Simple Example

This past weekend, some of my team members and I attended the C-Sharpen event here in Philly.  It was a valuable day, made particularly so by the presentations given by Steve Bohlen.  He gave an excellent presentation on the topic I discussed above – how we need to start thinking as producers of generics and not merely consumers.  In that presentation, he gave a simple example of how generics are powerful and can make your code simpler, more effective, and more powerful.  So, I’m going to show that example here.  It’s a bit contrived, but very illustrative of how to think about generics.

First, consider the following code:

type

  TOrderItem = class
    ID: integer;
  end;

  TOrder = class
    ID: integer;
  end;

  TCustomer = class
    ID: TGUID;
  end;

Now this code is very simple – a set of classes that might represent a simple order entry system.  But right away, something should occur to you.  All three have something in common – they are entities in your system. Down the road, you may want to act upon all the entities in your system, so you might create a superclass like so:

type

  TEntity = class  
  end;

  TOrderItem = class(TEntity)
    ID: integer;
  end;

  TOrder = class(TEntity)
    ID: integer;
  end;

  TCustomer = class(TEntity)
    ID: TGUID;
  end;

But you might be frustrated because you want your TEntity class to have an ID field that is in use by all descendants, but that pesky TCustomer class can’t oblige – it needs a GUID for its ID tag instead of an integer like the other classes.

Instead of fretting about the different types of ID tags, how about just create one that doesn’t care what type it is?  Well, this is where the power of generics comes in.   How about you give the TEntity class a parameterized type – a generic – as  it’s ID, and then just tell all the classes what type their ID tag will be?

type

  TEntity<T> = class
    ID: T; 
  end;

  TOrderItem = class(TEntity<integer>)
  end;

  TOrder = class(TEntity<integer>)
  end; 

  TCustomer = class(TEntity<TGUID>)
  end;

Now, given the above, your entities can all have ID’s, but you don’t have to have the same type for all of them.  If a TEntity descendant needs an ID of a different type, you can just descend from TEntity and pass the correct ID type in the class declaration.  

Again, the example is quite contrived, but I think it does a nice job of showing how you can starting “thinking generically” and not just accept a rigid type structure. 

In addition, it illustrates why the more formal name for generics is “parameterized types”.  The type in the brackets is passed in to the type declaration, and then used within the class, just as method parameters are passed in to functions and procedures. 

A simple, contrived example, sure.  But hopefully it illustrates how generics can be used to turn you from a consumer of generic classes to making them a common tool in your code.

 

Update:

As a number of commenters have pointed out, my original example was sub-optimal.  Rather than post a "correction" post, I've taken the liberty of updating and improving the original article.

Comments (31) -

3/20/2012 4:04:10 AM #

Dorin Duminica

I think this is one of the best and simplest examples of using generics!

Dorin Duminica Romania |

3/20/2012 4:45:04 AM #

Pierre Yager

Hi Nick,

What about :

type
  TEntity<T> = class
    ID: T;
  end;

  TOrderItem = class(TEntity<Integer>)
  end;

  TOrder = class(TEntity<Integer>)
  end;

  TCustomer = class(TEntity<TGUID>)
  end;

var
  MyOrder: TOrder;
  

Pierre Yager France |

3/20/2012 10:00:23 PM #

nick

That will work, and may actually be a better example.

nick United States |

3/20/2012 4:46:08 AM #

Fredrik Loftheim

I would believe TCustomer should be defined like this:

TCustomer = class(TEntity<TGUID>)
end;

Because a TCustomer always has a TGUID as an Id, this is not something you should decide when declaring a TCustomer variable like in your example.

Similar for the other concrete subclasses of TEntity<T>.

Don't you agree?

Fredrik Loftheim Norway |

3/20/2012 10:01:19 PM #

nick

Yeah, that's a similar way to do it to the way Pierre did.

nick United States |

3/20/2012 4:54:21 AM #

Bas

I'm very confused by this example.
So using this, every time i declare a TCustomer object I have to somehow know that it should be  declared at TCustomer<TGUID> ...??

I would suspect that this domain info would be somehow stated when declaring the TCustomer class, something like this:

TCustomer = class(TEntity<TGUID>)

(I have no experience with generics but I suspect this syntax is not accepted byt the way)

Bas Netherlands |

3/20/2012 10:01:38 PM #

nick

Yep -- good point.

nick United States |

3/20/2012 5:24:01 AM #

Francis Ruiz

Good example. You could name it "Generics usage : Beyond TList<T>"

Francis Ruiz Spain |

3/20/2012 5:50:26 AM #

Alexander Elagin

Thank you for this simple but very useful example! An introduction to any concept must anyway start from something like this, easily understandable. I remember how it took me some time to make a shift from procedural approach to object-oriented thinking back in BP7 times until something 'clicked' and everything got clear and logical.

Alexander Elagin Russia |

3/20/2012 6:15:26 AM #

Bas

Why is my more critical comment removed by the moderators?

Bas Netherlands |

3/20/2012 10:02:30 PM #

nick

I didn't remove any comments from this post.  All posts are moderated, though.

I generally won't remove a comment unless the poster is being a really big jerk.

nick United States |

3/21/2012 3:15:26 AM #

Bas

Ok, sorry ... guess that was my impatiens calling out

Bas Netherlands |

3/20/2012 7:14:13 AM #

Paweł Głowacki

That's a very nice example, because it strives to be as simple as possible to illustrate just one thing at a time.
Good stuff, Nick and greeting from Embt Dutch officeSmile

BTW: any chances for a FireMonkey version of your famous "TSmiley" component?

best regards,
Pawel Glowacki

Paweł Głowacki Netherlands |

3/20/2012 8:29:05 AM #

MANOEL F CSILVA

How could I cast back to TEntity<T> ?

I mean: if I have:

MyEntity: TEntity<Integer>, I can't cast TCustomer<TGUID> to it, right?

Or if I can, what type is MyEntity.ID ?

MANOEL F CSILVA Brazil |

3/20/2012 8:31:15 AM #

Cesar Romero

Hi Nick,

Nice example, really simple and at same time shows a big picture.

Cesar Romero Brazil |

3/20/2012 8:41:06 AM #

Rolphy Reyes

Hi Nick.

Do you plan to extend this articles?

Rolphy Reyes Dominican Republic |

3/20/2012 4:27:03 PM #

Jolyon Smith

Maybe I missed something, but this strikes me as a ridiculous example.

It creates a situation where two different TCustomer<T> instances may be identical in every respect other than the underlying type of their ID.  It might serve to provide an illustration of what can be done with generics but I simply cannot envisage this ever being useful in the way used in the "illustration".

If the point is to demonstrate what generics can be used for, then the good old stand by of "containers" does that very nicely and has the added advantage of having a practical use.  (on that note, it remains to be shown how for generics as implemented in Delphi, there really is anything "Beyond TList").

Examples that don't map onto real world usage suffer the problem that someone will never-the-less try to apply the example onto a real world scenario, find that it doesn't "fit" and end up confused as to what the example was trying to tell them in the first place.

Jolyon Smith New Zealand |

3/21/2012 7:25:34 AM #

nick

Jolyon --

Leave it to you to be a hopeless pedant about a simple example.  

nick United States |

3/20/2012 4:42:19 PM #

Jolyon Smith

To explain why this is such a ridiculous example.

Consider I have some method that operates on a TEntity.  With a "parameterised" TEntity there is no simple check that can be performed to ask "Is this a TEntity of some specific type?" without being forced to specify the type of the ID, even when the type of the ID is of no interest to the method:

  if entityRef is TCustomer then   //  Does not compile

  if entityRef is TCustomer<Integer> // will only compile if entityRef is already declared as TEntity<Integer>

Simply put, the type of the ID becomes an impractical burden on writing polymorphic code in an application using such types.  There is no way to declare a variable or parameter that will hold a reference to some TEntity, ANY TEntity, without specific reference to the type of the ID of that entity.

Furthermore, the idea that you would have a class hierarchy where the "root" of that class hierarchy introduces a property that is required to have a type that is then varied by decendant classes in that hierarchy is itself an artificial problem.  I cannot think of a single case in 20+ years of OO software development where such a situation has existed.

Even if it did exist, the simple solution is of course to implement the ID in TEntity as a String or byte array, with descendant classes able to maintain their type variations on the ID as they see fit, as long as they provide the mechanism to represent their ID in that corresponding base ID type.

All this article did for me was to re-affirm my doubt that generics have any real practical use beyond TList.

Jolyon Smith New Zealand |

3/21/2012 7:24:52 AM #

nick

"All this article did for me was to re-affirm my doubt that generics have any real practical use beyond TList."

I'm glad to be of service.

nick United States |

3/22/2012 11:34:34 AM #

David Robb

Jolyon: I can assure you that Generics have practical value in the real world.  I am applying generics to a product written originally without generics, and it has these benefits:

1. Simplification of code, and occasionally even the removal of classes;

2. Parameterization allows descendants to have different types or (perhaps more importantly) different numbers of parameters, leading to ...

3. ... type-safety replacing what were previously typecasts.  The benefits of this are surely obvious to all.

4. Slightly improved performance on some types.


The problem you describe in your post isn't real.  At least I haven't encountered it yet.  I recommend defining a non-generic base type as others pointed out, e.g.  

TCustomer = class(TEntity<integer>)

David Robb United States |

3/20/2012 4:44:21 PM #

Moz

Surely the actual code would have type-locked subtypes:
  type
    TOrderItem = class(TEntity<integer>)
    end;


That way you get the benefit that TOrderItem is still a TEntity, but you know that it has an integer ID. Otherwise you're just displacing the coding effort downwards (which is what you're trying to avoid). Note that I don't have a modern Delphi handy so I'm not even sure the above is valid. IMO it should be Smile

Moz Czech Republic |

3/20/2012 10:04:40 PM #

nick

Moz --

You are right -- a number of folks pointed that out.  I'll follow up with another article.

nick United States |

3/21/2012 3:22:20 AM #

Fredrik Loftheim

Hmmmm. This moderation of comments taking time, makes us look silly for posting almost the same suggestions. :-( But it's become a necessity I know. Damn you spammers and trolls.

Fredrik Loftheim Norway |

3/21/2012 7:21:12 AM #

nick

A note to all -- I've updated the article to incorporate your excellent suggetions.

nick United States |

3/22/2012 2:02:06 PM #

David S

It's strange reading the article and comments, post-updates, and the comments make suggestions that are already in the article.

Anyway, AFAIK, some type

TEntity = class

is not the same as the type

TEntity<T> = class

or

TMyClass = class(TEntity<TSomeClass>)

for any concrete declaration of some type 'T'.

That is, the original example was to create a common base class TEntity so you could know that the others were of that type.

But switching TEntity to TEntity<T> goes right back to the original problem, except that you're essentially subclassing your entities based on the type of ID they have, which should, IMO, be of no concern to anybody using the classes. It's an implementation detail.  This solution smacks of all the same issues we have with so many different string types now -- there's no  way to ask if a given string variable is of "Type" string, because there are a half-dozen distinct string types that are not type compatible.  And in order to handle all of this variability, we're looking at a steadily growing list of overloaded functions, like Pos(string,string), Pos(AnsiString,AnsiString), Pos(WideString,WideString), Pos(ShortString,ShortString), Pos(PChar,PChar), Pos(lpstr,lpstr), and so on.  It's ridiculous!  

TCustomer = class(TEntity<GUID>)   <>   TOrder = class(TEntity<Integer>)

That is, you cannot test to see if TCustomer = TOrder at the level of "TEntity" because TEntity needs to be additionally qualified with the value of <T> given in the class declaration, right?

In other words, TCustomer <> TEntity, but TCustomer = TEntity<GUID>

So I don't see how replacing a common base class TEntity with TEntity<T> solves the problem where you have a way of identifying that everything declared as a type of TEntity<T> is all the same base class, because they'll vary with <T>.

Or am I missing something?

David S United States |

3/22/2012 4:55:06 PM #

nick

David --

I think the root of the problem is that people are acting as if this were a real world example, instead of a contrived example for the purpose of illustrating a point.  

Nick

nick United States |

3/22/2012 7:13:34 PM #

Moz

I think it actually works as an example if you think of it as (say) the base of an OPF. Which, of course, you should never write yourself, but it's typical of a class of problems that we do actually write code for. Especially with the new RTTI it's become much, much easier to stream groups of objects and the "how do I maintain links" question has thus started to affect more people. "Everything has an ID" becomes a bit like the C# "Everything has an .AsString()" - sure, in a few cases it's a bit pointless, but you will still find yourself needing to use the object ID's more often than you might think.

This particular thing actually annoys me precisely because TPersistent doesn't have an ID. WTF, that's one of the essential basic things you need to persist anything. I am actually hoping that the next evolution of Delphi will start to deal with persistent objects in some slightly less basic way. So I can stream (say) TCustomers with the many-to-many relationship to TProducts included.

Moz Czech Republic |

3/25/2012 3:34:27 PM #

Dalija Prasnikar

Your example is nice and simple and it does show possibilities of generics.
The only problem is that most of the time you do need to create some class
hierarchies, you also need a common root class.
In that case you would have to introduce TEntity class before TEntity<T>.
That would probably solve the problem in creating class hierarchy.

The problem with generics, as I see it, is that generics are not very good at
creating class hierarchies at the lowest level. They work fine if you use them on
some leaf classes, but I had serious troubles trying to use them with base
ground level classes. Sooner or later the whole thing would crash down beyond
repair. I have to mention that some current generics bugs also make this process
a bit harder. I do hope they would be resolved soon.

For instance, I am succesfully using generics and interfaces in combining common
code that handles forms and dialogs both in VCL and in FMX. Something like this:

  IForm = interface
    procedure Show;
    procedure Hide;
    function ShowModal: TModalResult;
    procedure CloseModal(aModalResult: TModalResult);
    procedure Release;
    function IsModal: boolean;
    function IsVisible: boolean;
  end;

  TVCLForm = class(TForm, IForm);

  TFMXForm = class(TForm, IForm);

  TDialog<T: IForm> = class...

  TVCLDialog = TDialog<TVCLForm>;
  TFMXDialog = TDialog<TFMXForm>;

The whole thing works very nice and I am saving myself from
having a lot of copy paste code. And that is just the beginning.

Dalija Prasnikar Croatia |

3/25/2012 8:58:10 PM #

Nick Hodges

Dalija --  Thanks, great comment.

Nick Hodges United States |

3/26/2012 9:12:06 AM #

Steven

Hi Nick,
Thanks for the example.
I'd really like to learn more about generics - where would you recommend I go from here?

Steven United States |

Pingbacks and trackbacks (1)+

Comments are closed

A Pithy Quote for You

"Just the knowledge that a good book is awaiting one at the end of a long day makes that day happier."    –  Kathleen Norris

Amazon Gift Cards

General Disclaimer

The views I express here are entirely my own and not necessarily those of any other rational person or organization.  However, I strongly recommend that you agree with pretty much everything I say because, well, I'm right.  Most of the time. Except when I'm not, in which case, you shouldn't agree with me.

Month List