One of the novelty of the recent versions of Delphi is the built-in record helper for the primitive types (string, Integer, etc.). Record helper are essentially class helper for record (and apparently primitive types). The new class helper allows code syntax like
to compile and work the same as the good old
One of the drawback of record helper is, like class helper, there can only be 1 “active” at any given point in the source code. Any new helper we declare will hide or be hidden by (depending on scope resolution) Delphi’s helper. Also, record helpers are pretty good for generic operations on primitives, but what about more specific operations? It is not ideal to have something like this compile:
var sFirstName : string; begin [...] sFirstName.GetAreaCode;
This problem is especially true for string, as we quite often use strings formatted in specific ways to give them special meanings.
My favorite approach when I have to work with such string is to create what I like to call “Smart Primitive”.
What I like about this concept is that, for one, the validity of the format is done at the moment of initialization. That means that any routine that use the record can assume the value is in an accepted format. 
So, what is a smart primitive?
- Smart primitives are opaque records. Only their methods should use their internal fields. Very often, they will only have a single internal field.
- They implement at least 1 implicit conversion to and from a primitive type. That is how the internal field of the record should be initialized.
Now, lets get a little more concrete. Earlier, I gave the GetAreaCode example, so lets see how a PhoneNumber basic smart primitive would look like.
In North America, phone numbers always have 10 digits, the area code being the first 3 . Now, lets say we want to allow users to enter any of those formats :
Here’s a basic implementation :
TabPhoneNum = record private FPhoneNum : INT64; FOriginalValue : string; public class operator Implicit(S : String) : TabPhoneNum; class operator Implicit(APhoneNum : TabPhoneNum) : String; [...] function GetAreaCode : Integer; end; class operator TabPhoneNum.Implicit(S: String): TabPhoneNum; var sNormalized : String; begin sNormalized := NormalizePhoneNum(S); //Remove "(",")","-" and " " FPhoneNum := StrToInt64(sNormalized ); //If the normalized string is not a valid integer, we want to raise if GetDigitsCount(FPhoneNum) <> 10 then Raise Exception.Create(S + ' is not a valid phone number'); FOriginalValue := S; end; function TabPhoneNum.GetAreaCode : Integer; begin //We don't need to validate the format of FOriginalValue, it's been done already //We don't need to validate the number of digits, it's been done already Result := (FPhoneNum div 10000000); // we don't need to "mod 100" because we validated it is 10 digits end; function TForm1.GetAreaCode(const APhoneNum : TabPhoneNum) : Integer; begin Result := APhoneNum.GetAreaCode; end; procedure TForm1.Button1Click(Sender : TObject); begin ShowMessage(GetAreaCode(Edit1.Text).ToString); //here, if Edit1.Text is not in a valid format, it will raise an exception before calling Tform1.GetAreaCode. end;
In the implementation of GetAreaCode, we see one of the main advantage of smart primitives. We don’t have to deal with all the different string format and we don’t have to validate the number of digits in FPhoneNum because it was already done when the record was first initialized.
Of course, we could simply implement GetAreaCode like this :
function GetAreaCode(const S : string) : Integer; begin Result := Copy(S, 2, 3).ToInteger; end;
and go based on the design by contract philosophy, and state that the input string needs to start with “(” followed immediately by the area code, etc. But that puts the burden of the contract on the caller. For a single method, it might make sense, but if you have a collection of methods having the exact same preconditions in the contract, it would make more sense laying out the contract’s preconditions as being the smart primitive’s contract instead, and then let its methods work with an already enforced contract.
This also has the advantage of “scoping” the methods where they belong. One too many time I have opened a unit filled with routine taking a single string as a parameter, none of them accepting the same “kind” of string.
So, how do you like this? Already doing it? Like it? Hate it? Let me know what you think!