Об этом уже писали: http://edn.embarcadero.com/article/27603 или вот http://18delphi.blogspot.ru/2013/09/templates-in-object-pascal.html.
Напишу своими словами, надеюсь в доступной для массового читателя форме.
Итак, допустим у нас есть пара классов: TSomeType1 и TSomeType2, которые объявлены в SomeUnit.pas. И допустим, что эти классы имеют одинаковый набор некоторых свойств и методов, но при этом эти свойства и методы не объявлены у их общего предка. К примеру, пусть будет так:
unit SomeUnit; interface type TSomeType1 = class(TObject) // .. public procedure DoSomething; end; TSomeType2 = class(TObject) // .. public procedure DoSomething; end; .. end.
Здесь видим метод DoSomething, который есть у обоих классов. Но это два разных метода.
HINT: И допустим, что у нас нет возможности вносить изменения в SomeUnit.pas. Это важно для понимания, почему именно такой подход я описываю.
А теперь допустим, что у нас есть задача: написать класс, который будет работать с экземплярами наших классов, вызывая у них DoSomething (по некоторым событиям). А т.к. TSomeType1 и TSomeType2 – это разные классы, то нам придётся писать два новых класса. Назовём их T1 и T2 соответственно. Как это можно сделать? Ну, например, в лоб (файл MyUnit.pas):
unit MyUnit; uses SomeUnit; interface type T1 = class(TObject) private FObject: TSomeType1; public constructor Create(AObject: TSomeType1); end; T2 = class(TObject) private FObject: TSomeType2; public constructor Create(AObject: TSomeType2); end; implementation constructor T1.Create(AObject: TSomeType1); begin FObject := AObject; FObject.DoSomething; end; constructor T2.Create(AObject: TSomeType2); begin FObject := AObject; FObject.DoSomething; end; end.
HINT: Здесь вызов DoSomething происходит всего один раз в конструкторе классов T1 и T2. Это совсем не практично, но это всего лишь пример, на котором я хочу продемонстрировать описываемую технику.
И использовать это где-то в коде так:
uses SomeUnit, MyUnit; .. var Obj1: TSomeType1; Obj2: TSomeType2; ObjA: T1; ObjB: T2; .. ObjA := T1.Create(Obj1); ObjB := T2.Create(Obj2); ..
А теперь представьте, что наши классы T1 и T2 делают гораздо больше работы. Т.е. кода будет больше. Но при этом, код будет совпадать. Налицо – дублирование кода. Как же его избежать?
Мысль первая – обобщения
Попробуем обобщения, они же – дженерики. Наш MyUnit.pas будет выглядеть так:
unit MyUnit; uses SomeUnit; interface type TX<T> = class(TObject) private FObject: T; public constructor Create(AObject: T); end; T1 = TX<TSomeType1>; T2 = TX<TSomeType2>; implementation constructor TX.Create(AObject: T); begin FObject := AObject; FObject.DoSomething; end; end.
Но, к сожалению, такой код не скомпилируется. Потому что для компиляции строки FObject.DoSomething нужно знать, что метод DoSomething есть у типа T, а этого компилятор знать просто не может. И я не нашел способов это каким-либо образом определить. Можно явно указать, от какого класса должен быть унаследован тип T, или какой интерфейс тип T должен поддерживать. Но в нашем примере это не подходит (мы же не можем вносить правки в SomeUnit.pas, помните?).
UPD: В комментариях мне напомнили ещё и про RTTI. Да, начиная с Delphi 2010, с использованием RTTI можно достучаться даже до приватных полей объекта. Но мне это не нравится в плане читаемости кода и скорости исполнения.
Мысль вторая – шаблоны кода
Попробуем include-файлы (обычные inc-файлы, которые существуют в Delphi в наследство от Pascal’я). Идея простая: дублирующийся код выносим в inc-файл, который подставляем в нужных нам местах директивой {$I filename}. Смотрите, создадим файл X.inc с таким содержанием:
type TX = class(TObject) private FObject: TReplacement; public constructor Create(AObject: TReplacement); end; implementation constructor TX.Create(AObject: TReplacement); begin FObject := AObject; FObject.DoSomething; end; end.
Этот код надо будет подставить два раза. Но в один файл это не получится сделать, поэтому создаём два вспомогательных файла.
Файл "A.pas":
unit A; interface uses SomeUnit; type TX = class; // forward declaration T1 = TX; TReplacement = TSomeType1; {$i X.inc}
Файл "B.pas":
unit B; interface uses SomeUnit; type TX = class; // forward declaration T2 = TX; TReplacement = TSomeType2; {$i X.inc}
HINT: Тут я немного схитрил. Чтобы не делать два inc-файла (один для интерфейсной части, а второй для части реализации – как это предлагают авторы по ссылкам выше), в модулях A и B, перед включением inc-файла, добавил forward declaration (строка номер 8) для класса TX (который и описывается в inc-файле). После чего этого я могу напрямую сослаться на этот класс (ещё до его интерфейсной части – строка 9). В строке 10 указывается тот тип данных, который будет использоваться inc-файлом вместо TReplacement.
HINT: Если Вы не совсем понимаете, что тут происходит, то просто подставьте содержимое файла X.inc в файлы A.pas и B.pas вместо директивы {$i X.inc}. И Вы увидите, что получилось два полноценных модуля.
HINT: Строго говоря, дублирования исполняемого кода (как и с дженериками) мы не избежали. При компиляции нашего примера inc-файл будет подставлен два раза. Но нам удалось избежать дублирования на уровне исходного кода – а это, порой, многого стоит.
После этого, наш модуль MyUnit.pas надо переписать, он станет таким:
unit MyUnit; uses A, B; interface type T1 = A.T1; T2 = B.T2; implementation end.
И мы можем его использовать точно так же, как это было показано выше.
Зачем это нужно
В случае, когда нет возможности внести изменения в исходный модуль типа SomeUnit.pas, такая техника может оказаться единственной, позволяющей решить подобные задачи.
Например, в VCL есть компонент TCombobox. В режиме Style = csSimple или Style = csDropdown он работает как обычный TEdit. И у TCombobox и у TEdit есть набор схожих свойств: MaxLength, SelStart, SelLength, SelText и т.п. Но эти свойства объявлены не в их общем предке (TWinControl), а в классах TCustomComboBox и TCustomEdit соответственно.
В предыдущей заметке я описывал, как можно немного изменить стандартное поведение свойства MaxLength для наследников от TCustomEdit (т.е. TEdit, TMemo и т.п.). А для того, чтобы его можно было применить и к TCustomComboBox – воспользуемся описанной выше техникой.
Тестовое приложение:
Скачать: исходник или тестовое приложение.
Плюсы и минусы данной техники
Плюсы:
- Мы решили частную задачу, когда нет возможности (или нет желания) внести изменения в исходный код сторонней библиотеки.
- Мы избежали дублирования кода.
Минусы:
- Когда мы выносим код в inc-файл, надо чуть больше воображения, чем обычно: придумать имена для файлов и замещаемых типов, а также представлять, как это будет выглядеть в итоге.
- IDE, работая с inc-файлами, заранее не знает, в какие места этот inc-файл будет подставляться. Как следствие – тут не работает CodeInsight, могут возникнуть проблемы с рефакторингом и автоматическим форматированием кода.
И такой нюанс. Отладчик с inc-файлами работает так же, как и с обычными pas-файлами. Но (как и с дженериками) ставя точку останова в inc-файле, это точка включается и для T1 и для T2. Решается указанием условия для точки останова, например: Self is T1.
14 коммент.:
bq. Тут я немного схитрил. Чтобы не делать два inc-файла (один для интерфейсной части, а второй для части реализации)
А я писал же - как не делать. Через IfDef.
Немного позанудничаю. Create конкретно здесь - лишний :-)
Если ты пишешь вспомогательный класс, без собственного состояния - достаточно class function. Для дёрганья методов - TReplacement.
"Строго говоря, дублирования исполняемого кода (как и с дженериками) мы не избежали."
И НИ В ОДНОМ другом языке со статической типизацией - не избежим.
Ну а динамическая - вносит дополнительные накладные расходы.
Это такой duck typing? А-ля оксигеновский http://wiki.oxygenelanguage.com/en/Duck_Typing ? Жаль что в дельфях подобного нету "из коробки"!
Inc-файлы - зло. Как правило отваливается рефакторинг и нормальная навигация по коду. В подобных задачах проще и правильнее использовать интерфейсы
Спасибо за комментарии.
Александр Люлин
> А я писал же - как не делать. Через IfDef.
Это мы обсуждали немного другой вопрос. "{$IFDEF control_is_combo}" vs "if Control is TCombobox then". Скачайте исходник по ссылке из заметки - там как раз дефайны и используются.
> Немного позанудничаю
Да пожалуйста :) Только конкретный пример (не исходник по ссылке) - это всего лишь пример.
В исходнике - как раз с собственным состоянием. Одними класс-методами там не обойтись.
deksden
Да, в делфи порой не хватает всяких таких вкусностей...
Анонимный
> Inc-файлы - зло.
Да, у inc-файлов есть недостатки. IDE с ними плохо работает. Но отладчик вполне себе дружит.
> В подобных задачах проще и правильнее использовать интерфейсы
Согласен. Но иногда по другому никак. Как в примере с VCL, или другими сторонними библиотеками - не вносить же изменения в "чужой" и обновляемый код?
Добрый день.
Если я правильно понял и мы можем дополнять реализацию FObject: TSomeType1, то есть еще вариант через посылку сообщения TObject.Dispatch. Тогда можно будет оформить шаблонный класс TX и слать сообщение с заранее известным кодом. А в объектах TSomeType1 принимать его и выполнять нужные действия.
Плюсы:
1. Слабая зависимости от TSomeType без интерфейсов и строгих знаний о всех доступных методах класса.
2. Можно использовать шаблоны.
Минус:
1. Требуется создать отдельный метод для приема сообщения и делать вызов DoSomeMethod.
2. При росте количества методов требуется или выделять отдельные типы сообщений или использовать шаблоны проектирования типа команд, стратегий и прочих...
В данной постановке задачи возможно имеет смысл использовать директиву компилятора
$typeinfo ($M) и реализовать вызов требуемого метода средствами rtti
Ярослав
Простите, но либо я Вас не так понял, либо Ваше замечание не по теме. Обратите внимание, что в данном примере создаётся два объекта: один Obj1: TSomeType1, а другой ObjA: T1; второй объект управляет первым.
И, наверное, тут RTTI будет более уместным, чем Dispatch, который предназначен для обработки сообщений.
Анонимный
Спасибо, про RTTI я не стал писать сознательно, но раз Вы вспомнили - добавил.
Ещё добавил в заметку заметку пару хинтов, написал про плюсы/минусы.
Всем
Хочу акцентировать внимание: надо понимать, что экземляры T1 могут создаваться не для всех экземпляров TSomeType1. Т.е. "наращивать" функционал можно для "избранных" объектов (а не для всех).
Лично мне такая техника не нравится. Практика показывает, что если есть возможность, то лучше вводить промежуточный абстрактный класс, либо интерфейсы.
Но в заметке приведён реальный пример, где это работает и это используется в наших проектах. (И это совместимо со старыми версиями Delphi.)
А ещё отдельное спасибо Александру Люлину. За то, что постами в своём блоге напомнил про эту технику.
//насчет более медленного решения с rtti замечено верно, однако его использование imho в таких задачах является предпочтительней.
program RTTICallMethodByName;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
System.Rtti;
type
{$M+}
TSomeType1 = class(TObject) public procedure DoSomething; end;
TSomeType2 = class(TObject) public procedure DoSomething; end;
{$M-}
procedure TSomeType2.DoSomething; begin Writeln('TSomeType2'); end;
procedure TSomeType1.DoSomething; begin Writeln('TSomeType1'); end;
//вызов метода по имени
procedure _RTTICallMethodByName(aObj: TObject; aMethodName: string);
var
_RttiCtx: TRttiContext;
_riType: TRttiType;
_riMethod: TRttiMethod;
begin
_RttiCtx:= TRttiContext.Create;
_riType:= _RttiCtx.GetType(aObj.ClassType);
_riMethod:= _riType.GetMethod(aMethodName);
if not assigned(_riMethod) then raise Exception.Create(format('not faund method "%s"',[aMethodName]));
_riMethod.Invoke(aObj,[]);
end;
var
_Obj1: TSomeType1; _Obj2: TSomeType2;
begin
try
try
_Obj1:= TSomeType1.Create;
_Obj2:= TSomeType2.Create;
_RTTICallMethodByName(_obj1,'DoSomething');
_RTTICallMethodByName(_obj2,'DoSomething');
_RTTICallMethodByName(_obj2,'DoSomething_test');
finally
_Obj1.Free;
_Obj2.Free;
end;
Readln;
except
on E: Exception do begin Writeln(E.ClassName, ': ', E.Message); readln; end;
end;
end.
type
TSomeType1 = class(TComponent)
public
procedure DoSomething;
end;
TSomeType2 = class(TComponent)
public
procedure DoSomething;
end;
IDoSomething = interface
procedure DoSomething;
end;
TISomeType2 = class(TSomeType2, IDoSomething);
TISomeType1 = class(TSomeType1, IDoSomething);
Одного только вы не поняли... Это НЕ Duck-Typing... Это ПРИМЕШИВАНИЕ функциональности...
Все мысли правильние, каждая применяется тогда, когда это нужно.
RTTI нельзя использовать в дженериках т.к. Method.Invoke(T,[]) - скажет Вам о недопустимости приведения типов. А использовать один класс который будет дёргать методы других классов не всегда вписывается в архитектуру
Отправить комментарий