0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DTOを使って柔軟にAPIの切り替えを行いたいということで、、、

Posted at

今回はDTO(Data Transfer Object)というデザインパターンを学んだので、アウトプットしていこうと思います。

DTOとは?

  1. データを転送するために使用されるオブジェクトで、受け渡す時は利用しやすい形にします。(ビジネスロジックを利用した具体的な処理は書きません。)
  2. 必要なデータのみを渡すため、パフォーマンスが向上します。(必要のないデータは記述しない)
  3. エンティティとは異なるデータ構造を作るため、データ構造の変換が容易になります。(エンティティにべったりくっつかないため、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を使ってスタイリッシュにしていきます。

流れ

  1. 使用するAPIと説明
  2. DTO
  3. private extension
  4. PokemonListClient
  5. Entity
  6. 全体

使用する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

上のDTOprivate 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の技術を用いて、テストコードなんかにも挑戦していきたいと思っています。初学者なので、至らぬ点がたくさんあると思います。もしも、気づいたことがあればコメントで教えていただけると嬉しいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?