10

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.

Jens Mühlenhoff
  • 14,565
  • 6
  • 56
  • 113

4 Answers4

7

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.

Community
  • 1
  • 1
NGLN
  • 43,011
  • 8
  • 105
  • 200
  • Because of last paragraph (order of initialization depends on order in uses clause), I'm using slightly different approach to initalizing shared modules by making each of those module to have class procedure Initialize & Deinitialize, which I then call from main form create section, or wherever its appropriate. Thats how I have more clear view of initialization order, instead taking care of order in uses clause. – rsrx Mar 29 '14 at 16:21
7

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.
SpeedFreak
  • 876
  • 6
  • 16
  • That's not a pattern. That's a load of code. Could you possibly add text to explain the intent. – David Heffernan Mar 29 '14 at 18:33
  • 6
    @David, It's _a good solution for a decentralized module registration_, which is what the OP asked for. Beyond that I don't feel an urge to explain it. Sorry. – SpeedFreak Mar 29 '14 at 19:14
  • 2
    Answers are much more useful if you use prose to explain them – David Heffernan Mar 29 '14 at 19:22
  • 1
    This is simple SoC because TForm2s job is not being a form registry but being a form. – Stefan Glienke Mar 29 '14 at 20:05
  • @David, I agree, but I just don't have time to do so. – SpeedFreak Mar 29 '14 at 20:18
  • What is the advantage over using just a `var Modules: TDictionary`? – Jens Mühlenhoff Mar 29 '14 at 21:56
  • My method is more work, but it pays in the long run. The advantages are that you are hiding the implementation details, the list is created on-demand (i.e. no worry about initialization order) and you have an interface that does just what you need and nothing more (contrary to the full TDictionary interface). – SpeedFreak Mar 29 '14 at 23:01
  • It's kind of weak that all the good stuff is in these comments. – David Heffernan Mar 30 '14 at 07:01
  • Nice answer with alternative implementation, but this doesn't answer the question of whether or not and how to use the initialization section. – NGLN Mar 30 '14 at 18:15
  • @NGLN, Since my solution _does_ use the initialization section I think it implicitly answers both how and if to use the initialization section. Besides there's no need for me to Rudy^H^H^H^H repeat what you already covered. – SpeedFreak Mar 30 '14 at 19:03
6

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.
Community
  • 1
  • 1
Disillusioned
  • 14,635
  • 3
  • 43
  • 77
  • _then `UnitA` initialises before `UnitB` and `UnitB` initialises before `UnitA`._: NO! Due to `Unit1`, `UnitA` initializes before `UnitB`, and when the compiler reaches `Unit2`, then `UnitB` and `UnitA` don't need initialization because they are already initialized. Whether `UnitA` or `UnitB` is initialized first depends entirely on the order in which `Unit1` and `Unit2` are used. – NGLN Mar 30 '14 at 18:03
  • Besides: the "problem" you are describing doesn't exist in OP's question: before `TForm2` can be used (the registration handler), `Unit2` **wíll** be initialized. No matter in what order `Unit2`, `Unit3` and `Unit4` are mentioned in the project file. – NGLN Mar 30 '14 at 18:07
  • @NGLN NO! You're assuming `Unit1` initialised before `Unit2` ... nowhere did I say that. Also, you don't know if `UnitA` uses `UnitB`. ... and the point is it's not easy to predict without knowing more information, which makes the documentation fundamentally wrong. – Disillusioned Mar 30 '14 at 18:19
  • @NGLN I'll change Unit 1&2 to X&Y to hopefully remove the confusion. As to your second comment, the point is your conclusion is reached from the units you can see - it cannot be conclusive, and can easily change with the introduction of other units and dependency changes. – Disillusioned Mar 30 '14 at 18:23
  • @NGLN I also added clarification: "So the actual initialisation sequence may also depend on other factors" – Disillusioned Mar 30 '14 at 18:27
  • 1
    Why is order important in this particular context? – David Heffernan Mar 30 '14 at 18:44
  • @DavidHeffernan I don't know. But OP seemed concerned about it: "Is it guaranteed that my registration units (in this case Unit2s) initialization section is always run first?" – Disillusioned Mar 30 '14 at 18:45
  • 1
    What does first mean? I see nothing in the question that suggests it matters which order the initialization sections run with respect to each other. – David Heffernan Mar 30 '14 at 18:56
  • @DavidHeffernan You're asking the !wrong! person. I quoted OP's question. Although with the code shown, if some other unit dependencies elsewhere causes his Unit3 to initialise before Unit2, he would probably get an Access Violation. – Disillusioned Mar 30 '14 at 19:08
  • Exactly my point is that that can never happen! `Unit3` cannot become initialized before `Unit2`, because `Unit3` uses `Unit2`. – NGLN Mar 30 '14 at 19:11
  • 1
    My point is why you believe relative order of initialization is the key here. I cannot see evidence for that. – David Heffernan Mar 30 '14 at 19:17
  • @NGLN Of course it **can** happen, I proved in my answer how the documentation oversimplifies initialisation sequence. Did you notice that `Forms` is missing from his uses clause? How many other units are missing? What in those dependencies and others in the DPR might affect initialisation sequence in unexpected ways? What might change and unexpectedly break the application? A unit's uses clause **by itself** does not provide conclusive proof about the initialisation sequence of its dependencies. – Disillusioned Mar 30 '14 at 19:29
  • 4
    @DavidHeffernan I've listed 5 distinct disadvantages to to using initialization section. A part of my answer responds directly to a specific question from OP. What exactly are you not comprehending about "OP asked the question!"? Or are you just being your usual argumentative self? – Disillusioned Mar 30 '14 at 19:38
  • 4
    @DavidHeffernan Seriously dude, what the hell is up with you? Time and again I see you engaging in pointless circular arguments with other contributors on this forum. Well, I'm not biting - I'm done here! – Disillusioned Mar 30 '14 at 19:40
  • @Craig Instead of responding to a straightforward technical query by addressing it, you resort to personal abuse. – David Heffernan Mar 30 '14 at 19:45
  • And as for your last comment to Craig, I respectfully suggest that you demonstrate that it can happen. It cannot. When unit A uses unit B in the interface section, then unit B is initialized first. That's the beginning and end of the story of unit initialization order. – David Heffernan Mar 30 '14 at 19:47
  • I actually agree with you that side effects due to init ordering are nasty and to be avoided. I don't agree that all global state is always bad. I agree that explicit init is to be preferred. I just asked why you felt that init order was so important to the question that was asked. Your reaction was surprising. Would you prefer an uncritical eulogy? – David Heffernan Mar 30 '14 at 19:52
  • I was only concerned that the registration unit might not be initialized before the module units (as I wrote in my question). – Jens Mühlenhoff Mar 30 '14 at 19:55
  • As you wrote there is already a "god unit" (the .dpr file) and I do not want to repeat that unnecessarily. – Jens Mühlenhoff Mar 30 '14 at 19:57
  • @Jens It depends what you mean by "always run first". I don't find that clear. It could mean multiple things. – David Heffernan Mar 30 '14 at 22:12
  • @DavidHeffernan The dictionary has to be created before any module unit is trying to add its class type(s) to it (from its initialization section). In my design I only read from the dictionary after all initialization sections are done and the application is running. – Jens Mühlenhoff Mar 30 '14 at 22:41
  • @DavidHeffernan Yes, that would work as well (and is essentially what SpeedFreaks code is doing). – Jens Mühlenhoff Mar 30 '14 at 22:45
3

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.

Jens Mühlenhoff
  • 14,565
  • 6
  • 56
  • 113
  • 2
    Class constructors and destructors are executed by the unit initialization and finalization code. – David Heffernan Mar 30 '14 at 20:49
  • That is good to know, so everything that applies to initialization and finalization code also applies to class constructors and class destructors. – Jens Mühlenhoff Mar 30 '14 at 21:00
  • Well, not entirely. The linker will strip the class if it's not referenced. That won't happen to init/finit code. See http://docwiki.embarcadero.com/RADStudio/en/Methods#Destructors and http://stackoverflow.com/questions/6231871/class-constructor-not-called-when-class-registration-is-done-in-that-class-const so I think class constructors are no good for you. – David Heffernan Mar 30 '14 at 22:04
  • They do not work for the modules, but they do work for the registry. – Jens Mühlenhoff Mar 30 '14 at 22:36
  • I don't know what you mean by modules. Anyway, I think it's clear when they get stripped. – David Heffernan Mar 30 '14 at 22:41
  • In the question the module units are `Unit3` and `Unit4`, because they contain program module classes. `Unit2` is the registry unit, I probably should have used better unit names. – Jens Mühlenhoff Mar 30 '14 at 22:43