【DTO】Swiftで学ぶDTO(Data Transfer Object)とは?

【DTO】Swiftで学ぶDTO(Data Transfer Object)とは?

この記事からわかること

  • DTO(Data Transfer Object)とは?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

DTO(Data Transfer Object)とは?

DTO」とは「Data Transfer Object / データ転送オブジェクト」の略称でデータをやり取りするためだけのシンプルなクラスのことを指します。例えばサーバーとクライアント間でやり取りするデータなどを扱う上でDTOが活用されることが多く、サーバーで扱うデータとクライアントで扱うデータの仲介役クラスがDTOになるイメージです。

DTOを使用することでサーバーの変更がDTOを介することで直接アプリに影響しなくなり、変更に強い設計を保つことができます。このように異なる層(レイヤー)間で発生するデータの連携をDTOを介することでレイヤー同士の依存度を下げ、各レイヤーが独立しやすくなります。

層(レイヤー)の切り分けや責務の違いはClean Architectureなどを参考にするとわかりやすいと思います。

DTOの特徴

DTOで一番大事なのがデータを操作するためのビジネスロジックやメソッドを保持しないことです。DTOはシンプルにプロパティ(属性)だけを持たせるような構造で定義します。

この構造自体は凝集度(データとデータ操作に関連する処理をまとめる原則)という観点で見るとアンチパターンのように見えてしまいますが、そもそも「ビジネスロジックやメソッドを保持しない」ことが原則のDTOなのでビジネスロジックなどが必要になる場合はDTOではなく別の設計を検討した方が良いと思います。

ただメソッドに関してはtoJsonなどのようなデータ変換メソッドに関しては許容されていることが多い印象です。

特徴

Swiftで見るDTO

例として「APIでUser情報を取得してアプリで表示するようなアプリ」を想定してDTOの使い方を見ていきたいと思います。

API(JSON) ⇨ DTO(API層のデータ) ⇨ Domain Model ⇨ ViewModel ⇨ View

まずはAPI ⇨ DTO(API層のデータ)の変換になるためAPI(JSON)からのレスポンスとして受け取る型として定義します。大概CodableでJSONへの相互変換が可能な型になっていることが多いと思います。


struct UserResponse: Codable {
    let id: Int
    let name: String
    let email: String
}

次にこのUserResponseをアプリ内で扱うための「Domain Model」へと変換します。高凝集になるように意識して操作するロジックはここに集約するようにします。


struct User {
    let id: Int
    let name: String
    let email: String

    func isValidEmail() -> Bool {
        email.contains("@")
    }
}

DTO ⇨ Domain Modelの変換ロジックはDTO自体に持たせることが多いです。


extension UserResponse {
    func toDomain() -> User {
        return User(id: id, name: name, email: email)
    }
}

さらにここにオフラインでもアプリを扱えるようにUser情報をローカルにも保存する必要が出てきたと想定します。DB(例:Realm)のモデルにはEntityと命名することが多い印象です。

API(JSON) ⇨ DTO(API層のデータ) ⇨ Domain Model ⇨ ViewModel ⇨ View
DB ⇦⇨ Entity ⇦⇨ Domain Model ⇨ ViewModel ⇨ View

Entityは以下のように定義されます。DB層の場合はEntity ⇨ DomainだけでなくDomain ⇨ Entityも必要になるケースもあると思います。


class UserEntity: Object {
    @Persisted(primaryKey: true) var id: Int
    @Persisted var name: String
    @Persisted var email: String
}

/// Entity ⇨ Domain
extension UserEntity {
    func toDomain() -> User {
      return User(id: id, name: name, email: email)
    }
}

/// Domain ⇨ Entity
extension UserEntity {
    convenience init(domain: User) {
        self.init()
        self.id = domain.id
        self.name = domain.name
        self.email = domain.email
    }
}

また規模が大きい場合はテストしやすくクリーンアーキテクチャにも沿う形で専用のMapperクラスを用意して管理する方が良いかもしれません。

struct UserMapper {

    static func toDomain(from entity: UserEntity) -> User {
        return User(id: id, name: name, email: email)
    }

    static func toEntity(from domain: User) -> UserEntity {
        let entity = UserEntity()
        entity.id = domain.id
        entity.name = domain.name
        entity.email = domain.email
        return entity
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index