使用初始化部分进行模块注册是个好主意吗?

Jen*_*off 10 delphi module delphi-units initialization-order delphi-xe3

我正在为分散式模块注册寻找一个好的解决方案.

我不想要一个使用项目所有模块单元的单元,但我宁愿让模块单元自己注册.

我能想到的唯一解决方案就是依赖initializationDelphi单元.

我写了一个测试项目:

单元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;
Run Code Online (Sandbox Code Playgroud)

UNIT3

TForm3 = class(TForm)

implementation

uses
  Unit2;

initialization   
  TForm2.Modules.Add('Form3', TForm3);
Run Code Online (Sandbox Code Playgroud)

UNIT4

TForm4 = class(TForm)

implementation

uses
  Unit2;

initialization   
  TForm2.Modules.Add('Form4', TForm4);
Run Code Online (Sandbox Code Playgroud)

这有一个缺点.是否保证我的注册单元(在本例中为Unit2s)initialization部分始终先运行?

我经常阅读关于initialization部分的警告,我知道我必须避免在其中引发异常.

NGL*_*GLN 7

使用初始化部分进行模块注册是个好主意吗?

是.德尔福自己的框架也使用它,例如TGraphic-descendents 的注册.

是否保证我的注册单元(在本例中为Unit2s)初始化部分始终先运行?

是的,根据文件:

对于接口使用列表中的单元,客户端使用的单元的初始化部分按照单元出现在客户端的uses子句中的顺序执行.

但要注意使用运行时包的情况.


Spe*_*eak 7

我会使用以下"模式":

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;
Run Code Online (Sandbox Code Playgroud)

单元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;
Run Code Online (Sandbox Code Playgroud)

UNIT3

TForm3 = class(TForm)

implementation

uses
  ModuleService;

initialization
  ModuleManager.RegisterModule('Form3', TForm3);
finalization
  ModuleManager.UnregisterModuleClass(TForm3);
end.
Run Code Online (Sandbox Code Playgroud)

UNIT4

TForm4 = class(TForm)

implementation

uses
  ModuleService;

initialization   
  ModuleManager.RegisterModule('Form4', TForm4);
finalization
  ModuleManager.UnregisterModule('Form4');
end.
Run Code Online (Sandbox Code Playgroud)

  • @David,这是分散模块注册_的一个很好的解决方案,这是OP所要求的.除此之外,我不觉得有解释它的冲动.抱歉. (6认同)
  • 如果你使用散文来解释它们,答案会更有用 (2认同)

Dis*_*ned 5

我的回答与NGLN的答案形成鲜明对比.但是,我建议你认真考虑我的推理.然后,即使你仍然希望使用initialization,至少你的眼睛会对潜在的陷阱和建议的预防措施开放.


使用初始化部分进行模块注册是个好主意吗?

不幸的是,NGL​​N赞成的论点有点像是在争论你是否应该根据你最喜欢的摇滚明星是否这样做而吸毒.

争论应该基于该功能的使用如何影响代码可维护性.

  • 好的方面来说,只需包含一个单元即可为应用程序添加功能.(很好的例子是异常处理程序,日志框架.)
  • 端,您只需通过包括单元功能添加到您的应用程序.(无论你是否有意.)

为什么"加"点也可以被认为是"减"点的几个现实世界的例子:

  1. 我们有一个单元通过搜索路径包含在一些项目中.该部门在该initialization部分进行了自我登记.进行了一些重构,重新排列了一些单元依赖关系.接下来,该单元不再被包含在我们的一个应用程序中,打破了它的一个功能.

  2. 我们想要更改我们的第三方异常处理程序.听起来很容易:将旧处理程序的单元从项目文件中取出,并添加新处理程序的单位.问题是我们有一些单元有自己的直接引用一些旧处理程序的单元.
    您认为哪个异常处理程序首先注册了它的异常挂钩?哪个注册正确?

但是,存在更严重的可维护性问题.这就是单位初始化顺序的可预测性.尽管有一些规则可以严格确定单元初始化(并最终确定)的顺序,但作为程序员,您很难准确地预测出前几个单元之外的情况.

对于initialization依赖于其他单位初始化的任何部分,这显然会产生严重后果.例如,考虑如果你的某个initialization部分中有错误会发生什么,但是在你的异常处理程序/ logger初始化之前碰巧会调用它...你的应用程序将无法启动,你将陷入困境搞清楚为什么.


是否保证我的注册单元(在本例中为Unit2s)初始化部分始终先运行?

这是Delphi的文档完全错误的许多案例之一.

对于接口使用列表中的单元,客户端使用的单元的初始化部分按照单元出现在客户端的uses子句中的顺序执行.

考虑以下两个单元:

unit UnitY;

interface

uses UnitA, UnitB;
...

unit UnitX;

interface

uses UnitB, UnitA;
... 
Run Code Online (Sandbox Code Playgroud)

因此,如果两个单元都在同一个项目中,那么(根据文档):UnitAUnitB AND UnitB之前初始化之前的初始化UnitA.这显然是不可能的.因此,实际的初始化顺序也可能取决于其他因素:使用A或B的其他单位.X和Y初始化的顺序.

因此,支持文档的最佳案例论点是:为了使解释简单,省略了一些基本细节.然而,效果是,在现实世界的情况下,这是完全错误的.

是的,您" 可以 "理论上微调您的uses子句以保证特定的初始化序列.然而,现实情况是,在一个拥有数千个单位的大型项目中,这样做是非常不切实际的,而且太容易破解.


对于initialization部分还有其他参数:

  • 通常,初始化的需要仅仅是因为您拥有全局共享实体.有很多材料解释了为什么全球数据是一个坏主意.
  • 初始化错误可能很难调试.在应用程序无法启动的客户端计算机上更是如此.当您明确控制初始化时,您至少可以首先确保您的应用程序处于一种状态,在该状态下,如果出现故障,您将能够告诉用户出了什么问题.
  • 初始化部分妨碍了可测试性,因为在测试项目中简单地包含一个单元现在包括副作用.如果你有针对这个单元的测试用例,它们可能会紧密耦合,因为每个测试几乎肯定会将全局变化"泄漏"到其他测试中.

结论

我理解你希望避免引入所有依赖关系的"神单位".但是,应用程序本身不是定义所有依赖项的东西,将它们组合在一起并使它们根据需求进行协作吗?我认为将特定单位专门用于此目的并不会造成任何伤害.作为一个额外的好处,如果从单个入口点完成所有操作,则调试启动序列要容易得多.

但是,如果您仍想使用initialization,我建议您遵循以下准则:

  • 确保这些单位明确包含在您的项目中.由于单元依赖性的更改,您不希望意外中断功能.
  • 您的initialization部分必须绝对没有订单依赖.(不幸的是,你的问题意味着此时失败了.)
  • 您的finalization部分中也必须没有订单依赖项.(Delphi本身在这方面存在一些问题.一个例子是ComObj.如果它太快完成,它可能会使COM支持无法初始化并导致应用程序在关闭期间失败.)
  • 确定您认为对应用程序的运行和调试至关重要的事情,并从DPR文件的顶部验证其初始化顺序.
  • 确保为了可测试性,您可以"关闭"或更好地完全禁用初始化.

  • @DavidHeffernan我列出了使用初始化部分的5个明显的缺点.我的答案的一部分直接回应OP的具体问题.究竟你究竟对"OP问这个问题!"的理解是什么?或者你只是成为你通常的议论自我? (3认同)
  • @DavidHeffernan严肃的老兄,你到底怎么了?我一次又一次地看到你在这个论坛上与其他贡献者进行毫无意义的循环论证.好吧,我不是在咬 - 我在这里完成了! (3认同)