ようこそ。睡眠不足なプログラマのチラ裏です。

デザインパターン第13回「NullObjectパターン」

デスマプロジェクトに放り込まれて、かなりヤラレていますが、
久しぶりに独りよがりなデザパタ講座です。
今回はGoFデザインパターン以外のパターンを取り扱いたいと思います。
スマートにコードを表現することができ且つ、ケアレスなバグを減らす効能のある、
有用なパターンの1つである「NullObjectパターン*1」をぬるーく解説します。

■NullObjectパターンの概要■
NullObjectとはその名のとおり、null扱いをされるObjectのことを言います。
つまり、「何もしない、何もできないObject」を意味します。


NullObjectパターンは、なんらかの処理をするにあたり、
呼び出し側でnullチェックをしなくても済むように、
「何もしないクラスのインスタンス」を用意してあげましょうというアイディアのパターンである。
NullObjectは、「役に立たない」ことが目的で、「役に立たない」がゆえに、非常に役にたつというパターンです。


NullObjectパターンの特徴
なんらかの処理で、結果にnullを返してくる可能性のあるメソッドがある場合、
nullであるか否かという判定をせずに、その戻り値nullのオブジェクトに対してメソッドを呼び出そうとしたり、
プロパティにアクセスしようとすると、.NET Framework下ではSystem.NullReferenceExceptionが発生してしまう。
しかしこのとき、nullではなく、メソッドが呼び出されても「何も処理をしないクラス」が返されていたらどうだろう。
メソッドが呼び出されても何もしないので、そもそも処理自体がなかったかのようにスルーされる。
このアイディアを適用することにより、結果がnullであるか否かという判定処理を省くことができるケースが生まれる。


NullObjectパターンの効能と欠点
効能としては、前述したように、NullObjectパターンを適用したクラスのインスタンスについては、
nullチェックをする必要を最小限に抑えられるため、コードをシンプルにすることができる。
また同時に、nullチェックのし忘れによるバグの減少も期待できる。


ただし欠点もある。NullObjectパターンを、さまざまなクラスに適用しようとすると、
クラスごとに「何もしないクラス」を作成する必要に迫られてしまう。
そうすると、クラスの異常増殖を産むという欠点があり、通常このパターンを適用する場合、
コードの単純化とクラス数の増加についてのトレードオフを考慮する必要がある。
しかし、この問題は工夫次第で解決することができるようだ。


MarshalByRefObjectから派生したクラスについてのNullObjcetを生成するProxyクラス

NullObjectパターン(匣の向こう側 - あまりに.NETな)


上記リンク先のソースが大変参考になりました。ソースを眺めて、あ〜なるほど。その手あったか!
これでNullObjectなクラスが異常増殖することを防げるゼッ!と、感動しました。感謝です。
確かに.NET Frameworkの割り込みメカニズムの1つであるRealProxyクラスを利用することで、
いくつかの条件付ではあるものの、「何もしないクラス」の提供を一般化することができますね。


リンク先のソースをそのまま使うこともできますが、メソッドの戻り値が参照型の場合にnullを返してしまうと、
その先でまたnullチェックをしなければならなくなってしまうので、上記ソースを参考にさせていただいて、
戻り値がSystem.MarshalByRefObjectの派生クラスである場合、
その派生クラスに応じたNullObjectインスタンスをNullProxyを通して生成して返すようにしてみました。
また、メソッドが持つoutパラメータおよびrefパラメータについても、
System.MarshalByRefObjectの派生クラスであれば、同様にNullObjectを返すようにしました。
インターフェイスや抽象クラスの場合はインスタンス化が不可能なので、仕方なくnullを返します。
まだ考慮不足だったり、改善できる部分があるやもしれませんが・・・、とりあえず以下コードで完成とします。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Text.RegularExpressions;

namespace WindowsApplication1
{
    /// <summary>
    /// ジェネリックなNullObjectを提供します。
    /// プロキシの基本機能を提供するRealProxyを継承します。
    /// </summary>
    /// <typeparam name="T">
    /// NullObject対象の型を指定します。制約:publicなデフォルトコンストラクタを有するMarshalByRefObject型のクラスであること。
    /// </typeparam>
    public class NullProxy<T> : RealProxy where T : MarshalByRefObject ,new()
    {
        /// <summary>
        /// ジェネリックなSingletonを提供するためのコレクション。
        /// ガベコレで収集されるまでの間の弱い参照を保持します。
        /// </summary>
        private static readonly Dictionary<Type, WeakReference> dicNullObject = new Dictionary<Type, WeakReference>();

        /// <summary>
        /// 排他ロックオブジェクト
        /// </summary>
        static readonly object syncObj = new object();

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public NullProxy() : base(typeof(T)) { 
        }

        /// <summary>
        /// メソッド呼び出しに割り込んでなにもしません。
        /// メソッドに戻り値がある場合、値型なら初期値を返します。
        /// 参照型の場合、抽象クラスおよびインターフェイスであればnullを返します。
        /// いずれでもなければ、System.MarshalByRefObjectの派生クラスの場合、
        /// NullProxy<T>を通して生成したNullObjectを返します。
        /// 
        /// メソッドの引数について、outパラメータおよびrefパラメータである場合、
        /// そのパラメータがSystem.MarshalByRefObjectの派生クラスである場合、
        /// そのパラメータに対して、NullProxy<T>を通して生成したNullObjectを返します。
        /// </summary>
        /// <param name="msg">メッセージ通信オブジェクト</param>
        /// <returns>ReturnMessageオブジェクト</returns>
        public override IMessage Invoke(IMessage msg)
        {
            IMethodCallMessage req = msg as IMethodCallMessage;
            object response = null;

            //メソッドのメタデータを取得
            MethodInfo methodInfo = ((MethodInfo)req.MethodBase);

            //Outパラメータ配列
            ArrayList  outList = new ArrayList();

            //メソッドの各パラメータ情報を検索
            foreach (ParameterInfo paramInfo in methodInfo.GetParameters())
            {
                if (paramInfo.ParameterType.IsByRef){
                    //out/refパラメータの場合
                    //メンバの参照型の型に対する、NullObjectとして適切なインスタンスを取得

                    //正規表現でByRef属性を意味する 末尾にある"&" を型の名前から取り除く
                    string typeString = paramInfo.ParameterType.ToString();
                    Regex regex = new Regex("&$");
                    typeString = regex.Replace(typeString, string.Empty);
                    
                    outList.Add(GetResponceInstance(Type.GetType(typeString)));          
                }
            }
            
            //戻り値の型に対する、NullObjectとして適切なインスタンスを取得
            response = GetResponceInstance(methodInfo.ReturnType);

            //リターンメッセージ
            return new ReturnMessage(response,outList.ToArray(),outList.Count, req.LogicalCallContext, req);
        }

        /// <summary>
        /// 対象の型について、NullObjectとして返す適切なインスタンスを取得します。
        /// </summary>
        /// <param name="t">対象の型</param>
        /// <returns>NullObjectとして返す適切なインスタンスを取得します。</returns>
        private object GetResponceInstance(Type t){
            object response = null;

            if (t.IsValueType)
            {
                //値型の場合
                if (t != typeof(void))
                {
                    //戻り値がある場合
                    response = Activator.CreateInstance(t);
                }
            }else{
                //参照型の場合
                
                if (t != typeof(void))
                {
                    //voidではない場合
                    
                    //抽象クラス、あるいはインターフェイスであればnullを返す
                    if (t.IsAbstract || t.IsInterface) { return null; }

                    if (t.IsMarshalByRef)
                    {
                        //参照渡しでマーシャリングされる場合

                        //NullProxy<>の型を取得
                        Type genericType = typeof(NullProxy<>);
                        //NullProxyに必要なジェネリック型を生成
                        Type constructGenericType = genericType.MakeGenericType(new Type[] { t });
                        //要求されているNullProxy<>インスタンスを生成
                        object nullproxy = Activator.CreateInstance(constructGenericType);

                        //リフレクションを利用してNullObjectを生成
                        response = nullproxy.GetType().InvokeMember("GetInstance", BindingFlags.InvokeMethod, null, response, null);
                    }
                }
            }        
            return response;
        }

        /// <summary>
        /// 対象の型のNullObjectインスタンスを取得します。
        /// 各型のNullObjectインスタンスは、ガベコレで収集されるまでの間Singletonで提供されます。
        /// </summary>
        /// <returns>対象の型のNullObjectインスタンス</returns>
        public static T GetInstance()
        {
            lock (syncObj)
            {
                NullProxy<T> result = null;
                Type t = typeof(T);

                //ガベコレ対象のまま参照します。
                WeakReference w = dicNullObject.ContainsKey(t) ? dicNullObject[t] : null;

                if (w != null){
                    result = w.Target as NullProxy<T>;
                }
                
                if (result == null)
                {
                    result = new NullProxy<T>();
                    
                    dicNullObject[t] = new WeakReference(result,true);
                }

                // NullObject対象の型の透過プロキシを返します。
                return (T)result.GetTransparentProxy();
            }
        }
    }
}

NullObjectパターンオマケ
NullObjectパターン - C# - 雑なサンプルソース右クリックで保存


RealProxyを利用した割り込みについて

最後に、.NET FrameworkのRealProxyを利用した割り込みについておさらいを。


.NET FrameworkのRealProxyを利用した割り込みでは、割り込み対象(ターゲット)のクラスごとに、
Proxyオブジェクトと呼ばれるヘルパーオブジェクトを作成します。
Proxyオブジェクトには、ターゲットのインスタンスを偽装する透過プロキシ (transparent proxy)と、
割り込みハンドラを実装する実プロキシ(real proxy)の2種類が存在し、
ふたつのプロキシが協調し合う形で割り込み機構をプログラマに提供します。

透過プロキシに対してなんらかのメソッドがコールされると、
この呼び出しに関する情報をパックしたコールメッセージ(IMessage)がRealProxyに送られ、
これに対してRealProxyはコールメッセージから呼び出し情報をもとに、
メソッドの戻り値に相当するリターンメッセージ(IMessage)を生成して返すという仕組み。
最終的にRealProxyによる割り込みで返すリターンメッセージは、
System.Runtime.Remoting.Messaging.ReturnMessageのコンストラクタを呼ぶなどして生成し、
透過プロキシのメソッドの戻り値として呼び出し元に返されることとなる。
RealProxyによる割り込みハンドラは、Invokeメソッドをオーバーライドすることで実装することができる。


また、RealProxyでターゲットクラスのインスタンスを知ることができる場合、
ターゲットインスタンスにメソッド本来の仕事をさせるためには、
ターゲットインスタンスとコールメッセージを用いて、System.Runtime.Remoting.RemotingServicesクラスの
ExecuteMessageメソッドに渡すことで実現することができる。


以上のことから、ターゲットのメソッド呼び出しに対して柔軟な干渉が行えることがわかる。
System.Runtime.Remoting.RemotingServices.ExecuteMessageを呼び出すにしても、
自前でSystem.Runtime.Remoting.Messaging.ReturnMessageのコンストラクタを呼ぶしても、
実際にターゲットのメソッドを呼び出すかどうかも自由だし、
ターゲットのメソッドの前後に、なんらかの関心事を挿入ることもできるし、
渡された引数をそのままターゲットに渡すかどうかも、ターゲットから返された戻り値を
そのまま呼び出し元に返すかも、RealProxy次第となる。
Proxyというぐらいですからあたり前っちゃー当たり前ですがw


割り込みターゲットとなるクラスの制限
RealProxyを利用する場合、割り込みターゲットとなるクラスに制限が必要となる。
JITコンパイラによりターゲットクラスのメソッド呼び出しがインライン化されると、
ソースコードの上では期待される割り込みが回避されてしまう。この問題を避けるために、
JITコンパイラがインライン化を抑制するトリガーとなる特別なクラスを使用する必要がある。
そのクラスこそがSystem.MarshalByRefObject。System.MarshalByRefObjectから派生したクラスは、
JITコンパイラのインライン展開対象から外れるので、RealProxyクラスによる割り込みが許可された状態となる。

*1:NullObjectパターンは、J2EE設計者であるBobby Woolfによって提案されました。