.NETで作る!

.NETに関するあれこれ(C#、VB.NET)

Actionデリゲートでアスペクト指向なロギング

アスペクト指向」って書くと多少語弊がある気がしますが、横断要素を一箇所に記述できるような言語要素って意味合いでとっていただければと。

2015/02/28追記

デリゲートのコードがラッピングしていないかったので全く意味が違うものになっていました。訂正。

普通にロギングする

何にも考えずにサブルーチンのログを取ろうとするとこんな感じのコードになります。

Public Sub Method1()
    logger.Info("Start")
    'TODO
    logger.Info("End")
End Sub
Public Sub Method2()
    logger.Info("Start")
    'TODO
    logger.Info("End")
End SubPublic Sub MethodN()
    logger.Info("Start")
    'TODO
    logger.Info("End")
End Sub

ログに関する処理は同じだけど実体は違うので、これ以上まとめることはできません。しかしこのままでは単に記述が面倒なだけでなく、保守性にも問題がありそうです。

定型とはいえロギング処理書くのメンドイ…

あ、例外時もロギングしなきゃ…

え、もうこの書式で大量にコーディングしちゃったよ…

Actionデリゲートにしてみる

現時点ではまったく意味はないですが、処理をActionデリゲートにして抽象化してみます。

Public Sub Method1()
    Dim act = AddressOf Me.Method1Core
    act.Invoke()
End Sub
Private Sub Method1Core()
    logger.Info("Start")
    'TODO
    logger.Info("End")
End Sub
Public Sub Method2()
    Dim act = AddressOf Me.Method2Core
    act.Invoke()
End Sub
Private Sub Method2Core()
    logger.Info("Start")
    'TODO
    logger.Info("End")
End SubPublic Sub MethodN()
    Dim act = AddressOf Me.Method2Core
    act.Invoke()
End Sub
Private Sub MethodNCore()
    logger.Info("Start")
    'TODO
    logger.Info("End")
End Sub

ロギング処理と本来の処理を切り離す

ロギング処理をMethodNに移してみましょう。

Public Sub Method1()
    Dim act = AddressOf Me.Method1Core
    logger.Info("Start")
    act.Invoke()
    logger.Info("End")
End Sub
Private Sub Method1Core()
    'TODO
End Sub
Public Sub Method2()
    Dim act = AddressOf Me.Method2Core
    logger.Info("Start")
    act.Invoke()
    logger.Info("End")
End Sub
Private Sub Method2Core()
    'TODO
End SubPublic Sub MethodN()
    Dim act = AddressOf Me.Method2Core
    logger.Info("Start")
    act.Invoke()
    logger.Info("End")
End Sub
Private Sub MethodNCore()
    'TODO
End Sub

これで、MethodNCoreは本当に必要なロジックだけになりました…ってところも注目ですが、もう一つ注目するところがあります。

    logger.Info("Start")
    act.Invoke()
    logger.Info("End")

このコードが何回も出てきてますね。拡張メソッドで汎化させてしまいましょう。

ロギング処理を汎化させる

拡張メソッドを使わなくても汎化できますが、拡張メソッドで実装する方が自然でしょう。(しれっと例外処理もいれちゃいましょう)

Public Module
    <Runtime.CompilerServices.Extension>
    Public Sub InjectLogging(source As action) As Action
        Dim act = Sub()
                      logger.Info("Start")
                      Try
                        source.Invoke()
                        ogger.Info("End")
                      Catch ex As Exception
                        logger.Error(ex)
                        Throw
                      End Try
                  End Sub
        Return Sub
    End Sub
End Module
Public Sub Method1()
    Dim act = AddressOf Me.Method1Core
    act.InjectLogging.Invoke()
End Sub
Private Sub Method1Core()
    'TODO
End Sub
Public Sub Method2()
    Dim act = AddressOf Me.Method2Core
    act.InjectLogging.Invoke()
End Sub
Private Sub Method2Core()
    'TODO
End SubPublic Sub MethodN()
    Dim act = AddressOf Me.Method2Core
    act.InjectLogging.Invoke()
End Sub
Private Sub MethodNCore()
    'TODO
End Sub

これで横断要素(ロギング処理)を一箇所に記述できました!

が、書くにはかけましたが、だれがこんな奇妙なコード書くか疑問が出そうです。初見でこれ見たら「なんだコレ」と思うでしょう。

このコードを本当に書くかどうかはともかく、この考え方はMVVMのDelegateCommand(RelayCommand)と相性がいいんです。というか、これが本題です。

MVVMでの実例

PrismのDelegateCommandでの例

Public Class DelegateCommandEx
    Inherits DelegateCommand

    Public Sub New(executeMethod As Action)
        MyBase.New(executeMethod.InjectLogging)
    End Sub

    Public Sub New(executeMethod As Action, canExecuteFunction As Func(Of Boolean))
        MyBase.New(executeMethod.InjectLogging, canExecuteFunction)
    End Sub
End Class
    Public Sub New()Me.SaveCommand = New DelegateCommandEx(AddressOf Me.OnSave, AddressOf Me.CanSave)End Sub

    Public Property SaveCommand As DelegateCommand

    Public Sub OnSave()
        'TODO
    End Sub

    Public Overridable Function CanSave() As Boolean
        'TODO
    End Function

DelegateCommandを継承*1して、強制的にロギング処理を注入しています。見た目には全くそんな風情はありませんが、DelegateCommandではなく、DelegateCommandEx使用とするだけでロギング処理が実装できるわけですね。とっても便利。

さらに手を加えて「コマンド処理中はWait表示をする」なんてことも簡単に実装できますね。ざっくり書くとこんな感じ。

Public Module
    <Runtime.CompilerServices.Extension>
    Public Sub InjectWaiting(source As action) As Action   
        Dim act = Sub()
                      Try
                          Mouse.SetCursor(Cursors.Wait)
                              source.Invoke()
                      Finally
                          Mouse.SetCursor(Cursors.Arrow)
                      End Try
                  End Sub
        Return act
    End Sub
End Module
Public Class DelegateCommandEx
    Inherits DelegateCommand

    Public Sub New(executeMethod As Action)
        MyBase.New(executeMethod.InjectLogging.InjectWaiting)
    End Sub

    Public Sub New(executeMethod As Action, canExecuteFunction As Func(Of Boolean))
        MyBase.New(executeMethod.InjectLogging.InjectWaiting, canExecuteFunction)
    End Sub
End Class

*1:継承しなくてもいいですけど、ロギング処理を注入漏れを防ぐため継承しています

. .