VB.NETとログについての伺か
はじめに
弊社はロギングライブラリとかの扱いについて割と長い間悩んでいたのですが、自分の中のとりあえずの答えが出たので今回はそんな感じです。
ログライブラリへの強依存
さて、弊社はログライブラリとしてNLogに慣れ親しんでいるのでサンプル中で使うライブラリとしてNLogを使用しますが、Serilogやlog4netを使用しても大して変わらないと思います。たぶん*1
さて、NLogでログを取得するコードを書くとすると以下のような感じになるのではないでしょうか。
Module Module1 Sub Main() Dim model = New SomeModel() Console.WriteLine($"{model.Add(1, 2)}") Console.WriteLine($"{model.Subtract(2, 1)}") Console.WriteLine($"{model.Subtract(1, 2)}") End Sub End Module Class SomeModel Private Shared ReadOnly Logger As NLog.ILogger = NLog.LogManager.GetCurrentClassLogger() Function Add(x As Integer, y As Integer) As Integer Dim result = x + y Logger.Info("{0} + {1} = {2}", x, y, result) Return result End Function Function Subtract(x As Integer, y As Integer) As Integer Dim result = x - y If result < 0 Then Logger.Warn("{0} - {1} is negative ({2})", x, y, result) Return 0 Else Logger.Info("{0} - {1} = {2}", x, y, result) Return result End If End Function End Class
はい、そうですね。べっとりNLogに依存しちゃってます。 仮に他のロギングライブラリに移行することになった場合、ロギングコードを修正する必要が出てきますし、細かくログを取得しようとしていた場合には修正箇所は相当な数になるはずです。
インタフェース化とIoCコンテナ
はいはいインターフェースインターフェース。 メソッドをインターフェースに切り出すことができればあとはIoCコンテナとかを使って外部からロガーを注入してみんなハッピーになれます。
と、ここで壁にぶち当たりました。
どのようなメソッドとしてインタフェースを切り出しましょう
NLogやSerilog、log4netではログの書き出しメソッドはログレベルとして実装されています。 まずはログレベルで検討してみましょう。
NLog | Serilog | log4net | 簡単な説明 |
---|---|---|---|
Trace | Verbose | 該当なし | めっちゃ細かい |
Debug | Debug | Debug | デバッグ情報 |
Info | Information | Info | 通常のイベント情報 |
Warn | Warning | Warn | クリティカルではない警告 |
Error | Error | Error | エラー |
Fatal | Fatal | Fatal | ちょー致命的 |
という感じで多少であるものの、各ロギングライブラリで使用できるログレベルには差異があります。
また、NLogとlog4netはメッセージを出力するタイプのログライブラリですが、Serilogは構造化ログライブラリなのでインターフェースでメソッドのシグネチャを統一しても幸せになれそうではありません。
ではどのようなインタフェースを用意すればいいのかと言いますと、ログイベントを意味論としてメソッドを用意してあげれば良いのではとの結論に至りました。
先ほどの例ですと、以下のような感じになります。
Interface IMyLogger Sub CalcAdd(x As Integer, y As Integer, result As Integer) Sub CalcSubtract(x As Integer, y As Integer, result As Integer) Sub SubtractResultIsNegative(x As Integer, y As Integer, result As Integer) End Interface
この方法ですとプロジェクトごとにインターフェースが異なりますが、そもそもログイベントもプロジェクトごとに異なるんだからそもそもプロジェクトにまたがって共通化する必要はありませんよね。
また、イベントの種類だけメソッドが増大するという問題もありますが、そちらは、まぁ、その、いい感じにお願いします。
そんな感じで、Simple Injectorを使いつつ書き直したのが以下になります。
Imports NLog Imports SimpleInjector Module Module1 Sub Main() Dim c = New Container() Dim logger = New MyLogger(LogManager.GetCurrentClassLogger()) c.RegisterSingleton(Of IMyLogger)(logger) c.Register(Of SomeModel)() c.Verify() Dim model = c.GetInstance(Of SomeModel)() Console.WriteLine($"{model.Add(1, 2)}") Console.WriteLine($"{model.Subtract(2, 1)}") Console.WriteLine($"{model.Subtract(1, 2)}") End Sub End Module Class SomeModel Private ReadOnly _logger As IMyLogger Sub New(logger As IMyLogger) _logger = logger End Sub Function Add(x As Integer, y As Integer) As Integer Dim result = x + y _logger.CalcAdd(x, y, result) Return result End Function Function Subtract(x As Integer, y As Integer) As Integer Dim result = x - y If result < 0 Then _logger.SubtractResultIsNegative(x, y, result) Return 0 Else _logger.CalcSubtract(x, y, result) Return result End If End Function End Class Interface IMyLogger Sub CalcAdd(x As Integer, y As Integer, result As Integer) Sub CalcSubtract(x As Integer, y As Integer, result As Integer) Sub SubtractResultIsNegative(x As Integer, y As Integer, result As Integer) End Interface Class MyLogger Implements IMyLogger Private ReadOnly _nlog As ILogger Public Sub New(logger As ILogger) _nlog = logger End Sub Public Sub CalcAdd(x As Integer, y As Integer, result As Integer) Implements IMyLogger.CalcAdd _nlog.Info("{0} + {1} = {2}", x, y, result) End Sub Public Sub CalcSubtract(x As Integer, y As Integer, result As Integer) Implements IMyLogger.CalcSubtract _nlog.Info("{0} - {1} = {2}", x, y, result) End Sub Public Sub SubtractResultIsNegative(x As Integer, y As Integer, result As Integer) Implements IMyLogger.SubtractResultIsNegative _nlog.Warn("{0} - {1} is negative ({2})", x, y, result) End Sub End Class
この辺のアイデアは弊社が独自で思いついたというよりはSLABの概念とかLightNodeのコードをパクった参考にしたところがあるのでオリジナルへのリンクを付け加えておきます。
余談(ってほど余談でもない)
NLogの${callsite}
とかのスタックフレームのスキップ
<target xsi:type="ColoredConsole" name="console" layout="${longdate} ${uppercase:${level}} ${message}${newline}${stacktrace}${newline}${callsite}" />
みたいなレイアウトを設定している場合、出力結果は
2017-05-06 01:26:54.5080 INFO 1 + 2 = 3 Module1.Main => SomeModel.Add => MyLogger.CalcAdd ConsoleApp2.MyLogger.CalcAdd 3 2017-05-06 01:26:54.5281 INFO 2 - 1 = 1 Module1.Main => SomeModel.Subtract => MyLogger.CalcSubtract ConsoleApp2.MyLogger.CalcSubtract 1 2017-05-06 01:26:54.5281 WARN 1 - 2 is negative (-1) Module1.Main => SomeModel.Subtract => MyLogger.SubtractResultIsNegative ConsoleApp2.MyLogger.SubtractResultIsNegative 0
みたいになって、ログの実装クラスがしゃしゃり出てきてもんにょりします。
そんな時はskipFrames=Integer
を設定してあげるといい感じになります。
<target xsi:type="ColoredConsole" name="console" layout="${longdate} ${uppercase:${level}} ${message}${newline}${stacktrace:skipFrames=1}${newline}${callsite:skipFrames=1}" />
2017-05-06 01:30:57.3469 INFO 1 + 2 = 3 Module1.Main => SomeModel.Add ConsoleApp2.SomeModel.Add 3 2017-05-06 01:30:57.3700 INFO 2 - 1 = 1 Module1.Main => SomeModel.Subtract ConsoleApp2.SomeModel.Subtract 1 2017-05-06 01:30:57.3700 WARN 1 - 2 is negative (-1) Module1.Main => SomeModel.Subtract ConsoleApp2.SomeModel.Subtract 0
IoCコンテナとサービスロケーター
IoCコンテナを使い始めると
コンテナをコンストラクタ引数で渡して、コンテナから必要なオブジェクトを取り出せばいいんだ
みたいに勘違いしやすいと思います。というよりは弊社は勘違いしていました。
今回ので例を示すとこんな感じですね。
Class SomeModel Private ReadOnly _logger As IMyLogger Sub New(container As Container) _logger = container.GetInstance(Of IMyLogger)() End Sub Function Add(x As Integer, y As Integer) As Integer '... End Function Function Subtract(x As Integer, y As Integer) As Integer '... End Function End Class
これには以下のデメリットが発生します。
- 本来
IMyLogger
だけに依存したいはずなのに、IoCコンテナ(Container
)にも依存してしまっている- 本来なら必要のない別のクラスに依存することになり、依存を減らしたいはずなのに逆に増えてしまっている
- そのクラスが依存しているインターフェース・クラスがコンストラクタのシグネチャから判別がつかない
- コンテナが保持するインターフェース・クラスをすべて利用することが出来てしまうため、不用意に不要なクラスへアクセス出来てしまうため、なんかやばい
なんで、まぁサービスロケーターとして使うのはやめましょうねって感じで。
おわりに
弊社は最近では自動化ツールを作ったりコンソールアプリケーションを作成する機会が多いです。
そんな中、どのみち必要になるから最初からNuGetでぶち込んでおけと思うのが以下の3つです。
今回のでロギングライブラリのもんにょりが消えたので、残るはコマンドラインオプションパーサーになるのですが、今のところしっくりくるのがありません。
なければ作るしかないっぽいのでアレですが、当面はいい感じのを探していこうと思っています。
おわり
VB.NETでもAutofacで依存性を注入したい
はじめに
OpenCoverのコードを読んでいたら、AutofacというDIライブラリが使用されており、気になったのでそれについてです。
チュートリアル
Getting Started — Autofac 4.0 documentation
大体の手順をざっくりと列挙すると
- 脳内でいい感じに制御の反転を組み立てる
Autofac
の参照を追加する- アプリケーションの開始時に
ContainerBuilder
のインスタンスを生成し- コンポーネントを登録して
- コンテナを生成して
- アプリケーションの実行の間
- ライフタイムスコープを生成して
- ライフタイムスコープを使ってコンポーネントのインスタンスを解決する
といった感じです。
Imports Autofac Module Module1 Sub Main() Dim builder = New ContainerBuilder() builder.RegisterType(Of ConsoleOutput)().As(Of IOutput)() Dim container = builder.Build() Using scope = container.BeginLifetimeScope() Dim out = scope.Resolve(Of IOutput)() out.WriteLine("hello world") End Using End Sub End Module Interface IOutput Sub WriteLine(value As String) End Interface Class ConsoleOutput Implements IOutput, IDisposable Public Sub WriteLine(value As String) Implements IOutput.WriteLine Console.WriteLine(value) End Sub Public Sub Dispose() Implements IDisposable.Dispose Console.WriteLine("IDisposable.Dispose") End Sub End Class
hello world IDisposable.Dispose
直接コンテナからコンポーネントのインスタンスを生成せずに、ライフタイムスコープからインスタンスを生成することでライフタイムスコープが破棄された時点でライフタイムスコープから生成されたインスタンスも同時に破棄されます。
また、コンポーネントに複数のコンストラクタが存在する場合、コンテナ内に引数となりうる他のコンポーネントが登録されていればそのコンストラクタを使用してインスタンスを生成してくれます。
Module Module1 Sub Main() With "ジャムおじさんとバタコ" Dim builder = New ContainerBuilder() builder.RegisterType(Of AnpanMan)().As(Of IAnpanMan)() builder.RegisterType(Of Jam)().As(Of IJam)() builder.RegisterType(Of Batako)().As(Of IBatako)() Dim container = builder.Build() Using scope = container.BeginLifetimeScope() scope.Resolve(Of IAnpanMan)().Anpanchi() End Using End With With "バタコのみ" Dim builder = New ContainerBuilder() builder.RegisterType(Of AnpanMan)().As(Of IAnpanMan)() builder.RegisterType(Of Batako)().As(Of IBatako)() Dim container = builder.Build() Using scope = container.BeginLifetimeScope() scope.Resolve(Of IAnpanMan)().Anpanchi() End Using End With End Sub End Module Interface IBatako End Interface Class Batako Implements IBatako End Class Interface IJam End Interface Class Jam Implements IJam End Class Interface IAnpanMan Sub Anpanchi() End Interface Class AnpanMan Implements IAnpanMan Private _anpanchi As String Public Sub New() _anpanchi = "顔が濡れて力が出ない" End Sub Public Sub New(batako As IBatako, jam As IJam) _anpanchi = "元気100倍アンパンマン" End Sub Public Sub Anpanchi() Implements IAnpanMan.Anpanchi Console.WriteLine(_anpanchi) End Sub End Class
元気100倍アンパンマン 顔が濡れて力が出ない
おわりに
無事に元気100倍アンパンチが出来ました。
余談ですが、ジャムおじさんやバタコさんは人間じゃなかったんですね。
おわり
VB15の新機能の確認したい
はじめに
3月7日にVisual Studio 2017がリリースされました。
と言うことで今回も露骨なアクセス稼ぎVB15の新機能を確認していきましょう。
どうでもいいですが、VS2017のアイコンって梵字っぽいですよね。筆で書かれたっぽくして色を周りに合わせたら紛れてても気付かなそう。
新機能
新機能はこちらにまとまっているっぽいです。
今回もプログラミングの効率を上げる嬉しい新機能が目白押しではありませんでした。ざんねん
- 値タプル
ByRef
による使用量の戻り値- 二進数リテラルと桁区切り
上二つはなんかC#7.0との互換性の為に導入された感がありますし、二進数リテラルも『はぁ、そうですか』感が否めません。
まぁ、とりあえず確認していきましょう。
値タプル
タプルと言えば.NET Framework 4からSystem.Tuple
が存在しました。
存在しましたが2つ程欠点が存在しました。
System.Tuple
は参照型で何だかんだ言ってメモリの使用効率とGCのペナルティが発生する- タプルの要素へのアクセスに
Item1
やItem2
などといった人の温もりの感じられない名前で扱わないといけない
この2つの問題の解決策として導入されたのが値型のタプルであるSystem.ValueTuple
と言語機能による名前付きタプルのサポートです。
値型であるため、何だかんだ言ったメモリの使用効率の改善とGCペナルティの削減が図られています。
がこちらは遅延バインディングと暗黙の型変換とアンリミテッドボックス化ワークスにまみれているVBプログラマには関係ありませんGCによる予測困難な遅延の発生を極限まで抑えたい(≒ゲーム実装)などで効いてくることですので、VBの主戦場である生産性アプリケーションではほとんど関係ありません。正直パフォーマンスは二の次で良いから読めるコードを書いてくれ頼む
で、前置きが長くなりましたがVB15の新機能の1つ目の名前付きタプルのお時間です。
あの無味乾燥で温かみの感じられないItem1
などの名前ではなく、Syohizei
やKingaku
といった温かみのある名前で扱えるようになります。
Function HogeOld(a As IEnumerable(Of Integer)) As Tuple(Of Integer, Integer) Return Tuple.Create(a.Min(), a.Max()) End Function Function Hoge(a As IEnumerable(Of Integer)) As (min As Integer, max As Integer) Return (a.Min(), a.Max()) End Function
Dim a = {1, 2, 3, 4, 5, 6, 7, 8, 9} Dim b = HogeOld(a) Dim c = Hoge(a) Console.WriteLine($"min={b.Item1} max={b.Item2}") Console.WriteLine($"min={c.min} max={c.max}")
もしかしたら弊社の環境が壊れているだけかもしれませんが、この機能はなぜかソリューションを作成しただけでは使えません。
定義済みの型は定義されていません
とは面妖なエラーメッセージですが、どうにかして解決しましょう。
System.ValueTuple
の方はNuGetパッケージをインストールすると解決します。
PM> Install-Package System.ValueTuple
属性の方はNuGetパッケージの方にもないっぽいのでCoreFXのコードからコピペで対応します。
C#なら単純なコピペで行けますが、VBの場合はVBコードに脳内トランスパイルするか別プロジェクトとしてC#でコピペして参照を追加する必要があり軽い敗北感すら感じます。
ということでパパッとVBコードとして書き換えます。
Namespace Global.System.Runtime.CompilerServices <CLSCompliant(False)> <AttributeUsage(AttributeTargets.Field Or AttributeTargets.Parameter Or AttributeTargets.Property Or AttributeTargets.ReturnValue Or AttributeTargets.Class Or AttributeTargets.Struct Or AttributeTargets.Event)> Public NotInheritable Class TupleElementNamesAttribute Inherits Attribute Private ReadOnly _transformNames As String() Public Sub New(transformNames As String()) If transformNames Is Nothing Then Throw New ArgumentNullException(NameOf(transformNames)) End If _transformNames = transformNames End Sub Public Function TransformNames() As IList(Of String) Return _transformNames End Function End Class End Namespace
ByRef
による使用量の戻り値
誤訳なんじゃねーのってくらい意味が分かりませんが、機能の方も同じくらい意味が分かりません。
何よりも動くサンプルコードが現状見当たりません。
たぶんC#のref return
に対応する何かなんだと思います。
二進数リテラルと桁区切り
Dim answer = &B0010_1010 Console.WriteLine($"生命、宇宙、そして万物についての究極の疑問の答え = {answer}")
生命、宇宙、そして万物についての究極の疑問の答え = 42
おわりに
VB14でC#6.0との言語格差がかなり縮まったのでVBの今後に期待したのですが、VB15とC#7.0で差はまた開きつつありますね。
ちなみにC#7.0の新機能はこちらです。
あくまでも個人的な感想ですが、VBとC#は手続き型・オブジェクト指向プログラミング言語として(F#と比べて)かなり近い立ち位置にいると思います。 そんなパラダイムもベースとなるクラスライブラリ群もランタイムも共有する2つの言語を同じようにマイクロソフトがメンテナンスしていくかどうかには疑問を感じます。
VB.NETはVB6からC#への過渡期にVB6プログラマーをC#にスムーズに移行させるために作られた。かどうかは分かりませんが現状を見るとC#とVBのどちらに未来があるかといわれるとC#に軍配が上がるのは明らかです。 VBは初心者向けなんて言われることがありますが、少なくともVB.NETとC#は同じオブジェクト指向というパラダイムの上に成り立っている言語ですから言語としての複雑さは同程度なはずです。むしろ書籍・コミュニティの充実しているC#のほうが学びやすいのではないでしょうか。 仮にVBプログラマがC#を理解できないというのなら、それはC#が複雑だからではなくVBすら良く分かってない可能性があります。
とまぁ、お前はVBの何なんだよと言われそうな感じですが、弊社としてはVBは嫌いではないです。 ただ、恐らくはいずれC#(もしくは次の言語)に乗り換えないといけない時が来るのでいつまでも塩漬けにしておくのは得策ではありませんよって事です。
おわり