C#/friendクラスの代替案 のバックアップソース(No.13)

更新

[[公開メモ]]

#contents

2009/02/10:初稿

* 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 の代替が必要になった際に使える
デザインパターンを挙げて考察します。

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

* [2011/02/25 追記] C# のアセンブリの粒度を調節して解決 [#p7956ba4]

実はこれより下の記事は自分自身あまりC#に慣れていない段階で書いた物でした。

そこそこの意味はあると思うので残しておきますが、
今となってはちょっと方向違い、というか C# の流儀には反するように感じています。

結局、C# ではアセンブリの粒度を小さくして、
friend のかわりに internal を使え、というのが本来の解答なのだと思います。

従来の DLL を使った機能分割に比べて .Net のアセンブリは非常に使いやすいです。

密接に関連するクラスをまとめたコンポーネント単位でアセンブリに分割して切り分けることで、
より良い開発・リリースサイクルを作るというような話が、書籍 Effective C# および More 
Effective C# に紹介されていました。

C# 開発者にとってこの2冊は必読ですね。

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

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

この例では、ClassB からのみアクセス可能な private メンバーを
ClassA に実装します。

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

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

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

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 を public にしなければなりません。

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

* [2009/08/28 追記] static メンバーについて [#p7956ba4]

この方法では static なメンバーに対応できないことに気づきました。

インターフェースには static メンバーを持たせることができません。

static なメンバーは internal 指定にしておくしかないようです。

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

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

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

internal をアセンブリ単位の private だと考えれば、
これは friend アセンブリとでも言えそうな話題になっています。

** 方法 [#e8b20b1c]

ソリューションエクスプローラで プロジェクト の Properties の下にある 
AssemblyInfo.cs に以下の内容を入れる。

 #if DEBUG
 [assembly: InternalsVisibleTo("SomeOtherAssemblyName")]
 #endif

SomeOtherAssemblyName.dll や SomeOtherAssemblyName.exe から
internal なメンバーにアクセスできるようになる。

* コメント [#p09af981]

#article_kcaptcha


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