Главная arrow В помощь студентам arrow Объектно-ориентированное программирование arrow ООП и паттерны проектирования: практическое применение  
03.12.2024 г.
ООП и паттерны проектирования: практическое применение Печать E-mail
Автор Тагир Юмагузин   
26.01.2010 г.

В Интернете выложено достаточно большое количество статей на тему применения паттернов (шаблонов) проектирования. К сожалению, примеров кода на Delphi немного, да и сами примеры достаточно упрощенные, и не дают возможность оценить всю мощь использования этой технологии. Предлагаемый цикл статей – попытка показать начинающим программистам пользу применения паттернов проектирования на реальном коммерческом проекте, пошагово решая часто возникающие практические задачи.

Это не обзор паттернов, и не учебник по их применению, это – практические занятия. Надеюсь, их получится несколько.

Занятие 1. Чтение настроек программы.

Постановка задачи: программа при начальном запуске должна прочитать настройки подключения к базе данных и соединиться с ней.

Такую задачу решал, пожалуй, каждый программист, и в любом учебнике программирования Вы легко найдете решение:

procedure ConnectDataBase;
var
  IniFile : TIniFile;
  Section, FileName, Password, UserName : String;
Begin
  IniFile := TIniFile.Create(IniFilePath);
  Section := 'Main';
  with IniFile do begin
    FileName := ReadString(Section, 'FileName', 'Application.gdb');
    Password := ReadString (Section, 'Password', 'masterkey');
    UserName := ReadString (Section, 'UserName', 'SYSDBA'); 
  end;
  MainDatabase.Params.Clear;
  MainDatabase.Params.Add('password='+ Password);
  MainDatabase.Params.Add('user_name='+'UserName')
  MainDatabase.FileName := FileName;
  MainDatabase.Connected := true;
end;

Вроде все легко и просто?

Тогда зададим себе несколько вопросов...

  1. А если у нас несколько подключаемых БД, да еще и с разными вариантами подключения?
  2. А если мы хотим добавить новые параметры настройки без перекомпиляции кода?
  3. А если мы решим использовать для сохранения настроек не Ini-файл, а реестр, или cookie?

И мы видим, что предлагаемое решение нарушает несколько принципов объектно-ориентированного программирования!

Первое нарушение - решение не приспособлено к изменениям требований (а требования к программе меняются всегда!).

Второе нарушение - нарушение принципа «разделения ответственностей» - процедура выполняет одновременно несколько обязанностей: хранит параметры настройки (FileName, Password, UserName), читает их из файла и производит подключение к БД.

Рассмотрим эти вопросы по очереди. Если у нас несколько БД - необходимо хранить отдельно настройки для каждой.Можно в одном ini-файле в разных секциях, но лучше - для каждой БД создать свой файл.

В моей программе - две базы данных, БД проекта, и БД приложения. Соответственно, будет четыре файла -

ApplicationLocalConnection.ini и ApplicationRemoteConnection.ini для приложения, ProjectLocalConnection.ini и ProjectRemoteConnection.ini для проекта.

А чтобы при запуске узнать, какие соединения использовать - еще и главный ini-файл для приложения, тут удобнее использовать название ini-файла, совпадающее с названием приложения. А поскольку одни и те же настройки могут использовать разные программы, для каждой надо создать свой каталог для хранения настроек:

  • Bin (тут лежат сами exe-файлы)
  • Application1.LocalSettings (тут лежат настроечные файлы приложения 1)
  • Application2.LocalSettings (тут лежат настроечные файлы приложения 2)

Второй вопрос - параметры настройки... Чтобы менять настройки без перекомпиляции кода, параметры надо хранить отдельно в виде списка строк, и регистрировать при создании экземпляра класса. Тут есть два пути - можно создать один общий класс хранения настроек, и параметризовать его списком переменных, либо использовать полиморфизм, и создать несколько классов по типам настроек.

Третий вопрос - сохранения настроек различными способами (в Ini-файле, в реестр, cookie или где-то еще).

Вот тут мы используем один из главных принципов объектно-ориентированного программирования - «инкапсулируй то, что может измениться». У нас может измениться способ чтения-сохранения настроек, так «спрячем» возможные изменения за «стеной» абстракции - создадим абстрактный класс TSettingsReader - «читатель» настроек. Он описывает интерфейс для чтения и сохранения настроек без конкретизации - как это делается.

Второй принцип, который мы применим - принцип «разделения ответственностей». Каждый класс должен выполнять только то, что нужно, и ничего лишнего. И для выполнения разных обязанностей нам необходимо создать 3 класса:

  • TSettingsHolder. Класс для хранения параметров настройки. Он знает - что хранить.
  • TSettingsReader. Класс для чтения и сохранения их. Он знает - как хранить.
  • Класс для подключения к БД. Он знает - как использовать настройки.

Как видим, каждый класс занимается своим делом и не зависит от других. Более того, все эти классы - абстрактные, а использовать мы будем конкретных наследников!

Внимательный читатель вправе задать вопрос: а где же паттерны проектирования? А они - в реализации использования наших абстрактных классов! Обратимся к классикам - к «банде четырех» (Э.Гамма, Р.Хелм, Р.Джонсон, Дж.Влиссидес. Приемы объектно-ориентированного проектирования)!

Рассмотрим описание паттерна проектирования "Мост" (Bridge).

Применимость: используйте паттерн "Мост", когда:

  • хотите избежать постоянной привязки абстракции к реализации. Так, например, бывает, когда реализацию необходимо выбирать во время выполнения программы.
  • абстракции, и реализации должны расширяться новыми подклассами. В таком случае паттерн "Мост" позволяет комбинировать разные абстракции и реализации и изменять их независимо.
  • изменения в реализации абстракции не должны сказываться на клиентах, то есть клиентский код не должен перекомпилироваться. - вы хотите скрыть от клиентов реализацию абстракции.

Image 

А это как раз наш случай! И вот как выглядит диаграмма для нашей конкретной реализации паттерна:

Image 

Каковы основные результаты применения паттерна?

  • отделение реализации от интерфейса. Реализации больше не имеет постоянной привязки к интерфейсу. Реализацию абстракции можно конфигурировать во время выполнения. Разделение классов TSettingsHolder и TSettingsReader устраняет зависимость от их реализации.
  • повышение степени расширяемости. Можно расширять независимо иерархии классов TSettingsHolder и TSettingsReader.


Вот теперь - первый пример кода:

Класс TSettingsReader - «читатель» настроек. Описывает абстрактный интерфейс для чтения и сохранения настроек.

Для возможности добавлять параметры настройки без перекомпиляции кода они создаются динамически при инициализации,и названия параметров хранятся в отдельном IniKeys : TStringList и регистрируются в нем при создании конкретного класса.

type TSettingsReader = class
private
  fIniSection: String; 
  fOwnerName: String; 
  fIsReadOnly: Boolean;
  fSourcePath: String;
  fDriveType : Word;
  fDriveTypeDescription : String;
  fIniKeys: TStringList;
  procedure SetSourcePath(const Value: String);
  function CheckKeyName(const KeyName: string): Boolean;
 
protected
  property IniKeys : TStringList read fIniKeys;
public
  class function GetStorageType: String; virtual; abstract;       // В наследниках выдает способ сохранения параметров
  function CheckExists: Boolean; virtual; abstract;              // Проверка наличия файла или ветки реестра
  procedure RegisterIniKey(const KeyName, DefaulValue : String);// Процедура регистрации параметра настройки
  function SectionExists(const Section: string): Boolean; virtual; abstract;// Проверка наличия секции
  property IniSection: String read fIniSection write fIniSection;   // Секция хранения
  property SourcePath: String read fSourcePath write SetSourcePath; // Путь к ini-файлу или ветке реестра
  property IsReadOnly: Boolean read fIsReadOnly; // Доступ к записи файла (программа может быть на CD-Rom)
  property DriveType : Word read fDriveType;     // Тип устройства хранения 
  property DriveTypeDescription: String read fDriveTypeDescription; // Описание устройства хранения 
  property OwnerName: String read fOwnerName write fOwnerName;      // Имя владельца - хранителя
  constructor Create;
  destructor Destroy; override;
 
  function Load:Boolean; virtual;              // Функция загрузки параметров из файла в IniKeys
  function Update:Boolean; virtual; abstract; // Функция сохранения параметров в файле
 
  function GetIniValue(const KeyName : string): string;
  procedure SetIniValue(const KeyName, Value : string);
end;
 
implementation
 
constructor TSettingsReader.Create;
begin
  inherited;
  fIniKeys := TStringList.Create;
  fIniKeys.CaseSensitive := False;
end;
 
destructor TSettingsReader.Destroy;
begin
  IniKeys.Free;
  inherited;
end;
 
function TSettingsReader.CheckKeyName(const KeyName: string): Boolean;
begin
  Result := (IniKeys.IndexOfName(KeyName)>=0);
  if not Result then
  MsgError('['+Self.IniSection+'] '+KeyName+' not registered!', Self.OwnerName+'-GetIniValue');
end;
 
function TSettingsReader.GetIniValue(const KeyName: string): string;
begin
  if CheckKeyName(KeyName) then
  Result := Trim(IniKeys.Values[KeyName])
end;
 
function TSettingsReader.Load: Boolean;
begin
  Result := CheckExists; // В базовом классе - только проверка. В наследниках будет добавлена сама процедура чтения.
end;
 
procedure TSettingsReader.RegisterIniKey(const KeyName, DefaulValue: String);
begin
  IniKeys.Add(KeyName+'='+DefaulValue);
end;
 
procedure TSettingsReader.SetIniValue(const KeyName, Value: string);
begin
  if CheckKeyName(KeyName) then
  IniKeys.Values[KeyName] := Value;
end;
 
procedure TSettingsReader.SetSourcePath(const Value: String);
begin
  fSourcePath := Value;
  fDriveType := CheckDriveType(Value);
  case fDriveType of
    DRIVE_FIXED : fDriveTypeDescription := 'Fixed drive';
    DRIVE_REMOTE : fDriveTypeDescription := 'Remote (network) drive';
    DRIVE_CDROM : fDriveTypeDescription := 'CD-ROM drive';
    DRIVE_RAMDISK : fDriveTypeDescription := 'RAM disk';
    DRIVE_REMOVABLE : fDriveTypeDescription := 'Removable drive (USB)';
  end;
  fIsReadOnly := (fDriveType = DRIVE_CDROM); //Надо определить, не является-ли диск CD-диском?
end;
 
function CheckDriveType(const FilePath: String): Word;
var
  FileDrive : string;
begin
  FileDrive := ExtractFileDrive(FilePath);
  Result := GetDriveType(PChar(FileDrive)); 
end;
Как видим, класс TSettingsReader содержит две виртуальные функции Load и Update, которые, будучи перекрыты в наследниках, определяют способ чтения и сохранения настроек. В то же время класс имеет всю базовую функциональность для хранения настроек.

Класс TSettingsReader - абстрактный, и конкретная реализация методов перекрыта в наследниках:

  • TFBIniFileReader - «читатель» ini-файла
  • TFBRegistryReader - «читатель» реестра 
  • TFBCookieReader - «читатель» файла cookie
 
Соответствующий экземпляр класса будет создаваться в процессе загрузки программы в зависимости от настроек.

Вот пример реализации класса для чтения из ini-файла (я намеренно убрал все проверки и реакцию на ошибки чтения/записи, чтобы не загромождать листинг, полный код можно получить по ссылке):

type TFBIniFileReader = class (TSettingsReader)
private
  fIniFile: TIniFile;
  function GetIniFile: TIniFile; // "Отложенное" создание Ini-File - когда понадобится!
public
  class function GetStorageType: String; override;
  constructor Create;
  destructor Destroy; override;
  function CheckExists: Boolean; override;
  function SectionExists(const Section: string): Boolean; override;
  function Load:Boolean; override;
  function Update:Boolean; override;
  property IniFile: TIniFile read GetIniFile;
end;
 
implementation;
class function TFBIniFileReader.GetStorageType: String;
begin
  Result := 'IniFile';
end;
 
function TFBIniFileReader.CheckExists: Boolean;
begin
  Result := FileExists(SourcePath);
end;
 
constructor TFBIniFileReader.Create;
begin
  inherited;
end;
 
destructor TFBIniFileReader.Destroy;
begin
  inherited;
  fIniFile.Free;
end;
 
function TFBIniFileReader.Load: Boolean;
//При чтении из ini-файла при отсутствии значений им присваиваются значения по-умолчанию!
var
  i: Integer;
  KeyName , KeyValue, DefaultValue : string;
begin
  Result := inherited Load;
  if not Result then
    Exit;
  try
    with IniFile do begin // Мы обращаемся к свойству IniFile, сам файл будет открыт в момент обращения!
      for i := 0 to IniKeys.Count - 1 do begin
        KeyName := IniKeys.Names[i];
        DefaultValue := IniKeys.ValueFromIndex[i];
        KeyValue := ReadString(IniSection, KeyName, DefaultValue);
        if Trim(KeyValue) = '' then 
          KeyValue := DefaultValue; 
        IniKeys.ValueFromIndex[i] := KeyValue;
      end;
      Result := true;
    end;
    except // ... реакция на ошибки чтения/записи
  end;
end;
 
 
function TFBIniFileReader.Update: Boolean;
var
  i: Integer;
  KeyName : string;
  ActualValue : string;
begin
  Result := True;
  if IsReadOnly then begin
    MsgError('Путь: '+Self.SourcePath+#10+'Ошибка - '+ExtractFileDrive(Self.SourcePath)+
    ' ('+Self.DriveTypeDescription+') - только для чтения!', 'Невозможно записать ini-файл');
    Exit;
  end;
  BackUpFile(Self.SourcePath);
  try
    with IniFile do begin
      for i := 0 to IniKeys.Count - 1 do begin
        KeyName := IniKeys.Names[i];
        ActualValue := IniKeys.ValueFromIndex[i];
        WriteString(IniSection, KeyName, ActualValue);
      end;
      UpdateFile;
    end;
    except //... реакция на ошибки чтения/записи
  end;
end;
 
function TFBIniFileReader.GetIniFile: TIniFile; // "Отложенное" создание Ini-File - когда понадобится!
begin
  if not Assigned(fIniFile) then
    try
      fIniFile := TIniFile.Create(Self.SourcePath);
    except //... реакция на ошибки чтения/записи
  end;
  Result:=fIniFile;
end;
Опишем класс TCustomSettingsHolder, который содержит абстрактный интерфейс для хранителя настроек.
type TCustomSettingsHolder = class
private
  fSettingsReader: TSettingsReader;
  procedure SetSettingsReader(const Value: TSettingsReader);
public
  class function GetConnectionType: String;virtual;abstract;//Метод класса, выдает описание типа соединения c БД
  class function GetDescription : String;virtual;abstract;//Метод класса, выдает описание подключенной БД
  property SettingsReader: TSettingsReader read fSettingsReader write SetSettingsReader; //Читатель настроек
end;
 
implementation
procedure TCustomSettingsHolder.SetSettingsReader (const Value: TSettingsReader);
//Как видим, хранитель настроек ссылается на абстрактный класс читателя настроек, что позволяет при инициализации 
//приложения назначить конкретного наследника класса!
begin
  fSettingsReader := Value;
  fSettingsReader.OwnerName := Self.GetDescription;//Чтобы хранитель мог при ошибке сообщить, из какого соединения 
                                                   //он вызван
end;
Класс TFBSettingsHolder (наследник TCustomSettingsHolder)реализует функциональность по чтению и сохранению настроек.
type TFBSettingsHolder = class(TCustomSettingsHolder)
public
// Начальная инициализация - создание параметров настроек.Функция должна вызываться после установки SettingsReader!
  procedure Initialize; virtual; abstract;
  // Чтение настроек из файла.
  function ReadParameters:Boolean;virtual;
end;
От него унаследованы классы TMainConnection, TApplicationLocalConnection, TApplicationRemoteConnection, TProjectLocalConnection, TProjectRemoteConnection, которые отличаются реализацией процедуры инициализации.

Для примера приводятся только процедуры для настроек самого приложения и локального подключения к БД:

 

procedure TMainConnection.Initialize;
<p> </p>begin
  with Self.SettingsReader do begin
    IniSection:='Main';
    RegisterIniKey('SettingsStorage', 'ini'); // Способ хранения настроек
    RegisterIniKey('Application', 'ApplicationLocalConnection'); // Способ подключения к БД приложения
    RegisterIniKey('Project', 'ProjectLocalConnection'); // Способ подключения к БД проекта
    RegisterIniKey('RootDir', 'C:\Program Files\FastBase'); // Корневой каталог программы
    RegisterIniKey('TempDir', 'C:\Program Files\FastBase\TMP');
    RegisterIniKey('LogDir', 'C:\Program Files\FastBase\Log');
    RegisterIniKey('WebServer', 'WebServerLocalConnection');
    RegisterIniKey('RememberUser', 'no');
    RegisterIniKey('Login', 'no');
    RegisterIniKey('Category', 'no'); 
  end;
end;
 
procedure TApplicationLocalConnection.Initialize;
begin
  with Self.SettingsReader do begin
    IniSection:='ApplicationLocalConnection';
    RegisterIniKey('WorkDirectory', 'c:\Program Files\FastBase\DB');
    RegisterIniKey('FileName', 'Application.gdb');
    RegisterIniKey('Password', 'masterkey');
    RegisterIniKey('UserName', 'SYSDBA');
    RegisterIniKey('ServerName', 'localhost');
  end;
end;
 
Как видим, параметры настроек для разных классов соединения различаются, причем легко добавить либо изменить их без внесения правок в отлаженный код. Чтение настроек производится вызовом функции GetIniValue(IniKey).

На следующем занятии мы рассмотрим применение паттерна «Абстрактная фабрика».

Последнее обновление ( 27.01.2010 г. )
 

Ivanovo State University of Chemical Technology has entered into an academic partnership with Visual Paradigm to better facilitate the teaching of software design & modeling through the use of Visual Paradigm.
Enterprise Architect
Sparx Systems Enterprise Arctitect provides Ivanovo State University of Chemical Technology with Enterprise Architect, Eclipse Integration, Visual Studio Integration, SysML Technology, Zachman Framework and much more for use in educational purposes, offered by the Enterprise Architect Academic Site License.