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

更新


公開メモ

friend クラスとは

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

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

C# の問題

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

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

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

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

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

インターフェースを用いた解決策

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

LANG:C#
    // ClassB からのみアクセス可能な private なメンバー 
    // PrivateMemberForClassB を持つクラス
    public class ClassA: ClassB.IClassA
    {
        void ClassB.IClassA.PrivateMemberForClassB()
        {
            // このメンバーは private
        }
    }
 
    // 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 との両立

問題があるとすれば、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 なクラス

この方法で相互に 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 な関数を外部アセンブリに公開する

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

C# 2.0で、InternalsVisibleTo属性を用いてアセンブリ内でのみ使用される機能を単体テストする方法

(2009/02/09)

コメント





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