VB.NETで依存性注入(Dependency Injection)をどうやって実装するのか?

こんにちは!
今日は「VB.NETで依存性注入(Dependency Injection)をどうやって実装するのか?」というテーマでじっくり書きたいと思います。クラスの中で New しまくっていたあの頃にお別れを告げ、テストも保守もラクになる―そんな未来を一歩ずつ作っていきます。


そもそも依存性注入ってなんですか?

「依存性注入(DI)」は、オブジェクトが必要とするサービス(依存性)を自分で new しないで外部から受け取るテクニックです。これをものすごくざっくりいうと「欲しい物は自分で買いに行かず、届けてもらう仕組み」です。クラス内部で実装を決め打ちしないので、モックを渡して単体テストをしやすくなりますし、「Logger を Console 用からファイル用に差し替えたい」といった場面でもコードの変更が最小限ですみます。


一般的な “ありがちコード” とその困りごと

まずは昔ながらのコードを思い出してみます。

Public Class OrderService
    Private ReadOnly _logger As Logger = New Logger()
    Private ReadOnly _repository As OrderRepository = New OrderRepository()

    Public Sub Create(order As Order)
        _logger.Info("注文を登録します")
        _repository.Save(order)
    End Sub
End Class
  • OrderServiceLoggerOrderRepository の具体型を 直接 new しています。
  • 実装がガチガチに結び付いているので、あとから差し替えようとすると大手術になりがちです。
  • テスト時にモックを入れたくても new が邪魔で差し込みづらいです。

「とりあえず動くからいいや」と放置すると、気付けば “New 地獄” です。そこで DI の出番というわけです。


DI を実装するための大まかな3ステップ

  1. 契約(Interface)を切り出す
  2. サービスコンテナに登録する
  3. コンストラクター経由で注入(受け取り)する

これだけ聞くと拍子抜けするほどシンプルですが、各ステップでつまずきポイントがありますので順番に見ていきましょう。


1. 契約(Interface)を切り出す

まずは “差し替え可能” にするための土台を作ります。VB.NET でも Interface を使えば OK です。

Public Interface ILogger
    Sub Info(message As String)
End Interface

Public Class ConsoleLogger
    Implements ILogger

    Public Sub Info(message As String) Implements ILogger.Info
        Console.WriteLine($"[INFO] {message}")
    End Sub
End Class

ILogger は「情報を記録する」という役割だけを示します。実際の出力先(コンソール・ファイル・クラウドログサービスなど)は実装クラスが受け持ちます。


2. サービスコンテナに登録する

.NET(5 以降)の世界では Microsoft.Extensions.DependencyInjection事実上の標準コンテナになっています。VB.NET でも NuGet でパッケージを追加すればすぐに使えます。

Imports Microsoft.Extensions.DependencyInjection

Module Program
    Sub Main()
        Dim services = New ServiceCollection()

        ' ★ ここが登録ポイント
        services.AddSingleton(Of ILogger, ConsoleLogger)()
        services.AddTransient(Of OrderRepository)()
        services.AddTransient(Of OrderService)()

        Dim provider = services.BuildServiceProvider()

        ' アプリのエントリで DI されたクラスを呼び出す
        Dim svc = provider.GetRequiredService(Of OrderService)()
        svc.Create(New Order())
    End Sub
End Module
  • AddSingleton → アプリ全体で1個だけ共有したいもの(設定・キャッシュなど)
  • AddTransient → 呼び出すたびに新しいインスタンスが欲しいもの(軽量オブジェクトやステートレスなサービス)
  • AddScoped → Web アプリの HTTP リクエストやデスクトップアプリの「1画面」単位で共有したい場合に使います

WinForms/WPF のようなデスクトップアプリでは AddScoped が少し特殊で、「フォームをスコープの境界」にするケースが多いです。後述しますね。


3. コンストラクターインジェクション

登録だけで満足してはいけません。実際にサービスをもらわないと意味がありませんので、コンストラクターに引数として書くのが王道です。

Public Class OrderService
    Private ReadOnly _logger As ILogger
    Private ReadOnly _repository As OrderRepository

    ' ★ コンストラクターで受け取る
    Public Sub New(logger As ILogger, repository As OrderRepository)
        _logger = logger
        _repository = repository
    End Sub

    Public Sub Create(order As Order)
        _logger.Info("注文を登録します")
        _repository.Save(order)
    End Sub
End Class

DI コンテナは引数の型を見て「どの実装を渡すか」を判断します。先ほど AddSingleton(Of ILogger, ConsoleLogger) と登録したおかげで、OrderService を要求すると自動的に ConsoleLogger が注入されます。


WinForms/WPF で DI を使うときの “ちょっとしたコツ”

デスクトップアプリでは「いきなり Application.Run(New MainForm())」と書きたくなりますが、ここも DI 経由で解決できます。

Dim provider = services.BuildServiceProvider()
Application.Run(provider.GetRequiredService(Of MainForm)())

こうすれば、MainForm 自体や、その中で new しているコントロールにまで DI を流し込めます。さらにフォームごとにスコープを切りたい場合は、Using provider.CreateScope() を使い、フォームが閉じるタイミングでスコープを破棄するとリソースリークを防げます。

Using scope = provider.CreateScope()
    Dim form = scope.ServiceProvider.GetRequiredService(Of DetailForm)()
    form.ShowDialog()
End Using

よくある疑問とハマりポイント

「Setter Injection や Property Injection じゃダメ?」

急いでいると Public Property Logger As ILogger のようにプロパティで注入したくなりますが、コンストラクターで依存性を明示したほうが必須依存を逃さないので安全です。後付けで設定し忘れる事故も減ります。

「循環参照が出たらどうする?」

A → BB → A を同時に必要とすると DI コンテナが解決不能になります。この場合は Mediator パターンなどで依存の向きを一本化するとスッキリします。ユーザーさんは以前 Mediator に興味があると仰っていましたので、ぜひ合わせ技で試してみてください。

「Dispose されないって警告が出るんですが?」

AddSingleton したクラスが IDisposable を実装していると、アプリ終了時に自動的に Dispose されます。ただし自前で New したインスタンスを AddSingleton(Of ILogger)(New ConsoleLogger()) のように手渡した場合は、コンテナは Dispose しません。ライフサイクルを委譲したいときは、型+実装を渡す形にしておくとミスしにくいです。


DI × テストがもたらすうれしい未来

DI を導入するとテストダブル(モック・スタブなど)を差し込むのが劇的に楽になります。ILogger に対して

Public Class TestLogger
    Implements ILogger
    Public ReadOnly Messages As New List(Of String)()

    Public Sub Info(message As String) Implements ILogger.Info
        Messages.Add(message)
    End Sub
End Class

という簡易実装を渡せば、テストコード側で「呼ばれたかどうか」を assert できます。New 直書き時代では考えられなかった快適さです。


とはいえ、導入ハードルはゼロじゃない

  • 初学者には概念が抽象的で「何が良いのか分からない」と感じやすいです
  • クラス数が少ない小規模アプリだと「設定ファイルが増えて逆に複雑」と言われがちです
  • コンストラクターの引数が増えすぎて“インジェクション地獄”に陥るケースもあります

このあたりは「メリットがコストを上回る箇所だけ適用する」くらいの柔軟さがちょうどいいです。いきなり全クラスを DI 化するのではなく、テストしづらい箇所・差し替え頻度が高い箇所から始めてみてください。


というわけで、まとめ

  • 依存性注入は「new を外に出す」だけのシンプルなアイデアですが、保守性とテスト容易性に大きく効きます
  • VB.NET でも Microsoft.Extensions.DependencyInjection を使えば わずか数行で DI コンテナを導入できます
  • コンストラクターインジェクションが王道。Setter Injection は必須依存を見落としやすいので注意です
  • ライフサイクル(Singleton/Scoped/Transient)を正しく選ぶと メモリリークや Dispose 忘れを防げます

DI は魔法ではありませんが、使いどころを押さえれば開発効率がぐっと上がります。この記事が皆さんの「New 地獄」脱出の一歩になればうれしいです。それでは、楽しい VB.NET ライフを!