簡體   English   中英

使用初始化部分進行模塊注冊是個好主意嗎?

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

我正在為分散式模塊注冊尋找一個好的解決方案。

我不想要一個使用項目所有模塊單元的單元,但我寧願讓模塊單元自己注冊。

我能想到的唯一解決方案是依賴於Delphi單元的initialization

我寫了一個測試項目:

單元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

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

這有一個缺點。 是否保證我的注冊單元(在本例中為Unit2initialization部分始終先運行?

我經常閱讀有關initialization部分的警告,我知道我必須避免在其中引發異常。

使用初始化部分進行模塊注冊是個好主意嗎?

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

是否保證我的注冊單元(在本例中為Unit2s)初始化部分始終先運行?

是的,根據文件

對於接口使用列表中的單元,客戶端使用的單元的初始化部分按照單元出現在客戶端的uses子句中的順序執行。

但要注意使用運行時包的情況

我會使用以下“模式”:

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;

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.

我的回答與NGLN的答案形成鮮明對比。 但是,我建議你認真考慮我的推理。 然后,即使你仍然希望使用initialization ,至少你的眼睛會對潛在的陷阱和建議的預防措施開放。


使用初始化部分進行模塊注冊是個好主意嗎?

不幸的是,NGL​​N贊成的論點有點像是在爭論你是否應該根據你最喜歡的搖滾明星是否這樣做而吸毒。

爭論應該基於該功能的使用如何影響代碼可維護性。

  • 好的方面來說,只需包含一個單元即可為應用程序添加功能。 (很好的例子是異常處理程序,日志框架。)
  • 消極方面,你只需通過包括單元功能添加到您的應用程序。 (無論你是否有意。)

為什么“加”點也可以被認為是“減”點的幾個現實世界的例子:

  1. 我們有一個單元通過搜索路徑包含在一些項目中。 該單元在initialization部分中執行自注冊。 進行了一些重構,重新排列了一些單元依賴關系。 接下來,該單元不再被包含在我們的一個應用程序中,打破了它的一個功能。

  2. 我們想要更改我們的第三方異常處理程序。 聽起來很容易:將舊處理程序的單元從項目文件中取出,並添加新處理程序的單位。問題是我們有一些單元有自己的直接引用一些舊處理程序的單元。
    您認為哪個異常處理程序首先注冊了它的異常掛鈎? 哪個注冊正確?

但是,存在更嚴重的可維護性問題。 這就是單位初始化順序的可預測性。 盡管有一些規則可以嚴格確定單元初始化(並最終確定)的順序,但作為程序員,您很難准確地預測出前幾個單元之外的單元。

對於任何依賴於其他單元初始化的initialization部分,這顯然會產生嚴重后果。 例如,考慮如果你的一個initialization部分中有錯誤會發生什么,但它恰好在你的異常處理程序/ logger初始化之前被調用...你的應用程序將無法啟動,你將陷入困境找出原因。


是否保證我的注冊單元(在本例中為Unit2s)初始化部分始終先運行?

這是Delphi的文檔完全錯誤的許多案例之一。

對於接口使用列表中的單元,客戶端使用的單元的初始化部分按照單元出現在客戶端的uses子句中的順序執行。

考慮以下兩個單元:

unit UnitY;

interface

uses UnitA, UnitB;
...

unit UnitX;

interface

uses UnitB, UnitA;
... 

因此,如果兩個單元都在同一個項目,然后(根據文檔): UnitA前初始化UnitB UnitB之前初始化UnitA 這顯然是不可能的 因此,實際的初始化順序也可能取決於其他因素:使用A或B的其他單位.X和Y初始化的順序。

因此,支持文檔的最佳案例論點是:為了使解釋簡單,省略了一些基本細節。 然而,效果是,在現實世界的情況下,這是完全錯誤的。

是的,你“理所當然” 可以微調你的uses條款,以保證特定的初始化順序。 然而,現實情況是,在一個擁有數千個單位的大型項目中,這樣做是非常不切實際的,而且太容易破解。


還有其他反對initialization部分的參數:

  • 通常,初始化的需要僅僅是因為您擁有全局共享實體。 有很多材料解釋了為什么全球數據是一個壞主意。
  • 初始化錯誤可能很難調試。 在應用程序無法啟動的客戶端計算機上更是如此。 當你明確控制初始化,你至少可以先確保你的應用是在哪里,你就可以說,如果沒有的東西不能什么地方出了錯用戶的狀態。
  • 初始化部分妨礙了可測試性,因為在測試項目中簡單地包含一個單元現在包括副作用。 如果你有針對這個單元的測試用例,它們可能會緊密耦合,因為每個測試幾乎肯定會將全局變化“泄漏”到其他測試中。

結論

我理解你希望避免引入所有依賴關系的“神單位”。 但是,應用程序本身不是定義所有依賴項的東西,將它們組合在一起並使它們根據需求進行協作嗎? 我認為將特定單位專門用於此目的並不會造成任何傷害。 作為一個額外的好處,如果從單個入口點完成所有操作,則調試啟動序列要容易得多。

但是,如果您仍想使用initialization ,我建議您遵循以下准則:

  • 確保這些單位明確包含在您的項目中。 由於單元依賴性的更改,您不希望意外中斷功能。
  • initialization部分中必須絕對沒有順序依賴。 (不幸的是,你的問題意味着此時失敗了。)
  • finalization部分中也必須沒有訂單依賴。 (Delphi本身在這方面存在一些問題。一個例子是ComObj 。如果它太快完成,它可能會使COM支持無法初始化並導致應用程序在關閉期間失敗。)
  • 確定您認為對應用程序的運行和調試至關重要的事情,並從DPR文件的頂部驗證其初始化順序。
  • 確保為了可測試性,您可以“關閉”或更好地完全禁用初始化。

您也可以使用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;

TModuleRegistry是一個單例,因為它沒有實例成員。

編譯器將確保始終首先調用class constructor

這可以與RegisterUnregister類方法結合使用,與@SpeedFreak的答案非常相似。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM