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

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

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

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

Это имеет один недостаток, хотя. Гарантируется ли, что мои регистрационные единицы (в данном случае Unit2s) 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 немного похож на спор о том, следует ли вам принимать наркотики на основании того, поступил ли ваш любимый рок-звезда.

Аргумент должен скорее основываться на том, как использование этой функции влияет на удобство сопровождения кода.

  • С положительной стороны вы добавляете функциональность в ваше приложение, просто добавляя модуль. (Хорошие примеры - обработчики исключений, каркасы журналов.)
  • С другой стороны, вы добавляете функциональность в ваше приложение, просто добавляя модуль. (Независимо от того, намеревались или нет.)

Пара реальных примеров того, почему точку "плюс" также можно считать точкой "минус":

  1. У нас был модуль, который был включен в некоторые проекты через путь поиска. Это устройство выполняло самостоятельную регистрацию в initialization раздел. Небольшой рефакторинг был сделан, реорганизовав некоторые юнит-зависимости. Следующее, что устройство больше не было включено в одно из наших приложений, нарушая одну из его функций.

  2. Мы хотели изменить наш сторонний обработчик исключений. Звучит достаточно просто: вынуть единицы старого обработчика из файла проекта и добавить модули нового обработчика. Проблема заключалась в том, что у нас было несколько модулей, которые имели свою прямую ссылку на некоторые из модулей старого обработчика.
    Какой обработчик исключений, как вы думаете, зарегистрировал его в первую очередь? Который зарегистрирован правильно?

Однако существует гораздо более серьезная проблема с ремонтопригодностью. И это предсказуемость порядка, в котором инициализируются единицы. Несмотря на то, что существуют правила, которые будут строго определять последовательность инициализации (и завершения) модулей, вам, как программисту, очень сложно точно предсказать это за пределами первых нескольких модулей.

Это, очевидно, имеет серьезные последствия для любого 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.

Другие вопросы по тегам