Полезно ли использовать разделы инициализации для регистрации модуля?

Я ищу хорошее решение для регистрации децентрализованного модуля.

Я не хочу, чтобы один модуль использовал все модульные модули проекта, но я бы хотел, чтобы модули модуля регистрировались.

Единственное решение, о котором я могу думать, опирается на 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.