C#2.0でMix-inを実現する

C#3.0で導入される拡張メソッドを使うとMix-in的なことができます。非常に便利な仕組みで是非C#2.0+VS2005でも使ってみたいと思いチャレンジします。*1

外部メソッド(Foreign Method)を使う

C#3.0で導入された拡張メソッドは、外部メソッド・パターンをコンパイラが自動的に解釈できる仕組みを用意したと捉えることができます。
外部メソッドは第1引数に処理対象のオブジェクトを受け取り、処理対象オブジェクトを利用して処理を行うことで、処理対象のオブジェクトを機能拡張するアイデアです。
以下は、Humanクラスに対してOutputGreetingメソッドを外部メソッドを利用して機能拡張する例です。

public class Human
{
    public string Greeting
    {
        get { return "Hello"; }
    }
}

public static class GreetingableModule 
{
    public static void OutputGreeting(Human target)
    {
        Console.WriteLine(target.Greeting);
    }
}

Human h = new Human();
GreetingableModule.OutputGreeting(h);

この方式では、Humanクラスを直接拡張していないので、Humanクラスのメソッドとしてアクセスできません。外部メソッドの利用は多くの場合、元のクラスのコードを変更できない場合に利用される方法で一時的な機能拡張として利用されます。

継承を利用して元クラスを拡張する

元のクラスのメソッドとして利用できるように継承を利用して元のクラスを拡張します。外部メソッドで定義したメソッドに対応するメソッドを継承を行い追加します。追加したメソッドでは、単に外部メソッドを呼び出するようにすればOKです。

public class GreetingableHuman : Human
{
    public void OutputGreeting()
    {
        GreetingableModule.OutputGreeting(this);
    }
}

これで、GreetingableHumanを利用すればOutputGreetingメソッドを利用することができます。

機能別(役割)インタフェースを導入する

先程作成したGreetingableHumanはOutputGreetingをというメソッドを持っています。このメソッドは他のクラスでも同様に実装することができ呼び出し先のGreetingableModuleクラスの実装を共有します。
しかし、OutputGreeting機能は継承している訳ではないのでOutputGreeting機能をもつクラスをクライアントから統一的に扱うことができません。そこで、挨拶機能インタフェースを定義してクライアントから利用可能にします。

//挨拶機能インタフェース
public interface IGreetingable 
{
    void OutputGreeting();
}

public class GreetingableHuman : Human, IGreetingable
{
    public string Greeting
    {
        get { return "Hello"; }
    }

    public void OutputGreeting()
    {
        GreetingableModule.OutputGreeting(this);
    }
}

IGreetingable gh = new GreetingableHuman();
gh.OutputGreeting();

GreetingableModuleを汎用化する

GreetingableModuleを汎用的にするために、GreetingableModuleが依存している挨拶機能のインタフェースを定義します。Humanクラスの呼び出しているメソッドをインターフェースとして定義し利用します。

public interface IGreeting
{
    string Greeting { get;}
}

public static class GreetingableModule 
{
    public static void OutputGreeting(IGreeting target)
    {
        Console.WriteLine(target.Greeting);
    }
}

public class GreetingableHuman : Human, IGreeting, IGreetingable
{
    ...
}

パーシャルクラスで実装する

C#2.0にはパーシャルクラスという便利な機能があります。同じプロジェクトに限定されますが元のソースコードと別のファイルにクラスの拡張を記述することができます。継承を利用してMixInを実装してきましたが、この機能を利用して同じようなことを実現することができます。

public partial class Human : IGreeting  //元のクラスをパーシャル定義
{
  ...
}

public partial class Human : IGreetingable
{
    public void OutputGreeting()
    {
        GreetingableModule.OutputGreeting(this);
    }
}

わざわざサブクラスを作成しなくても良いのが利点です。

コードを自動生成する

C#3.0の拡張メソッドに近いことをC#2.0でも外部メソッドを使うことで実装できました。しかし、MixIn用の外部メソッドを呼び出しするためのコードを追加記述しないといけないため手間がかかります。こんな面倒なことはできればしたくありません。幸にも問題のコードは単純で自動的に生成することも難しくなさそうです。試しに作ってみましたので、サンプルコードを付けておきます。あとはVSのカスタムツールとして組み込めばより簡単に利用することができます。

public class MixinCodeGenerator
{
    private Type _mixinType;
    private Type _includeType;
    private Type _interfaceType;

    private CodeCompileUnit code;

    /// <summary>
    /// MixInのコードを生成します。
    /// </summary>
    /// <param name="provider">コードプロバイダー(C#またはVBのみ想定)</param>
    /// <param name="mixinType">MixInする元のクラス</param>
    /// <param name="includeType">MixInする外部メソッドを持つ静的クラス</param>
    /// <param name="interfaceType">MixInで提供される機能を宣言したインタフェース</param>
    /// <returns></returns>
    /// <remarks>
    /// <code>
    /// <para>
    /// </para>
    /// 生成されるコード例
    /// <![CDATA[
    /// partial class Frog : MixInSample.IGreetingable
    /// {
    /// 
    ///     public virtual void OutputGreeting()
    ///     {
    ///         MixInSample.GreetingableModule.OutputGreeting(this);
    ///     }
    /// 
    ///     public virtual void OutputGreeting(string msg)
    ///     {
    ///         MixInSample.GreetingableModule.OutputGreeting(this, msg);
    ///     }
    /// }
    /// ]]>
    /// </code>
    /// </remarks>
    public string GenerateMixinClass(CodeDomProvider provider, Type mixinType, 
        Type includeType, Type interfaceType)
    {
        this._mixinType = mixinType;
        this._includeType = includeType;
        this._interfaceType = interfaceType;

        code = new CodeCompileUnit();
        CodeNamespace nameSpace = new CodeNamespace(mixinType.Namespace);
        code.Namespaces.Add(nameSpace);
        CodeTypeDeclaration classObject = new CodeTypeDeclaration(mixinType.Name);
        classObject.BaseTypes.Add(interfaceType);
        classObject.IsPartial = true;
        classObject.TypeAttributes = 
            mixinType.IsPublic ? TypeAttributes.Public : TypeAttributes.NotPublic;

        nameSpace.Types.Add(classObject);
        foreach (MethodInfo method in CodeHelper.GetInterfaceMethodList(interfaceType,true))
        {
            CodeMemberMethod geneMethod = new CodeMemberMethod();
            geneMethod.ReturnType = new CodeTypeReference(method.ReturnType);
            geneMethod.Name = method.Name;
            geneMethod.Attributes = MemberAttributes.Public;
            geneMethod.Parameters.AddRange(CodeHelper.GenerateMethodArgs(method));
            geneMethod.Statements.Add(GenerateMethodBody(method));
            classObject.Members.Add(geneMethod);
        }
        using (StringWriter writer = new StringWriter())
        {
            provider.GenerateCodeFromCompileUnit(code, writer, null);
            return writer.ToString();
        }
    }

    /// <summary>
    /// 外部メソッドを呼び出すコードを生成する
    /// </summary>
    /// <param name="method"></param>
    /// <returns></returns>
    private CodeStatement GenerateMethodBody(MethodInfo method)
    {
        //外部メソッドの参照を取得
        CodeTypeReferenceExpression targetClass
            = new CodeTypeReferenceExpression(_includeType);
        CodeMethodReferenceExpression targetMethod
            = new CodeMethodReferenceExpression(targetClass, method.Name);

        //メソッドの呼び出し文を作成し、thisおよび自メソッドの仮引数を引数に設定
        CodeMethodInvokeExpression invoke = new CodeMethodInvokeExpression();
        invoke.Method = targetMethod;
        invoke.Parameters.Add(new CodeThisReferenceExpression());
        foreach (ParameterInfo arg in method.GetParameters())
        {
            CodeArgumentReferenceExpression para = new CodeArgumentReferenceExpression(arg.Name);
            invoke.Parameters.Add(new CodeDirectionExpression(CodeHelper.GetFieldDirection(arg), para));
        }

        if (method.ReturnType.Name == "Void")
        {
            return new CodeExpressionStatement(invoke);
        }
        else
        {
            return new CodeMethodReturnStatement(invoke);
        }
    }

}

internal static class CodeHelper
{
    /// <summary>
    /// インタフェースに定義されているメソッドの一覧を取得します
    /// </summary>
    /// <param name="interfaceType">インタフェースの型</param>
    /// <param name="includeSub">サブインタフェースで定義されているメソッドを含めるかどうか</param>
    /// <returns></returns>
    public static MethodInfo[] GetInterfaceMethodList(Type interfaceType, bool includeSub)
    {
        List<MethodInfo> methodList = new List<MethodInfo>();
        List<Type> typeList = new List<Type>();
        CollectMethodList(interfaceType, methodList, typeList, includeSub);
        return methodList.ToArray();
    }

    private static void CollectMethodList(Type classType, List<MethodInfo> methodList, 
        List<Type> typeList, bool includeSub)
    {
        foreach (MethodInfo method in classType.GetMethods())
        {
            if (!method.Name.StartsWith("set_") && !method.Name.StartsWith("get_"))
            {
                methodList.Add(method);
            }
        }
        typeList.Add(classType);
        if (!includeSub) return;
        foreach (Type subInterface in classType.GetInterfaces())
        {
            if (typeList.Contains(subInterface)) continue;
            CollectMethodList(subInterface, methodList, typeList, includeSub);
        }
    }

    /// <summary>
    /// メソッド引数のコード生成
    /// </summary>
    /// <param name="method"></param>
    /// <returns></returns>
    public static CodeParameterDeclarationExpression[] GenerateMethodArgs(MethodInfo method)
    {
        List<CodeParameterDeclarationExpression> parameters 
            = new List<CodeParameterDeclarationExpression>();
        foreach (ParameterInfo arg in method.GetParameters())
        {
            CodeParameterDeclarationExpression para = new CodeParameterDeclarationExpression();
            para.Name = arg.Name;
            para.Type = GetCodeTypeReference(arg);
            para.Direction = GetFieldDirection(arg);
            parameters.Add(para);
        }
        return parameters.ToArray();
    }

    /// <summary>
    /// 引数の型を取得する
    /// </summary>
    /// <param name="arg"></param>
    /// <returns></returns>
    /// <remarks>ByRefの場合はByRefではない型を返す</remarks>
    public static CodeTypeReference GetCodeTypeReference(ParameterInfo arg)
    {
        if (!arg.ParameterType.IsByRef) return new CodeTypeReference(arg.ParameterType);
        return new CodeTypeReference(arg.ParameterType.FullName.TrimEnd('&'));
    }

    /// <summary>
    /// パラメータの修飾の変換
    /// </summary>
    /// <param name="arg"></param>
    /// <returns></returns>
    public static FieldDirection GetFieldDirection(ParameterInfo arg)
    {
        if (arg.IsOut) return FieldDirection.Out;
        if (arg.ParameterType.IsByRef) return FieldDirection.Ref;
        return FieldDirection.In;
    }
}

*1:Castle ProjectのDynamicProxyを利用すると動的にMixinクラスを作成することもできるのですが、生成時の特別な処理やキャストが必要だったりしてイマイチ使いにくい、何より特別なライブラリが必要になるのが足かせになることが多い