【DTO】Swiftで学ぶDTO(Data Transfer Object)とは?
この記事からわかること
- DTO(Data Transfer Object)とは?
index
[open]
\ アプリをリリースしました /
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
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。





