C# 歴1週間のう~です。
あるプロジェクト(すでに流れてしまいましたが( 前記事 ))で、ユーザ操作やエラー(例外)をログに残したいという要件があったので、その実現方法について何かうまいやり方は無いかなとちょっと考えていました(あちこちに try-catch を埋め込むなんて嫌ですからね)。
NUnit が属性とリフレクションの機能を使って実行するテストを指定できるようになっているのを見て、操作ログも同じように属性を使って記録できないかなぁ~なんて思って調べていたら、やっぱりやっている人たちがいるんですね…
http://www.ne.jp/asahi/nami/mei/cstips/methodlog.html
http://d.hatena.ne.jp/zecl/searchdiary?word=*%5BC%23%5D
上の記事を参考にして自分でもプロトタイプを書いてみたのですが、結論から言うとこれは使えないなぁ~といった感じです。理由は後で述べますが、とりあえず、以下、サンプルコードです…(C# 2.0)
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Proxies;
using System.Runtime.Remoting.Activation;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Services;
using System.Diagnostics;
namespace LogginAspectPrototype {
[AttributeUsage(AttributeTargets.Class)]
public class LoggingAttribute : ProxyAttribute
{
public override MarshalByRefObject CreateInstance(Type serverType)
{
return (MarshalByRefObject)(new LoggingProxy(
base.CreateInstance(serverType), serverType
)).GetTransparentProxy();
}
}
[AttributeUsage(AttributeTargets.Method)]
public class TraceAttribute : Attribute
{
public string Message { get { return message; } }
protected TraceAttribute(string s)
{ message = s; }
private readonly string message;
}
public class MethodTraceAttribute : TraceAttribute
{
public MethodTraceAttribute(string s)
: base(s)
{}
}
public class ExceptionTraceAttribute : TraceAttribute
{
public ExceptionTraceAttribute(string s)
: base(s)
{}
}
public class LoggingProxy : RealProxy
{
public LoggingProxy(MarshalByRefObject target_, Type t)
: base(t)
{ target = target_; }
public override IMessage Invoke(IMessage msg)
{
if (msg is IConstructionCallMessage) {
RemotingServices.GetRealProxy(target)
.InitializeServerObject((IConstructionCallMessage)msg);
return EnterpriseServicesHelper
.CreateConstructionReturnMessage(
(IConstructionCallMessage)msg,
(MarshalByRefObject)this.GetTransparentProxy());
}
MethodBase methodBase = ((IMethodCallMessage)msg).MethodBase;
foreach (MethodTraceAttribute i
in methodBase.GetCustomAttributes(
typeof(MethodTraceAttribute), false))
{
Debug.Print(i.Message);
break;
}
try {
return RemotingServices.ExecuteMessage(
target, (IMethodCallMessage)msg);
}
catch (Exception e) {
※期待に反して残念ながらここでは例外を捕捉できないらしい…
foreach (ExceptionTraceAttribute i
in methodBase.GetCustomAttributes(
typeof(ExceptionTraceAttribute), false))
{
Debug.Print(i.Message);
break;
}
throw;
}
}
private readonly MarshalByRefObject target;
}
[Logging()]
public class Widget: ContextBoundObject
{
[MethodTrace("何かの操作")]
public void DoSomething()
{}
[ExceptionTrace("例外")]
public void ThrowException()
{
throw new Exception("!!!");
}
}
class Program
{
static void Main(string[] args)
{
Widget widget = new Widget();
widget.DoSomething();
try {
widget.ThrowException();
}
catch (Exception e) {
Debug.Print(e.ToString());
}
}
}
}
ざっくりと説明すると、ProxyAttribute を継承した属性クラスで CreateInstance メソッドをオーバーライドし、そこでプロキシクラス(上の例だと LoggingProxy)を生成し、プロキシクラスの方は RealProxy を継承して Invoke メソッドをオーバーライドし、そこでメソッドやコンストラクタをフックします。
さて肝心の動作についてですが、上のプロトタイプで [ExceptionTrace] 属性を付けたメソッドが例外を投げたらログに記録しようという試みは失敗でした(ちなみに試したのは .NET Framework 2.0 です)。理由はよくわかりませんが、LoggingProxy クラスでオーバーライドした Invoke メソッド内では Widget.ThrowException メソッドが投げる例外を捕捉できませんでした(とほほ…)。.NET Framework とはそういうものなのでしょう…、例外ログに関しては別のやり方を考えないといけないみたいです。
次に操作ログについてですが、確かに上のプロトタイプでメソッドのログを採れるのですが、これはあんまり実用的ではないなぁ~というのが率直な感想です。以下がその理由なんですが、まず、属性を使う場合とメソッド内にインラインで書く場合とで、メンテナンス性でたいして差はないということです。次の2通りの書き方を見ればそれはわかると思います。
// 属性を使う場合
[MethodTrace("何とかの操作")]
public void DoSomething()
{
...
}
// メソッド内に直接埋め込む場合
public void DoSomething()
{
XXXX.MethodTrace("何とかの操作");
...
}
現状、Aspect Weaver が使えない以上、手作業で目的のファイルを開いてログを埋め込んでいかなければならないことに変わりありません。属性がメンテナンス性で特に優位というわけでもなく(あくまでログに限った話)、他に何かメリットでもあればよいのですが、特に思い浮かぶメリットもありません。それどころか以下のようなデメリットの方が目立ちます。
1.オーバーヘッド
2.Mixin できない(クラス設計の自由度を奪われる)
(1)のオーバーヘッドについてですが、Proxy を使ってメソッド呼び出しをフックする以上、通常の呼び出しよりも余分な処理を必要とします。しかもこれは、属性が付いていないメソッドの呼び出しにも付いて回ります。
(2)の Mixin できないというのは、ログを残す対象となるクラスが ContextBoundObject を継承しなければならないところから来ています。既存の継承階層の中で属性を使ったログ機能を付加しようと思っても、多重継承できない C# ではうまくいきません。継承のルートまで遡って親クラスに ContextBoundObject を継承させればできなくもありませんが、しかし、そうするとすべてのサブクラスのメソッド呼び出しに(1)のオーバーヘッドが付いて回ることになるのです。さすがにそれは嫌でしょう。(C# 3.0 の拡張メソッドを使えば Mixin できるかな??)
言語がアスペクト指向プログラミングをサポートしてくれれば、こういったことももっと簡単に行えるようになるんでしょうが…、いつになるやらですね。
結局、上の2つのデメリットを上回るメリットが見出せない以上、このテクニックは使えないな~ということになります(例外ログも残すことができたならちょっと考えなくもないんですけどね)。