Question

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.

I have written a test project:

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);

This has one drawback though. Is it guaranteed that my registration units (in this case Unit2s) initialization section is always run first?

I have often read warnings about initialization sections, I know that I have to avoid raising exceptions in them.

Was it helpful?

Solution

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

Yes. Delphi's own framework uses it too, e.g. the registration of TGraphic-descendents.

Is it guaranteed that my registration units (in this case Unit2s) initialization section is always run first?

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.

But beware of the situation wherein you work with runtime packages.

OTHER TIPS

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

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.

My answer is a stark contrast to NGLN's answer. 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.


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.

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. 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. 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.


Is it guaranteed that my registration units (in this case Unit2s) initialization section is always run first?

This is one of many cases in which Delphi's documentation is simply wrong.

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.

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. 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.

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. 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:

  • 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:

  • 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. (Unfortunately your question implies failure at this point.)
  • There must also be no order dependency in your finalization sections. (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.)
  • 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.
  • 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:

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.

The compiler will make sure that the class constructor is always called first.

This can be combined with a Register and Unregister class method to somthing very similar as in the answer of @SpeedFreak.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top