После этого добавьте интерфейс в класс
Можно ли добавить и реализовать интерфейс к уже существующему классу (который является потомком TInterfaced
или TInterfacedPersistent
), чтобы выполнить разделение модели и представления на 2 единицы?
Небольшое объяснение, почему мне нужно что-то вроде этого:
Я разрабатываю древовидную структуру открытого типа, которая имеет следующую структуру (ОЧЕНЬ упрощенная и неполная, просто для иллюстрации контура проблемы):
Database_Kernel.pas
TVMDNode = class(TInterfacedPersistent);
public
class function ClassGUID: TGUID; virtual; abstract; // constant. used for RTTI
property RawData: TBytes {...};
constructor Create(ARawData: TBytes);
function GetParent: TVMDNode;
function GetChildNodes: TList<TVMDNode>;
end;
Vendor_Specific_Stuff.pas
TImageNode = class(TVMDNode)
public
class function ClassGUID: TGUID; override; // constant. used for RTTI
// Will be interpreted out of the raw binary data of the inherited class
property Image: TImage {...};
end;
TUTF8Node = class(TVMDNode)
public
class function ClassGUID: TGUID; override; // constant. used for RTTI
// Will be interpreted out of the raw binary data of the inherited class
property StringContent: WideString {...};
end;
TContactNode = class(TVMDNode)
public
class function ClassGUID: TGUID; override; // constant. used for RTTI
// Will be interpreted out of the raw binary data of the inherited class
property PreName: WideString {...};
property FamilyName: WideString {...};
property Address: WideString {...};
property Birthday: TDate {...};
end;
Используя RTTI с использованием GUID (который использует ClassGUID
), функция GetChildNodes
может найти соответствующий класс и инициализировать его необработанными данными. (Каждый набор данных содержит ClassGUID
и RawData
рядом с другими данными, такими как созданные/обновленные временные метки)
Важно заметить, что мой API (Database_Kernel.pas
) строго отделен от классов node поставщика (Vendor_Specific_Stuff.pas
).
GUI программы, зависящей от поставщика, хочет визуализировать узлы, например. давая им удобное имя, значок и т.д.
Следующие идеи работают:
IGraphicNode = interface(IInterface)
function Visible: boolean;
function Icon: TIcon;
function UserFriendlyName: string;
end;
Относящиеся к поставщику потомки TVMDNode
в Vendor_Specific_Stuff.pas
будут реализовывать интерфейс IGraphicNode
.
Но поставщику также необходимо изменить Database_Kernel.pas
для реализации IGraphicNode
в базовый класс node TVMDNode
(который используется для "неизвестных" узлов, где RTTI не удалось найти соответствующий класс набора данных, поэтому, по крайней мере, двоичные исходные данные могут быть прочитаны с помощью TVMDNode.RawData
).
Поэтому он изменит мой класс следующим образом:
TVMDNode = class(TInterfacedPersistent, IGraphicNode);
public
property RawData: TBytes {...};
class function ClassGUID: TGUID; virtual; abstract; // constant. used for RTTI
constructor Create(ARawData: TBytes);
function GetParent: TVMDNode;
function GetChildNodes: TList<TVMDNode>;
// --- IGraphicNode
function Visible: boolean; virtual; // default behavior for unknown nodes: False
function Icon: TIcon; virtual; // default behavior for unknown nodes: "?" icon
function UserfriendlyName: string; virtual; // default behavior for unknown nodes: "Unknown"
end;
Проблема заключается в том, что IGraphicNode
зависит от поставщика/программы и не должен находиться в API Database_Kernel.pas
, так как GUI и Model/API должны быть строго разделены.
Мое желание состояло в том, что interace IGraphicNode
можно было бы добавить и реализовать в существующий класс TVMDNode
(который уже является потомком TInterfacedPersistent
для разрешения интерфейсов) в отдельном модуле. Насколько мне известно, Delphi не поддерживает что-то вроде этого.
Помимо того, что смешивать модель и представление в одном отдельном модуле/классе нехорошо, будет существовать следующая реальная проблема: если поставщик должен изменить мой API Database_Kernel.pas
для расширения TVMDNode
с помощью IGraphicNode
, ему необходимо повторно выполнить все его изменения, как только я выпущу новую версию своего API Database_Kernel.pas
.
Что мне делать? Я очень долго думал о возможных решениях с Delphi OOP. Обходной путь может вставлять TVMDNode в класс контейнера, который имеет вторичный RTTI, поэтому после того, как я нашел класс TVMDNode
, я мог бы искать класс TVMDNodeGUIContainer
. Но это звучит очень душно и как грязный хак.
PS: Этот API представляет собой проект OpenSource/GPL. Я стараюсь оставаться совместимым со старыми поколениями Delphi (например, 6), так как я хочу максимизировать число возможных пользователей. Однако, если решение проблемы выше возможно только с новым поколением языков Delphi, я мог бы рассмотреть возможность отмены поддержки Delphi 6 для этого API.
Ответы
Ответ 1
Вы можете сохранить возможность сохранения данных и реализовать их через наследование и по-прежнему создавать правильные экземпляры для ClassGUID, хранящихся в таблицах, если вы примените factory шаблон дизайна.
Для каждого класса node должен быть один класс factory (или просто указатель на функцию), ответственный за создание правильного класса Delphi. Заводы класса могут регистрироваться в секции инициализации устройства (один раз при запуске приложения) на объекте singleton ядра.
Ядро singleton затем сопоставило бы GUID, чтобы исправить factory, который в свою очередь вызовет правильный конструктор экземпляра класса (как показано на http://delphipatterns.blog.com/2011/03/23/abstract-factory)
Пакеты могут быть разделены на отдельные библиотеки DLL и классы, реализованные в отдельных единицах, все еще наследуемых от одного базового класса TVMNode.
Функции, которые вы используете RTTI для, могут быть легко доступны в классах потомков или в классах factory с помощью некоторых виртуальных методов.
Вы также можете использовать более простой Объекты передачи данных для сохранения/загрузки TVMNodes и, возможно, вдохнуть вдохновение в уже хорошо воспринимаемый Объект Relational Mapper или Объектно-ориентированная структура как проблема, которую вы пытаются решить, как мне кажется, именно те проблемы, с которыми они справляются (уже)
Я не знаю о хороших фреймах с открытым исходным кодом Delphi этого класса. Но с других языков вы можете посмотреть Java Hibernate, Microsoft.NET Entity Framework или минималистичный Сериализатор буферов протоколов Google
![enter image description here]()
Ответ 2
Да, это возможно.
Мы реализовали нечто подобное, чтобы получить контроль над глобальными/синглтонами для целей тестирования. Мы изменили наши синглтоны на доступность в качестве интерфейсов приложения (не TApplication
, наш собственный эквивалент). Затем мы добавили возможность динамически добавлять/удалять интерфейсы во время выполнения. Теперь наши тестовые примеры могут подключать подходящие макеты по мере необходимости.
Я опишу общий подход, надеюсь, вы сможете применить его к особенностям вашей ситуации.
- Добавить поле для хранения списка динамически добавленного интерфейса.
TInterfaceList
работает красиво.
- Добавить методы добавления/удаления динамических интерфейсов.
- Переопределить
function QueryInterface(const IID: TGUID; out Obj): HResult; virtual;
. Ваша реализация сначала проверит список интерфейсов, и если не найден, будет отложена базовая реализация.
Изменить: Пример кода
Чтобы ответить на ваш вопрос:
Я понимаю, что теперь класс может сказать другим, что он теперь поддерживает интерфейс X, поэтому интерфейс был добавлен во время выполнения. Но мне также нужно ОСУЩЕСТВИТЬ методы интерфейса извне (другое устройство). Как это делается?
При добавлении интерфейса вы добавляете экземпляр объекта, который реализует интерфейс. Это очень похоже на обычное свойство ... реализует <interface> чтобы делегировать реализацию интерфейса другому объекту. Главное различие заключается в динамическом. Таким образом, он будет иметь такие же ограничения: например. нет доступа к "хозяину", если явно не указана ссылка.
Следующий тестовый пример DUnit демонстрирует упрощенную версию метода в действии.
unit tdDynamicInterfaces;
interface
uses
SysUtils,
Classes,
TestFramework;
type
TTestDynamicInterfaces = class(TTestCase)
published
procedure TestUseDynamicInterface;
end;
type
ISayHello = interface
['{6F6DDDE3-F9A5-407E-B5A4-CDF91791A05B}']
function SayHello: string;
end;
implementation
{ ImpGlobal }
type
TDynamicInterfaces = class(TInterfacedObject, IInterface)
{ We must explicitly state that we are implementing IInterface so that
our implementation of QueryInterface is used. }
private
FDynamicInterfaces: TInterfaceList;
protected
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
public
constructor Create;
destructor Destroy; override;
procedure AddInterface(AImplementedInterface: IInterface);
end;
type
TImplementor = class (TInterfacedObject, ISayHello)
{ NOTE: This could easily have been implemented in a separate unit. }
protected
{ISayHello}
function SayHello: string;
end;
{ TDynamicInterfaces }
procedure TDynamicInterfaces.AddInterface(AImplementedInterface: IInterface);
begin
{ The simplest, but least flexible approach (see also QueryInterface).
Other options entail tagging specific GUIDs to be associated with given
implementation instance. Then it becomes feasible to check for duplicates
and also dynamically remove specific interfaces. }
FDynamicInterfaces.Add(AImplementedInterface);
end;
constructor TDynamicInterfaces.Create;
begin
inherited Create;
FDynamicInterfaces := TInterfaceList.Create;
end;
destructor TDynamicInterfaces.Destroy;
begin
FDynamicInterfaces.Free;
inherited Destroy;
end;
function TDynamicInterfaces.QueryInterface(const IID: TGUID; out Obj): HResult;
var
LIntf: IInterface;
begin
{ This implementation basically means the first implementor added will be
returned in cases where multiple implementors support the same interface. }
for LIntf in FDynamicInterfaces do
begin
if Supports(LIntf, IID, Obj) then
begin
Result := S_OK;
Exit;
end;
end;
Result := inherited QueryInterface(IID, Obj);
end;
{ TImplementor }
function TImplementor.SayHello: string;
begin
Result := 'Hello. My name is, ' + ClassName;
end;
{ TTestDynamicInterfaces }
procedure TTestDynamicInterfaces.TestUseDynamicInterface;
var
LDynamicInterfaceObject: TDynamicInterfaces;
LInterfaceRef: IUnknown;
LFriend: ISayHello;
LActualResult: string;
begin
LActualResult := '';
{ Use ObjRef for convenience to not declare interface with "AddInterface" }
LDynamicInterfaceObject := TDynamicInterfaces.Create;
{ But lifetime is still managed by the InterfaceRef. }
LInterfaceRef := LDynamicInterfaceObject;
{ Comment out the next line to see what happens when support for
interface is not dynamically added. }
LDynamicInterfaceObject.AddInterface(TImplementor.Create);
if Supports(LInterfaceRef, ISayHello, LFriend) then
begin
LFriend := LInterfaceRef as ISayHello;
LActualResult := LFriend.SayHello;
end;
CheckEqualsString('Hello. My name is, TImplementor', LActualResult);
end;
end.