Using Value Objects to simplify development

In this post I will show one simple example of designing the Value Object pattern for handling the ADO MSSQL Server connection string. This is just a fictional task but will be used to illustrate the usage.

So let’s imagine we need to handle the connection using the good known connection strings in format like this:

 Provider="SQLOLEDB.1";Data Source="Local";Initial Catalog="AdventureWorks";UserID="sa";Password="drowssap";

Often we will want to setup this parameters in INI file, JSON, registry, database fields or any other configuration source. So all parts of the connection string are stored/configured separately. But in the same time when you use the final connection string to set it to the TADOConnection object you always use the whole string and you do not care about individual parameters – the only thing you know is the single atomic value of the connection string which holds the parameters needed for the connection.

Using the Value Object pattern we can make handling of the connection string centralized. Let’s prototype the TConnectionString type and see how we want to use it:

var
 LCon: TConnectionString;
begin
  LCon := TConnectionString.Empty
    .WithProvider('SLQOLEDB.1')
    .WithServer('Local')
    .WithDatabase('AdventureWorks')
    .WithUser('sa')
    .WithPassword('drowssap');

  Writeln(LCon.AsString);
  LCon := LCon.AsString; { Compatible with String type via class operator }
  Writeln(LCon.WithPassword('******').AsString);
  Writeln(LCon.Provider);
  Writeln(LCon.Server);
  Writeln(LCon.Database);
  Writeln(LCon.User);
  Writeln(LCon.Password);
end.

We want so that the code above will output the following lines:

Provider="SLQOLEDB.1";Data Source="Local";Initial Catalog="AdventureWorks";UserID="sa";Password="drowssap";
Provider="SLQOLEDB.1";Data Source="Local";Initial Catalog="AdventureWorks";UserID="sa";Password="******";
SLQOLEDB.1
Local
AdventureWorks
sa
drowssap

So from the use case above we can conclude following properties of our new type TConnectionString:

  • No methods change the state of the TConnectionString instance;
  • You can access separate parameters via properties;
  • You can easily create a new instance with a new parameter value changing;
  • The type is immutable;
  • The type is compatible with the String.

The illustrative implementation of the TConnectionString is listed below:

unit uConnectionString.Types;

interface

type
  TConnectionString = record
  private
    FText: String;
    FProvider, FServer, FDatabase, FUser, FPassword: String;

    procedure SplitTextToParams;
    procedure BuildTextFromParams;
  public
    class function Create(const AProvider, AServer, ADatabase, AUser, APassword: String): TConnectionString; static;

    property Provider: String read FProvider;
    property Server: String read FServer;
    property Database: String read FDatabase;
    property User: String read FUser;
    property Password: String read FPassword;

    function AsString: String;

    function WithProvider(const AProvider: String): TConnectionString;
    function WithServer(const AServer: String): TConnectionString;
    function WithDatabase(const ADatabase: String): TConnectionString;
    function WithUser(const AUser: String): TConnectionString;
    function WithPassword(const APassword: String): TConnectionString;

    class operator Implicit(AValue: TConnectionString): String;
    class operator Implicit(AValue: String): TConnectionString;
    class function Empty: TConnectionString; static;
  end;

implementation

uses
 System.SysUtils,
 System.RegularExpressions;

class function TConnectionString.Create(const AProvider, AServer, ADatabase, AUser, APassword: String): TConnectionString;
begin
  Result := Empty;
  Result.FProvider := AProvider;
  Result.FServer := AServer;
  Result.FDatabase := ADatabase;
  Result.FUser := AUser;
  Result.FPassword := APassword;
end;

function TConnectionString.WithProvider(const AProvider: String): TConnectionString;
begin
  Result := Self;
  Result.FProvider := AProvider;
  Result.BuildTextFromParams;
end;

function TConnectionString.WithServer(const AServer: String): TConnectionString;
begin
  Result := Self;
  Result.FServer := AServer;
  Result.BuildTextFromParams;
end;

function TConnectionString.WithDatabase(const ADatabase: String): TConnectionString;
begin
  Result := Self;
  Result.FDatabase := ADatabase;
  Result.BuildTextFromParams;
end;

function TConnectionString.WithUser(const AUser: String): TConnectionString;
begin
  Result := Self;
  Result.FUser:= AUser;
  Result.BuildTextFromParams;
end;

function TConnectionString.WithPassword(const APassword: String): TConnectionString;
begin
  Result := Self;
  Result.FPassword := APassword;
  Result.BuildTextFromParams;
end;

function TConnectionString.AsString: String;
begin
  Result := Self;
end;

procedure TConnectionString.SplitTextToParams;

  function ExtractParameterValueFromText(const AParameterName, AText: String): String; inline;
  var
    LMath: TMatch;
  begin
    LMath := TRegEx.Match(AText, AParameterName + '=(".*?"|[^;]+?);?', [roIgnoreCase]);
    if LMath.Success then
      Result := LMath.Groups[1].Value
    else
      Result := '';
  end;

begin
  FProvider := ExtractParameterValueFromText('Provider', FText);
  FServer := ExtractParameterValueFromText('Data Source', FText);
  FDatabase := ExtractParameterValueFromText('Initial Catalog', FText);
  FUser := ExtractParameterValueFromText('UserID', FText);
  FPassword := ExtractParameterValueFromText('Password', FText);
end;

procedure TConnectionString.BuildTextFromParams;

  function ParameterWithText(const AParameter, AValue: String): String; inline;
  begin
    if AValue.IsEmpty then
      Result := ''
    else
      Result := AParameter + '="' + AValue + '";';
  end;

begin
  FText :=
    ParameterWithText('Provider', FProvider) +
    ParameterWithText('Data Source', FServer) +
    ParameterWithText('Initial Catalog', FDatabase) +
    ParameterWithText('UserID', FUser) +
    ParameterWithText('Password', FPassword);
end;

class function TConnectionString.Empty: TConnectionString;
begin
  Result := Default(TConnectionString);
end;

class operator TConnectionString.Implicit(AValue: TConnectionString): String;
begin
  Result := AValue.FText;
end;

class operator TConnectionString.Implicit(AValue: String): TConnectionString;
begin
  Result := Default(TConnectionString);
  Result.FText := AValue;
  Result.SplitTextToParams;
end;

end.

So now your “homework” will be to add the comparison possibilities for this type 🙂 But that is achieved in a very simple way by adding the class operators for equality. Also the parameter extraction is not assumed to be the best solution – I used the regular expressions for simplicity and compactness, but if you want to use this example or modify it then these methods will need refactoring.

Hope this example gave you some general idea of the Value Objects and how easy and robust can their usage be if designed in a good way. This was just a simple case which is similar (or even is 🙂 ) to a primitive obsession as we only “managed” single field with type String inside.

Have a nice weekend guys!

Leave a Reply