Przechowywanie konfiguracji

migajek

Przechowywanie ustawień

Pominę standardowy wstęp, który powinien traktować o tym kiedy to każdy z nas staje przed koniecznością przechowywania danych itp, itd... Pominę również w artykule omówienie sposobów przechowywania danych, jako że jest to kwestia wtórna, ponadto zostało to omówione w artykule [[http://4programmers.net/Delphi/Artykuły/Zapisywanie_ustawień_w_Delphi]] (nota bene był on dla mnie inspiracją do napisania tego właśnie.) Skupię się tutaj głównie na kwestii tego, w jaki sposób przechowywać ustawienia programu w trakcie jego działania... Zaznaczam na wstępie że nie jest to profesjonalny artykuł omawiający wszystkie znane i nieznane metody, a jedynie próba podzielenia się z Wami swoimi doświadczeniami ;) Ponadto jest zupełnie subiektywny...

Dostępne sposoby

Lista nazwa = wartosc

Metoda polegająca na przechowywaniu w pamięci programu listy wpisów zawierających dwie pozycję: nazwę i wartość. Program wczytuje listę z dysku, następnie w celu sprawdzenia danej wartości konfiguracyjnej wywołuje się przygotowaną wcześniej funkcję, zwracającą z listy wartość przypisaną danej nazwie.

Zalety:
*łatwa implementacja
*łatwość w dodawaniu nowych opcji w programie (brak konieczności jakiejkolwiek wcześniejszej deklaracji)

Wady:
*niska wydajność (wyobraźmy sobie konieczność sprawdzania danej wartości w jakiejś długiej pętli)
*łatwo o pomyłkę (wystarczy pomylić nazwę wpisu pomiędzy odczytem a zapisem, brak jakiejkolwiek walidacji ze strony kompilatora)

Bezpośredni odczyt z pliku/rejestru

Tutaj przy wywoływaniu funkcji odczytującej/zapisującej, zmiany są dokonywane bezpośrednio na "nośniku" informacji - pliku (np. INI lub XML) bądź rejestrze. Kluczem jest, podobnie jak w metodzie wyżej, nazwa wartości.

Zalety:
*te same co wyżej
*brak konieczności zapisu / odczytu danych przy starcie/końcu programu

Wady:
*podobnie jak wyżej

Rekord, zmienne

Przy wczytywaniu, szukamy w "nośniku" danych kluczy, następnie ich wartości przypisujemy do określonych zmiennych. Analogicznie, przy zapisie - zapisujemy wartości zmiennych do pliku. W przypadku rekordu, którego pola są wyłącznie zmiennymi o stałym rozmiarze (brak stringów, wskaźników, tablic dynamicznych itp) można pokusić się o zapis do pliku binarnego, jednak w przypadku zmian w programie (dodanie,usunięcie zmiennych z rekordu) plik staje się niekompatybilny ...

Zalety:
*wysoka wydajność
*kompilator poinformuje nas w przypadku literówki itp, w końcu są to odwołania do zmiennych

Wady:
*konieczność "ręcznego" pisania kodu w celu zapisania czy odczytu zmiennych (wyjątkowo uciążliwe przy większej ilości ustawień)
*konieczność deklaracji zmiennych dla wszystkich opcji programu

Klasa przechowująca ustawienia

Wbrew pozorom, metoda ta różni się od powyższej w sposób dosyć znaczący. Jeśli nasza klasa dziedziczy po TPersistent lub pochodnych, możemy w sposób łatwy i wygodny przechowywać informacje w pliku, za sprawą magicznego RTTI.

Zalety:
*wszystkie z punktu wyżej
*pełna automatyzacja procesu zapisu/odczytu

Wady:
*konieczność deklaracji zmiennych dla wszystkich opcji programu

Wybór, implementacja

Z przyczyn subiektywnych zdecydowałem się na ostatnią opcję, to jest użycie klasy i wykorzystanie RTTI, uznając to za rozwiązanie najwygodniejsze. Do przechowywania konfiguracji potrzebny mi był "nośnik", to jest format pliku wraz z klasą obsługującą go. Przy wyborze formatu priorytetem były dla mnie: dobrze przemyślana struktura pliku (tutaj pliki INI odpadają, potrzebne mi było coś na kształt "drzewka" - np XML) oraz wygoda użycia. Dodatkowo ważne dla mnie było pełne wsparcie Unicode (dla Delphi < 2009), tak więc ostatecznie porzuciłem pliki XML (nie znalazłem wygodnej w użyciu implementacji na pasującej mi licencji) i zwróciłem się w stronę mało popularnego w przypadku Delphi formatu - JSON. Oraz bilbioteki obsługującej, SuperObject ( http://www.progdigy.com/?page_id=6 ).

zasada tworzenia konfiguracji jest niezwykle prosta, mianowicie deklarujemy klasę będącą swego rodzaju "grupą" ustawień, np:

type
 TMainConfig = class(TPersistent)
 private
  FSomeOption: boolean;
 published
  property SomeOption: boolean read FSomeOption write FSomeOption;
 end;

analogicznie dopisujemy resztę ustawień, ponadto nadpisując konstruktor możemy ustawić domyślne wartości (dla prostych typów o stałym rozmiarze, możemy ustawić ją używając słowa "default" przy deklaracji property - odsyłam do dokumentacji).

Najważniejsze, czyli kod odczytu/zapisu - nieco okrojony, mój dodatkowo sprawdza czy zapisywany obiekt wspiera interfejs ISDK_CustomConfig, jeśli tak - wywołuje pewne zdarzenia na obiekcie konfiguracji (dzięki czemu nasza klasa "wie" że została właśnie "wczytana" z pliku, lub że zostanie za chwilę do niego zapisana). Ponadto implementując interfejs ISDK_CustomConfig, możemy zdecydować pod jaką nazwą obiekt zostanie zapisany na liście (w przypadku gdy obiekt nie implementuje interfejsu, zostanie użyta nazwa klasy - nie mylić z nazwą obiektu!!)

Ponadto, w celu łatwego przechowywania list pod-obiektów konfiguracji (np obiekt konfiguracji klienta FTP (TFTPConfig) musi zapisać listę wszystkich zdefiniowanych połączeń, gdzie połączenie jest również obiektem (TFTPConnection) - przechowującym nazwę hosta, użytkownika etc) stworzyłem odpowiednią klase, dziedziczącą po TList. Różni się ona tylko tym że posiada funkcję AddItem, która to funkcja jest wywoływana w czasie wczytywania - powinna ona tworzyć obiekt właściwy dla swojego typu (w naszym przykładzie - TFTPConnection) i dodawać do "samej siebie" ... Dzięki temu podczas wczytywania konfiguracji, możliwe jest "odtworzenie" takiej listy oraz wczytanie konfiguracji jej obiektów.

Przykładowa implementacja, używająca wspomnianego wyżej SuperObject (nieco okrojona...):

//  Configuration storage utilities
//  Copyright 2009 by Michal Gajek
//  http://migajek.com/

unit uConfig;

interface
uses windows, classes, SuperObject, typinfo;

type
  ICustomConfig = interface
    ['{163245C3-B4CF-4097-AFE9-9F673A8CCB62}']
    procedure OnLoaded;
    procedure OnBeforeLoad;
    procedure OnSaved;
    procedure OnBeforeSave;
    function GetName: WideString; 
  end;

  TCustomConfigsList = class(TList)
  private

  public
    function AddItem(): integer; virtual;
  end;


...

// w naszym przykładzie, kod tej funkcji powinien wyglądać następująco
// result:= Add( TFTPConnection.create );
function TCustomConfigsList.AddItem(): integer;
begin 
 result:= -1;
end;





// -- kod zapisu i odczytu 


procedure LoadConfigSO(ACfg: TObject; AFileName: WideString);   overload;
var
 doc: ISuperObject;
begin
 with TUnicodeStringList.Create do
  begin
    LoadFromFile(AFileName);
    doc:= SO(text);
    Free;
  end;
  LoadConfigSO(ACfg, doc);
end;

procedure LoadConfigSO(ACfg: TObject; const doc: ISuperObject);overload;
var
 aintf: ICustomConfig;
 procedure ReadObj(AObj: TObject; APath: WideString);
 var
  item, itm2: TSuperObjectIter;
  inf: PPropInfo;
  obj: TObject;
  intf: ICustomConfig;
  i: integer;
 begin
  if not Supports(Aobj, ICustomConfig, intf) then
    intf:= nil;

  if intf <> nil then
    intf.OnBeforeLoad;
  if ObjectFindFirst(doc.O[APath], item) then
  while item.val <> nil do
    begin

      inf:= GetPropInfo(AObj, item.key);
      if (inf.PropType^.Kind = tkWString) then
        SetWideStrProp(AObj, inf, item.val.AsString)
      else if (inf.PropType^.Kind = tkString) then
        SetStrProp(AObj, inf, item.val.AsString)
      else if (inf.PropType^.Kind in [tkInteger, tkChar, tkWChar]) then
        SetOrdProp(AObj, inf, item.val.AsInteger)
      else if (inf.PropType^.Kind = tkEnumeration) then
        SetEnumProp(AObj, inf, item.val.AsString)
      else if (inf.PropType^.Kind = tkClass) then
        begin
          obj:= GetObjectProp(AObj, inf);
          if (obj <> nil) and (obj is TCustomConfigsList) then
            begin
              if ObjectFindFirst(doc.O[APath + '.' + item.key], itm2) then
                while item.val <> nil do
                  begin
                  i:= (obj as TCustomConfigsList).AddItem;
                  if i > -1 then
                    ReadObj((obj as TCustomConfigsList).Items[i], APath + '.' + item.key + '.' + itm2.key);
                  if not ObjectFindNext(itm2) then
                    break;
                  end;
              ObjectFindClose(itm2);
            end
          else if (obj <> nil) and (obj is TTntStrings) then
            (obj as TTntStringList).commaText:= item.val.AsString
          else if (obj <> nil) and (obj is TUnicodeStrings) then
            (obj as TUnicodeStrings).CommaText:= item.val.AsString
          else if (obj <> nil) then
            ReadObj(obj, APath + '.' + item.key)

        end;

      if not ObjectFindNext(item) then
        break;
    end;
  ObjectFindClose(item);
  if intf <> nil then
    intf.OnLoaded;
 end;

begin
if Supports(ACfg, ICustomConfig, aintf) then
  ReadObj(Acfg, aintf.GetName)
else if (ACfg is TPersistent) then
  ReadObj(Acfg, ACfg.ClassName)
end;


procedure SaveConfigSO(ACfg: TObject; AFileName: WideString); overload;
var
  doc: ISuperObject;
begin
  doc:= SO();
  SaveConfigSO(ACfg, doc);
  with TUnicodeStringList.Create do
    begin
     text:= doc.AsJSon(true, false);
     SaveToFile(AFileName);
     Free;
    end;

end;

procedure SaveConfigSO(ACfg: TObject; const doc: ISuperObject);overload;
var
  aintf: ICustomConfig;
  procedure DoWrite(AObject: TObject; APath: WideString);
  var
    Count, i, j : Integer;
    List : PPropList;
    str, pth: WideString;
    obj: TObject;
    intf: ICustomConfig;
  begin
  if not Supports(AObject, ICustomConfig, intf) then
    intf:= nil;

  if intf <> nil then
    intf.OnBeforeSave;
  Count := GetPropList (AObject.ClassInfo, tkProperties, nil) ;
  GetMem (List, Count * SizeOf (PPropInfo)) ;
  GetPropList (AObject.ClassInfo, tkProperties, List);
  for i:= 0 to Count - 1 do
    begin
      str:= '';
      pth:= APath + '.' + List[i]^.Name;

      if (List[i]^.PropType^.Kind = tkWString) then
        doc.S[pth]:= GetWideStrProp(AObject, List[i])
      else if (List[i]^.PropType^.Kind = tkString) then
        doc.S[pth]:= GetStrProp(AObject, List[i])
      else if ((List[i]^.PropType^.Kind in [tkInteger, tkChar, tkWChar]) and (List[i]^.Default <>GetOrdProp(AObject, List[i]) )) then
        doc.I[pth]:= GetOrdProp(AObject, List[i])
      else if (List[i]^.PropType^.Kind = tkEnumeration ) and (List[i]^.Default <>GetOrdProp(AObject, List[i]) ) then
        doc.S[pth]:= GetEnumName(List[i]^.PropType^, GetOrdProp(AObject, List[i]))
      else if (List[i]^.PropType^.Kind = tkClass) then
        begin
          obj:= typinfo.GetObjectProp(AObject, List[i]);
          if (obj <> nil) and (obj is TCustomConfigsList) then
            begin
            for j:= 0 to (obj as TCustomConfigsList).Count - 1 do
              if TObject((obj as TCustomConfigsList).Items[j]) is TCustomConfig then
                DoWrite( TCustomConfig((obj as TCustomConfigsList).Items[j]), pth + '.' + inttostr(j));
            end
          else if (obj <> nil) and (obj is TTntStrings) then
            doc.S[pth]:= (obj as TTntStrings).CommaText
          else if (obj <> nil) and (obj is TUnicodeStrings) then
            doc.S[pth]:= (obj as TUnicodeStrings).CommaText
          else if (obj <> nil) then
            DoWrite(obj, pth)

        end;
    end;

  if intf <> nil then
    intf.OnSaved;
  end;

begin
  if Supports(acfg, ICustomConfig, aintf) then
    DoWrite(ACfg, aintf.GetName)
  else if (Acfg is TPersistent) then
    DoWrite(ACfg, ACfg.Classname);
end;

Podsumowanie

Tu również wypadałoby napisać coś standardowego, ale nie jestem humanistą to i nie potrafię ;) Powiem krótko - zdecydowałem się na ostatnią metodę, bo na moje potrzeby jest najwygodniejsza w użyciu. Dzielę się paroma przemyśleniami ale nikogo do niczego nie zmuszam, mam nadzieję że może chociaż zainspiruję do jeszcze lepszych rozwiązań ...

1 komentarz