Plutôt INI ou JSON pour stocker vos paramètres ?

Même si certains développeurs s'en servent pour ça, la base de registres de Windows n'est à mon avis pas le meilleur endroit pour stocker des paramètres d'applications. La raison principale en est la quasi impossibilité de s'y retrouver ou de la sauvegarder dans un contexte de déplacement d'un logiciel d'une machine à une autre ou de réinstallation.

Bon, bien entendu ces inconvénients ont un côté pratique pour stocker "au hasard" des clés de licence pour un logiciel payant, mais est-ce vraiment toujours d'actualité ?

Dans un contexte de développement multiplateforme ou de potentialité d'évolution d'une application vers du multiplateforme, il faut tendre à utiliser au maximum la même base de code pour toutes les cibles. Ca diminue les pertes de temps de débogage et de maintenance. Il y a ainsi deux solution "simples" pour archiver des paramètres depuis Delphi : les fichiers INI à l'aide de l'unité IniFile et l'utilisation d'objects ou tableaux en format JSON.

L'utilisation des fichiers INI est un classique depuis la sortie de Windows 3 il y a une éternité. Pour JSON c'est quand même plus inhabituel mais ça a beaucoup d'avantages comme par exemple d'avoir un format exploitable dans différents langages de développements sur différents systèmes d'exploitation, en standard, alors qu'ils ne proposent pas de fonctions de gestion des .ini

Mais voilà, pour utiliser un stockage en format JSON sous Delphi ce n'est pas si aisé. Il faut se dépatouiller entre les TJSONValue, TJSONPair, TJSONObject, TJSONArray, TJSONBidule et TJSONTruc. C'est l'inconvénient d'un langage typé qui fait aussi tout son charme et sa sécurité.

Comme j'en avais besoin dans le cadre du développement d'une API, j'ai créé une classe me permettant d'utiliser des objets JSON comme espace de stockage de paramètres et les archiver automatiquement en fermeture "propre" de l'application.

Je vous livre ici les résultats de mon travail de cette nuit. Pas testée avec une application VCL mais elle devrait fonctionner comme avec n'importe quelle application Firemonkey.

unit uParam;

interface

type
  tParams = class
    class procedure save;
    class procedure load;
    class function getValue(key: string; default: string): string; overload;
    class function getValue(key: string; default: boolean): boolean; overload;
    class function getValue(key: string; default: integer): integer; overload;
    class procedure setValue(key, value: string); overload;
    class procedure setValue(key: string; value: boolean); overload;
    class procedure setValue(key: string; value: integer); overload;
  end;

implementation

uses
  System.Generics.collections, System.IOUtils, System.SysUtils, System.JSON,
  System.Classes;

var
  paramChanged: boolean;
  paramList: TJSONObject;
  paramFileName: string;

function getParamsFileName: string;
var
  folder: string;
  filename: string;
  app_name: string;
begin
  app_name := TPath.GetFileNameWithoutExtension(paramstr(0));
  folder := TPath.Combine(TPath.GetDocumentsPath, app_name);
  if not tdirectory.Exists(folder) then
    tdirectory.CreateDirectory(folder);
  filename := app_name + '.par';
  result := TPath.Combine(folder, filename);
end;

function getParamValue(key: string): TJSONValue;
begin
  result := nil;
  if assigned(paramList) then
    if (paramList.Count > 0) then
      result := paramList.getValue(key);
end;

procedure setParamValue(key: string; value: TJSONValue);
var
  jsonvalue: TJSONValue;
begin
  if not assigned(paramList) then
    paramList := TJSONObject.Create
  else if (paramList.Count > 0) then
  begin
    jsonvalue := paramList.getValue(key);
    if assigned(paramList.getValue(key)) then
      if (jsonvalue.value <> value.value) then
        paramList.RemovePair(key).Free
      else
        exit;
  end;
  paramList.AddPair(key, value);
  paramChanged := true;
end;

class function tParams.getValue(key: string; default: boolean): boolean;
var
  jsonvalue: TJSONValue;
begin
  jsonvalue := getParamValue(key);
  if assigned(jsonvalue) then
    result := jsonvalue.value.ToBoolean
  else
    result := default;
end;

class function tParams.getValue(key: string; default: string): string;
var
  jsonvalue: TJSONValue;
begin
  jsonvalue := getParamValue(key);
  if assigned(jsonvalue) then
    result := jsonvalue.value
  else
    result := default;
end;

class function tParams.getValue(key: string; default: integer): integer;
var
  jsonvalue: TJSONValue;
begin
  jsonvalue := getParamValue(key);
  if assigned(jsonvalue) then
    result := jsonvalue.value.ToInteger
  else
    result := default;
end;

class procedure tParams.load;
var
  filename: string;
  buffer: tStringStream;
begin
  filename := getParamsFileName;
  if tfile.Exists(filename) then
  begin
    if assigned(paramList) then
      FreeAndNil(paramList);
    buffer := tStringStream.Create(tfile.ReadAllText(filename, TEncoding.UTF8),
      TEncoding.UTF8);
    try
      paramList := TJSONObject.Create;
      paramList.Parse(buffer.Bytes, 0);
    finally
      buffer.free;
    end;
  end;
end;

class procedure tParams.save;
var
  filename: string;
begin
  if (paramChanged) then
  begin
    filename := getParamsFileName;
    if assigned(paramList) and (paramList.Count > 0) then
      tfile.WriteAllText(filename, paramList.ToJSON, TEncoding.UTF8)
    else if tfile.Exists(filename) then
      tfile.Delete(filename);
    paramChanged := false;
  end;
end;

class procedure tParams.setValue(key, value: string);
var
  jsonvalue: TJSONString;
begin
  jsonvalue := TJSONString.Create(value);
  try
    setParamValue(key, jsonvalue);
  except
    jsonvalue.free;
  end;
end;

class procedure tParams.setValue(key: string; value: boolean);
var
  jsonvalue: TJSONBool;
begin
  jsonvalue := TJSONBool.Create(value);
  try
    setParamValue(key, jsonvalue);
  except
    jsonvalue.free;
  end;
end;

class procedure tParams.setValue(key: string; value: integer);
var
  jsonvalue: TJSONNumber;
begin
  jsonvalue := TJSONNumber.Create(value);
  try
    setParamValue(key, jsonvalue);
  except
    jsonvalue.free;
  end;
end;

initialization

paramChanged := false;
paramList := TJSONObject.Create;
tParams.load;

finalization

tParams.save;
if assigned(paramList) then
  FreeAndNil(paramList);

end.

J'ai donc déclaré une classe tParams dans laquelle j'ai des méthodes de classe.
Pas besoin d'instancier cette classe dans mes applications : je n'ai besoin que d'un seul espace de stockage de paramètres, mais vous pouvez bien entendu adapter le tout s'il vous faut des objets de stockage différents et des actions à gérer en fonction de valeurs stockées.

Dans mon cas le chargement des paramètres est fait au lancement du programme et leur sauvegarde systématique se fait en fermeture du programme.
Par mesure de précaution, je recommande quand même d'appeler tParams.save lorsque vos utilisateurs sortent d'un écran de configuration, on ne sait jamais trop ce qui pourrait se passer, surtout sur un smartphone ou une tablette.

Notez aussi que je stocke le fichier de paramètres en clair, dans un dossier créé dans le dossier des documents de l'utilisateur.
Par mesure de précaution il vaudrait mieux crypter ces informations ou ajouter une information de contrôle de cohérence afin d'éviter toute modification manuelle du fichier. Surtout si vous y stockez des indicateurs liés à des achats inApp.

Une fois l'unité incluse dans le projet et utilisée en implémentation d'une fiche ou autre unité, il vous suffit d'utiliser directement les appels aux méthodes set et get, comme vous le feriez avec un fichier INI.

Pour enregistrer une informations :

  tParams.setValue('app_token', edtAppToken.Text);

Pour lire une information :

  edtAppToken.Text := tParams.getValue('app_token', '');

Dans les points importants à noter pour l'utilisation d'un objet JSON avec les librairies fournies par Embarcadero, il faut savoir que la lecture d'une arborescence plante lorsqu'il n'y en a pas. C'est pour cela que je teste systématiquement le nombre de composants de l'objet avant toute recherche/lecture d'une de ses propriétés.

    if (paramList.Count > 0) then
      result := paramList.getValue(key);

Je vous recommande vivement de faire pareil si vous voulez éviter d'éventuelles erreurs de violation d'accès en cours d'utilisation de vos programmes. La méthode getValue retourne la valeur désirée ou nil si elle ne la trouve pas, il faut donc ensuite s'assurer de ce qu'on a reçu avant de le retourner à l'appelant.

Autre point important à avoir en tête : on peut ajouter plusieurs fois des paires avec la même clé dans un TJSONObject. Par conséquent, lorsqu'on gère une modification il est préférable de supprimer la paire initiale si elle existe puis ajouter la nouvelle ou de modifier le TJSONValue associé au TJSONPair.

  if not assigned(paramList) then
    paramList := TJSONObject.Create
  else if (paramList.Count > 0) then
  begin
    jsonvalue := paramList.getValue(key);
    if assigned(paramList.getValue(key)) then
      if (jsonvalue.value <> value.value) then
        paramList.RemovePair(key).Free
      else
        exit;
  end;
  paramList.AddPair(key, value);

J'ai opté pour la première solution. C'est une façon étrange de procéder, mais au moins elle fonctionne à tous les coups.
Reste à voir dans quelle mesure l'abus de modification d'une information ne génère pas des zones mémoires à trous qui ne seraient pas réutilisables car trop petites selon ce qu'on stocke.

Je mettrai cette classe dans la boite à outils prochainement dispo sur mon compte GitHub. En attendant vous pouvez l'utiliser en la copiant/collant si vous le désirez. Et bien entendu si vous avez des questions ou suggestions, faites m'en part sans hésiter.

EDIT : Je vous recommande de lire également cet article concernant les directives de compilation et le conditionnement des blocs de code car il apporte une modification importante à la classe proposée ici. Bien entendu c'est la dernière version valide de la classe qui est disponible sur GitHub donc pas de soucis si vous la piochez dessus plutôt qu'en faisant un copier/coller de cette page.


Liens associés

Ces liens s'ouvrent dans la même fenêtre que cette page. En cliquant dessus vous quitterez Les trucs et astuces d'un développeur Pascal.
Pensez à les ouvrir dans un nouvel onglet si vous préférez rester ici pour y revenir plus facilement.


Mug Toucan DX dans la baie de RioMug Chinese New Year 2023 : year of the rabbit