2015年10月5日

Spring4Dのマルチキャストイベントを使う

Delphi/VCLの大きな特徴のひとつである"PMEモデル(Properties, Methods and Events model)"の"E"、イベントハンドラは基本的に関数ポインタ(実際にはTMethod = データポインタ + 関数ポインタ)で実装されており、結果としてイベントはユニキャスト、つまりある事象の通知を受け取れるのは一つのイベントハンドラのみ、ということになっています。しかし状況によってはマルチキャスト、つまり複数のイベントハンドラに事象の通知を送りたい、ということがあります。デザインパターンでいうとオブザーバパターンとなるような状況です。デザインパターンの定石に従って、たとえばBranko StojakovicさんのGoF Patterns in DelphiObserverNick HodgesさんMore Coding in Delphiにあるように、IObserverとISubjectを実装して…というやりかたも考えましたが、マルチキャストで通知したい事象が何種類もある状況だと、何らかの形でジェネリックス化されている実装でないと厳しそうです。そこで探してみると、Spring4DというDI(dependency injection)フレームワークでマルチキャストイベントが実装されているということだったので、これを試してみることにします。なおSpring4DのライセンスはApache License 2.0です(日本語参考訳)。またSpring4Dそのもの(DIコンテナとしてのSpring4D)については第28回エンバカデロ・デベロッパーキャンプのLTの岡野さんプレゼンテーション資料が非常に参考になります。

まずSpring4Dのなかでマルチキャストを実現しているのはEvent<T>レコード型になります。これを使用するのに必要なユニットですが、Source\Baseフォルダにある
  • Spring.pas
  • Spring.inc
  • Spring.ResourceStrings.pas
  • Spring.Events.Base.pas
  • Spring.Events.pas
  • jedi.inc
です。もちろんSpring4Dをインストールして検索パスにSpring4D関係のフォルダを追加しても構いませんが、マルチキャストだけを試すのであればこれらのファイルをコピーして.pasファイルをプロジェクトに追加するだけでも大丈夫です。

では実際にEvent<T>でマルチキャストを試してみます。今回はメインフォームからモードレスなサブフォームをいくつか表示して、フォーム上のボタンクリックの発生と、その累計回数を各フォームに通知します。

まず通知のためのイベントと、イベントを登録/削除するインタフェースを定義します。
type
  TNotifyNumber = procedure (Sender: TObject; Number: Integer) of object;

  ISubject = interface
    procedure Add(AEvent: TNotifyNumber);
    procedure Remove(AEvent: TNotifyNumber);
  end;
TNotifyNumberが通知のためのイベントハンドラの型、ISubjectがそのイベントハンドラを登録/削除する機能を持たせたインタフェースです。メインフォーム、サブフォームの両方で使用するので別ユニットで定義しておきます。次はメインフォームです。
uses
  Spring, ...

type
  TForm1 = class(TForm,ISubject)
    ...
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FEvent: Event<TNotifyNumber>
  public
    procedure Add(AEvent: TNotifyNumber);
    procedure Remove(AEvent: TNotifyNumber);
  end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FEvent := Event<TNotifyNumber>.Create;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FEvent.Clear;
end;

procedure TForm1.Add(AEvent: TNotifyNumber);
begin
  FEvent.Add(AEvent);
end;

procedure TForm1.Remove(AEvent: TNotifyNumber);
begin
  FEvent.Remove(AEvent);
end;
メインフォームはISubjectインタフェースを継承するようにして、AddとRemoveを実装します。Event<T>はレコード型ですが、内部の適切な初期化のためにclass function Createを呼び出しておく必要があります。AddとRemoveでは単にEvent<T>のAddRemoveを呼び出すだけです。一方サブフォームでは
type
  TForm2 = class(TForm)
    ...
  private
    FSubject: ISubject;
  public
    property Subject: ISubject
               read  FSubject
               write FSubject;
  end;
メインフォームをISubjectとして保持するためのプロパティを用意します。これでメインフォームからサブフォームを生成、表示する処理は
procedure TForm1.Button1Click(Sender: TObject);
begin
  with TForm2.Create(Self) do
  begin
    Subject := Self;
    Show;
  end;
end;
となります。サブフォーム側ではボタンクリックで任意にマルチキャストイベントを登録、削除できるようにしてみます、
type
  TForm2 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Label1: TLabel;
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    ...
    procedure DoNotifyNumber(Sender: TObject; Number: Integer);
  public
    ...
  end;

procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Subject.Remove(DoNotifyNumber);
  Action := caFree;
end;

procedure TForm2.Button1Click(Sender: TObject);
begin
  Subject.Add(DoNotifyNumber);
end;

procedure TForm2.Button2Click(Sender: TObject);
begin
  Subject.Remove(DoNotifyNumber);
end;

procedure TForm2.DoNotifyNumber(Sender: TObject; Number: Integer);
begin
  Label1.Caption := 'Notified number is ' + IntToStr(Number);
end;
procedure DoNotifyNumberがマルチキャストイベントのイベントハンドラで、これをButton1/Button2のクリックイベントでSubjectにAdd/Removeすることで登録、削除します。最後にメインフォーム側からのマルチキャストイベントの呼び出しです。
type
  TForm1 = class(TForm,ISubject)
    ...
    Label1: TLabel;
    Button2: TButton;
    procedure Button2Click(Sender: TObject);
  private
    FCount: Integer;
    ...
  public
    ...
  end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  FCount := FCount + 1;
  Label1.Caption := IntToSTr(FCount);
  FEvent.Invoke(Self,FCount);
end;
Selfと1ずつ増える整数値(FCount)でEvent<T>のInvokeメソッドを呼び出すだけです(正確にはInvokeはイベントハンドラ型(<T> = <TNotifyNumber>)のプロパティで、Invoke(...)とすることでメソッドの呼び出しになる)。

Spring4DのEventがよくできていることもあってそれほど難しくはないと思います(気をつけなければならないのはclass function CreateによるEvent<T>の明示的な初期化くらい)。ただEvent<T>のTには通常のイベントハンドラ(procedure ... of object)だけでなく、無名メソッド(reference to ...)も入れられるのですが、この場合Event<T>で保持されるものが自動的に生成される無名メソッド値(TInterfacedObjectの派生クラスのインスタンス)であり、そのポインタをAddの呼び出し側(TForm2)では知ることができないためRemoveできない、という点には注意が必要です。

0 件のコメント: