LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック:第72回 概念的循環依存 鶏が先か、卵が先か?

こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発しているTuan Chauです。

この記事は、"Review Committee Report" 共有の連載第 72 回です。LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を定期的に社内に共有しており、その一部を本ブログ上でも公開しています。(Review Committee Report の詳細については、過去の記事一覧を参照してください)

鶏が先か、卵が先か?

写真を提供することに特化したphoto-providerというモジュールと、ギャラリーで写真を表示するhome-galleryというモジュールがあると想像してください。photo-providerから提供された写真をホームギャラリーに表示したいとします。

以下は簡略化されたコードです。

photo-providerモジュール:

// PhotoProvider.kt
class PhotoProvider {
    fun getPhotos(): List<HomeGalleryPhotoItem> {
        // リポジトリから写真を取得
        return listOf(HomeGalleryPhotoItem("vacation.jpg", "Summer Vacation", ...))
    }
}

// プロバイダーモジュール内のデータモデル
data class HomeGalleryPhotoItem(val fileName: String, val description: String, ...)

home-galleryモジュール:

// build.gradle.kts
dependencies {
    implementation(project(":photo-provider"))
}
// HomeGallery.kt (別のモジュール内)
class HomeGallery(private val photoProvider: PhotoProvider) {
    fun displayPhotos() {
        val photos = photoProvider.getPhotos()
        // ホームギャラリーに写真を表示
        photos.forEach { renderPhoto(it) }
    }
    
    private fun renderPhoto(photo: HomeGalleryPhotoItem) {
        // UIで写真をレンダリング
    }
}

このコードに何か問題はありますか?

概念的循環依存

問題は、PhotoProviderHomeGalleryPhotoItemを定義して返していることです。このクラスは名前に「HomeGallery」への参照を含んでいます。これにより概念的循環依存が発生します。

  1. プロバイダーモジュールが特定の利用者(HomeGallery)について知っている
  2. プロバイダーが1つの特定のユースケースに密結合している
  3. PhotoProviderを使用したい他のモジュールは、異なる機能のために名付けられたモデルを使用するか、photo-providerが新しいユースケースごとに新しいデータモデルとAPIを追加する必要がある

写真も必要とするmuseum-galleryモジュールを追加することを考えてみてください。ミュージアムギャラリーはHomeGalleryPhotoItemを使用すべきでしょうか?それともMuseumGalleryPhotoItemと新しいAPIをphoto-providerに追加すべきでしょうか?どちらを選んでも、この抽象化は実装の詳細を漏らしていることがわかります。

これは、ビルドシステムが検出する典型的な循環依存とは異なります。明示的な双方向参照がないため、モジュールは正常にコンパイルされます。しかし、プロバイダーモジュールは命名規則を通じてその利用者について暗黙的な知識を持っており、これは依存性逆転の原則に違反します。

問題の解決方法

解決策は、プロバイダーでより汎用的な用語を使用し、適切な抽象化境界を確立することです。

改善されたphoto-providerモジュール:

// PhotoProvider.kt
class PhotoProvider {
    fun getPhotos(): List<PhotoItem> {
        // リポジトリから写真を取得
        return listOf(PhotoItem("vacation.jpg", "Summer Vacation"))
    }
}

// プロバイダーモジュール内の汎用データモデル
data class PhotoItem(val fileName: String, val description: String, ...)

改善されたhome-galleryモジュール:

// HomeGallery.kt
class HomeGallery(private val photoProvider: PhotoProvider) {
    fun displayPhotos() {
        val photos = photoProvider.getPhotos()
        // 必要に応じてHomeGallery固有のモデルにマッピング
        val galleryItems = photos.map { it.toHomeGalleryItem() }
        galleryItems.forEach { renderPhoto(it) }
    }
    
    private fun PhotoItem.toHomeGalleryItem(): HomeGalleryPhotoItem {
        return HomeGalleryPhotoItem(this.fileName, this.description, ...)
    }
    
    private fun renderPhoto(photo: HomeGalleryPhotoItem) {
        // UIで写真をレンダリング
    }
}

// HomeGallery固有のモデル
data class HomeGalleryPhotoItem(val fileName: String, val description: String, ...)

プロバイダーで汎用的なPhotoItemを使用することで、専門用語の依存関係を取り除きました。この結果、以下のようになります。

  • プロバイダーはその利用者について何も知らない
  • 各利用者は汎用モデルを独自のドメイン固有モデルにマッピングできる
  • プロバイダーは異なる機能間で適切に再利用可能
  • 適切な関心の分離を維持
  • プロバイダーが利用者固有のモデルとAPIで肥大化しない

一言まとめ

命名規則による循環依存の作成を避けましょう。すなわち、プロバイダーモジュールは汎用的に保ち、利用者にドメイン固有の適応を処理させましょう。

キーワード: architecture, circular-dependency, naming

コード品質向上のテクニックの他の記事を読む

コード品質向上のテクニックの記事一覧