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 Sub … Public 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 Sub … Public 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 Sub … Public 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 Sub … Public 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:継承しなくてもいいですけど、ロギング処理を注入漏れを防ぐため継承しています