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;
Tag <delphi[.]>?