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

Protocolを利用したポリモーフィズム(Polymorphism)とそのメリット

 Protocolを利用したポリモーフィズム(Polymorphism)とそのメリット

2023年04月12日にオンラインで開催されたYUMEMI.grow Mobile #2で発表した資料です。
プログラムにおけるポリモーフィズムの考え方とそれを実現するSwiftのProtocolという機能について、なるべく皆様の実際の業務でご活用いただけるようなイメージが湧くよう、実際のコードや、いくつかの事例などを多く含めながらわかりやすさを優先して発表させていただきました。

GO Inc. dev

April 24, 2023
Tweet

More Decks by GO Inc. dev

Other Decks in Programming

Transcript

  1. © GO Inc.
    Protocolを利用したポリモーフィズ
    ム(Polymorphism)とそのメリット
    2023.04.12
    開発本部ソフトウェア開発部ユーザーシステムグループ / 髙橋秀宗
    GO株式会社

    View Slide

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

    View Slide

  3. Index
    © GO Inc.
    1. ポリモーフィズムとは
    2. モデリングから見るポリモーフィズム
    a. ユースーケースの定義
    b. 実装
    3. 終わりに
    4. 参考資料
    3

    View Slide

  4. © GO Inc.
    ポリモーフィズムと

    01

    View Slide

  5. © GO Inc.
    複数の「型」に対してアクセスできる共通の接点です。
    SwiftではProtocolを利用して表現できます。
    5
    ポリモーフィズムとは

    View Slide

  6. © GO Inc. 6
    🤔
    ポリモーフィズムとは

    View Slide

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

    View Slide

  8. © GO Inc. 8
    protocol Animal {
    associatedtype Feed // 食べ物をFeedという型でもつ
    func eat(_ food: Feed) // 食べるためのeatという関数を持つ
    }
    struct Human: Animal {
    typealias Feed = Meat // FeedはMeat型と宣言
    func eat(_ food: Feed) { } // Meat型を引数に取るeat関数を宣言
    }
    struct Cow: Animal {
    typealias Feed = Hay // FeedはHay型と宣言
    func eat(_ food: Hay) { } // Hay型を引数に取るeat関数を宣言
    }
    例えば...

    View Slide

  9. © GO Inc.
    ポリモーフィズムが何故嬉しいのか、イメージしにくいですよね。
    9
    でも...

    View Slide

  10. © GO Inc. 10
    そこで!
    学割の映画鑑賞券チケット発行フローをどのようにソースコード上に表現していくか、実際のコードととも
    に説明することを通じて、ポリモーフィズムの具体的なメリットについて説明します。

    View Slide

  11. © GO Inc.
    モデリングから見
    るポリモーフィズム
    02

    View Slide

  12. © GO Inc.
    ユースケースの定

    2.a

    View Slide

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

    View Slide

  14. © GO Inc.
    実装
    2.b

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  19. © GO Inc. 19
    // 学割チケットの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

  20. © GO Inc. 20
    各Protocol・各Structの関係性

    View Slide

  21. © GO Inc. 21
    // 学割チケットリクエストの生成
    let studentDiscountTicketRequest = StudentDiscountTicketRequest(
    document: StudentCard()
    )
    // Ticketの発行をリクエスト
    let ticket = try? studentDiscountTicketRequest.issue()
    利用側の実装イメージ

    View Slide

  22. © GO Inc.
    おわりに
    03

    View Slide

  23. © GO Inc.
    ● 「1つのチケットリクエストが1つのチケットについての発行知識を知っている」という形を強制できる
    ので、オープン/クロースドの原則(※1)を守ることができる
    ● 振る舞いと実挙動を分けることができるので、テストが書きやすい
    ● 振る舞いに対してユニットテストを書くため、仕様変更が起きた際もユニットテストの変更量を少なく
    することができる
    ● 各Protocolに振る舞いがある程度表現されているので、具体的な型を把握しなくてもプログラムを
    書くことができるケースが有る
    ○ some・anyキーワードとの相性が良い
    ポリモーフィズムのメリット
    23
    ※1: 既存のコードを変更せず、プログラムの機能を追加できること

    View Slide

  24. © GO Inc.
    参考資料
    04

    View Slide

  25. © GO Inc. 25
    ● ポリモーフィズム(Wikipedia)
    参考資料

    View Slide

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

    View Slide

  27. © GO Inc.
    おまけ
    XX

    View Slide

  28. © GO Inc.
    class Ticket { } // チケットのSuperクラス
    final class NormalTicket: Ticket {} // 通常チケット
    final class StudentDiscountTicket: Ticket { } // 学割チケット
    // 発行するチケットタイプ
    enum IssueTicketType {
    case normal, studentDiscount
    }
    28
    ポリモーフィズムを利用せずに表現する場合 - 1

    View Slide

  29. © GO Inc.
    struct TicketRepository {
    let issueTicketType: IssueTicketType
    func isValid() -> Bool {
    switch issueTicketType {
    case .normal:
    // 通常チケット時の有効判定処理
    case .studentDiscount:
    // 学割チケット時の有効判定処理
    }
    }
    29
    ポリモーフィズムを利用せずに表現する場合 - 2

    View Slide

  30. © GO Inc. 30
    func issue() -> Ticket? {
    guard isValid() else { return nil }
    switch issueTicketType {
    case .normal:
    // 通常チケットの発行処理
    case .studentDiscount:
    // 学割チケットの発行処理
    }
    }
    }
    ポリモーフィズムを利用せずに表現する場合 - 3

    View Slide

  31. © GO Inc.
    ● 発行するチケットの種別を追加するたびに
    TicketRepositoryをメンテナンスし続けていく必要があ
    る。具体的には...
    ○ 検証処理は各caseで異なるので、追加するたびに isValid()関数が長くなる
    ○ 発行処理も各caseで異なるので、追加するたびに issue()関数が長くなる
    ○ 検証が必要なチケットの場合、必要なドキュメントをプロパティとして持つ必要がある
    ■ 実装によるが普通のチケットを発行したいだけなのに、学割チケットの検証処理に必要なインスタ
    ンスもinit時に渡さないといけなくなったりする
    ■ 特定のチケットの処理を変更した時、全体をテストし直す必要がある
    → コード全体の見通しが悪くなり、意図せぬバグの発生や、メンテコストの増大、テスト工数の増大な
    ど、比較的大きな負の影響が発生する
    31
    ポリモーフィズムを用いなくても実装はできる。しかし...

    View Slide

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

    View Slide