К написанию этого поста меня подтолкнула статья на хабре про написание ORM для Delphi. Велосипед с квадратными колесами, конечно, но идея интересная. Хочу попробовать подойти к этому вопросу с другой стороны. Очень кратко.
В Delphi, начиная с версии 6 2007 (поправьте меня, если я ошибаюсь), в модуле Variants есть малоизвестный, но весьма занимательный класс TInvokeableVariantType.
Чтобы сэкономить время, я опишу совершенно абстрактный пример. Этого будет достаточно, чтобы продемонстрировать, что применить этот подход в работе над ORM или где-то еще достаточно просто. Здесь именно тот случай, когда придумать достойное применение намного сложнее, чем применить :)
В классе наследующем TInvokeableVariantType можно обрабатывать обращения к свойствам и методам динамически. То есть нет необходимости предварительно описывать интерфейс класса. В каких-то случаях это может помочь сэкономить немалое количество строк кода. Но любые плюсы тянут за собой и минусы — как минимум, IDE не будет иметь никакой информации о таком классе и подсказки в коде не будут работать.
Ниже простой класс-счетчик. Пусть он имеет поле Counter, с помощью которого можно получать или изменять значение счетчика, и два метода — процедуру Reset, которая сбрасывает счетчик в ноль, и функцию Pow2, возвращающую квадрат значения счетчика и не изменяющую сам счетчик.
TVariantCounter = class(TInvokeableVariantType) private FCounter: Integer; public procedure Clear(var V: TVarData); override; function GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean; override; function SetProperty(const V: TVarData; const Name: string; const Value: TVarData): Boolean; override; function DoFunction(var Dest: TVarData; const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; override; function DoProcedure(const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; override; end;
При взгляде на объявление класса многое становится ясно. В нем нет ни поля Counter, ни методов Reset и Pow2, но зато есть методы GetProperty, SetProperty, DoFunction и DoProcedure, в которые передаются все нужные имена и значения для динамической обработки.
Рассматриваемый пример очень прост, поэтому меньше слов — больше кода :)
function TVariantCounter.DoFunction(var Dest: TVarData; const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; begin Result := False; if SameText(Name, 'Pow2') and (Length(Arguments) = 0) then begin Variant(Dest) := FCounter * FCounter; Result := True; end; end; function TVariantCounter.DoProcedure(const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; begin Result := False; if SameText(Name, 'Reset') and (Length(Arguments) = 0) then begin FCounter := 0; Result := True; end; end; function TVariantCounter.GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean; begin if SameText(Name, 'Counter') then begin Variant(Dest) := FCounter; Result := True; end else Result := False; end; function TVariantCounter.SetProperty(const V: TVarData; const Name: string; const Value: TVarData): Boolean; begin if SameText(Name, 'Counter') then begin FCounter := Variant(Value); Result := True; end else Result := False; end;
Использовать TVariantCounter можно практически как обычный класс.
var Cntr: Variant; begin Cntr := NewVariantCounter; Cntr.Reset; Memo1.Lines.Add(Cntr.Counter); Cntr.Counter := 25; Memo1.Lines.Add(Cntr.Counter); Memo1.Lines.Add(Cntr.Pow2); Cntr.Reset; Memo1.Lines.Add(Cntr.Counter); end;
В результате в Memo появляется следующий текст:
0 25 625 0
Пока все радужно, но я ничего не сказал про функцию NewVariantCounter. В разговоре об инициализации придется упомянуть некоторые нюансы. Неприятность в том, что данный класс — синглтон. Со всеми вытекающими.
var VariantCounter: TVariantCounter; function NewVariantCounter: Variant; begin VarClear(Result); TVarData(Result).VType := VariantCounter.VarType; end; initialization VariantCounter := TVariantCounter.Create; finalization VariantCounter.Free;
То есть в этом коде счетчик для всех экземпляров общий. А в этом мало смысла.
Если подходить серьезно, то TVariantCounter не должен быть счетчиком, он должен управлять счетчиками, а значения счетчика для каждого экземпляра можно хранить в структуре TVarData (ссылка на конкретный экземпляр TVarData в свою очередь передается в каждый из методов класса).
Определим свою собственную TVarData вот так:
TCounterVarData = packed record VType: TVarType; Reserved1, Reserved2, Reserved3: Word; CounterValue: PInteger; Reserved4: LongWord; end;
PInteger вместо Integer здесь используется в связи с тем, что саму запись во время работы изменять нельзя. Поэтому пусть сам указатель остается постоянным, а изменяемую память мы выделим где-то еще.
Функцию NewVariantCounter придется изменить соответствующим образом:
function NewVariantCounter: Variant; begin VarClear(Result); TCounterVarData(Result).VType := VariantCounter.VarType; New(TCounterVarData(Result).CounterValue); TCounterVarData(Result).CounterValue^ := 0; end;
Изменения также коснутся и остального кода. Вот так:
function TVariantCounter.DoFunction(var Dest: TVarData; const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; begin Result := False; if SameText(Name, 'Pow2') and (Length(Arguments) = 0) then begin Variant(Dest) := TCounterVarData(V).CounterValue^ * TCounterVarData(V).CounterValue^; Result := True; end; end;
Остальные методы я опущу, так как здесь все в общем-то очевидно.
И вот тут становится ясно для чего классу метод Clear. Этот метод позволяет корректно финализировать структуру. В данном конкретном случае — освободить память выделенную для CounterValue.
procedure TVariantCounter.Clear(var V: TVarData); begin Dispose(TCounterVarData(V).CounterValue); inherited; end;
Пример очень простой, но сам механизм очень мощный. Если вернуться к тому с чего начался этот пост, то при разработке ORM, вместо CounterValue структура TVarData могла бы хранить ссылку на TDataSet и работать с полями этого датасета как со свойствами класса.
Полный код примера можно скачать тут.
1. Kazantsev Alexey
1 Июл 2013 10:08 пп
Поправляю, как просили :) TInvokeableVariantType, как и кастомные варианты вообще, появились в Delphi 6. И нужно добавить, что пользователи не юникод версий Delphi (младше Delphi 2009) не должны использовать локализованные имена свойств и методов т.к. TInvokeableVariantType их не поддерживает. Тем кому это все же требуется может помочь самостоятельная реализация интерфейса IDispatch.
2. Роман Янковский
1 Июл 2013 10:15 пп
Kazantsev Alexey, спасибо :)
3. Kazantsev Alexey
1 Июл 2013 11:08 пп
Пардон за неверную информацию, оказалось, что IDispatch не спасет — не умеет анси-Delphi корректно передавать имена в юникоде :( Кому интересно могут глянуть в ComObj.GetIDsOfNames и посмотреть как они получают юникод из анси.
4. Keeper
3 Июл 2013 11:32 пп
А как у всего этого подхода обстоит дело с IntelliSense?
5. Kazantsev Alexey
4 Июл 2013 2:36 дп
CodeInsight с динамикой не работает, что логично. Хуже то, что ошибки именования будут обнаружены только в рантайме.
6. Keeper
4 Июл 2013 2:03 пп
Ну, с Variant это ожидаемо, просто усточнил :) Для ORM это несомненно плохо.
7. Роман Янковский
4 Июл 2013 3:56 пп
Да, это большой минус для ORM.
Я, честно говоря, не смог придумать ситуацию, когда я бы стал нечто подобное в реальной практике использовать.
8. Александр Люлин
2 Фев 2017 11:05 пп
Вот ещё есть путь оказывается — http://programmingmindstream.blogspot.ru/2017/02/1347-tvirtualinterface.html