Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

© GO Inc. Class 03

Slide 11

Slide 11 text

© GO Inc. Classの実装 3.a

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

© GO Inc. Classの特徴 3.b

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

© 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していいのか判断がつかない }

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

© 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

Slide 23

Slide 23 text

© GO Inc. Protocol 4

Slide 24

Slide 24 text

© GO Inc. Protocolの実装 4.a

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

© 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

Slide 30

Slide 30 text

© GO Inc. Protocolの特徴 4.b

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

© 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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

© GO Inc. まとめ 05

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

© GO Inc. 参考資料 05

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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