Как связать «параллельную» иерархию классов?
-
20-08-2019 - |
Вопрос
У меня есть небольшая иерархия классов, где каждый класс соответствует определенному потомку TComponent (скажем, базовый класс TDefaultFrobber с потомками TActionFrobber и TMenuItemFrobber, соответствующими TComponent, TCustomAction и TMenuItem соответственно).Теперь мне нужна фабричная (?) функция примерно такая:
function CreateFrobber(AComponent: TComponent): IFrobber;
begin
if AComponent is TCustomAction then
Result := TActionFrobber.Create(TCustomAction(AComponent))
else if AComponent is TMenuItem then
Result := TMenuItemFrobber.Create(TMenuItem(AComponent))
else
Result := TDefaultFrobber.Create(AComponent);
end;
Могу ли я как-то реорганизовать это, чтобы использовать виртуальные функции или что-то подобное вместо каскада if-else или RTTI?
Редактировать: Мое решение на данный момент:
unit Frobbers;
interface
uses
Classes;
type
IComponentFrobber = interface
end;
TComponentFrobberClass = class of TComponentFrobber;
TComponentFrobber = class(TInterfacedObject, IComponentFrobber)
strict private
FComponent: TComponent;
protected
constructor Create(AComponent: TComponent);
property Component: TComponent read FComponent;
public
class function FindFrobberClass(AComponentClass: TComponentClass): TComponentFrobberClass; overload; static;
class function FindFrobberClass(AComponent: TComponent): TComponentFrobberClass; overload; static;
class procedure RegisterFrobber(AComponentClass: TComponentClass; AFrobberClass: TComponentFrobberClass); static;
end;
implementation
uses
ActnList,
Menus;
type
TComponentFrobberRegistryItem = record
ComponentClass: TComponentClass;
FrobberClass: TComponentFrobberClass;
end;
var
FComponentFrobberRegistry: array of TComponentFrobberRegistryItem;
class function TComponentFrobber.FindFrobberClass(AComponentClass: TComponentClass): TComponentFrobberClass;
var
i: Integer;
begin
// Search backwards, so that more specialized frobbers are found first:
for i := High(FComponentFrobberRegistry) downto Low(FComponentFrobberRegistry) do
if FComponentFrobberRegistry[i].ComponentClass = AComponentClass then
begin
Result := FComponentFrobberRegistry[i].FrobberClass;
Exit;
end;
Result := nil;
end;
constructor TComponentFrobber.Create(AComponent: TComponent);
begin
inherited Create;
FComponent := AComponent;
end;
class function TComponentFrobber.FindFrobberClass(AComponent: TComponent): TComponentFrobberClass;
var
i: Integer;
begin
// Search backwards, so that more specialized frobbers are found first:
for i := High(FComponentFrobberRegistry) downto Low(FComponentFrobberRegistry) do
if AComponent is FComponentFrobberRegistry[i].ComponentClass then
begin
Result := FComponentFrobberRegistry[i].FrobberClass;
Exit;
end;
Result := nil;
end;
class procedure TComponentFrobber.RegisterFrobber(AComponentClass: TComponentClass;
AFrobberClass: TComponentFrobberClass);
var
i: Integer;
begin
Assert(FindFrobberClass(AComponentClass) = nil, 'Duplicate Frobber class');
i := Length(FComponentFrobberRegistry);
SetLength(FComponentFrobberRegistry, Succ(i));
FComponentFrobberRegistry[i].ComponentClass := AComponentClass;
FComponentFrobberRegistry[i].FrobberClass := AFrobberClass;
end;
function CreateComponentFrobber(AComponent: TComponent): IComponentFrobber;
var
FrobberClass: TComponentFrobberClass;
begin
FrobberClass := TComponentFrobber.FindFrobberClass(AComponent);
Assert(FrobberClass <> nil);
Result := FrobberClass.Create(AComponent);
end;
type
TActionFrobber = class(TComponentFrobber);
TMenuItemFrobber = class(TComponentFrobber);
initialization
TComponentFrobber.RegisterFrobber(TCustomAction, TActionFrobber);
TComponentFrobber.RegisterFrobber(TMenuItem, TMenuItemFrobber);
end.
Спасибо Cesar, Gamecat и mghie.
Решение
2 предложения:Создайте массив пар классов, затем вы сможете получить индекс и использовать пару конструктора класса,
var
ArrayItem: array[0..1] of TComponentClass = (TActionFrobber, TMenuItemFrobber);
ArrayOwner: array[0..1] of TComponentClass = (TCustomAction, TMenuItem);
function CreateFrobber(AComponent: TComponentClass): IFrobber;
var
Index: Integer;
begin
Result:= nil;
for I := Low(ArrayOwner) to High(ArrayOwner) do
if AComponent is ArrayOwner[I] then
begin
Result:= ArrayItem[I].Create(AComponent);
Break;
end;
if Result = nil then
Result:= TDefaultFrobber.Create(AComponent);
end;
или используйте соглашения RTTI + ClassName, например:
function CreateFrobber(AComponent: TComponentClass): IFrobber;
const
FrobberClassSuffix = 'Frobber';
var
LClass: TComponentClass;
LComponent: TComponent;
begin
LClass:= Classes.FindClass(AComponent.ClassName + FrobberClassSuffix);
if LClass <> nil then
LComponent:= LClass.Create(AComponent)
else
LComponent:= TDefaultFrobber.Create(AComponent);
if not Supports(LComponent, IFrobber, Result) then
Result:= nil;
end;
Другие советы
Если вы создаете класс с виртуальным конструктором и создаете тип класса для этого класса.Вы можете создать список поиска на основе имени класса компонента.
Пример:
type
TFrobber = class
public
constructor Create; virtual;
class function CreateFrobber(const AComponent: TComponent): TFrobber;
end;
TFrobberClass = class of TFrobber;
type
TFrobberRec = record
ClassName: ShortString;
ClassType: TFrobberClass;
end;
const
cFrobberCount = 3;
cFrobberList : array[1..cFrobberCount] of TFrobberRec = (
(ClassName : 'TAction'; ClassType: TActionFrobber),
(ClassName : 'TButton'; ClassType: TButtonFrobber),
(ClassName : 'TMenuItem'; ClassType: TMenuItemFrobber)
);
class function TFrobber.CreateFrobber(const AComponent: TComponent): TFrobber;
var
i : Integer;
begin
Result := nil;
for i := 1 to cFrobberCount do begin
if AComponent.ClassName = cFrobberList[i].ClassName then begin
Result := cFrobberList[i].ClassType.Create();
Exit;
end;
end;
end;
Можно, конечно, работать и с динамическим списком (словарем), но тогда придется как-то прописывать каждую комбинацию.
Обновлять
Прокомментировать высказывания мгие.
Вы совершенно правы.Но тут не обойтись без действительно безобразных приемов.Прямо сейчас вам нужно использовать разделы инициализации/финализации модуля, чтобы повторно зарегистрировать класс.Но было бы здорово добавить в класс метод инициализации/финализации.Их необходимо вызывать вместе с инициализацией (и финализацией) модуля.Так:
class
TFrobber = class
private
initialization Init; // Called at program start just after unit initialization
finalization Exit; // called at program end just before unit finalization.
end;
Я хотел бы добавить несколько комментариев к вашему текущему решению, ответив здесь, поскольку это невозможно сделать в разделе комментариев:
type
IComponentFrobber = interface
end;
TComponentFrobberClass = class of TComponentFrobber;
TComponentFrobber = class(TInterfacedObject, IComponentFrobber)
strict private
FComponent: TComponent;
protected
constructor Create(AComponent: TComponent);
property Component: TComponent read FComponent;
public
class function FindFrobberClass(AComponentClass: TComponentClass):
TComponentFrobberClass; overload; static;
class function FindFrobberClass(AComponent: TComponent):
TComponentFrobberClass; overload; static;
class procedure RegisterFrobber(AComponentClass: TComponentClass;
AFrobberClass: TComponentFrobberClass); static;
end;
Нет особого смысла использовать TInterfacedObject для базового класса, поскольку вам всегда будет нужен объект, а не интерфейс, который он реализует — как еще вы могли бы найти свой конкретный класс Frobber?Я бы разделил его на TComponentFrobber, происходящий от TInterfacedObject, и класс TComponentRegistry (наследний от TObject), который имеет методы класса.Тогда вы, конечно, можете сделать класс реестра более универсальным, он не будет привязан к TComponentFrobber и его можно будет использовать повторно.
Редактировать: Я использовал аналогичные реестры классов, например, при загрузке файлов:загрузите идентификатор следующего объекта (например, строку, целое число или GUID), затем получите правильный класс для создания экземпляра из реестра, затем создайте и загрузите объект.
type
TComponentFrobberRegistryItem = record
ComponentClass: TComponentClass;
FrobberClass: TComponentFrobberClass;
end;
var
FComponentFrobberRegistry: array of TComponentFrobberRegistryItem;
Это нормально, если вы никогда не будете добавлять или удалять классы в/из реестра, но обычно я бы использовал не массив, а список записей реестра.
class function TComponentFrobber.FindFrobberClass(AComponentClass: TComponentClass):
TComponentFrobberClass;
var
i: Integer;
begin
// Search backwards, so that more specialized frobbers are found first:
for i := High(FComponentFrobberRegistry) downto Low(FComponentFrobberRegistry) do
if FComponentFrobberRegistry[i].ComponentClass = AComponentClass then
begin
Result := FComponentFrobberRegistry[i].FrobberClass;
Exit;
end;
Result := nil;
end;
Поиск в массиве назад не поможет найти наиболее специализированный фроббер, если вы не добавите их в правильном порядке (наименее специализированный первым).Почему бы вам не проверить равенство ClassType?Существует также ClassParent для перемещения по иерархии классов, если вам также нужно протестировать базовые классы.