ООП и паттерны проектирования: практическое применение.

ООП и паттерны проектирования: практическое применение.

В Интернете выложено достаточно большое количество статей на тему применения паттернов (шаблонов) проектирования. К сожалению, примеров кода на 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;

Вроде все легко и просто?
Тогда зададим себе несколько вопросов…

А если у нас несколько подключаемых БД, да еще и с разными вариантами подключения?

А если мы хотим добавить новые параметры настройки без перекомпиляции кода?

А если мы решим использовать для сохранения настроек не 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).
Применимость: используйте паттерн «Мост», когда:
— хотите избежать постоянной привязки абстракции к реализации. Так, например, бывает, когда реализацию необходимо выбирать во время выполнения программы.
— и абстракции, и реализации должны расширяться новыми подклассами. В таком случае паттерн «Мост» позволяет комбинировать разные абстракции и реализации и изменять их независимо.
— изменения в реализации абстракции не должны сказываться на клиентах, то есть клиентский код не должен перекомпилироваться. — вы хотите скрыть от клиентов реализацию абстракции.

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

Реализация паттерна Bridge

Каковы основные результаты применения паттерна?
— отделение реализации от интерфейса. Реализации больше не имеет постоянной привязки к интерфейсу. Реализацию абстракции можно конфигурировать во время выполнения. Разделение классов 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;
begin
with Self.SettingsReader do begin
IniSection:='Main';
RegisterIniKey('SettingsStorage', 'ini'); // Способ хранения настроек
RegisterIniKey('Application', 'ApplicationLocalConnection'); // Способ подключения к БД приложения
RegisterIniKey('Project', 'ProjectLocalConnection'); // Способ подключения к БД проекта
RegisterIniKey('RootDir', 'C:Program FilesFastBase'); // Корневой каталог программы
RegisterIniKey('TempDir', 'C:Program FilesFastBaseTMP');
RegisterIniKey('LogDir', 'C:Program FilesFastBaseLog');
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 FilesFastBaseDB');
RegisterIniKey('FileName', 'Application.gdb');
RegisterIniKey('Password', 'masterkey');
RegisterIniKey('UserName', 'SYSDBA');
RegisterIniKey('ServerName', 'localhost');
end;
end;

Как видим, параметры настроек для разных классов соединения различаются, причем легко добавить либо изменить их без внесения правок в отлаженный код. Чтение настроек производится вызовом функции GetIniValue(IniKey).
На следующем занятии мы рассмотрим применение паттерна «Абстрактная фабрика».

Некие негодяи залезли на мой форум и разместили там спам-сообщение.
Я, как водится, полез по ссылке, чтобы посмотреть, что рекламируют. И, о ужас! Не удержусь от публикации ссылки — http://www.clientbase.ru/download/ — здесь можно скачать программу CRM с MySQL! А программа-то без исходников! Вот я им завтра-то позвоню, и спрошу, где исходники, или оплачивали-ли они лицензию. Контора, похоже, большая, т.к. даже номер с 8-800 зарегистрирован.

upd: по признакам «большой» контора вроде проходит — спам и телефон 8-800…, но наименование организации: ИП Гарифуллин Марат Мидхатович, ИНН 165700291820, Адрес: 420024, Казань, Ямашева, 36

Читать на сайте автора.