🐣

DIについて。AIバイブコーディングでバグらないためには?アウトプット

に公開

はじめに

こんにちは、2月から本格的にプログラマーになるために個人開発を始めた yuma です。

とりあえず Java はしっかり学ぶと良いと聞いて Java Silver を取りました。でも実際に開発を始めてみると、資格は取れたけど全体の開発の流れがいまいち掴めてないことに気づきました。「じゃあもう実際に作りながら覚えよう」と思って、AI の力も借りつつ個人開発に取り組むことにしました。

ただ、ちゃんとしたコーディングルールも決めずにやり始めたもんだから、ちょっと修正しただけでバグが出まくって、もう収集がつかない。

そんなとき「もっと保守性とか可読性を上げるにはどうすればいいんだろう?」ってAIに相談したら、**DI(Dependency Injection:依存性の注入)**って概念があることを教えてもらいました。実は自分でもなんとなく似たようなことやってるときやってないときバラバラでした。

この記事では、DIって何なのか、なぜそれがコードの可読性保守性(改修しやすさ)の向上に繋がるのか、簡単なコード例(Before/After)を交えながら解説していきます。

ちなみに、この記事を書くにあたって自分のコードを見直してみたら、DI ライブラリとか使ってなかったので、可読性もへったくれもありませんでした・・。

DI って何? なぜ必要?

Q: DI って、一言で言うと何ですか?

A: 「クラスが必要とするオブジェクト(依存関係)を、そのクラス自身が作るのではなく、外部から与える(注入する)ことで、クラス間の結合度を下げ、テストや変更をしやすくする設計手法」 です。

Q: DI を使わないと、どういう問題があるんですか?

A: クラスが他のクラスのインスタンスを内部で直接作ってしまうと、クラス同士が強く結びついちゃいます(密結合)。

// DIを使わない例
class MessageRepository {
    fun getMessage(): String = "Hello, DI! (Before)"
}

class MessageService {
    // MessageService が MessageRepository を直接生成! (密結合)
    private val repository = MessageRepository()

    fun printMessage() {
        val message = repository.getMessage()
        println("Service: $message")
    }
}

class MainViewModel {
    // MainViewModel が MessageService を直接生成! (密結合)
    private val messageService = MessageService()

    fun showMessage() {
        println("ViewModel: メッセージ表示を開始します")
        messageService.printMessage()
    }
}

fun main() {
    val viewModel = MainViewModel()
    viewModel.showMessage()
}

これだと、例えば MessageRepository の作り方を変えたい時に MessageService も修正が必要になったり、MainViewModel のテストをする際に本物の MessageService まで動いてしまってテストしにくかったりします。「機能がまとまりすぎてたら一個変えたら他のとこに影響が出ちゃう」 状態ですね。

手動で DI をやってみる (Before/After)

Q: じゃあ、DI を使うとどう変わるんですか?

A: クラスが必要なものを外部から受け取るようにします。よく使われるのが、コンストラクタで受け取る方法(コンストラクタインジェクション)です。

// --- 依存される側 (インターフェースと実装) ---
interface MessageRepository { // ← インターフェースを定義
    fun getMessage(): String
}

class MessageRepositoryImpl : MessageRepository { // ← 実装クラス
    override fun getMessage(): String = "Hello, Manual DI!"
}

interface MessageService {
    fun printMessage()
}

class MessageServiceImpl(
    private val repository: MessageRepository // ← コンストラクタで受け取る!
) : MessageService {
    override fun printMessage() { /* ... */ }
}

// --- 依存する側 (ViewModel) ---
class MainViewModel(
    private val messageService: MessageService // ← コンストラクタで受け取る!
) {
    fun showMessage() { /* ... */ }
}

// --- 使う側でインスタンスを組み立てて渡す(注入) ---
fun main() {
    println("--- 手動DI ---")
    // 外部でインスタンスを作成し、注入していく
    val repository: MessageRepository = MessageRepositoryImpl()
    val service: MessageService = MessageServiceImpl(repository)
    val viewModel = MainViewModel(service)

    viewModel.showMessage()

    // テスト時に差し替えも簡単!
    println("\n--- 手動DI (テスト時) ---")
    class TestMessageRepository : MessageRepository { /* テスト用実装 */ }
    val testRepository: MessageRepository = TestMessageRepository()
    val testService: MessageService = MessageServiceImpl(testRepository)
    val testViewModel = MainViewModel(testService)
    testViewModel.showMessage()
}

ポイント:

  • クラスは具体的な実装ではなく、インターフェースに依存するようになる(例: MessageRepository)。
  • 依存関係が疎になる(疎結合)。
  • テスト時に偽物の実装(TestMessageRepository など)を簡単に差し替えられる。

Q: つまり、クラス外でインスタンスを作って、使うクラスに引数で渡すって感じ?

A: はい、そのイメージです!「作る責任」と「使う責任」を分けるんですね。

DI ライブラリって便利なの? (Hilt の例)

Q: 手動で DI するのは、クラスが増えると大変そうですね…

A: その通りです。依存関係が複雑になると、インスタンスを組み立てるコードを書くのが大変になります。そこで登場するのが DI ライブラリ (Hilt, Koin など) です。

Q: DI ライブラリを使うと、何が楽になるんですか?

A: インスタンスの生成や注入といった面倒な作業を自動化・簡略化してくれます。

例えば Hilt を使うと、アノテーション (@Inject, @HiltViewModel など) を使って「このクラスはこれが必要」「このクラスの作り方はこう」と宣言するだけで、Hilt が良しなにやってくれます。

// (Hilt の設定や Module は省略。詳細は公式ドキュメント等を参照)

// --- 実装クラスに @Inject constructor をつける ---
class MessageRepositoryImpl @Inject constructor() : MessageRepository { /* ... */ }

class MessageServiceImpl @Inject constructor(
    private val repository: MessageRepository // Hiltが自動で注入
) : MessageService { /* ... */ }

// --- ViewModel に @HiltViewModel と @Inject constructor をつける ---
@HiltViewModel
class MainViewModel @Inject constructor(
    private val messageService: MessageService // Hiltが自動で注入
) : ViewModel() { /* ... */ }

// --- Activity で Hilt を有効にし、ViewModel を取得 ---
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // by viewModels() で Hilt が ViewModel を自動生成&注入!
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.showMessage() // すぐ使える!
    }
}

手動 DI で必要だったインスタンスの組み立てコードがほとんど不要になり、非常にスッキリします。

DI でコード量は増える?

Q: DI を使うと、インターフェース定義とか設定とかで、コード量は増えませんか?

A: 短期的に見ると、インターフェースや設定コードの分だけ増える部分はあります。

でも、単純に「増える」とは言えません。

  • DI ライブラリを使えば、手動 DI の面倒な組み立てコードが大幅に減ります。
  • DI なしの場合、インスタンス生成などのコードがクラス内に隠れているだけです。DI はそれを明示的に管理する手法です。
  • テストコードが書きやすくなるので、テスト関連のコードは減る可能性があります。
  • コードの再利用性や変更容易性が上がるので、将来的なコード追加・修正の手間は減ります。

「コードが短ければいいわけではない」 という視点が重要です。DI は、少しコードが増えたとしても、それを上回る構造化、テスト容易性、保守性・拡張性の向上というメリットをもたらします。

まとめ

DI(依存性の注入)は、クラス間の依存関係を疎結合にし、コードの可読性、保守性、テスト容易性を高めるための重要な設計手法です。

  • DI なし: クラス同士が密結合し、変更やテストが難しい。
  • 手動 DI: 疎結合になるが、依存関係の管理が煩雑になることがある。
  • DI ライブラリ (Hilt, Koin): DI のメリットを享受しつつ、実装の手間を大幅に削減できる。

Discussion