Social Icons

суббота, 10 июля 2010 г.

Упростите свои Delphi-приложения - Части 3 и 4

Теперь у нас есть идея - то, чего мы хотим достичь и как мы собираемся это сделать, время написать код и спроектировать классы.

1. Введение

В качестве основы нам потребуется класс/объект, который мы сможем использовать для чтения и записи настроек приложения из и в реестр Windows. Звучит вполне просто... но, как вы помните, мы подумали предусмотреть расширение функциональности в дальнейшем.

2. Требования к коду

2.1. Совместимость с Delphi 7

Хотя в последние годы язык пополнился некоторыми новыми элементами, мы же пока использовать их не будем. Наша цель - компиляция кода в Delphi 7.

Вы можете задаться вопросом: "Кто еще работает в этой старой Delphi?". Я заметил, что даже сегодня, некоторые мои клиенты используют Delphi 7 для компиляции своих проектов. В дальнейших статьях я может быть покажу Вам, как сделать то, что мы делаем, с использованием современных методов, но сейчас давайте остановимся на этом.

2.2. Отсутствие "привязки" к реестру Windows

Несмотря на то, что мы будем писать код для чтения и записи данных в реестр Windows, хочется легко адаптировать его для других хранилищ, например XML или INI-файла. Да и кто знает, что будет завтра. Не исключено, что мы получим возможность писать приложения для Windows Mobile, Mac, iPhone или даже iPad (было бы неплохо), и реестра Windows на этих платформах может не оказаться.

Сейчас сфокусируемся на реестре Windows, но, как Вы уже поняли, иметь ввиду другие хранилища - хорошая идея. Главное, о чем должна болеть наша голова в данный момент - возможность хранения/загрузки настроек. Как или где они будут хранится не так важно, сделаем то, что требуется.

2.3. Еще ньюансы

Пока мы знаем, что нам нужно что-то, что сможет хранить наши настройки. Нам понадобится загружать и сохранять их, а также, возможно, наличие имени для каждой настройки или даже значения по умолчанию или ее описание. Мы будем хранить Целые числа, Строки, а может быть и Пароли, Даты, ...

3. Время кодинга!

3.1. ... ну почти ...

Так, ... вообще, до того, как начать писать код, стоит посмотреть, как подобные вещи реализованы в VCL. Конечно, мы все можем сделать и сами, но позволить новым классам наследоваться от существующих было бы неплохой идеей. Т.к. нам нужен список, Вы можете взять, например, класс TList.

В моем случае, я знал, что хочу иметь настройки, в которых будут храниться строки, целые числа и булевы значения. После я решил, что нужны настройки и для хранения значений типа DateTime, а также некоторых других типов. В конце концов я пришел к чему-то похожему на TField и TIntegerField, TStringField, ...

Итак, зная, что я буду использовать различные типы данных в настройках и хочу хранить список этих настроек, я решил, что неплохо было бы подключить к работе TObjectList.

3.2. Создание класса TdvSetting

3.2.1. Преамбула

В общем случае, мне нужен объект со следующими свойствами:
  • Идентификатор (или Имя, Заголовок)
  • Описание (или Подсказка)
  • Значение
Необходимо иметь возможность чтения и записи значения конкретного идентификатора в реестр. Кроме того, при чтении значения, я хочу проверять, если ли уже значение у данного идентификатора, а в случае его отсутствия использовать значение по умолчанию.

Я хотел бы получать значения TdvSetting в виде String или Variant (подобно TField и TStringField), поэтому реализовал эту возможность в коде. Плюс, я хочу устанавливать значение TdvSetting. И наконец, как и с TField в VCL, я добавил код, вызывающий исключение, если потомок не реализует какой-либо метод.

Это может показаться несколько сложным, но давайте сравним TdvSetting с TField и TStringField еще раз. С TStringField Вы можете присвоить значение, используя
aField.Value := theValue
или
aField.AsString := theValue
Оба варианта присваивания верны, но, если aField - экземпляр TField, а не TStringField - возникнет исключение. Ту же функциональность сделал и я.

Сейчас я сосредоточусь на реестре Windows, но как Вы знаете неплохо было бы иметь и другие возможности хранения. В конце концов, единственная вещь, на которой мы сейчас акцентируем внимание - хранение/загрузка некоторых настроек. Как или где они будут храниться не так важно, сначала сделаем то, что хотели.

3.2.2. Код

TdvSetting = class(TObject)
  private
    FValue:  Variant;
    FDefaultValue:  Variant;
    FIdentifier:  string;
    FCaption:  string;
    procedure SetCaption(const Value: string);
    procedure SetVisible(const Value: Boolean);
  protected
    function GetAsBoolean: Boolean; virtual;
    function GetAsDateTime: TDateTime; virtual;
    function GetAsFloat: Double; virtual;
    function GetAsInteger: Longint; virtual;
    function GetAsString: string; virtual;
    function GetAsVariant: Variant; virtual;

    procedure SetAsBoolean(const Value: Boolean); virtual;
    procedure SetAsDateTime(const Value: TDateTime); virtual;
    procedure SetHint(const Value: string);
    procedure SetAsFloat(const Value: Double); virtual;
    procedure SetIdentifier(const Value: string);
    procedure SetAsInteger(const Value: Longint); virtual;
    procedure SetAsString(const Value: string); virtual;
    procedure SetAsVariant(const Value: Variant); virtual;
  protected
    function AccessError(const TypeName: string): Exception; dynamic;
    procedure SetVarValue(const Value: Variant); virtual;
  public
    constructor Create(const aIdentifier, aCaption: string;
                       const aDefaultValue: Variant); virtual;

    destructor Destroy; override;

    procedure SaveToRegIni(aRegIni: TRegistryIniFile;
                           const aSection:  string); virtual;
    procedure LoadFromRegIni(aRegIni: TRegistryIniFile;
                             const aSection: string); virtual;

    procedure Clear; virtual;

    property DefaultValue: Variant read FDefaultValue;
    property AsBoolean: Boolean read GetAsBoolean write SetAsBoolean;
    property AsDateTime: TDateTime read GetAsDateTime write SetAsDateTime;
    property AsFloat: Double  read GetAsFloat write SetAsFloat;
    property AsInteger: Longint read GetAsInteger write SetAsInteger;
    property AsString: string  read GetAsString write SetAsString;
    property AsVariant: Variant read GetAsVariant write SetAsVariant;
    property Identifier: string read FIdentifier write SetIdentifier;
    property Caption: string read FCaption write SetCaption;
    property Value: Variant read GetAsVariant write SetAsVariant;
  end;

...

function TdvSetting.AccessError(const TypeName: string): Exception;
resourcestring
  SSettingAccessError = 'Невозможно получить значение ''%s'' (%s) как %s';
begin
  Result := Exception.CreateResFmt(@SSettingAccessError,
                                    [ Identifier, Caption, TypeName ]);
end;

procedure TdvSetting.Clear;
begin
  FValue := Null;  
end;

constructor TdvSetting.Create(const aIdentifier, aCaption: string;
  const aDefaultValue: Variant);
begin
  Create(aIdentifier, aCaption, aCaption, True, aDefaultValue);
end;

function TdvSetting.GetAsBoolean: Boolean;
begin
  raise AccessError('Boolean'); { Do not localize }
end;

function TdvSetting.GetAsDateTime: TDateTime;
begin
  raise AccessError('DateTime'); { Do not localize }
end;

function TdvSetting.GetAsFloat: Double;
begin
  raise AccessError('Float'); { Do not localize }
end;

function TdvSetting.GetAsInteger: Longint;
begin
  raise AccessError('Integer'); { Do not localize }
end;

function TdvSetting.GetAsString: string;
begin
  Result := ClassName;
end;

function TdvSetting.GetAsVariant: Variant;
begin
  raise AccessError('Variant'); { Do not localize }
end;

procedure TdvSetting.LoadFromRegIni(aRegIni: TRegistryIniFile;
  const aSection: string);
begin
  Assert(Assigned(aRegIni), 'Параметр aRegIni должен содержать экземпляр TRegIni');
end;

procedure TdvSetting.SaveToRegIni(aRegIni: TRegistryIniFile;
  const aSection: string);
begin
  Assert(Assigned(aRegIni), 'Параметр aRegIni должен содержать экземпляр TRegIni');
end;

procedure TdvSetting.SetAsBoolean(const Value: Boolean);
begin
  raise AccessError('Boolean'); { Do not localize }
end;

procedure TdvSetting.SetAsDateTime(const Value: TDateTime);
begin
  raise AccessError('DateTime'); { Do not localize }
end;

procedure TdvSetting.SetAsFloat(const Value: Double);
begin
  raise AccessError('Float'); { Do not localize }
end;

procedure TdvSetting.SetAsInteger(const Value: Longint);
begin
  raise AccessError('Integer'); { Do not localize }
end;

procedure TdvSetting.SetAsString(const Value: string);
begin
  raise AccessError('string'); { Do not localize }
end;

procedure TdvSetting.SetAsVariant(const Value: Variant);
begin
  if (VarIsNull(Value)) then
  begin
    Clear;
  end
  else
  begin
    SetVarValue(Value);
  end;
end;

procedure TdvSetting.SetCaption(const Value: string);
begin
  FCaption := Value;
end;

procedure TdvSetting.SetHint(const Value: string);
begin
  FHint := Value;
end;

procedure TdvSetting.SetIdentifier(const Value: string);
begin
  FIdentifier := Value;
end;

procedure TdvSetting.SetVarValue(const Value: Variant);
begin
  raise AccessError('Variant'); { Do not localize }
end;

3.2.3. Что он делает?

На самом деле, этот кусок кода практически ничего не делает. Он просто приводит создание базового класса, которые мы сможем использовать в качестве родительского для остальных классов. Попросту, мы имеет некоторую обработку ошибок и скелет для наших классов настроек.

3.3. Класс TdvStringSetting

3.3.1. Преамбула

В общем, класс TdvSetting предоставляет нам скелет, который мы можем использовать для создания настроек-потомков. Я уже завершил написание TdvStringSetting, TdvIntegerSetting, TdvBooleanSetting и некоторых других, но давайте начнем с TdvStringSetting.

3.3.2. Код

TdvStringSetting = class(TdvSetting)
private
  function GetDefaultValueAsString: string;
protected
  function GetAsBoolean: Boolean; override;
  function GetAsDateTime: TDateTime; override;
  function GetAsFloat: Double; override;
  function GetAsInteger: Longint; override;
  function GetAsString: string; override;
  function GetAsVariant: Variant; override;

  function GetValue(var Value: string): Boolean;

  procedure SetAsBoolean(const Value: Boolean); override;
  procedure SetAsDateTime(const Value: TDateTime); override;
  procedure SetAsFloat(const Value: Double); override;
  procedure SetAsInteger(const Value: Longint); override;
  procedure SetAsString(const aValue: string); override;
  procedure SetVarValue(const aValue: Variant); override;
public
  procedure SaveToRegIni(aRegIni: TRegistryIniFile; const aSection: string); override;
  procedure LoadFromRegIni(aRegIni: TRegistryIniFile; const aSection: string); override;

  property DefaultValue: string read GetDefaultValueAsString;
  property Value: string read GetAsString write SetAsString;
end;

...

{ TdvStringSetting }

function TdvStringSetting.GetAsBoolean: Boolean;
var
  S: string;
begin
  S := GetAsString;
  Result := (Length(S) > 0) and (S[1] in ['T', 't', 'Y', 'y']);
end;

function TdvStringSetting.GetAsDateTime: TDateTime;
begin
  Result := StrToDateTime(GetAsString);
end;

function TdvStringSetting.GetAsFloat: Double;
begin
  Result := StrToFloat(GetAsString);
end;

function TdvStringSetting.GetAsInteger: Longint;
begin
  Result := StrToInt(GetAsString);
end;

function TdvStringSetting.GetAsString: string;
begin
  if not GetValue(Result) then Result := '';
end;

function TdvStringSetting.GetAsVariant: Variant;
var
  S: string;
begin
  if GetValue(S) then Result := S else Result := Null;
end;

function TdvStringSetting.GetDefaultValueAsString: string;
begin
  Result := FDefaultValue;
end;

function TdvStringSetting.GetValue(var Value: string): Boolean;
begin
  Value  := FValue;
  Result := True;
end;

procedure TdvStringSetting.LoadFromRegIni(aRegIni: TRegistryIniFile;
  const aSection: string);
begin
  inherited LoadFromRegIni(aRegIni, aSection);

  Value := aRegIni.ReadString(aSection, Identifier, DefaultValue);
end;

procedure TdvStringSetting.SaveToRegIni(aRegIni: TRegistryIniFile;
  const aSection: string);
begin
  inherited SaveToRegIni(aRegIni, aSection);

  aRegIni.WriteString(aSection, Identifier, Value);
end;

procedure TdvStringSetting.SetAsBoolean(const Value: Boolean);
const
  Values: array[Boolean] of string[1] = ('F', 'T');
begin
  SetAsString(Values[Value]);
end;

procedure TdvStringSetting.SetAsDateTime(const Value: TDateTime);
begin
  SetAsString(DateTimeToStr(Value));
end;

procedure TdvStringSetting.SetAsFloat(const Value: Double);
begin
  SetAsString(FloatToStr(Value));
end;

procedure TdvStringSetting.SetAsInteger(const Value: Integer);
begin
  SetAsString(IntToStr(Value));
end;

procedure TdvStringSetting.SetAsString(const aValue: string);
begin
  FValue := aValue;
end;

procedure TdvStringSetting.SetVarValue(const aValue: Variant);
begin
  SetAsString(aValue);
end;

3.3.3. Что он делает?

Т.к. у нас уже имеется скелет от TdvSetting, нам остается лишь переопределить некоторые методы и реализовать свои собственные функции. Как видите, мы добавили примерно такой же код, который имеет TStringField в VCL.

Дополнительно реализованы лишь методы SaveToRegIni и LoadFromRegIni. Они позволяют загрузить и сохранить значение настройки в реестр. Кроме того, при загрузке мы используем значение по умолчанию, если настройка не найдена в реестре. Секция реестра, откуда/куда мы будем загружать/сохранять значение, имеет такое же имя, как и идентификатор настройки.

3.4. Создание класса TdvSetting

3.4.1. Преамбула

Теперь, когда мы спроектировали различные типы настроек, нам понадобится какой-либо контейнер для их хранения. Например, приложение может иметь несколько настроек: PrintInColor (Цветная печать), CheckForUpdates (Проверять обновления), AutoConnect (Автоподключение), ..., и мы должны иметь к ним доступ. Как уже было сказано ранее, я создал TdvSettings на основе TObjectList. Так мы сможем хранить ссылку на экземпляр каждого TdvSetting-объекта и иметь к ним доступ.

3.4.2. Код

TdvSettings = class(TObjectList)
private
  FRootKey: string;
protected
  procedure CreateSettings; virtual;

  function GetItems(Index: Integer): TdvSetting;
  procedure SetItems(Index: Integer; ASetting: TdvSetting);
public
  constructor Create(const aRootKey : string);

  function Add(ASetting: TdvSetting): Integer;
  function Extract(Item: TdvSetting): TdvSetting;
  function Remove(ASetting: TdvSetting): Integer;
  function IndexOf(ASetting: TdvSetting): Integer;
  function First: TdvSetting;
  function Last: TdvSetting;
  function SettingByIdentifier(const aIdentifier : string) : TdvSetting;

  procedure LoadFromRegistry;
  procedure SaveToRegistry;

  procedure Insert(Index: Integer; ASetting: TdvSetting);
  property Items[Index: Integer]: TdvSetting read GetItems write SetItems; default;
  property RootKey : string read FRootKey write FRootKey;
end;

  ...

{ TdvSettings }

function TdvSettings.Add(ASetting: TdvSetting): Integer;
begin
  Result := inherited Add(ASetting);
end;

constructor TdvSettings.Create(const aRootKey: string);
begin
  inherited Create(True);
  FRootKey := aRootKey;
  CreateSettings;
  // Читаем значения из реестра при создании списка
  LoadFromRegistry;
end;

procedure TdvSettings.CreateSettings;
begin

end;

function TdvSettings.Extract(Item: TdvSetting): TdvSetting;
begin
  Result := TdvSetting(inherited Extract(Item));
end;

function TdvSettings.First: TdvSetting;
begin
  Result := TdvSetting(inherited First);
end;

function TdvSettings.GetItems(Index: Integer): TdvSetting;
begin
  Result := TdvSetting(inherited Items[Index]);
end;

function TdvSettings.IndexOf(ASetting: TdvSetting): Integer;
begin
  Result := inherited IndexOf(aSetting);
end;

procedure TdvSettings.Insert(Index: Integer; ASetting: TdvSetting);
begin
  inherited Insert(Index, aSetting);
end;

function TdvSettings.Last: TdvSetting;
begin
  Result := TdvSetting(inherited Last);
end;

procedure TdvSettings.LoadFromRegistry;
var
  lIndex: Integer;
  lSetting: TdvSetting;
  lRegIni: TRegistryIniFile;
begin
  lRegIni := TRegistryIniFile.Create('');
  try
    for lIndex := 0 to Pred(Count) do
    begin
      lSetting := Items[lIndex];
      lSetting.LoadFromRegIni(lRegIni, RootKey);
    end;
  finally
    FreeAndNil(lRegIni);
  end;
end;

function TdvSettings.Remove(ASetting: TdvSetting): Integer;
begin
  Result := inherited Remove(aSetting);
end;

procedure TdvSettings.SaveToRegistry;
var
  lIndex: Integer;
  lSetting: TdvSetting;
  lRegIni: TRegistryIniFile;
begin
  lRegIni := TRegistryIniFile.Create('');
  try
    for lIndex := 0 to Pred(Count) do
    begin
      lSetting := Items[lIndex];
      lSetting.SaveToRegIni(lRegIni, RootKey);
    end;
  finally
    FreeAndNil(lRegIni);
  end;
end;

procedure TdvSettings.SetItems(Index: Integer; ASetting: TdvSetting);
begin
  inherited Items[Index] := aSetting;
end;

function TdvSettings.SettingByIdentifier(
  const aIdentifier: string): TdvSetting;
var
  lcv: Integer;
begin
  Result := Nil;

  for lcv := 0 to Pred(Count) do
  begin
    if (Items[lcv].Identifier = aIdentifier) then
    begin
      Result := Items[lcv];
      Break;
    end;
  end;
end;

3.4.3. Что он делает?

Итак, это простой контейнер для нескольких объектов класса TdvSetting. Он предоставляет доступ к каждой настройке по ее индексу в списке или идентификатору (имени). Мы также сможем устанавливать корневой узел (RootKey) TdvSettings-объекта, задающий общий путь в реестре ко всем настройкам. Когда-нибудь мне захочется иметь возможность создавать настройки из базового класса и загружать их значения.

Вы заметите, что я добавил пустой метод CreateSettings. Цель - реализовать его в дочерних классах. На уровне TdvSettings мы не знаем, какие настройки у нас есть, как они называются и какой тип они имеют... В будущем же я хочу иметь возможность создавать настройки из базового класса и загружать их значения.

В TdvMyApplicationSettings я переопределю этот метод и добавлю необходимый код для настройки конкретных TdvSetting-объектов, которые мне нужны в приложении.

4. Что дальше?

На данный момент у нас есть скелет, от которого мы можем оттолкнуться. Если Вы готовы повозиться, можете создать свой собственный TdvSetting и его потомков. Я уже говорил, что мне нужны были Integer, DataTime и Boolean - их и попробуйте реализовать.

Я уже неоднократно отмечал, что не существует единственного подхода к решению нашей проблемы, и старался привести и другие возможные способы. Это позволило мне прийти к тому, что есть сейчас. Например, начальные версии содержали код для TRegistryIniFile непосредственно внутри класса TdvSetting.

Наличие одного и того же кода для LoadFromRegIni и SaveToRegIni во всех потомках TdvSetting, заставило меня в скором времени призадуматься. Я решил, что не стоит создавать и уничтожать TRegistryIniFile для каждой из 20 настроек - так код пришел к своему текущему состоянию.

5. Пишите!

Я уверен, что код может быть упрощен и даже улучшен, и я приму любые предложения. Оставляйте комментарии в моем блоге, и я буду стараться отвечать на них, как только будет возможность. Кто знает, может Ваше предложение может послужить темой следущей статьи...

Оригинал: http://www.devia.be/news/article/delphi-code-using-some-basic-rules-oo-techniques-refactoring-part-3/ и
Оригинал: http://www.devia.be/news/article/delphi-code-using-some-basic-rules-oo-techniques-refactoring-part-4/

Комментариев нет:

Отправить комментарий

Поделитесь с друзьями!

 

Подписчики

Статистика