简体   繁体   English

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

[英]Is it a good idea to use initialization sections for module registration?

I am looking for a good solution for a decentralized module registration. 我正在为分散式模块注册寻找一个好的解决方案。

I do not want a single unit that uses all module units of the project, but I would rather like to let the module units register themselves. 我不想要一个使用项目所有模块单元的单元,但我宁愿让模块单元自己注册。

The only solution I can think of is relying on initialization of Delphi units. 我能想到的唯一解决方案是依赖于Delphi单元的initialization

I have written a test project: 我写了一个测试项目:

Unit2 单元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 UNIT3

TForm3 = class(TForm)

implementation

uses
  Unit2;

initialization   
  TForm2.Modules.Add('Form3', TForm3);

Unit4 UNIT4

TForm4 = class(TForm)

implementation

uses
  Unit2;

initialization   
  TForm2.Modules.Add('Form4', TForm4);

This has one drawback though. 这有一个缺点。 Is it guaranteed that my registration units (in this case Unit2 s) initialization section is always run first? 是否保证我的注册单元(在本例中为Unit2initialization部分始终先运行?

I have often read warnings about initialization sections, I know that I have to avoid raising exceptions in them. 我经常阅读有关initialization部分的警告,我知道我必须避免在其中引发异常。

Is it a good idea to use initialization sections for module registration? 使用初始化部分进行模块注册是个好主意吗?

Yes. 是。 Delphi's own framework uses it too, eg the registration of TGraphic -descendents. Delphi自己的框架也使用它,例如TGraphic的注册。

Is it guaranteed that my registration units (in this case Unit2s) initialization section is always run first? 是否保证我的注册单元(在本例中为Unit2s)初始化部分始终先运行?

Yes, according to the docs : 是的,根据文件

For units in the interface uses list, the initialization sections of the units used by a client are executed in the order in which the units appear in the client's uses clause. 对于接口使用列表中的单元,客户端使用的单元的初始化部分按照单元出现在客户端的uses子句中的顺序执行。

But beware of the situation wherein you work with runtime packages . 但要注意使用运行时包的情况

I would use the following "pattern": 我会使用以下“模式”:

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

TForm3 = class(TForm)

implementation

uses
  ModuleService;

initialization
  ModuleManager.RegisterModule('Form3', TForm3);
finalization
  ModuleManager.UnregisterModuleClass(TForm3);
end.

Unit4 UNIT4

TForm4 = class(TForm)

implementation

uses
  ModuleService;

initialization   
  ModuleManager.RegisterModule('Form4', TForm4);
finalization
  ModuleManager.UnregisterModule('Form4');
end.

My answer is a stark contrast to NGLN's answer . 我的回答与NGLN的答案形成鲜明对比。 However, I suggest you seriously consider my reasoning. 但是,我建议你认真考虑我的推理。 Then, even if you do still wish to use initialization , and least your eyes will be open to the potential pitfalls and suggested precautions. 然后,即使你仍然希望使用initialization ,至少你的眼睛会对潜在的陷阱和建议的预防措施开放。


Is it a good idea to use initialization sections for module registration? 使用初始化部分进行模块注册是个好主意吗?

Unfortunately NGLN's argument in favour is a bit like arguing whether you should do drugs on the basis of whether your favourite rockstar did so. 不幸的是,NGL​​N赞成的论点有点像是在争论你是否应该根据你最喜欢的摇滚明星是否这样做而吸毒。

An argument should rather be based on how use of the feature affects code maintainability. 争论应该基于该功能的使用如何影响代码可维护性。

  • On the plus side you add functionality to your application simply by including a unit. 好的方面来说,只需包含一个单元即可为应用程序添加功能。 (Nice examples are exception handlers, logging frameworks.) (很好的例子是异常处理程序,日志框架。)
  • On the minus side you add functionality to your application simply by including a unit. 消极方面,你只需通过包括单元功能添加到您的应用程序。 (Whether you intended to or not.) (无论你是否有意。)

A couple of real-world examples why the "plus" point can also be considered a "minus" point: 为什么“加”点也可以被认为是“减”点的几个现实世界的例子:

  1. We had a unit that was included in some projects via search path. 我们有一个单元通过搜索路径包含在一些项目中。 This unit performed self-registration in the initialization section. 该单元在initialization部分中执行自注册。 A bit of refactoring was done, rearranging some unit dependencies. 进行了一些重构,重新排列了一些单元依赖关系。 Next thing the unit was no longer being included in one of our applications, breaking one of its features. 接下来,该单元不再被包含在我们的一个应用程序中,打破了它的一个功能。

  2. We wanted to change our third-party exception handler. 我们想要更改我们的第三方异常处理程序。 Sounds easy enough: take the old handler's units out of the project file, and add the new handler's units in. The problem was that we had a few units that had their own direct reference to some of the old handler's units. 听起来很容易:将旧处理程序的单元从项目文件中取出,并添加新处理程序的单位。问题是我们有一些单元有自己的直接引用一些旧处理程序的单元。
    Which exception handler do you think registered it's exception hooks first? 您认为哪个异常处理程序首先注册了它的异常挂钩? Which registered correctly? 哪个注册正确?

However, there is a far more serious maintainability issue. 但是,存在更严重的可维护性问题。 And that is the predictability of the order in which units are initialised. 这就是单位初始化顺序的可预测性。 Even though there are rules that will rigorously determine the sequence in which units initialise (and finalise), it is very difficult for you as a programmer to accurately predict this beyond the first few units. 尽管有一些规则可以严格确定单元初始化(并最终确定)的顺序,但作为程序员,您很难准确地预测出前几个单元之外的单元。

This obviously has grave ramifications for any initialization sections that are dependent on other units' initialisation. 对于任何依赖于其他单元初始化的initialization部分,这显然会产生严重后果。 Consider for example what would happen if you have an error in one of your initialization sections, but it happens to be called before your exception handler/logger has initialised... Your application will fail to start up, and you'll be hamstrung as to figuring out why. 例如,考虑如果你的一个initialization部分中有错误会发生什么,但它恰好在你的异常处理程序/ logger初始化之前被调用...你的应用程序将无法启动,你将陷入困境找出原因。


Is it guaranteed that my registration units (in this case Unit2s) initialization section is always run first? 是否保证我的注册单元(在本例中为Unit2s)初始化部分始终先运行?

This is one of many cases in which Delphi's documentation is simply wrong . 这是Delphi的文档完全错误的许多案例之一。

For units in the interface uses list, the initialization sections of the units used by a client are executed in the order in which the units appear in the client's uses clause. 对于接口使用列表中的单元,客户端使用的单元的初始化部分按照单元出现在客户端的uses子句中的顺序执行。

Consider the the following two units: 考虑以下两个单元:

unit UnitY;

interface

uses UnitA, UnitB;
...

unit UnitX;

interface

uses UnitB, UnitA;
... 

So if both units are in the same project, then (according to the documentation): UnitA initialises before UnitB AND UnitB initialises before UnitA . 因此,如果两个单元都在同一个项目,然后(根据文档): UnitA前初始化UnitB UnitB之前初始化UnitA This is quite obviously impossible . 这显然是不可能的 So the actual initialisation sequence may also depend on other factors: Other units that use A or B. The order in which X and Y initialise. 因此,实际的初始化顺序也可能取决于其他因素:使用A或B的其他单位.X和Y初始化的顺序。

So the best case argument in favour of the documentation is that: in an effort to keep the explanation simple, some essential details have been omitted. 因此,支持文档的最佳案例论点是:为了使解释简单,省略了一些基本细节。 The effect however is that in a real-world situation it's simply wrong. 然而,效果是,在现实世界的情况下,这是完全错误的。

Yes you " can " theoretically fine-tune your uses clauses to guarantee a particular initialisation sequence. 是的,你“理所当然” 可以微调你的uses条款,以保证特定的初始化顺序。 However, the reality is that on a large project with thousands of units this is humanly impractical to do and far too easy to break. 然而,现实情况是,在一个拥有数千个单位的大型项目中,这样做是非常不切实际的,而且太容易破解。


There are other arguments against initialization sections: 还有其他反对initialization部分的参数:

  • Typically the need for initialisation is only because you have a globally shared entity. 通常,初始化的需要仅仅是因为您拥有全局共享实体。 There's plenty of material explaining why global data is a bad idea. 有很多材料解释了为什么全球数据是一个坏主意。
  • Errors in initialisation can be tricky to debug. 初始化错误可能很难调试。 Even more so on a clients machine where an application can fail to start at all. 在应用程序无法启动的客户端计算机上更是如此。 When you explicitly control initialisation, you can at least first ensure your application is in a state where you'll be able to tell the user what went wrong if something does fail. 当你明确控制初始化,你至少可以先确保你的应用是在哪里,你就可以说,如果没有的东西不能什么地方出了错用户的状态。
  • Initialisation sections hamper testability because simply including a unit in a test project now includes a side-effect. 初始化部分妨碍了可测试性,因为在测试项目中简单地包含一个单元现在包括副作用。 And if you have test cases against this unit, they'll probably be tightly coupled because each test almost certainly "leaks" global changes into other tests. 如果你有针对这个单元的测试用例,它们可能会紧密耦合,因为每个测试几乎肯定会将全局变化“泄漏”到其他测试中。

Conclusion 结论

I understand your desire to avoid the "god-unit" that pulls in all dependencies. 我理解你希望避免引入所有依赖关系的“神单位”。 However, isn't the application itself something that defines all dependencies, pulls them together and makes them cooperate according to the requirements? 但是,应用程序本身不是定义所有依赖项的东西,将它们组合在一起并使它们根据需求进行协作吗? I don't see any harm in dedicating a specific unit to that purpose. 我认为将特定单位专门用于此目的并不会造成任何伤害。 As an added bonus, it is much easier to debug a startup sequence if it's all done from a single entry point. 作为一个额外的好处,如果从单个入口点完成所有操作,则调试启动序列要容易得多。

If however, you do still want to make use of initialization , I suggest you follow these guidelines: 但是,如果您仍想使用initialization ,我建议您遵循以下准则:

  • Make certain these units are explicitly included in your project. 确保这些单位明确包含在您的项目中。 You don't want to accidentally break features due to changes in unit dependencies. 由于单元依赖性的更改,您不希望意外中断功能。
  • There must be absolutely no order dependency in your initialization sections. initialization部分中必须绝对没有顺序依赖。 (Unfortunately your question implies failure at this point.) (不幸的是,你的问题意味着此时失败了。)
  • There must also be no order dependency in your finalization sections. finalization部分中也必须没有订单依赖。 (Delphi itself has some problems in this regard. One example is ComObj . If it finalises too soon, it may uninitialise COM support and cause your application to fail during shutdown.) (Delphi本身在这方面存在一些问题。一个例子是ComObj 。如果它太快完成,它可能会使COM支持无法初始化并导致应用程序在关闭期间失败。)
  • Determine the things that you consider absolutely essential to the running and debugging of your application, and verify their initialisation sequence from the top of your DPR file. 确定您认为对应用程序的运行和调试至关重要的事情,并从DPR文件的顶部验证其初始化顺序。
  • Ensure that for testability you are able to "turn off" or better yet entirely disable the initialisation. 确保为了可测试性,您可以“关闭”或更好地完全禁用初始化。

You can use class contructors and class destructors as well: 您也可以使用class contructors class destructorsclass 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;

The TModuleRegistry is a singleton, because it has no instance members. TModuleRegistry是一个单例,因为它没有实例成员。

The compiler will make sure that the class constructor is always called first. 编译器将确保始终首先调用class constructor

This can be combined with a Register and Unregister class method to somthing very similar as in the answer of @SpeedFreak. 这可以与RegisterUnregister类方法结合使用,与@SpeedFreak的答案非常相似。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 保护田地是个好主意吗? - Is it a good idea to make fields protected? 如果终止挂起的线程是个好主意,我该如何安全地进行? - If terminating a hung thread is a good idea, how do I do it safely? 为什么抑制符号可以用于Delphi中的控制字符,这是一个好主意吗? - Why can the circumflex sign be used for control chars in Delphi and is it a good idea? 调用ShowModal时设置PopupParent是一个好主意,是否有必要在较新的Delphi版本中? - Is calling ShowModal with setting PopupParent a good idea and is it necessary in newer Delphi versions? 我什么时候应该使用关键部分? - When should I use critical sections? 将自定义CodeInsight管理器注册到C ++ Builder,无需初始化/完成部分 - Registering a custom CodeInsight manager to C++Builder without initialization/finalization sections 如何在Delphi中使用Initialization and Finalization命令? - How to use the Initialization and Finalization command in Delphi? 在对象字段中使用动态数组是一种好习惯吗? - Is it good practice to use a Dynamic Array in an object field? 与Indy一起使用最新版本的OpenSSL是否合适? - Is it good to use the latest version of OpenSSL with Indy or not? 如何在没有注册的情况下从Win32使用.Net程序集? - How to use .Net assembly from Win32 without registration?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM