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 Unit2
s) 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.
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.
A couple of real-world examples why the "plus" point can also be considered a "minus" point:
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.
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:
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
sections. (Unfortunately your question implies failure at this point.)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.)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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With