はじめに
Unity プロジェクトが大きくなるにつれて、スクリプト間の依存関係が複雑になりがちです。
「このクラス、どこでインスタンス化されてる?」
「この機能を差し替えたいけど影響範囲がわからない……」
といった経験はないでしょうか。
DI(Dependency Injection:依存性注入) は、こうした問題を解決するためのデザインパターンの一つです。
本記事では、DI の基本概念から Unity での実践的な考え方までを、具体例を交えながら解説します。
注意:DI はあくまで目的を達成するための手段であり、導入することが目的ではありません。プロジェクトの規模や特性に応じて、適切に判断することが重要です。
1. DI(依存性注入)とは?
1-1. そもそも「依存」とは?
オブジェクト A がオブジェクト B を使用しているとき、A は B に依存していると言います。
例えば、Player クラスが AudioManager を使って効果音を再生する場合、Player は AudioManager に依存しています。
1-2. 「依存性注入」の正しい理解
「依存性注入」という日本語訳は、しばしば誤解を生みます。
英語の Dependency Injection における Dependency とは「依存性」ではなく、「依存している対象(=オブジェクト)」 を指します。
つまり DI とは、
あるオブジェクトが依存している別のオブジェクトを、外部から渡す(注入する)デザインパターン
です。
1-3. DI を使わない場合の問題
以下のコードを考えます。
public class MessageWriter
{
public void Write(string message)
{
Console.WriteLine("Message: " + message);
}
}
public class FooService
{
// 内部で MessageWriter を new して保持する
private readonly MessageWriter _messageWriter = new();
public void Execute()
{
_messageWriter.Write("Foo!");
}
}
一見問題なく動作しますが、設計上は以下の問題があります。
-
FooServiceがMessageWriterの具体的な実装に直接依存している -
MessageWriterを別の実装に差し替えたい場合、FooService自体を修正する必要がある - 単体テストで
MessageWriterの代わりにモックを使えない
1-4. DI を使った場合
// インターフェースを定義
public interface IMessageWriter
{
void Write(string message);
}
// 具体的な実装
public class ConsoleMessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine("Message: " + message);
}
}
// 依存をコンストラクタで受け取る
public class FooService
{
private readonly IMessageWriter _messageWriter;
public FooService(IMessageWriter messageWriter) // ← ここで注入
{
_messageWriter = messageWriter;
}
public void Execute()
{
_messageWriter.Write("Foo!");
}
}
このようにすることで、
- 依存関係がコンストラクタのシグネチャから明確になる
- 実装の差し替えが容易になる(テスト用のモックを渡せる)
- クラスは生成方法を気にしなくなる
といったメリットがあります。
2. DI のメリット・デメリット
メリット
| メリット | 説明 |
|---|---|
| 疎結合 | 依存関係がインターフェース経由になるため、実装の変更が影響しにくい |
| テスト容易性 | モックオブジェクトを注入することで単体テストが書きやすくなる |
| 依存関係の明確化 | コンストラクタ引数で依存が可視化されるため、可読性・保守性が向上する |
| ライフサイクルの一元管理 | DI コンテナがインスタンスの生成・破棄を管理するため、シングルトン管理などが容易になる |
| 拡張性 | 新しい依存が増えても、既存コードへの修正箇所を最小限に抑えられる |
デメリット
| デメリット | 説明 |
|---|---|
| 学習コスト | DI の概念やコンテナの使い方をチームで習得する必要がある |
| コード量の増加 | 設定やアノテーションが増え、シンプルな実装ではオーバーエンジニアリングになりうる |
| 処理のブラックボックス化 | DI コンテナによる自動注入は、デバッグ時に処理の流れが追いにくくなることがある |
| コンテナ依存 | 特定の DI フレームワークに依存すると、移行が難しくなる場合がある |
小規模なプロジェクトでは導入コストが見合わないこともあるため、規模や拡張性の見込みを考慮して判断しましょう。
3. Unity における DI の特殊性
3-1. 一般的な DI との違い
Unity の MonoBehaviour は new できない(UnityEngine.Object の制約)ため、コンストラクタインジェクションが使いにくいという特徴があります。
そのため、多くの Unity 向け DI フレームワーク(Zenject / Extenject、VContainer など)では、フィールドインジェクション([Inject] 属性)やメソッドインジェクションが採用されています。
public class Player : MonoBehaviour
{
[Inject] private IAudio _audio; // フィールドインジェクション
public void Attack()
{
_audio.PlaySE("Attack");
}
}
3-2. [Inject] 属性の注意点
[Inject] 属性は便利な一方で、以下の点に注意が必要です。
- DI コンテナの機能であり、DI そのものではない
- コンテナへの依存度が上がる(コンテナを変更する際の影響が大きい)
- 依存関係の解決がクラス外部で行われるため、どこで何が注入されているかが追いにくくなる
- 「よくわかんないけどなんか動いてる!」状態を助長する恐れがある
可能な限りコンストラクタインジェクションを優先し、どうしても無理な場合にのみフィールドインジェクションを使用するのが望ましいでしょう。
3-3. Unity における DI の位置づけ
Unity において DI(特に DI コンテナ)は、従来シングルトンで管理していたものの代替手段として捉えると理解しやすいでしょう。
シングルトンの問題
- グローバルアクセスによる結合の強まり
- テストの難しさ
- ライフサイクルの管理が難しい
DI による解決
- インターフェース経由の疎結合
- テスト容易性
- ライフサイクルの一元管理
ただし、DI コンテナを使うこととDI という設計パターンを使うことは別物です。
DI コンテナは DI を実現するためのツールに過ぎず、コンテナなしでも DI パターンは実践できます。
4. 実践例:ScenarioCommandFactory における DI の効果
ここでは、実際のプロジェクトでよくあるコマンドパターンとファクトリーの例を用いて、DI の具体的な効果を解説します。
4-1. DI を使わない場合
各コマンドは IPublisher<T> などの依存を持ちます。
ファクトリーで new する場合、依存関係をすべてファクトリーが知っている必要があります。
public class ScenarioCommandFactory
{
public IScenarioCommand Create(EventKind kind)
{
switch (kind)
{
case EventKind.ObjectMove:
// 依存をファクトリー内で解決
var publisher = EventLocator.GetPublisher<EventMessage.Move>();
return new ObjectMoveCommand(publisher);
// 他のコマンド...
}
throw new ArgumentOutOfRangeException(nameof(kind));
}
}
問題点
- コマンドが増えるたびにファクトリーに修正が入る
- 依存関係の変更(引数追加など)がファクトリーに波及する
- ファクトリーがコマンドの内部事情を知りすぎている(生成責務を超えている)
-
EventLocatorのようなサービスロケーターを使うと、依存関係がコード内に散らばり、可視性が低下する
4-2. DI コンテナを利用する場合
DI コンテナ(VContainer など)を用いて、各コマンドの依存を自動注入するようにします。
コマンドのコンストラクタで依存を要求します。
public class ObjectMoveCommand : IScenarioCommand
{
private readonly IPublisher<EventMessage.Move> _publisher;
public ObjectMoveCommand(IPublisher<EventMessage.Move> publisher)
{
_publisher = publisher;
}
public void Execute()
{
// ...
}
}
ファクトリーではコンテナからコマンドを取得するだけにします。
public class ScenarioCommandFactory
{
private readonly IObjectResolver _container;
public ScenarioCommandFactory(IObjectResolver container)
{
_container = container;
}
public IScenarioCommand Create(EventKind kind)
{
// コンテナにコマンドの生成を委譲
return _container.Resolve<IScenarioCommand>(kind);
}
}
利点
- ファクトリーはコマンドの依存構造を知らなくてよい
- 新しいコマンド追加時にファクトリーの修正が不要(コンテナの設定のみ)
- 各コマンドのコンストラクタで依存が明確になる
5. DI の適切な使いどころ
5-1. DI を導入すべきケース
- モジュール境界やレイヤー境界での依存性逆転が必要な場合
- 同じインターフェースで実装を置き換える必要がある場合(入力システムの差し替えなど)
- テスタビリティ向上が求められる箇所
- ロギングなど横断的関心事を分離したい場合
- プロジェクトが大規模化し、依存関係が複雑で増え続ける見込みがある場合
5-2. DI を導入すべきでない(または慎重にすべき)ケース
- 小規模なプロジェクトで、オーバーエンジニアリングになる場合
- チームが DI の概念に慣れておらず、学習コストが開発速度を著しく低下させる場合
- 依存関係が単純で、コンストラクタで明示的に渡すだけで十分な場合
5-3. DI コンテナの使いどころ
DI コンテナは以下のような場面で真価を発揮します。
- ServiceLocator / Factory パターンの代替として、レイヤ間の結合に利用する
- オブジェクトのライフサイクル管理(シングルトン、スコープごとの生成など)を一元化する
- 複雑な依存関係の自動解決をコンテナに委譲する
6. DI 導入の判断基準
すべてのプロジェクトに DI が必要というわけではありません。
以下の観点で判断するとよいでしょう。
- 依存関係が複雑で増え続ける見込みがあるか
- テストを重視するか(単体テストを多く書くか)
- チームが DI の考え方に慣れているか
- プロジェクトの規模が大きく、拡張性が求められるか
特に、コマンドやサービスが多数存在し、それらが頻繁に追加・変更されるようなシステムでは、DI の恩恵が大きいと言えます。
7. DI の本質的な課題と注意点
DI、特に DI コンテナを使用する際の主な課題を理解しておくことも重要です。
- 静的なコード解析では、参照関係やオブジェクトのライフサイクルが追いづらい
- 動的なコード解析では、実行順やある時点での相関関係を把握しづらい
- DI はクラス単位で見れば可読性が高まるが、システム全体として見たときのリーダビリティやデバッグ容易性は低下する可能性がある
- これは特に保守運用フェーズにおいて致命的になりかねない
- コードは書く時間より読む時間の方が長いと言われる。参照が辿れないことは、読むことを難しくする
また、DI コンテナはオブジェクトの生成責務をユーザーから奪います。
その結果、注入は連鎖し、依存関係のチェーンが形成されます。
プログラムが成長するとこの連鎖は巨大になり、手に負えなくなる可能性があります。
8. まとめ
DI(依存性注入)は、依存関係の構築とロジックの分離を実現し、保守性・テスト容易性を高めるパターンです。
Unity では、コンテナを利用したライフサイクル管理と組み合わせることで、シングルトンの代替としても有用です。
重要なポイント
- DI は手段であり、目的ではない
- DI パターンとDI コンテナは別物である
- コンストラクタインジェクションを優先し、どうしても無理な場合にのみフィールドインジェクションを使用する
- 適切な使いどころを見極めることが重要(すべてに導入する必要はない)
- システム全体の可読性とデバッグ容易性を常に意識する
まずは小さなモジュールから導入してみて、DI のメリットを体感してみてください。
参考リンク
- 【C#】Dependency Injection(依存性の注入)とは - Annulus Games
- 【C#】SOLID原則を学ぼう - Annulus Games
- UnityにおけるDI依存症への警告 - yotiky Tech Blog
- DIコンテナの本当の使いどころ - ULSコンサルティング
- DIすると何がいいんだっけ - Zenn
- Inversion of Control コンテナと Dependency Injection パターン - Martin Fowler(翻訳)
- Unity VContainerを利用した実用的なMVP設計について - Qiita
- やはりあなた方のDependency Injectionはまちがっている。
- 【Unity】依存性注入とは?と、なっているのでZenject(Extenject)を入門してみた - Qiita
- Unityでの依存性の注入は世間のそれとは意味が違う - Qiita