Это хорошая идея использовать разделы инициализации для регистрации модуля?
Я ищу хорошее решение для децентрализованной регистрации модуля.
Я не хочу, чтобы один модуль использовал все модульные модули проекта, но я бы хотел, чтобы модульные модули регистрировались сами.
Единственное решение, о котором я могу думать, - это полагаться на initialization
Delphi единиц.
Я написал тестовый проект:
Модуль 2
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);
Раздел 4
TForm4 = class(TForm)
implementation
uses
Unit2;
initialization
TForm2.Modules.Add('Form4', TForm4);
Это имеет один недостаток, хотя. Гарантируется ли, что мои регистрационные единицы (в данном случае Unit2
s) initialization
раздел всегда запускается первым?
Я часто читал предупреждения о initialization
разделы, я знаю, что я должен избегать повышения исключений в них.
4 ответа
Это хорошая идея использовать разделы инициализации для регистрации модуля?
Да. Его использует и собственная структура Delphi, например, регистрация TGraphic
-descendents.
Гарантируется ли, что мой раздел инициализации единиц регистрации (в данном случае Unit2s) всегда запускается первым?
Да, согласно документам:
Для блоков в списке использований интерфейса разделы инициализации блоков, используемых клиентом, выполняются в том порядке, в котором эти блоки отображаются в разделе использования клиента.
Но остерегайтесь ситуации, когда вы работаете с пакетами времени выполнения.
Я бы использовал следующий "шаблон":
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;
Модуль 2
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.
Раздел 4
TForm4 = class(TForm)
implementation
uses
ModuleService;
initialization
ModuleManager.RegisterModule('Form4', TForm4);
finalization
ModuleManager.UnregisterModule('Form4');
end.
Мой ответ резко контрастирует с ответом NGLN. Тем не менее, я предлагаю вам серьезно рассмотреть мои рассуждения. Тогда, даже если вы все еще хотите использовать initialization
и, по крайней мере, ваши глаза будут открыты для потенциальных ловушек и предлагаемых мер предосторожности.
Это хорошая идея использовать разделы инициализации для регистрации модуля?
К сожалению, аргумент в пользу NGLN немного похож на спор о том, следует ли вам принимать наркотики на основании того, поступил ли ваш любимый рок-звезда.
Аргумент должен скорее основываться на том, как использование этой функции влияет на удобство сопровождения кода.
- С положительной стороны вы добавляете функциональность в ваше приложение, просто добавляя модуль. (Хорошие примеры - обработчики исключений, каркасы журналов.)
- С другой стороны, вы добавляете функциональность в ваше приложение, просто добавляя модуль. (Независимо от того, намеревались или нет.)
Пара реальных примеров того, почему точку "плюс" также можно считать точкой "минус":
У нас был модуль, который был включен в некоторые проекты через путь поиска. Это устройство выполняло самостоятельную регистрацию в
initialization
раздел. Небольшой рефакторинг был сделан, реорганизовав некоторые юнит-зависимости. Следующее, что устройство больше не было включено в одно из наших приложений, нарушая одну из его функций.Мы хотели изменить наш сторонний обработчик исключений. Звучит достаточно просто: вынуть единицы старого обработчика из файла проекта и добавить модули нового обработчика. Проблема заключалась в том, что у нас было несколько модулей, которые имели свою прямую ссылку на некоторые из модулей старого обработчика.
Какой обработчик исключений, как вы думаете, зарегистрировал его в первую очередь? Который зарегистрирован правильно?
Однако существует гораздо более серьезная проблема с ремонтопригодностью. И это предсказуемость порядка, в котором инициализируются единицы. Несмотря на то, что существуют правила, которые будут строго определять последовательность инициализации (и завершения) модулей, вам, как программисту, очень сложно точно предсказать это за пределами первых нескольких модулей.
Это, очевидно, имеет серьезные последствия для любого initialization
разделы, которые зависят от инициализации других модулей. Рассмотрим, например, что произойдет, если у вас есть ошибка в одном из ваших initialization
разделы, но он вызывается до того, как ваш обработчик / регистратор исключений инициализируется... Ваше приложение не запустится, и вы не сможете понять, почему.
Гарантируется ли, что мой раздел инициализации единиц регистрации (в данном случае 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.
- Убедитесь, что для тестируемости вы можете "выключить" или, что еще лучше, полностью отключить инициализацию.
Ты можешь использовать 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
Метод класса на что-то очень похож, как в ответе @SpeedFreak.