Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Swiftにおけるポリモーフィズム実現手段の検討

 Swiftにおけるポリモーフィズム実現手段の検討

2023年05月10日にオンラインで開催されたYUMEMI.grow Mobile #3で発表した資料です。

Swiftでのポリモーフィズムを実現するときによく使われる方法であるClassとProcotolにはそれぞれどのような特徴があるのか、実際にユースケースをモデリングし、そのコードを提示しながらそれぞれのメリット・デメリットについて検討いたしました。

GO Inc. dev

May 11, 2023
Tweet

More Decks by GO Inc. dev

Other Decks in Programming

Transcript

  1. © GO Inc.
    Swiftにおけるポリモーフィズ
    ム実現手段の検討
    2023.05.10
    開発本部ソフトウェア開発部ユーザーシステムグループ / 髙橋秀宗
    GO株式会社

    View Slide

  2. © GO Inc. 2
    自己紹介
    プロフィール写真
    GO株式会社
    ユーザーシステムグループ / 髙橋秀宗
    タクシーアプリ『GO』のiOSアプリ開発を担当
    趣味はギターとカメラ
    @h1d3mun3

    View Slide

  3. Index
    © GO Inc.
    1. ポリモーフィズムを実現する手段について
    2. ユースケースの定義
    3. Class
    a. Classの実装
    b. Classの特徴
    4. Protocol
    a. Protocolの実装
    b. Protocolの特徴
    5. まとめ
    6. 参考資料
    3

    View Slide

  4. © GO Inc.
    ポリモーフィズ
    ムを実現する手
    段について
    01

    View Slide

  5. © GO Inc.
    複数の「型」に対してアクセスできる共通の接点のこと。
    5
    ポリモーフィズムとは

    View Slide

  6. © GO Inc.
    動物における「食べる」動作
    人 → 肉を、口で食べる
    牛 → 草を、丸呑みして胃ですりつぶして食べる
    → 「食べる」という動作は動物全般に存在するが、その詳細は各動物(≒型)によって異なる
    6
    例えば...

    View Slide

  7. © GO Inc.
    Swiftでは下記2つのやり方で、ポリモーフィズムを実現することができる
    ● Class
    ● Protocol
    学割チケットの発行フローをClassとProtocolでモデリングすることを通じて、どのような違い
    が出るのか確認する
    ポリモーフィズムを実現するための手段
    7

    View Slide

  8. © GO Inc.
    ユースケースの
    定義
    02

    View Slide

  9. © GO Inc.
    映画館での学割チケットは、下記フローで発行される
    1. 申込書を書いて窓口に並ぶ
    2. 窓口で学生証を提示する
    3. 条件に問題がなければ学割チケットを発行
    学割チケットの発行フロー
    9

    View Slide

  10. © GO Inc.
    Class
    03

    View Slide

  11. © GO Inc.
    Classの実装
    3.a

    View Slide

  12. © GO Inc.
    class Ticket {
    init() { } // 発行するための接点を定義
    }
    class ValidationRequiredTicket: Ticket { } // 検証が必要なTicketを表現
    // 学割チケットを検証が必要なTicketとして表現
    class StudentDiscountTicket: ValidationRequiredTicket { }
    チケットの表現
    12

    View Slide

  13. © GO Inc.
    // 申請に必要なドキュメントの表現
    class ApplicationDocument {
    // 自身が有効化を判定するisValidという関数を持つ
    func isValid() -> Bool {
    // 親クラス視点ではどのように判定してよいかわからないのでfatalErrorにする
    fatalError("must represent subclass!")
    }
    }
    // 学生証を表現
    class StudentCard: ApplicationDocument {
    override func isValid() -> Bool { } // 学生証が有効かを判定する
    }
    学生証の表現
    13

    View Slide

  14. © GO Inc.
    // チケット申込書を表現
    class TicketRequest {
    // チケットの発行を行うissueという関数を持つ
    func issue() throws -> Ticket {
    // 親クラス視点ではどのチケットを発行してよいかわからないのでfatalErrorにする
    fatalError("must represent subclass")
    }
    }
    // TicketRequestでTicketを発行できなかったときのエラー
    enum TicketRequestIssueError: Error {
    case validationFailure
    }
    申請書の表現 - 1
    14

    View Slide

  15. © GO Inc.
    // 検証が必要なTicketを発行するTicketRequestを表現
    class ValidationRequiredTicketRequest: TicketRequest {
    let document: ApplicationDocument
    init(document: ApplicationDocument) {
    self.document = document
    }
    }
    申請書の表現 - 2
    15

    View Slide

  16. © GO Inc.
    // 学割TicketのTicketRequest
    class StudentDiscountTicketRequest: ValidationRequiredTicketRequest {
    override func issue() throws -> Ticket {
    guard document.isValid() else { throw TicketRequestIssueError.validationFailure }
    // 子クラス視点では型がわかるにもかかわらず、Ticket型として返却せざるをえない
    return StudentDiscountTicket()
    }
    }
    申請書の表現 - 3
    16

    View Slide

  17. © GO Inc.
    Classの特徴
    3.b

    View Slide

  18. © GO Inc.
    ● コードを再利用できる
    ○ 子クラス側では、親クラスに実装したコードを再度実装しなくてよい
    メリット
    18

    View Slide

  19. © GO Inc.
    ● 親クラスでは子クラス側でoverrideされていることを前提とした実装を強制される
    ○ サブクラス側でのoverrideを強制できない
    ● 子クラスは1つの親クラスしか選択できない
    ○ 子クラスは親クラスに追加された変更をすべて受け入れなくてはならない
    ● 子クラスは親クラスで宣言されたメソッド宣言を変更できないので、具体的な型情報が消
    滅する
    ○ Genericsを利用することで少し安全に実装することが出来ますが、書きやすさ・読みやすさに影
    響するので、あまり良い解決策とは言えない
    デメリット
    19

    View Slide

  20. © GO Inc.
    overrideについて実際のコードで見ると...
    20
    class TicketCounter {
    var count = 0
    func issue() -> IssueTicket { // チケットを発行して、発行したカウントを記録する関数
    increment()
    fatalError("must inprement subclass!") // どのチケットを発行するべきかはわからないので
    fatalに落とす
    }
    func increment() { count += 1 } // 発行したカウントを
    incrementする関数
    }
    class StudentTicketCounter: TicketCounter {
    override func issue() -> StudentDiscountTicket {
    // increment関数を呼ぶ必要があることは、親クラスを見に行かないとわからない
    return StudentDiscountTicket()
    }
    override func increment() { } // overrideしていいのか判断がつかない
    }

    View Slide

  21. © GO Inc.
    class TicketCounter {
    // 親クラス視点ではどのチケットを発行していいか決定できないないので、Ticket型を返却させるしかない
    func issue() -> Ticket {
    fatalError("must implement subclass")
    }
    }
    class StudentTicketCounter: TicketCounter {
    // きちんとStudentDiscountTicketを返したのに、呼び出し元からはTicketとしか認識できない
    override func issue() -> Ticket {
    return StudentDiscountTicket()
    }
    }
    型情報消失を実際のコードで見ると...
    21

    View Slide

  22. © GO Inc.
    // Genericsを使うことで型情報が明確になった
    class TicketCounter {
    var document: Document
    init(document: Document) {
    self.document = document
    }
    func issue() -> IssuedTicket {
    fatalError("must inprement subclass!")
    }
    }
    // その代わり、記述が若干長くなる
    class StudentTicketCounter: TicketCounter {
    override func issue() -> StudentDiscountTicket {
    return StudentDiscountTicket() // 呼び出し側には
    StudentDiscountTicket型として返却できる
    }
    }
    型情報表現について、実際のコードで見ると...
    22

    View Slide

  23. © GO Inc.
    Protocol
    4

    View Slide

  24. © GO Inc.
    Protocolの実装
    4.a

    View Slide

  25. © GO Inc. 25
    protocol Ticket {
    static func issue() -> Self // 自分自身を発行するissueという接点を持つ
    }
    protocol ValidationRequiredTicket: Ticket { } // 検証が必要なTicketを表現
    // 学割チケットを検証が必要なTicketとして表現
    struct StudentDiscountTicket: ValidationRequiredTicket {
    static func issue() -> StudentDiscountTicket {
    return StudentDiscountTicket()
    }
    }
    チケットの表現

    View Slide

  26. © GO Inc.
    // 申請に必要なドキュメントの表現
    protocol ApplicationDocument {
    // 自身が有効かを判定するisValidという関数を持つ
    func isValid() -> Bool
    }
    struct StudentCard: ApplicationDocument {
    func isValid() -> Bool { } // 学生証が有効かを判定する
    }
    26
    学生証の表現

    View Slide

  27. © GO Inc. 27
    申請書の表現 - 1
    protocol TicketRequest {
    // 発行したいTicketをIssueTicketTypeという型で持つ
    associatedtype IssueTicketType: Ticket
    // チケットの発行を行うissueという関数を持つ
    func issue() throws -> IssueTicketType
    }
    // TicketRequestでTicketを発行できなかったときのエラー
    enum TicketRequestIssueError: Error {
    case validationFailure
    }

    View Slide

  28. © GO Inc.
    // 検証が必要なTicketを発行するTicketRequestを表現
    protocol ValidationRequiredTicketRequest: TicketRequest {
    // 申請に必要なドキュメントをDocumentという型で持つ
    associatedtype Document: ApplicationDocument
    var document: Document { get }
    }
    28
    申請書の表現 - 2

    View Slide

  29. © GO Inc. 29
    // 学割チケットのTicketRequest
    struct StudentDiscountTicketRequest: ValidationRequiredTicketRequest {
    // IssueTicketTypeがStudentDiscountTicket型と宣言
    typealias IssueTicketType = StudentDiscountTicket
    // DocumentはStudentCard型と宣言
    typealias Document = StudentCard
    let document: Document
    func issue() throws -> StudentDiscountTicket {
    guard document.isValid() else { throw TicketRequestIssueError.validationFailure }
    return IssueTicketType.issue()
    }
    }
    申請書の表現 - 3

    View Slide

  30. © GO Inc.
    Protocolの特徴
    4.b

    View Slide

  31. © GO Inc.
    ● 振る舞いだけを表現できる
    ○ Protocolの宣言は実装を持たないので、責務が分離されたり、UnitTestを書くときに好ましい
    ● 複数の振る舞いを同時に適用できる
    ● 型情報を適切に表現することができる
    メリット
    31

    View Slide

  32. © GO Inc.
    // 学割チケットのTicketRequest
    struct StudentDiscountTicketRequest: ValidationRequiredTicketRequest {
    // IssueTicketTypeがStudentDiscountTicket型と宣言
    typealias IssueTicketType = StudentDiscountTicket
    // DocumentはStudentCard型と宣言
    typealias Document = StudentCard
    let document: Document
    func issue() throws -> StudentDiscountTicket {
    guard document.isValid() else { throw TicketRequestIssueError.validationFailure }
    return IssueTicketType.issue()
    }
    }
    型情報表現について、実際のコードで見ると...
    32

    View Slide

  33. © GO Inc.
    ● 実装は別途記述しなくてはならない
    ○ Protocolは振る舞いしか表現できないので、実装は別途記述しなくてはならない
    ○ 適切じゃない使い方をすると、同じような実装を複数箇所に書く必要が出る
    デメリット
    33

    View Slide

  34. © GO Inc.
    まとめ
    05

    View Slide

  35. © GO Inc.
    まとめ - Class
    ● 仕組み上Classは、振る舞いと実装を分離できない
    ○ 振る舞い: 何をしたいのか
    ○ 実装: 具体的にどうやるのか
    ● この問題は振る舞いを中心に開発する際に大きな問題となる
    ● その一方、大きく実装を変更する必要がないものにおいては、実装が必ずついてくるとい
    うClassの特徴はメリットともなりうる
    35

    View Slide

  36. © GO Inc.
    ● Protocolは振る舞いだけを表現できる
    ● そのため、振る舞いを中心に開発する際はメリットが大きい
    ● その一方同じような実装を何度も実装する必要がある場合は、Protocolを利用すると同じ
    ようなコードを何度も書かなくてはならず、デメリットが有る
    まとめ - Protocol
    36

    View Slide

  37. © GO Inc.
    ● ProtocolかClassのどちらを使うべきか判断に迷ったときは、振る舞いと実装が分離され
    ていないと困るかどうかという観点で考えてみると良い
    まとめ
    37

    View Slide

  38. © GO Inc.
    参考資料
    05

    View Slide

  39. © GO Inc. 39
    ● Protocol-Oriented Programming in Swift (WWDC15)
    ● Embrace Swift Generics (WWDC22)
    参考資料

    View Slide

  40. 文章・画像等の内容の無断転載及び複製等の行為はご遠慮ください。
    © GO Inc.

    View Slide