Полезно ли использовать разделы инициализации для регистрации модуля?
Я ищу хорошее решение для регистрации децентрализованного модуля.
Я не хочу, чтобы один модуль использовал все модульные модули проекта, но я бы хотел, чтобы модули модуля регистрировались.
Единственное решение, о котором я могу думать, опирается на initialization
единиц Delphi.
Я написал тестовый проект:
Unit2
TForm2 = class(TForm)
private
class var FModules: TDictionary<string, TFormClass>;
public
class property Modules: TDictionary<string, TFormClass> read FModules;
procedure Run(const AName: string);
end;
procedure TForm2.Run(const AName: string);
begin
FModules[AName].Create(Self).ShowModal;
end;
initialization
TForm2.FModules := TDictionary<string, TFormClass>.Create;
finalization
TForm2.FModules.Free;
Unit3
TForm3 = class(TForm)
implementation
uses
Unit2;
initialization
TForm2.Modules.Add('Form3', TForm3);
Unit4
TForm4 = class(TForm)
implementation
uses
Unit2;
initialization
TForm2.Modules.Add('Form4', TForm4);
У этого есть один недостаток. Гарантировано ли, что мои регистры регистрации (в данном случае Unit2
s) initialization
всегда запускаются первыми?
Я часто читал предупреждения о разделах initialization
, я знаю, что мне нужно избегать привлечения исключений в них.
Ответы
Ответ 1
Полезно ли использовать разделы инициализации для регистрации модуля?
Да. Также используется его собственная структура Delphi, например. регистрация TGraphic
-descendents.
Гарантируется ли первая секция инициализации регистрационных единиц (в данном случае Unit2s)?
Да, согласно docs:
Для юнитов в интерфейсе используется список секций инициализации единиц, используемых клиентом, выполняется в порядке, в котором единицы отображаются на клиенте использует.
Но будьте осторожны с ситуацией, когда вы работаете с пакетами времени выполнения.
Ответ 2
Я бы использовал следующий "шаблон":
unit ModuleService;
interface
type
TModuleDictionary = class(TDictionary<string, TFormClass>);
IModuleManager = interface
procedure RegisterModule(const ModuleName: string; ModuleClass: TFormClass);
procedure UnregisterModule(const ModuleName: string);
procedure UnregisterModuleClass(ModuleClass: TFormClass);
function FindModule(const ModuleName: string): TFormClass;
function GetEnumerator: TModuleDictionary.TPairEnumerator;
end;
function ModuleManager: IModuleManager;
implementation
type
TModuleManager = class(TInterfacedObject, IModuleManager)
private
FModules: TModuleDictionary;
public
constructor Create;
destructor Destroy; override;
// IModuleManager
procedure RegisterModule(const ModuleName: string; ModuleClass: TFormClass);
procedure UnregisterModule(const ModuleName: string);
procedure UnregisterModuleClass(ModuleClass: TFormClass);
function FindModule(const ModuleName: string): TFormClass;
function GetEnumerator: TModuleDictionary.TPairEnumerator;
end;
procedure TModuleManager.RegisterModule(const ModuleName: string; ModuleClass: TFormClass);
begin
FModules.AddOrSetValue(ModuleName, ModuleClass);
end;
procedure TModuleManager.UnregisterModule(const ModuleName: string);
begin
FModules.Remove(ModuleName);
end;
procedure TModuleManager.UnregisterModuleClass(ModuleClass: TFormClass);
var
Pair: TPair<string, TFormClass>;
begin
while (FModules.ContainsValue(ModuleClass)) do
begin
for Pair in FModules do
if (ModuleClass = Pair.Value) then
begin
FModules.Remove(Pair.Key);
break;
end;
end;
end;
function TModuleManager.FindModule(const ModuleName: string): TFormClass;
begin
if (not FModules.TryGetValue(ModuleName, Result)) then
Result := nil;
end;
function TModuleManager.GetEnumerator: TModuleDictionary.TPairEnumerator;
begin
Result := FModules.GetEnumerator;
end;
var
FModuleManager: IModuleManager = nil;
function ModuleManager: IModuleManager;
begin
// Create the object on demand
if (FModuleManager = nil) then
FModuleManager := TModuleManager.Create;
Result := FModuleManager;
end;
initialization
finalization
FModuleManager := nil;
end;
Unit2
TForm2 = class(TForm)
public
procedure Run(const AName: string);
end;
implementation
uses
ModuleService;
procedure TForm2.Run(const AName: string);
var
ModuleClass: TFormClass;
begin
ModuleClass := ModuleManager.FindModule(AName);
ASSERT(ModuleClass <> nil);
ModuleClass.Create(Self).ShowModal;
end;
Unit3
TForm3 = class(TForm)
implementation
uses
ModuleService;
initialization
ModuleManager.RegisterModule('Form3', TForm3);
finalization
ModuleManager.UnregisterModuleClass(TForm3);
end.
Unit4
TForm4 = class(TForm)
implementation
uses
ModuleService;
initialization
ModuleManager.RegisterModule('Form4', TForm4);
finalization
ModuleManager.UnregisterModule('Form4');
end.
Ответ 3
Мой ответ резко контрастирует с ответом NGLN. Тем не менее, я предлагаю вам серьезно рассмотреть мои рассуждения. Затем, даже если вы все еще хотите использовать initialization
, и, как минимум, ваши глаза будут открыты для потенциальных ловушек и предлагаются меры предосторожности.
Полезно ли использовать разделы инициализации для регистрации модуля?
К сожалению, аргумент NGLN в пользу немного напоминает утверждение, следует ли вам делать наркотики на основе того, сделал ли ваш любимый рок-звезда это.
Аргумент скорее должен основываться на том, как использование функции влияет на работоспособность кода.
- На стороне плюс вы добавляете функциональность в свое приложение, просто включив устройство. (Хорошими примерами являются обработчики исключений, фреймворки протоколирования.)
- На стороне минус вы добавляете функциональность в свое приложение, просто включив устройство. (Предполагаете ли вы это или нет.)
Несколько примеров в реальном мире, почему точка "плюс" также может считаться "минусовой" точкой:
-
У нас было подразделение, которое было включено в некоторые проекты через путь поиска. Это устройство выполнило саморегистрацию в разделе initialization
. Немного рефакторинга было сделано, переупорядочивая некоторые зависимости от единицы. Следующее, что блок больше не включался в одно из наших приложений, нарушая одну из его функций.
-
Мы хотели изменить наш сторонний обработчик исключений. Звучит достаточно просто: выньте старые элементы обработчика из файла проекта и добавьте новые элементы обработчика. Проблема заключалась в том, что у нас было несколько единиц, которые имели прямую ссылку на некоторые из старых обработчиков.
Какой обработчик исключений вы считаете первым зарегистрированным в нем списком исключений? Что зарегистрировано правильно?
Тем не менее, существует гораздо более серьезная проблема ремонтопригодности. И это предсказуемость порядка, в котором единицы инициализируются. Несмотря на то, что существуют правила, которые будут строго определять последовательность, в которой единицы инициализируют (и завершают), для вас, как программиста, очень сложно точно предсказать это за пределами первых нескольких единиц.
Это, очевидно, имеет серьезные последствия для любых разделов initialization
, которые зависят от инициализации других юнитов. Рассмотрим, например, что произойдет, если у вас есть ошибка в одном из ваших разделов initialization
, но это произойдет, прежде чем ваш обработчик/регистратор ошибок инициализируется... Ваше приложение не запустится, и вы будете hamstrung, чтобы выяснить, почему.
Гарантируется ли первая секция инициализации регистрационных единиц (в данном случае Unit2s)?
Это один из многих случаев, когда Delphi документация просто неверна.
Для юнитов в интерфейсе используется список секций инициализации единиц, используемых клиентом, выполняется в порядке, в котором единицы отображаются на клиенте использует.
Рассмотрим следующие две единицы:
unit UnitY;
interface
uses UnitA, UnitB;
...
unit UnitX;
interface
uses UnitB, UnitA;
...
Итак, если оба блока находятся в одном проекте, то (согласно документации): UnitA
инициализирует до UnitB
И UnitB
инициализирует до UnitA
. Это совершенно очевидно невозможно. Таким образом, фактическая последовательность инициализации также может зависеть от других факторов: других единиц, которые используют A или B. Порядок, в котором инициализируются X и Y.
Таким образом, лучший аргумент в пользу документации состоит в том, что: в целях упрощения пояснения некоторые важные детали были опущены. Однако эффект заключается в том, что в реальной ситуации это просто неправильно.
Да, вы " можете" теоретически настроить ваши предложения uses
, чтобы гарантировать определенную последовательность инициализации. Однако реальность такова, что в большом проекте с тысячами единиц это по-человечески нецелесообразно и слишком легко сломается.
Другие аргументы против разделов initialization
:
- Как правило, необходимость инициализации заключается только в том, что у вас есть глобально разделяемая сущность. Там много материала, объясняющего, почему глобальные данные - плохая идея.
- Ошибки при инициализации могут быть сложными для отладки. Тем более на клиентской машине, где приложение может вообще не запускаться. Когда вы явно контролируете инициализацию, вы можете, по крайней мере, сначала убедиться, что ваше приложение находится в состоянии, когда вы сможете сообщить пользователю, что пошло не так, если что-то делает.
- Секции инициализации препятствуют тестированию, потому что просто включение блока в тестовый проект теперь включает побочный эффект. И если у вас есть тестовые примеры против этого устройства, они, вероятно, будут тесно связаны, потому что каждый тест почти наверняка "утешает" глобальные изменения в другие тесты.
Заключение
Я понимаю ваше желание избежать "единицы божества", которая тянет во всех зависимостях. Однако, не является ли само приложение чем-то, что определяет все зависимости, объединяет их и заставляет их взаимодействовать в соответствии с требованиями? Я не вижу никакого вреда в посвящении определенной единицы в эту цель. В качестве дополнительного бонуса гораздо проще отлаживать последовательность запуска, если все это делается из одной точки входа.
Если, однако, вы все еще хотите использовать initialization
, я предлагаю вам следовать этим рекомендациям:
- Убедитесь, что эти блоки явно включены в ваш проект. Вы не хотите случайно прерывать функции из-за изменений в зависимостях блоков.
- В ваших разделах
initialization
не должно быть никакой зависимости от заказа. (К сожалению, ваш вопрос подразумевает неудачу на этом этапе.)
- В ваших разделах
finalization
также не должно быть зависимости заказа. (У Delphi есть некоторые проблемы в этом отношении: один пример - ComObj
. Если он завершится слишком рано, он может не инициализировать поддержку COM и привести к сбою вашего приложения во время выключения.)
- Определите те вещи, которые вы считаете абсолютно необходимыми для запуска и отладки вашего приложения, и проверьте их последовательность инициализации сверху файла DPR.
- Убедитесь, что для проверки вы можете "выключить" или еще лучше отключить инициализацию.
Ответ 4
Вы также можете использовать class contructors
и class destructors
:
TModuleRegistry = class sealed
private
class var FModules: TDictionary<string, TFormClass>;
public
class property Modules: TDictionary<string, TFormClass> read FModules;
class constructor Create;
class destructor Destroy;
class procedure Run(const AName: string); static;
end;
class procedure TModuleRegistry.Run(const AName: string);
begin
// Do somthing with FModules[AName]
end;
class constructor TModuleRegistry.Create;
begin
FModules := TDictionary<string, TFormClass>.Create;
end;
class destructor TModuleRegistry.Destroy;
begin
FModules.Free;
end;
TModuleRegistry
является одноэлементным, поскольку он не имеет экземпляров экземпляра.
Компилятор будет убедиться, что class constructor
всегда вызывается первым.
Это можно объединить с методом класса Register
и Unregister
для somthing очень похожего, как в ответе @SpeedFreak.