C#/friendクラスの代替案 のバックアップ差分(No.3)

更新


  • 追加された行はこの色です。
  • 削除された行はこの色です。
[[公開メモ]]

#contents

* friend クラスとは [#t75f84fc]

密接に関連する2つのクラス、ClassA と ClassB とがあったとき、
他のクラスには公開しないが、お互いの間でのみ公開したいメソッドを
実装したくなる事が多々あります。

C++だとこのようなとき、お互いを friend クラスとして宣言することで、
外から見ると private なメソッドに、お互いの間でだけ自由にアクセスできる
ようになります。

* C# の問題 [#med36db1]

ところが C# には friend はありません。

似たような機能として internal というのがあるのですが、friend に比べて公開される範囲が
広すぎるので、完全に代替とすることができず、結構あちこちで議論の的になっています。

-[[フレンドクラスに対する代替案>http://www.google.co.jp/url?sa=U&start=1&q=http://bbs.wankuma.com/index.cgi%3Fmode%3Dal2%26namber%3D7138%26KLOG%3D7&ei=UJ-PScykFoiU6gPu0um1Cg&usg=AFQjCNEPQcXCNVnqZhsEq526MW3doVSF2Q]]
-[[C#への期待。アンダースからの返答 − @IT>http://www.google.co.jp/url?sa=U&start=4&q=http://www.atmarkit.co.jp/fdotnet/insiderseye/20060215cscommunity/cscommunity_02.html&ei=UJ-PScykFoiU6gPu0um1Cg&usg=AFQjCNHK3iwgAkMt0olEa3_MwaYBjo24fw]]
-[[C# には friend キーワードがない>http://frog.raindrop.jp/knowledge/archives/002237.html]]
-[[Google で "c#" friend を検索>http://www.google.co.jp/search?q=%22c%23%22+friend&lr=lang_ja&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:ja:official]]

おそらく一番の問題は、アセンブリを分けていない状況でコーディングしていると、
実際には必要とされない internal なメンバーがインテリセンスに現れてしまい、
誤って呼び出してしまったり、本当に呼び出したいメンバーを探すのが大変に
なってしまったりすることだと思います。

当記事では、C# において friend の代替が必要になった際に使える
デザインパターンを1つ挙げて考察します。

実は良く知られた方法なのかもしれませんが、他で同様の記事を発見できなかったので、
一応ここにメモしておきます。

* インターフェースを用いた解決策 [#t2ec0098]

こういう場合、インターフェースを使うと、friend よりも限定した形で、
特定のメソッドのみを、特定のクラスにのみ公開する事ができる事に気づきました。
この場合、以下のようにインターフェースを使うことで、friend よりも限定した形で、
特定のメソッドのみを、特定のクラスにのみ公開するのがベストではないかと考えました。

当たり前の方法なのかもしれませんが、不思議と他で同様の記事を発見できなかったので、
一応ここにメモしておきます。

 LANG:C#
     // ClassB からのみアクセス可能な private なメンバー 
     // privateMemberForClassB を持つクラス
     // PrivateMemberForClassB を持つクラス
     public class ClassA: ClassB.IClassA
     {
         void ClassB.IClassA.privateMemberForClassB()
         void ClassB.IClassA.PrivateMemberForClassB()
         {
             // このメンバーは private
         }
     }
  
     // ClassA の private メンバー
     // privateMemberForClassB にアクセスするクラス
     // PrivateMemberForClassB にアクセスするクラス
     public class ClassB
     {
         // 同じアセンブリ内、あるいは ClassB および
         // その子孫クラスからのみ使えるインターフェース
         protected internal interface IClassA
         {
              void privateMemberForClassB();
              void PrivateMemberForClassB();
         }
 
         // 実際にアクセスするコードの例
         public void AccessToPrivate(ClassA a)
         {
              // キャストしたときだけ操作できる
             ( (IClassA)a ).privateMemberForClassB();
             ( (IClassA)a ).PrivateMemberForClassB();
         }
     }

もちろん、同じアセンブリ内で、わざわざ a を B.IClassA にキャストするコードを
書けば該当コードにアクセスできてしまいますが、間違って呼び出してしまうとか、
もちろん、同じアセンブリ内で、わざわざ a を ClassB.IClassA にキャストするコードを
書けば該当メソッドにアクセスできてしまいますが、間違って呼び出してしまうとか、
インテリセンスを汚してしまうとか、そういった、本来解決したかった問題は
この方法で回避できていると思います。
この方法で回避できると思います。

ClassA のプライベート関数を virtual にすることはできませんが、もともと
インターフェース経由でのアクセスになりますので、子孫クラスで定義し直せば
virtual と同様の効果が得られます。

非常に頻繁に呼び出される関数を、
このようにインターフェース経由の呼び出しにしてしまうと、
パフォーマンス的な問題が発生する可能性もありますが、
それ以外に関しては、コーディングの手間としても、
コードの読みやすさとしても、
ほとんど不都合の感じられない記述になっていると思います。

friend クラスに対して無制限にアクセスを許してしまう C++ の friend 
の仕様に比べても、選び抜かれた操作だけを、選び抜かれた相手にのみ
公開するという形は、カプセル化の本筋に合っている気がします。

* virtual との両立 [#gaea155a]

問題があるとすれば、virtual との両立です。

ClassA の子孫クラスで PrivateMemberForClassB の動作を変更する可能性がある場合、
PrivateMemberForClassB を virtual にしたくなりますが、上のままの形で該当メソッドに 
virtual を付けることは言語仕様上許されていません。

元々 private になっているので、
virtual にできないのは当たり前と言えば当たり前なのですが、
やりたい事ができないのは困ったことです。

そこで解決策なのですが、
正攻法で行けば、PrivateMemberForClassB の中で、
別に定義した virtual な protected メンバーを呼ぶ形になります。

 LANG:C#
     public class ClassA: ClassB.IClassA
     {
         void ClassB.IClassA.PrivateMemberForClassB()
         {
             // この関数自体は private なので virtual にはできない
             // そこで protected virtual なメンバーに処理を委譲する
             ProtectedVirtualMemberForClassB();
         }
 
         protected virtual void ProtectedVirtualMemberForClassB() 
         {
             // このメンバーは protected
             // virtual なので、子孫クラスで動作を変更可能
 
             // PrivateMemberForClassB 経由で間接的に
             // ClassB からも呼び出す事ができる
         }
     }
 
     public class ClassC: ClassA
     {
         protected override void ProtectedVirtualMemberForClassB() 
         {
             // 子孫クラスで動作を変更できる
         }
     }
 ...
         void test()
         {
             ClassB b= new ClassB();
             ClassA c_as_a= new ClassC();
             b.AccessToPrivate(c_as_a);
             // ClassC.ProtectedVirtualMemberForClassB() が呼ばれる
         }
 ...

ClassB から呼び出さなければならない protected なメソッドがいくつもある場合、
対応する private 関数を書く作業が面倒と言えば面倒なのですが・・・
他に良い案も浮かびません。

ということで、コーディング自体はちょっと面倒になりますが、ClassA, ClassB を
(子孫クラスを含めて)外から見たときには、ほぼ理想的な形になりますので、
個人的には許せる範囲と思っています。

* 相互に friend なクラス [#w1f117e7]

この方法で相互に friend なクラスを作ろうとすると、

 LANG:C#
 // 相互に friend な関係を作ろうとした例
 // コンパイルエラーになる
 class ClassA: ClassB.IClassA
 {
     interface IClassB { }
 }
 
 class ClassB: ClassA.IClassB
 {
     interface IClassA { }
 }

などとしなければならなくなりますが、これは「循環する基本クラスの依存関係です」
というエラーを生じてしまい、コンパイルできません。

これについては、インターフェースをクラスから外に出して、

 LANG:C#
    class ClassA: IClassAForClassB
    {
    }
    internal interface IClassBForClassA { }
 
    class ClassB: IClassBForClassA
    {
    }
    internal interface IClassAForClassB { }

などとすれば良いのですが、インターフェースの所有権が曖昧になるのが
玉に瑕なところです。

上の一方通行の friend の例では ClassB の子孫クラスから IClassA 
にアクセスできるかどうかを選ぶ事ができましたが、
外に出してしまった事で ClassA や ClassB の子孫から
インターフェースにアクセスするには、interface を internal で
なくすしか方法が無くなってしまっています。

まあ、うまく設計すれば子孫クラスから IClassA をさわる必要を
なくす事はできそうですから、一応これで解決という事にしておきます。

* internal な関数を外部アセンブリに公開する [#g99916c5]

当記事は internal よりも公開範囲を狭めるという視点で、メソッドを特定の相手のみに
公開するための手法を記述しましたが、逆に、internal 宣言されたアイテムを、
外部アセンブリから参照するための手法がこちらに書かれています。

[[C# 2.0で、InternalsVisibleTo属性を用いてアセンブリ内でのみ使用される機能を単体テストする方法>http://mag.autumn.org/Content.modf?id=20060509190737]]

(2009/02/09)

* コメント [#p09af981]

#article_kcaptcha


Counter: 46260 (from 2010/06/03), today: 1, yesterday: 0