📦

あらためてDTOを学ぶ

に公開

はじめに

こんにちは、DTO使ってますか?
自分は「これはDTOだ」とは考えず無意識にDTOを作ることがよくあります。
普段なんとなく使っていてもある程度メリットがあるDTOについてあらためて調べてみました。

DTOとは

DTO (Data Transfer Object) とは「データをまとめて受け渡すための入れ物」です。
ロジックを持たないことが特徴で、フィールドとアクセサーのみを持つシンプルなオブジェクトです。
モジュール間やネットワーク間などのデータの転送に使用されます。

DTOの歴史

ネットワーク通信の負荷を下げるためのパターンとして誕生しました。
値一つ一つについて毎回通信していると負荷が高いので、一回の通信でまとめてデータをやりとりするためのコンテナを作ろうというパターンですね。

元々ネットワーク用のパターンですが、現在ではローカルでも頻繁に使われています。
レイヤー間やモジュール間などのやり取りのためにDTOが使用されることが多いです。
アーキテクチャの一部として取り入れられるなど、DTOのコストより、メリットが評価されています。

参考

DTOのメリット

DTOのメリットはデータのカプセル化です。
個人的にインターフェースに似ていると考えるとわかりやすいです。

  • モジュールのインターフェースとしてDTOを公開し、内部のEntityの変更の影響を吸収する。
  • DTOに含めるフィールドを選別して詳細情報を除外する。

DTOを使用することでデータ結合となり、Entityの変更の影響を吸収できます。
そのためEntityの変更を阻害しません。

逆にEntityを直接渡すとスタンプ結合となります。
スタンプ結合はEntityの変更時に使用側の知識が必要になる場合がありEntityの変更を阻害しがちです。

補足:データ結合とスタンプ結合

  • データ結合:必要なデータのみを受け渡す結合方式。使用側は最小限のデータ構造にのみ依存するため、結合度が低い。
  • スタンプ結合:オブジェクト全体を受け渡す結合方式。使用側がオブジェクトの一部しか使わない場合でも全体の構造に依存するため、結合度が高くなる。

DTOのデメリット

DTOにもデメリットがあります。
自分は安易にDTO作ってしまいますが、トレードオフについて考える必要があります。

  • 単純にDTO分のコードが増える
  • マッピングのコストがかかる

DTOが必要ないとき

DTOを作成するかどうかの自分の個人的な判断指標は以下のとおりです。

値が一つの時はDTOを作成するコストも少ないですがメリットも少ないと考えています。
単純にプリミティブな値を使うか、意味を持たせたい場合はValueObjectを使います。

判断が難しいのが、Entityと求めるDTOのデータ構造が同じ時です。
これは多くの場合Entityが変更されるとDTOも変更する必要が出てくるため、結果的にスタンプ結合とほぼ同じになってしまいます。
一旦Entityにinterfaceをつけて様子をみて、ギャップが生じてきたタイミングでDTOに切り出すのがベターかもしれません。
ただこの場合でも疎結合になることを評価してDTOを使用することも多いです。
プロジェクトのアーキテクチャを考慮することが重要です。

DTOの実装例

ここでは、ユーザー詳細情報をViewに表示するシンプルな例を通して、DTOの使い方を見ていきます。
言語はKotlinを使用して記述しています。

シナリオ

Model-Viewの2レイヤー構成
Modelではユーザーの「名字」と「名前」と「生年月日」を管理するEntityがあり
Viewでは「フルネーム」と「年齢」を表示したい
単純な例のためにフルネームと年齢計算はビジネスロジックとしています。

まず、Entityの定義です

data class UserEntity(
    val firstName: String,
    val lastName: String,
    val birthDate: LocalDate
)

パターン1: Entityを直接渡す(スタンプ結合)

最もシンプルな実装ですが、問題があります。

// Model層
class UserModel {
    fun getUserDetail(): UserEntity {
        return UserEntity(
            firstName = "太郎",
            lastName = "山田",
            birthDate = LocalDate.of(2000, 1, 1)
        )
    }
}

// View層
class UserDetailView {
    fun display(user: UserEntity) {
        // View側でデータを加工
        val fullName = "${user.lastName} ${user.firstName}"
        val age = Period.between(user.birthDate, LocalDate.now()).years
        
        println("名前: $fullName")
        println("年齢: $age")
    }
}

問題点

  • ViewがEntityに直接依存
  • Entityの変更の影響でViewの修正が必要になる可能性。
  • Viewがビジネスロジックを持ってしまっている

パターン2: スタンプDTO

DTOを作成しますが、Entityと同じ構造になっています。

// DTOの定義(Entityと同じ構造)
data class UserDto(
    val firstName: String,
    val lastName: String,
    val birthDate: LocalDate
)

// Model層
class UserModel {
    fun getUserDetail(): UserDto {
        val entity = UserEntity(
            firstName = "太郎",
            lastName = "山田",
            birthDate = LocalDate.of(2000, 1, 1)
        )
        
        // EntityをDTOに変換
        return UserDto(
            firstName = entity.firstName,
            lastName = entity.lastName,
            birthDate = entity.birthDate
        )
    }
}

// View層
class UserDetailView {
    fun display(user: UserDto) {
        // View側でデータを加工(パターン1と同じ)
        val fullName = "${user.lastName} ${user.firstName}"
        val age = Period.between(user.birthDate, LocalDate.now()).years
        
        println("名前: $fullName")
        println("年齢: $age")
    }
}

問題点

  • DTOがEntityの構造をミラーリングしているだけ
  • Entityの変更時にDTOも変更が必要になることが多い(スタンプ結合とほぼ同じ)
  • Viewがビジネスロジックを依然として保持している

パターン3: 一般的なDTO(データ結合)

Viewが必要とする形に最適化されたDTOです。

// DTOの定義(Viewが必要とする形)
data class UserDisplayDto(
    val fullName: String,
    val age: Int
)

// Model層
class UserModel {
    fun getUserDetail(): UserDisplayDto {
        val entity = UserEntity(
            firstName = "太郎",
            lastName = "山田",
            birthDate = LocalDate.of(2000, 1, 1)
        )
        val age = Period.between(entity.birthDate, LocalDate.now()).years
        
        return UserDisplayDto(
            fullName = "${entity.lastName} ${entity.firstName}",
            age = age
        )
    }
}

// View層
class UserDetailView {
    fun display(user: UserDisplayDto) {
        println("名前: ${user.fullName}")
        println("年齢: ${user.age}")
    }
}

メリット

  • ViewはDTOのみに依存(データ結合)
  • Entityが変更されても、Viewは直接影響を受けない
  • 年齢計算などのロジックがModel層に集約
  • Viewの表示ロジックがシンプルになる

おまけ

  • DTOへの変換ロジックをModel(ドメイン)から分離させるために中間層を作る場合があります。
  • View側で直接DTOを使わず、View層側でDTOをUIModelやPresentationModelなどView層用のモデルに変換してから使う場合があります。

おわりに

DTOは、単なるデータコンテナという以上に、モジュールのインターフェースとして機能する点が面白いと思います。
Entityをそのまま渡してしまうと、不要なフィールドや内部構造まで公開してスタンプ結合となり、変更が難しくなりがちです。
モジュールのインターフェースとしてDTOを使用することで、依存関係はより疎結合であるデータ結合にすることができます。
これにより、DTOはEntityの変更が外部に波及するのを防ぐ防波堤となります。

株式会社ソニックムーブ

Discussion