これから始めるSpring.NET その6

前回 の続きです。

宣言的インジェクション

アプリケーションでよく必要になる処理でロギング処理というのがあります。

.NETでロギング処理を行う場合、Traceクラスを使うこともありますが通常、log4netなどのロギングライブラリを使用することになるでしょう。ここではlog4netを使っていきます。

そういった場合、コードの中で直接log4netのクラスを使うのではなく、以下のようなクラスにラップしてlog4netを隠蔽することになると思います。これは特定のロギングライブラリに依存することを避けることで、ライブラリの入れ替えに柔軟に対応できるようにするためです*1

Logger.cs

using System;
using System.Diagnostics;

using Spring.Objects.Factory;

namespace SpringSample.Logging {
    public class Logger : ILogger, IInitializingObject {
        private string name;
        /// <summary>
        /// log4netのロガー
        /// </summary>
        private log4net.ILog logger;
        
        /// <summary>
        /// ロガー名を設定します。
        /// </summary>
        public string Name {
            private get { return name; }
            set { name = value; }
        }

        public Logger() {
        }
        
        public void Info(string message) {
            logger.Info(message);
        }
        public void Debug(string message) {
            logger.Debug(message);
        }
        public void Error(string message) {
            logger.Error(message);
        }
        
        void IInitializingObject.AfterPropertiesSet() {
            logger = log4net.LogManager.GetLogger(Name);
        }
    }
    
    public interface ILogger {
        void Info(string message);
        void Debug(string message);
        void Error(string message);
    }
}

Spring.Objects.Factory.InitializingObject というインターフェースを実装していますが、このインターフェースを実装するとAfterPropertiesSetというメソッドが、オブジェクトのプロパティへのインジェクションが終了した時に呼び出されるのを利用して、log4netのロガーを初期化しています。このインターフェースは便利なので重宝します*2

このロガーを使うには以下のようにフィールドとプロパティを定義すればいいでしょう。

ContentsManager.cs
public class ContentsManager : IContentsManager {
    private ILogger logger;
    private string fileEncoding;
    
    public ILogger Logger {
        get { return logger; }
        set { logger = value; }
    }
    public string FileEncoding {
        get { return fileEncoding; }
        set { fileEncoding = value; }
    }
    public ContentsManager() { }

    public void Save(string contents, string fileName) {
        using(var sw = new StreamWriter(fileName, false, Encoding.GetEncoding(FileEncoding))) {
            sw.Write(contents);
        }
        Logger.Info("ファイルを保存しました");
    }
}

これを機能させるためには、オブジェクト定義ファイルを以下のように変更します。

objects.config
<object name="contentsManager" type="SpringSample.IO.ContentsManager, SpringSample">
    <property name="Logger" ref="logger" />
    <property name="FileEncoding" value="${file.encoding}" />
</object>

<object name="logger" type="SpringSample.Logging.Logger, SpringSample">
    <property name="Name" value="SpringSample" />
</object>

もちろんこのようにすれば問題なく使えますが、Loggerのように頻繁に使用するクラスをいちいちこうやって設定するのは非常に面倒くさいものです。こういった事がSpring.NETの普及を妨げる一因になっているような気もします。

しかし、実はこれは非常にスマートな方法で解決する事ができます。

まず、以下のようなインターフェースを定義します。

interface ILoggerHolder {
    ILogger Logger {
        set;
    }
}

そして、先程のContentsManagerクラスをこのインターフェースを実装するように変更しておきます。

Spring.NETには多種多用なインターフェースやクラスが用意されていますが、その中の一つに先程Loggerクラスで使用したIInitializingObjectインターフェースがあります。このインターフェースを実装すると、プロパティへのインジェクションが終了した時点でAfterPropertiesSetというメソッドが呼ばれます。

この機構はSpring.NETが用意するオブジェクトに対する各処理への割り込みを可能にするIObjectPostProcessor というインターフェースによって実現されています。
このインターフェースは以下の二つのメソッドを実装します。

interface IObjectPostProcessor {
    object PostProcessBeforeInitialization(object instance, string name);
    object PostProcessAfterInitialization(object instance, string objectName);
}

PostProcessBeforeInitialization メソッドはオブジェクトのプロパティへのインジェクションが行われる前に呼び出されます(ちなみにこのメソッドはDIコンテナに登録された全オブジェクトの数だけ呼び出されます)。
PostProcessAfterInitialization メソッドはその逆でオブジェクトのプロパティへのインジェクションが行われた後に呼び出されます。IInitializingObjectがこの機構を使って呼び出されているのがわかるでしょう。

では、ILoggerHolderインターフェースを実装したオブジェクトにLoggerを設定するPostProcessorを作ってみます。

LoggerPostProcessor.cs

using System;
using System.Linq;
using System.Diagnostics;

using Spring.Objects.Factory.Config;

namespace SpringSample.Logging {
    public class LoggerPostProcessor : IObjectPostProcessor {
        private ILogger logger;

        public ILogger Logger {
            private get { return logger; }
            set { logger = value; }
        }
        
        public LoggerPostProcessor() { }

        object IObjectPostProcessor.PostProcessBeforeInitialization(object instance, string name) {
            return instance;
        }
        object IObjectPostProcessor.PostProcessAfterInitialization(object instance, string objectName) {
            if(instance is ILoggerHolder) {
                ((ILoggerHolder)instance).Logger = this.Logger;
            }
            return instance;
        }
    }
}

IObjectPostProcessorインターフェースを実装したLoggerPostProcessorを定義しました。

このPostProcessorを有効にするには、以下のようにオブジェクト定義ファイルにobject定義を追加するだけです。特別な事は何もしません。

objects.config
<object name="loggerPostProcessor" type="SpringSample.Logging.LoggerPostProcessor, SpringSample">
    <property name="Logger">
        <object type="SpringSample.Logging.Logger, SpringSample">
            <property name="Name" value="SpringSample" />
        </object>
    </property>
</object>

これで、ILoggerHolderを実装したオブジェクトに自動的にロガーが設定されます(もちろんそのオブジェクトがDIコンテナに登録されている前提ですが)。

これが宣言的インジェクション*3です。

実は他にもAutoWiringという方法で自動的にオブジェクトをインジェクションできるのですが、これは間違いが起こりやすい上にパフォーマンス上あまりよろしくないのでお勧めしません。

*1:オープンソースのライブラリは突然開発中止になることもあるので、そういった時に対応できるようにしておくことも重要です

*2:プロパティに値が設定されているかをAssertする時などにも利用します

*3:この名前は私が勝手につけたので、あしからず