2012年12月13日木曜日

publishedなメソッドを隠す(ネストした型宣言使用編)

前回の続きです。

さて、最近のDelphiでは、ネストした型宣言が可能で、クラス宣言の中にクラスの宣言ができます。

この機能を使って、クラスの使用者からpublishedなメソッドを隠すことを試してみました。

以下、ソースです。

先ず、 【関数名でメソッドが呼び出されるクラスのサンプル】

  1. unit Unit2;  
  2.   
  3. interface  
  4.   
  5.   type TMyCallCalc = class  
  6.     public  
  7.       function CallCalc(CalcName : string; a, b: double) : double;  
  8.     private  
  9.       type TMyCalc = class  
  10.         published  
  11.           function Add(a,b : double) : double;  
  12.           function Subtract(a,b : double) : double;  
  13.       end;  
  14.   
  15.   end;  
  16.   
  17.   
  18. implementation  
  19. { TMyCalc }  
  20.   
  21. function TMyCallCalc.TMyCalc.Add(a, b: double): double;  
  22. begin  
  23.   Result := a + b;  
  24. end;  
  25.   
  26. function TMyCallCalc.TMyCalc.Subtract(a, b: double): double;  
  27. begin  
  28.   Result := a - b;  
  29. end;  
  30.   
  31.   
  32. type TMyCalcFunc = function(a,b : double) : double of object;  
  33.   
  34. function TMyCallCalc.CallCalc(CalcName : string; a, b: double) : double;  
  35. var  
  36.   MyCalc     : TMyCalc;  
  37.   MyCalcFunc : TMyCalcFunc;  
  38.   MethodVar : TMethod;  
  39. begin  
  40.   
  41.   MyCalc := TMyCalc.Create;  
  42.   try  
  43.     MethodVar.Data := MyCalc;  
  44.     MethodVar.Code := MyCalc.MethodAddress(CalcName);  
  45.   
  46.   
  47.     if Assigned(MethodVar.Code) then  
  48.     begin  
  49.       MyCalcFunc := TMyCalcFunc(MethodVar);  
  50.       Result := MyCalcFunc(a,b);  
  51.     end;  
  52.   finally  
  53.     MyCalc.Free;  
  54.   end;  
  55.   
  56. end;  
  57.   
  58. end.  

親クラスのprivateセクションにpublishedなメソッドを持つ子クラスを宣言しています。
これで、ユニットの使用者からは、子クラスのメソッドの宣言が見えなくなり、直接呼び出す
ことができなくなります。
ユニットの使用者には、publicセクションにメソッドを宣言することで、間接的に目的の
メソッドが呼び出せるようにします。

次に上記のクラスを使用するコード

  1. unit Unit1;  
  2.   
  3. interface  
  4.   
  5. uses  
  6.   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,  
  7.   Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;  
  8.   
  9. type  
  10.   TForm1 = class(TForm)  
  11.     Button1: TButton;  
  12.     Button2: TButton;  
  13.     LabeledEdit1: TLabeledEdit;  
  14.     LabeledEdit2: TLabeledEdit;  
  15.     StaticText1: TStaticText;  
  16.     StaticText2: TStaticText;  
  17.     procedure OnCalcBtnClick(Sender: TObject);  
  18.   private  
  19.     { Private 宣言 }  
  20.   public  
  21.     { Public 宣言 }  
  22.   end;  
  23.   
  24. var  
  25.   Form1: TForm1;  
  26.   
  27. implementation  
  28.   
  29. {$R *.dfm}  
  30.   
  31. uses Unit2;  
  32.   
  33. procedure TForm1.OnCalcBtnClick(Sender: TObject);  
  34. var  
  35.   CallCalc : TMyCallCalc;  
  36. begin  
  37.   CallCalc := TMyCallCalc.Create;  
  38.   try  
  39.     StaticText2.Caption := FloatToStr(CallCalc.CallCalc(  
  40.                                     TButton(Sender).Caption,  
  41.                                     StrToFloat(LabeledEdit1.Text),  
  42.                                     StrToFloat(LabeledEdit2.Text)));  
  43.   finally  
  44.     CallCalc.Free;  
  45.   end;  
  46.   
  47. end;  
  48.   
  49. end.  

ユニットを使用する側からは子クラスが見えませんので、親クラスのpublicなメソッドのみが
使用できます。



publishedなメソッドを隠す

前回の続きです。

TObjectのMethodAddressメソッドを使用すれば関数名(文字列)でメソッドをコールできます。

しかし、 MethodAddressメソッドでメソッドのアドレスを取得するには、可視性をpublishedに
する必要があり、 publishedにした瞬間にメソッドの存在が丸わかりになってしまいます。

前回の例のような単純な演算であればまあ良いかと思うのですが、いわゆるFactoryメソッド
とかだと、使用者にその存在を隠したい場合があり、このままではちょっと不完全です。

さて、どうしようか?というのが今回のテーマになります。(2010以降の拡張Rttiを使えば良い
というのはあるのですが、今回は別の方法を考えます。)

ところで、DelphiのUnitのimplementationセクションで定義した型(クラスを含む)は別のユニット
から参照できない仕様となっています。

したがって、クラスの定義を  implementation で行えば、外部から処理の存在を隠したうえで
 MethodAddressメソッド が使用可能なクラスができそうです。

で、実際に、作ってみました。

先ずは、【メソッドポインタ経由で呼び出されるクラスのサンプル】
  1. unit Unit2;  
  2.   
  3. interface  
  4.   
  5.   function CallCalc(CalcName : string; a, b: double) : double;  
  6.   
  7. implementation  
  8.   
  9.   type TMyCalc = class  
  10.   published  
  11.     function Add(a,b : double) : double;  
  12.     function Subtract(a,b : double) : double;  
  13.   end;  
  14.   
  15.   type TMyCalcFunc = function(a,b : double) : double of object;  
  16.   
  17. { TMyCalc }  
  18.   
  19. function TMyCalc.Add(a, b: double): double;  
  20. begin  
  21.   Result := a + b;  
  22. end;  
  23.   
  24. function TMyCalc.Subtract(a, b: double): double;  
  25. begin  
  26.   Result := a - b;  
  27. end;  
  28.   
  29.   
  30. function CallCalc(CalcName : string; a, b: double) : double;  
  31. var  
  32.   MyCalc     : TMyCalc;  
  33.   MyCalcFunc : TMyCalcFunc;  
  34.   MethodVar : TMethod;  
  35. begin  
  36.   
  37.   MyCalc := TMyCalc.Create;  
  38.   try  
  39.     MethodVar.Data := MyCalc;  
  40.     MethodVar.Code := MyCalc.MethodAddress(CalcName);  
  41.   
  42.   
  43.     if Assigned(MethodVar.Code) then  
  44.     begin  
  45.       MyCalcFunc := TMyCalcFunc(MethodVar);  
  46.       Result := MyCalcFunc(a,b);  
  47.     end;  
  48.   finally  
  49.     MyCalc.Free;  
  50.   end;  
  51.   
  52. end;  
  53.   
  54. end.  

publishedの可視性を持つクラスをimplementationセクションに定義しています。
但し、そのままでは、外部のユニットからメソッドをコールできませんので、
外部からのアクセスようにラッパー関数を定義しています。

次に、【関数名を文字列で指定して処理を呼び出すサンプル】

  1. interface  
  2.   
  3. uses  
  4.   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,  
  5.   Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;  
  6.   
  7. type  
  8.   TForm1 = class(TForm)  
  9.     Button1: TButton;  
  10.     Button2: TButton;  
  11.     LabeledEdit1: TLabeledEdit;  
  12.     LabeledEdit2: TLabeledEdit;  
  13.     StaticText1: TStaticText;  
  14.     StaticText2: TStaticText;  
  15.     procedure OnCalcBtnClick(Sender: TObject);  
  16.   private  
  17.     { Private 宣言 }  
  18.   public  
  19.     { Public 宣言 }  
  20.   end;  
  21.   
  22. var  
  23.   Form1: TForm1;  
  24.   
  25. implementation  
  26.   
  27. {$R *.dfm}  
  28.   
  29. uses Unit2;  
  30.   
  31. procedure TForm1.OnCalcBtnClick(Sender: TObject);  
  32. begin  
  33.   StaticText2.Caption := FloatToStr(CallCalc(  
  34.                                     TButton(Sender).Caption,  
  35.                                     StrToFloat(LabeledEdit1.Text),  
  36.                                     StrToFloat(LabeledEdit2.Text)));  
  37. end;  
  38.   
  39. end.  

unit2のinterfaceセクションに定義したラッパーを使って処理を呼び出しています。


implementationセクションにクラスを定義することで、外部からその存在を隠しつつ
メソッド名を文字列で指定してメソッドをコールすることが可能です。

但し、この方法ですとクラスそのものも隠れてしまいすのでもう少しなんとかしたい
ところです。

ネストした型宣言が可能なバージョンのDelphiであれば、何とかできそうな気が
しますが、その検証はまた後日・・・

2012年12月12日水曜日

メソッドを名前で呼び出す

前回の続きです。

関数(function)のメソッドポインタが定義できれば、TObjectのMethodAddressを使って
関数名を(文字で)指定してコールすることが可能です。

以下、サンプルソース。

先ずは、【メソッドポインタ経由で呼び出されるクラスのサンプル】

  1. unit Unit2;  
  2.   
  3. interface  
  4.   
  5.   type TMyCalc = class  
  6.   published  
  7.     function Add(a,b : double) : double;  
  8.     function Subtract(a,b : double) : double;  
  9.   end;  
  10.   
  11. implementation  
  12.   
  13. { TMyCalc }  
  14.   
  15. function TMyCalc.Add(a, b: double): double;  
  16. begin  
  17.   Result := a + b;  
  18. end;  
  19.   
  20. function TMyCalc.Subtract(a, b: double): double;  
  21. begin  
  22.   Result := a - b;  
  23. end;  
  24.   
  25. end.  

MethodAddressを使用するためにpublishedを指定してることに注意願います。

次に、【関数名を文字列で指定して処理を呼び出すサンプル】

  1. unit Unit1;  
  2.   
  3. interface  
  4.   
  5. uses  
  6.   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,  
  7.   Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;  
  8.   
  9. type  
  10.   TForm1 = class(TForm)  
  11.     Button1: TButton;  
  12.     Button2: TButton;  
  13.     LabeledEdit1: TLabeledEdit;  
  14.     LabeledEdit2: TLabeledEdit;  
  15.     StaticText1: TStaticText;  
  16.     StaticText2: TStaticText;  
  17.     procedure OnCalcBtnClick(Sender: TObject);  
  18.   private  
  19.     { Private 宣言 }  
  20.   public  
  21.     { Public 宣言 }  
  22.   end;  
  23.   
  24. var  
  25.   Form1: TForm1;  
  26.   
  27. implementation  
  28.   
  29. {$R *.dfm}  
  30.   
  31. uses Unit2;  
  32.   
  33. type TMyCalcFunc = function(a,b : double) : double of object;  
  34.   
  35. procedure TForm1.OnCalcBtnClick(Sender: TObject);  
  36. var  
  37.   MyCalc     : TMyCalc;  
  38.   MyCalcFunc : TMyCalcFunc;  
  39.   MethodVar : TMethod;  
  40. begin  
  41.   
  42.   MyCalc := TMyCalc.Create;  
  43.   try  
  44.     MethodVar.Data := MyCalc;  
  45.     MethodVar.Code := MyCalc.MethodAddress(TButton(Sender).Caption);  
  46.   
  47.   
  48.     if Assigned(MethodVar.Code) then  
  49.     begin  
  50.       MyCalcFunc := TMyCalcFunc(MethodVar);  
  51.       StaticText2.Caption := FloatToStr(MyCalcFunc(StrToFloat(LabeledEdit1.Text),StrToFloat(LabeledEdit2.Text)));  
  52.     end;  
  53.   finally  
  54.     MyCalc.Free;  
  55.   end;  
  56.   
  57. end;  
  58.   
  59. end.  

ボタンのキャプション名が名前のメソッドが定義されているという前提で、ボタンのキャプションを
使って関数をコールして、結果を求めています。


関数のメソッドポインタ

メソッドポインタでfunctionを使った例のサンプルです。
あまり見当たらなかったので、備忘録がわりに公開しときます。


【メソッドポインタ経由でfunctionを呼び出すサンプル】

  1. unit Unit1;  
  2.   
  3. interface  
  4.   
  5. uses  
  6.   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,  
  7.   Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;  
  8.   
  9. type  
  10.   TForm1 = class(TForm)  
  11.     Button1: TButton;  
  12.     Button2: TButton;  
  13.     LabeledEdit1: TLabeledEdit;  
  14.     LabeledEdit2: TLabeledEdit;  
  15.     StaticText1: TStaticText;  
  16.     StaticText2: TStaticText;  
  17.     procedure OnCalcBtnClick(Sender: TObject);  
  18.   private  
  19.     { Private 宣言 }  
  20.   public  
  21.     { Public 宣言 }  
  22.   end;  
  23.   
  24. var  
  25.   Form1: TForm1;  
  26.   
  27. implementation  
  28.   
  29. {$R *.dfm}  
  30.   
  31. uses Unit2;  
  32.   
  33. type TMyCalcFunc = function(a,b : double) : double of object;  
  34.   
  35. procedure TForm1.OnCalcBtnClick(Sender: TObject);  
  36. var  
  37.   MyCalc     : TMyCalc;  
  38.   MyCalcFunc : TMyCalcFunc;  
  39.   Method: TMethod;  
  40. begin  
  41.   
  42.   MyCalc := TMyCalc.Create;  
  43.   try  
  44.     //Method.Data := MyCalc;  
  45.     if TButton(Sender).Caption = 'Add' then  
  46.     begin  
  47.       MyCalcFunc := MyCalc.Add;  
  48.     end  
  49.     else  
  50.     begin  
  51.       MyCalcFunc := MyCalc.Subtract;  
  52.     end;  
  53.     StaticText2.Caption := FloatToStr(MyCalcFunc(StrToFloat(LabeledEdit1.Text),StrToFloat(LabeledEdit2.Text)));  
  54.   finally  
  55.     MyCalc.Free;  
  56.   end;  
  57.   
  58. end;  
  59.   
  60. end.  


【メソッドポインタ経由で呼び出されるクラスのサンプル】

  1. unit Unit2;  
  2.   
  3. interface  
  4.   
  5.   type TMyCalc = class  
  6.     function Add(a,b : double) : double;  
  7.     function Subtract(a,b : double) : double;  
  8.   end;  
  9.   
  10. implementation  
  11.   
  12. { TMyCalc }  
  13.   
  14. function TMyCalc.Add(a, b: double): double;  
  15. begin  
  16.   Result := a + b;  
  17. end;  
  18.   
  19. function TMyCalc.Subtract(a, b: double): double;  
  20. begin  
  21.   Result := a - b;  
  22. end;  
  23.   
  24. end.  

メソッドポインタはヘルプにもあるように、

Type 型名 = "メソッドの定義" of object

のように宣言します。

ここで、"メソッドの定義"は、通常の手続き(関数)の名前を抜いたものになるので、
例えば、TObject型のSenderを引数に持ち、戻り値がない"メソッドの定義"は、

procedure(Sender : TObject)

同様に、Double型のaとbを引数に持ち、Double型の戻り値がある"メソッドの定義"は

function(a,b : Double) : Double

となります。

上記のサンプル1では、ボタンのキャプションに応じてif分で関数を切り替えています。

この例では、関数が2つだけなので、メソッドポインタ経由ではなく、直接目的の関数を
コールしたほうが良いのですが、
同じ型の引数を持つ関数が多数ある場合、メソッドポインタと、TObject.MethodAddressを
組み合わるとコード量を減らせる可能性があります。(上の例だとボタンのCaptionと関数名を
合わせおくことによりif文(あるいは、Case文)を無くすことが可能です。)

そのへんの話はまた後日・・・