10

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

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

我能想到的唯一解决方案是依赖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;

单元3

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

4 回答 4

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;

单元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;

单元3

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.
于 2014-03-29T18:01:50.600 回答
7

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

是的。Delphi 自己的框架也使用它,例如TGraphic-descendents 的注册。

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

是的,根据文档

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

但请注意使用运行时包的情况。

于 2014-03-29T15:54:17.443 回答
6

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


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

不幸的是,NGL​​N 支持的论点有点像根据你最喜欢的摇滚明星是否吸毒来争论你是否应该吸毒。

争论应该基于特性的使用如何影响代码的可维护性。

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

为什么“加”点也可以被视为“减”点的几个真实示例:

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

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

但是,还有一个更严重的可维护性问题。这就是单元初始化顺序的可预测性。尽管有一些规则会严格确定单元初始化(和最终确定)的顺序,但对于程序员来说,除了最初的几个单元之外,很难准确地预测这一点。

initialization这显然会对依赖于其他单元初始化的任何部分产生严重影响。例如,考虑一下如果您的某个initialization部分有错误会发生什么,但它恰好在您的异常处理程序/记录器初始化之前被调用...您的应用程序将无法启动,并且您将束手无策找出原因。


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

这是 Delphi 的文档完全错误的众多案例之一。

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

考虑以下两个单位:

unit UnitY;

interface

uses UnitA, UnitB;
...

unit UnitX;

interface

uses UnitB, UnitA;
... 

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

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

是的,您“可以”理论上微调您的uses子句以保证特定的初始化顺序。然而,现实情况是,在一个拥有数千个单元的大型项目中,这在人力上是不切实际的,而且太容易破坏。


还有其他反对initialization部分的论点:

  • 通常需要初始化只是因为你有一个全局共享的实体。有大量材料可以解释为什么全局数据是一个坏主意。
  • 初始化错误可能很难调试。在应用程序根本无法启动的客户端机器上更是如此。当您明确控制初始化时,您至少可以首先确保您的应用程序处于这样一种状态,即如果某些事情确实失败了,您将能够告诉用户出了什么问题。
  • 初始化部分妨碍了可测试性,因为在测试项目中简单地包含一个单元现在包含一个副作用。如果你有针对这个单元的测试用例,它们可能会紧密耦合,因为几乎可以肯定每个测试都会将全局更改“泄漏”到其他测试中。

结论

我理解您希望避免引入所有依赖项的“上帝单位”。但是,应用程序本身不是定义了所有依赖项,将它们拉到一起并让它们根据需求进行协作吗?我认为将特定单位专用于该目的没有任何害处。作为一个额外的好处,如果它是从单个入口点完成的,那么调试启动序列会容易得多。

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

  • 确保这些单元明确包含在您的项目中。您不希望由于单元依赖关系的变化而意外破坏功能。
  • initialization您的部分中必须绝对没有顺序依赖性。(不幸的是,您的问题暗示此时失败。)
  • finalization您的部分中也必须没有顺序依赖性。(Delphi 本身在这方面存在一些问题。一个例子是ComObj. 如果它结束得太快,它可能会取消初始化 COM 支持并导致您的应用程序在关闭期间失败。)
  • 确定您认为对应用程序的运行和调试绝对必要的事情,并从 DPR 文件的顶部验证它们的初始化顺序。
  • 确保为了可测试性您能够“关闭”或更好地完全禁用初始化。
于 2014-03-30T17:48:11.147 回答
3

您也可以使用class contructorsclass 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始终首先调用 。

这可以与一个RegisterUnregister类方法结合使用,与@SpeedFreak 的答案非常相似。

于 2014-03-30T13:09:27.513 回答