ООП и паттерны проектирования: практическое применение |
Автор Тагир Юмагузин | |
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; Вроде все легко и просто? Тогда зададим себе несколько вопросов...
И мы видим, что предлагаемое решение нарушает несколько принципов объектно-ориентированного программирования! Первое нарушение - решение не приспособлено к изменениям требований (а требования к программе меняются всегда!). Второе нарушение - нарушение принципа «разделения ответственностей» - процедура выполняет одновременно несколько обязанностей: хранит параметры настройки (FileName, Password, UserName), читает их из файла и производит подключение к БД. Рассмотрим эти вопросы по очереди. Если у нас несколько БД - необходимо хранить отдельно настройки для каждой.Можно в одном ini-файле в разных секциях, но лучше - для каждой БД создать свой файл. В моей программе - две базы данных, БД проекта, и БД приложения. Соответственно, будет четыре файла - ApplicationLocalConnection.ini и ApplicationRemoteConnection.ini для приложения, ProjectLocalConnection.ini и ProjectRemoteConnection.ini для проекта. А чтобы при запуске узнать, какие соединения использовать - еще и главный ini-файл для приложения, тут удобнее использовать название ini-файла, совпадающее с названием приложения. А поскольку одни и те же настройки могут использовать разные программы, для каждой надо создать свой каталог для хранения настроек:
Второй вопрос - параметры настройки... Чтобы менять настройки без перекомпиляции кода, параметры надо хранить отдельно в виде списка строк, и регистрировать при создании экземпляра класса. Тут есть два пути - можно создать один общий класс хранения настроек, и параметризовать его списком переменных, либо использовать полиморфизм, и создать несколько классов по типам настроек. Третий вопрос - сохранения настроек различными способами (в Ini-файле, в реестр, cookie или где-то еще). Вот тут мы используем один из главных принципов объектно-ориентированного программирования - «инкапсулируй то, что может измениться». У нас может измениться способ чтения-сохранения настроек, так «спрячем» возможные изменения за «стеной» абстракции - создадим абстрактный класс TSettingsReader - «читатель» настроек. Он описывает интерфейс для чтения и сохранения настроек без конкретизации - как это делается. Второй принцип, который мы применим - принцип «разделения ответственностей». Каждый класс должен выполнять только то, что нужно, и ничего лишнего. И для выполнения разных обязанностей нам необходимо создать 3 класса:
Как видим, каждый класс занимается своим делом и не зависит от других. Более того, все эти классы - абстрактные, а использовать мы будем конкретных наследников! Внимательный читатель вправе задать вопрос: а где же паттерны проектирования? А они - в реализации использования наших абстрактных классов! Обратимся к классикам - к «банде четырех» (Э.Гамма, Р.Хелм, Р.Джонсон, Дж.Влиссидес. Приемы объектно-ориентированного проектирования)! Рассмотрим описание паттерна проектирования "Мост" (Bridge). Применимость: используйте паттерн "Мост", когда:
А это как раз наш случай! И вот как выглядит диаграмма для нашей конкретной реализации паттерна:
Каковы основные результаты применения паттерна?
Класс 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 - абстрактный, и конкретная реализация методов перекрыта в наследниках:
Соответствующий экземпляр класса будет создаваться в процессе загрузки программы в зависимости от настроек. Вот пример реализации класса для чтения из 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 г. ) |