Шаблоны в Delphi

Шаблоны в Delphi Шаблоны программирования (Design Patterns) — это часто используемые в процессе программирования структуры, зависимости и связи в объектно-ориентированном проектировании. Знание, как правильно и наиболее полезно использовать шаблоны, может помочь вам проектировать свои приложения лучше, использовать более компактный, структурированный код многократного использования. Также это будет способствовать разработке больших и сложных систем.

Delphi — это объектно-ориентированный язык, в котором присутствует множество классов (объектов) и определенных зависимостей, которые уже реализованы и готовы к употреблению, что помогает упростить и облегчить разработку приложений. Но наиболее важными зависимостями в свете объектной модели шаблонов являются: наследование классов, виртуальные (virtual) и абстраткные (abstract) методы, использование защищенного (protected) и общедоступного (public) определения методов. Все это позволяет создавать шаблоны программирования, которые можно использовать много раз и легко расширять их возможности. Так же, как любой объектно-ориентированный язык, Delphi позволяет повысить функциональность и стабильность шаблонов путем изоляции основных свойств и методов от их собратьев, могущих подвергнуться модификации.
И самый простой пример — это сама Delphi, расширяемая среда разработки, благодаря своей пакетной архитектуре, интерфейсам среды разработки (IDE) и интерфейсам инструментов (Tools). Эти интерфейсы определяют множество абстрактных и виртуальных конструкторов и операций (методов), которые широко используются как при написании программного обеспечения, так и при расширении самой среды разработки (написание своих компонентов, мастеров, экспертов).
Хочу заметить, что в этой статье многие названия и определения представлены "как есть". Я посчитал это более удобным, чем заниматься переводом названий шаблонов и других терминов на русский, так как не всегда можно передать точное определение при переводе, а смысл термина терять не хотелось бы. Так что здесь и далее все названия шаблонов оставлены на английском языке, как, впрочем, и исходный код примеров, хотя я допускаю, что, возможно, в других источниках, отличных от моих, названия и термины могут варьироваться. Но пусть это вас не озадачивает, так как из описания впоследствии станет понятно о назначении каждого отдельно взятого шаблона. Также перечисленные шаблоны не претендуют на звание полного перечня шаблонов. Они просто отражают мои знания в этой области из тех источников, что будут перечислены ниже.
Список некоторых часто используемых шаблонов (всего их больше сорока) представлен в таблице.

Название шаблона Определение
Singleton Удостоверяется, что данный класс имеет только один экземпляр. Предоставляет глобальную информацию для доступа к классу.
Adapter Конвертирует интерфейс класса в другой интерфейс, понятный клиенту шаблона. Adapter позволяет классам с разными интерфейсами совместно работать, что невозможно в обычных условиях из-за различий в интерфейсах.
Template Method Определяет каркас (Skeleton) алгоритма или его части в глобальной операции, позволяя разбивать выполнение на несколько шагов, передавая его подклассам. То есть подклассы получают возможность переопределить некоторые шаги алгоритма без изменения всей структуры алгоритма.
Builder Отделяет создание объекта от его практического представления так, что один конструктор может создавать различные представления объекта.
Abstract Factory Предоставляет механизм создания семейства связанных между собой или зависимых друг от друга объектов, не требуя при этом описания конкретных классов.
Factory Method Определяет интерфейс, позволяющий создать объект, но дает возможность подклассам решать, какой класс реализовать. Factory Method позволяет классу иметь подклассы, то есть потомков.
Теперь перейдем непосредственно к описанию каждого шаблона и примерам, поясняющим их назначение.
Singleton. Этот шаблон — один из самых простейших. Его главная задача — обеспечивать глобальную (внутри приложения) единственность создаваемого объекта или класса. Существует несколько примеров реализации этого шаблона в Delphi VCL. К ним можно отнести TApplication (в одном проекте может существовать только один объект типа TАpplication — группу проектов *.bpg будем считать набором простых проектов), TScreen (опять же — только один объект подобного типа существует; операционную систему Unix не берем в расчет;), системный экран в Windows NT/2000 также не считается, так как до него практически не добраться, да он нам и не нужен), TClipBoard (даже в MS Office 2000 он один, несмотря на то, что можно хранить там несколько объектов копирования-вставки). Этот шаблон можно использовать везде, где есть необходимость в единственном (именно единственном, а не одном — тут надо разделить эти понятия) глобальном объекте (классе) в вашем приложении. Использование этого шаблона может быть полезным в проектировании глобального обработчика некорректных операций (exception handler), организации безопасности в приложении или для реализации единой (и единственной) точки входа интерфейса для другого приложения.
Чтобы реализовать класс подобного типа, достаточно перекрыть конструктор и деструктор класса, чтобы при их вызове обращение шло напрямую к глобальной переменной класса. В конструкторе необходимо сделать проверку на существование переменной класса. Если она существует, то конструктор должен прекратить свою работу, в противном случае — создать экземпляр и присвоить ему переменную класса. В деструкторе необходимо выполнить действия по освобождению переменной, если она ссылается на экземпляр, который уже был уничтожен.
Следующий пример показывает, как выглядят два подобных класса, отвечающих описанному выше шаблону. Один наследован от TComponent, другой — от TObject.

unit Singleton;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
TCSingleton = class(TComponent)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
constructor Create(AOwner: TComponent); override; // перекрываем конструктор
destructor Destroy; override; // перекрываем деструктор

published
{ Published declarations }
end;

type
TOSingleton = class(TComponent)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;

published
{ Published declarations }
end;

procedure Register;

implementation

var Global_CSingleton: TCSingleton;
Global_OSingleton: TOSingleton;

procedure Register;
begin
RegisterComponents('Patterns', [TCSingleton, TOSingleton]); // описанные компоненты будут зарегистрированы и помещены на закладке Patterns.
end;

{ TCSingleton }

constructor TCSingleton.Create(AOwner: TComponent);
begin
if Global_CSingleton <> nil then // проверяем единственность создаваемого компонента
begin
ShowMessage('Компонент уже существует! Вы не можете создавать более одного такого компонента.'); // если он уже существует, то пользователь получает отказ в создании еще одного экземпляра
Abort // прерываем выполнение без каких-либо уведомлений об ошибке
end
Else begin
inherited Create(AOwner); // иначе создаем объект
Global_CSingleton:= Self; // и передаем на него ссылку
end;
end;

destructor TCSingleton.Destroy;
begin
if Global_CSingleton = Self then // если существует объект, который нужно уничтожить
Global_CSingleton:= nil; // то уничтожаем его
inherited Destroy; // вызываем внешний деструктор
end;

{ TOSingleton }

constructor TOSingleton.Create(AOwner: TComponent);
begin
if Global_OSingleton <> nil then
begin
ShowMessage('Объект уже существует! Вы не можете создавать более одного такого объекта.');
Abort
end
Else begin
inherited Create(AOwner);
Global_OSingleton:= Self;
end;
end;

destructor TOSingleton.Destroy;
begin
if Global_OSingleton = Self then
Global_OSingleton:= nil;
inherited Destroy;
end;

end.

Следующий в нашем рассмотрении шаблон — Adapter. Типичный пример — это файл-оболочка, который генерирует среда Delphi, когда вы импортируете ActiveX-библиотеку или VB-Object. Этот файл-оболочка содержит новый класс, который транслирует интерфейс импортируемого элемента управления в интерфейс на языке Pascal. Другой очень широко распространенный пример: необходимо построить интерфейс для нескольких систем сразу, которые отличаются чем-то существенным друг от друга. Например, с помощью такого механизма можно реализовать совместимость написанного вами компонента в Delphi 5 с несколькими более ранними версиями Delphi.
Но так как Delphi не допускает подобного прямого множественного наследования между классами, вместо этого Adapter должен ссылаться к определенному экземпляру старого класса.
Другими словами, если во вселенной Delphi нет возможности соединить два острова насыпью из песка или гравия для проезда автотранспорта из-за разницы в колесах (на одном острове они квадратные, а на другом круглые (множественное наследование)), то есть возможность построить мост, на котором с машин будут снимать одни, а надевать другие колеса в зависимости от направления движения. При этом, конечно, надо учитывать, что на том острове, где колеса квадратные, они ездят как положено, но с колесами одного острова ездить по другому, скажем, запрещено законом. Самой же процедурой "замены колес" занимается программист, — именно он должен реализовать адаптацию для данных, идущих от источника к получателю через "адаптер".
Теперь обратимся к примерам. В следующем листинге приведен очень простой, так как выполняет только чтение, пример использования шаблона Adapter. В качестве проблемы поставим известную проблему 2000-го года и блестяще ее решим 8-).
Проблема заключается в следующем. Существуют сведения о заказчиках, но в старом формате, не приемлемом после 2000 года.
Наша задача: сделать данный факт прозрачным для пользователя, чтобы самый злейший враг программиста — пользователь — ничего и не заметил. Так ему, так сказать, и надо ;-).

unit Adapter;

interface

uses SysUtils, Classes;

type

TNewCustomer = class

private

FCustomerID: Longint;
FFirstName: string;
FLastName: string;
FDOB: TDateTime;

protected

function GetCustomerID: Longint; virtual;
function GetFirstName: string; virtual;
function GetLastName: string; virtual;
function GetDOB: TDateTime; virtual;

public

constructor Create(CustID: Longint); virtual;
property CustomerID: Longint read GetCustomerID;
property FirstName: string read GetFirstName;
property LastName: string read GetLastName;
property DOB: TDateTime read GetDOB;

end;

{Спрячем детали реализации класса TOldCustomer от пользователя }

function GetCustomer(CustomerID: Longint): TNewCustomer;

implementation

const

Last_OldCustomer_At_Year_2000 = 15722;
Last_OldCustomer_In_Database = 30000;

{ реализация нового класса }

constructor TNewCustomer.Create(CustID: Longint);

begin
FCustomerID:= CustID;
FFirstName:= 'A';
FLastName:= 'New_Customer';
FDOB:= Now;
еnd;

function TNewCustomer.GetCustomerID: Longint;

begin
Result:= FCustomerID;
end;

function TNewCustomer.GetFirstName: string;
begin
Result:= FFirstName;
end;

function TNewCustomer.GetLastName: string;
begin
Result:= FLastName;
end;

function TNewCustomer.GetDOB: TDateTime;
begin
Result:= FDOB;
end;

type

TOldDOB = record
Day: 0..31;
Month: 1..12;
Year: 0..99;
end;

TOldCustomer = class

FCustomerID: Integer;
FName: string;
FDOB: TOldDOB;

Public

constructor Create(CustID: Integer);
property CustomerID: Integer read FCustomerID;
property Name: string read FName;
property DOB: TOldDOB read FDOB;

end;

constructor TOldCustomer.Create(CustID: Integer);

begin
FCustomerID:= CustomerID;
FName:= 'An Old_Customer';
with FDOB do begin
Day:= 1;
Month:= 1;
Year:= 1;
end;
end;

type

{ Класс-адаптер, помогающий нам представить данные в новом формате }

TAdaptedCustomer = class(TNewCustomer)

private

FOldCustomer: TOldCustomer;

protected

function GetCustomerID: Longint; override;
function GetFirstName: string; override;
function GetLastName: string; override;
function GetDOB: TDateTime; override;

рublic

constructor Create(CustID: Longint); override;
destructor Destroy; override;

end;

{ TAdaptedCustomer }

constructor TAdaptedCustomer.Create(CustID: Longint);

begin
inherited Create(CustID);
FOldCustomer:= TOldCustomer.Create(CustID);
end;

destructor TAdaptedCustomer.Destroy;
begin
FOldCustomer.Free;
inherited Destroy;
end;

function TAdaptedCustomer.GetCustomerID: Longint;

begin
Result:= FOldCustomer.CustomerID;
end;

function TAdaptedCustomer.GetFirstName: string;
var SpacePos: integer;

begin
SpacePos:= Pos(' ', FOldCustomer.Name);
if SpacePos = 0 then
Result:= ''
else
Result:= Copy(FOldCustomer.Name,1,SpacePos-1);
end;

function TAdaptedCustomer.GetLastName: string;
var SpacePos: integer;

begin
SpacePos:= Pos(' ', FOldCustomer.Name);
if SpacePos = 0 then
Result:= FOldCustomer.Name
else
Result:= Copy(FOldCustomer.Name,SpacePos+1,255);
end;

function TAdaptedCustomer.GetDOB: TDateTime;
var FullYear: Word;

begin
if CustomerID > Last_OldCustomer_At_Year_2000 then
FullYear:= 2000 + FOldCustomer.DOB.Year
else
FullYear:= 1900 + FOldCustomer.DOB.Year;
Result:= EncodeDate(FullYear, FOldCustomer.DOB.Month, FOldCustomer.DOB.Day);
end;

function GetCustomer(CustomerID: Longint): TNewCustomer;

begin
if CustomerID > Last_OldCustomer_In_Database then
Result:= TNewCustomer.Create(CustomerID)
else
Result:= TAdaptedCustomer.Create(CustomerID) as TNewCustomer;
end;

end.

Таким образом, класс TАdaptedCustomer существует как прослойка между двумя другими классами, обеспечивая их нормальное взаимодействие — преобразование данных в понимаемый новым классом формат.
Это одни из самых простых шаблонов. Как я уже писал, всего их около 40, но я не буду настолько распространяться, и в следующей статье мы рассмотрим еще несколько шаблонов, более сложных, чем приведенные выше. Тем не менее, чем выше их сложность, тем большую пользу они могут принести вам при использовании в ваших разработках. И чем быстрее вы научитесь использовать полученные знания с пользой, тем проще и легче станет вам работать, программировать.

Денис Мигачев АКА Denver



Компьютерная газета. Статья была опубликована в номере 17 за 2002 год в рубрике программирование :: delphi

©1997-2024 Компьютерная газета