今回はDTO(Data Transfer Object)というデザインパターンを学んだので、アウトプットしていこうと思います。
DTOとは?
- データを転送するために使用されるオブジェクトで、受け渡す時は利用しやすい形にします。(ビジネスロジックを利用した具体的な処理は書きません。)
- 必要なデータのみを渡すため、パフォーマンスが向上します。(必要のないデータは記述しない)
- エンティティとは異なるデータ構造を作るため、データ構造の変換が容易になります。(エンティティにべったりくっつかないため、APIの変更に柔軟に対応することが可能!)。言い換えると、エンティティはDTOのことを全く知らないという状態です。
DTO解説の参考ページ
Entityにべったりくっついてしまっている例。
struct QiitaArticle: Decodable, Identifiable {
let id = UUID()
let title: String
let createdAt: String
let url: URL
let user: User
enum CodingKeys: String, CodingKey {
case title
case createdAt = "created_at"
case url
case user
}
}
今日は、上のようにAPIのデコード処理とべったりと結びついているエンティティを、DTOを使ってスタイリッシュにしていきます。
流れ
- 使用するAPIと説明
- DTO
- private extension
- PokemonListClient
- Entity
- 全体
使用するAPIと説明
PokeAPIというAPIを使っていきます。
ポケモンが可愛いので表示されると嬉しくなってしまいますよね。
あとは、APIのデータを取得してくるPokemonListClient
とエンティティのPokemon
です。
DTO
- 帰ってきたデータをResponseDTOでデコードします。
- このResponseDTO自体はPokemonListClientの中でしか使用しないため、このクラスの中で宣言します。
struct ResponseDTO: Decodable {
struct Pokemon: Decodable{
let id: Int
let name: String
let sprites: Sprites
struct Sprites: Decodable {
let frontDefault: URL
}
}
}
private extension
PokemonListClientのファイル内でのみ有効な初期化方法をprivate extension
で記述していきます。(APIのレスポンスからPokemonに変換する時、知識が閉じているため、他ファイルに影響を与えません)
APIのレスポンスをDTOで変換したものをPokemon型の初期化で使えるようにします。
これだと一回一回関係性を書くのはめんどくさいですが、共通化してしまうと困ったことも起こりえます。他のAPIに変換する時に、共通化できない構造になっているにも関わらず、共通化していることによって予期せぬバグを生み出す可能性が出てきてしまいます。なので、この部分は一回一回書く必要があります。
private extension Pokemon {
init(dto: PokemonListClient.ResponseDTO.Pokemon) {
self = .init(
id: dto.id,
name: dto.name,
sprite: Sprite(dto: dto.sprites)
)
}
}
private extension Sprite {
init(dto: PokemonListClient.ResponseDTO.Pokemon.Sprites){
self = .init(frontDefault: dto.frontDefault)
}
}
PokemonListClient
上のDTO
とprivate extension
を使用したPokemonListClientを作成しました。
withThrowingTaskGroup
の中でリクエストを送信し、返ってきたデータとレスポンスを受け取っています。(今回はレスポンスを使用しないので破棄します)JSONDecoderクラスの.keyDecodingStrategy
に.convertFromSnakeCase
を使ってあげることで、自動的にJSONを使いやすいようにパースしてくれるので便利です。(_
を削除し、その次に続く文字を大文字
へ変換してくれます。)
front_default
↓
frontDefault
keyDecodingStrategy
について参考にさせてもらった記事です。MockAPIを使って擬似的にAPI切り替えようとした時に、なぜかうまくデコードできない現象が発生してしまいました。その時に参考にさせていただいた記事になります。
struct PokemonListClient {
struct ResponseDTO: Decodable {
struct Pokemon: Decodable{
let id: Int
let name: String
let sprites: Sprites
struct Sprites: Decodable {
let frontDefault: URL
}
}
}
func fetchPokemonDataList() async throws -> [Pokemon] {
// URL取得
let urls:[URL?] = getURLs()
// 『withThrowingTaskGroup』でURLからポケモンの配列を取得
return try await withThrowingTaskGroup(of: Pokemon.self) { group in
for url in urls {
guard let url else { continue }
group.addTask {
//今回はResponseは使わないので破棄します。
let (data, _) = try await URLSession.shared.data(from: url)
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let dto = try jsonDecoder.decode(ResponseDTO.Pokemon.self, from: data)
return Pokemon(dto: dto)
}
}
var pokemonList: [Pokemon] = []
for try await pokemon in group {
pokemonList.append(pokemon)
}
//ポケモンを順番に表示したかったので並び替えました。
return pokemonList.sorted { $0.id < $1.id }
}
}
private func getURLs() -> [URL?] {
let pokeNumber = 1 ... 150
let pokeURLs = pokeNumber.map { URL(string: "https://pokeapi.co/api/v2/pokemon/\($0)")}
return pokeURLs
}
}
Entity
struct Pokemon {
let id: Int
let name: String
let sprite: Sprite
}
struct Sprite {
let frontDefault: URL
}
全体
struct PokemonListClient {
struct ResponseDTO: Decodable {
struct Pokemon: Decodable{
let id: Int
let name: String
let sprites: Sprites
struct Sprites: Decodable {
let frontDefault: URL
}
}
}
func fetchPokemonDataList() async throws -> [Pokemon] {
// URL取得
let urls:[URL?] = getURL()
// 『withThrowingTaskGroup』でURLからポケモンの配列を取得
return try await withThrowingTaskGroup(of: Pokemon.self) { group in
for url in urls {
guard let url else { continue }
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let dto = try jsonDecoder.decode(ResponseDTO.Pokemon.self, from: data)
return Pokemon(dto: dto)
}
}
var pokemonList: [Pokemon] = []
for try await pokemon in group {
pokemonList.append(pokemon)
}
return pokemonList.sorted { $0.id < $1.id }
}
}
private func getURL() -> [URL?] {
let pokeNumber = 1 ... 150
let pokeURLs = pokeNumber.map { URL(string: "https://pokeapi.co/api/v2/pokemon/\($0)")}
return pokeURLs
}
}
private extension Pokemon {
init(dto: PokemonListClient.ResponseDTO.Pokemon) {
self = .init(
id: dto.id,
name: dto.name,
sprite: Sprite(dto: dto.sprites)
)
}
}
private extension Sprite {
init(dto: PokemonListClient.ResponseDTO.Pokemon.Sprites){
self = .init(frontDefault: dto.frontDefault)
}
}
struct Pokemon {
let id: Int
let name: String
let sprite: Sprite
}
struct Sprite {
let frontDefault: URL
}
まとめ
APIの更新や切り替えの時に、Entityとべったりくっついてしまっていると書き換えが大変ですが、DTOを使うことで容易に切り替えることができました。このDTOの技術を用いて、テストコードなんかにも挑戦していきたいと思っています。初学者なので、至らぬ点がたくさんあると思います。もしも、気づいたことがあればコメントで教えていただけると嬉しいです。