2023年05月10日にオンラインで開催されたYUMEMI.grow Mobile #3で発表した資料です。
Swiftでのポリモーフィズムを実現するときによく使われる方法であるClassとProcotolにはそれぞれどのような特徴があるのか、実際にユースケースをモデリングし、そのコードを提示しながらそれぞれのメリット・デメリットについて検討いたしました。
© GO Inc.Swiftにおけるポリモーフィズム実現手段の検討2023.05.10開発本部ソフトウェア開発部ユーザーシステムグループ / 髙橋秀宗GO株式会社
View Slide
© GO Inc. 2自己紹介プロフィール写真GO株式会社ユーザーシステムグループ / 髙橋秀宗タクシーアプリ『GO』のiOSアプリ開発を担当趣味はギターとカメラ@h1d3mun3
Index© GO Inc.1. ポリモーフィズムを実現する手段について2. ユースケースの定義3. Classa. Classの実装b. Classの特徴4. Protocola. Protocolの実装b. Protocolの特徴5. まとめ6. 参考資料3
© GO Inc.ポリモーフィズムを実現する手段について01
© GO Inc.複数の「型」に対してアクセスできる共通の接点のこと。5ポリモーフィズムとは
© GO Inc.動物における「食べる」動作人 → 肉を、口で食べる牛 → 草を、丸呑みして胃ですりつぶして食べる→ 「食べる」という動作は動物全般に存在するが、その詳細は各動物(≒型)によって異なる6例えば...
© GO Inc.Swiftでは下記2つのやり方で、ポリモーフィズムを実現することができる● Class● Protocol学割チケットの発行フローをClassとProtocolでモデリングすることを通じて、どのような違いが出るのか確認するポリモーフィズムを実現するための手段7
© GO Inc.ユースケースの定義02
© GO Inc.映画館での学割チケットは、下記フローで発行される1. 申込書を書いて窓口に並ぶ2. 窓口で学生証を提示する3. 条件に問題がなければ学割チケットを発行学割チケットの発行フロー9
© GO Inc.Class03
© GO Inc.Classの実装3.a
© GO Inc.class Ticket {init() { } // 発行するための接点を定義}class ValidationRequiredTicket: Ticket { } // 検証が必要なTicketを表現// 学割チケットを検証が必要なTicketとして表現class StudentDiscountTicket: ValidationRequiredTicket { }チケットの表現12
© GO Inc.// 申請に必要なドキュメントの表現class ApplicationDocument {// 自身が有効化を判定するisValidという関数を持つfunc isValid() -> Bool {// 親クラス視点ではどのように判定してよいかわからないのでfatalErrorにするfatalError("must represent subclass!")}}// 学生証を表現class StudentCard: ApplicationDocument {override func isValid() -> Bool { } // 学生証が有効かを判定する}学生証の表現13
© GO Inc.// チケット申込書を表現class TicketRequest {// チケットの発行を行うissueという関数を持つfunc issue() throws -> Ticket {// 親クラス視点ではどのチケットを発行してよいかわからないのでfatalErrorにするfatalError("must represent subclass")}}// TicketRequestでTicketを発行できなかったときのエラーenum TicketRequestIssueError: Error {case validationFailure}申請書の表現 - 114
© GO Inc.// 検証が必要なTicketを発行するTicketRequestを表現class ValidationRequiredTicketRequest: TicketRequest {let document: ApplicationDocumentinit(document: ApplicationDocument) {self.document = document}}申請書の表現 - 215
© GO Inc.// 学割TicketのTicketRequestclass StudentDiscountTicketRequest: ValidationRequiredTicketRequest {override func issue() throws -> Ticket {guard document.isValid() else { throw TicketRequestIssueError.validationFailure }// 子クラス視点では型がわかるにもかかわらず、Ticket型として返却せざるをえないreturn StudentDiscountTicket()}}申請書の表現 - 316
© GO Inc.Classの特徴3.b
© GO Inc.● コードを再利用できる○ 子クラス側では、親クラスに実装したコードを再度実装しなくてよいメリット18
© GO Inc.● 親クラスでは子クラス側でoverrideされていることを前提とした実装を強制される○ サブクラス側でのoverrideを強制できない● 子クラスは1つの親クラスしか選択できない○ 子クラスは親クラスに追加された変更をすべて受け入れなくてはならない● 子クラスは親クラスで宣言されたメソッド宣言を変更できないので、具体的な型情報が消滅する○ Genericsを利用することで少し安全に実装することが出来ますが、書きやすさ・読みやすさに影響するので、あまり良い解決策とは言えないデメリット19
© GO Inc.overrideについて実際のコードで見ると...20class TicketCounter {var count = 0func 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していいのか判断がつかない}
© GO Inc.class TicketCounter {// 親クラス視点ではどのチケットを発行していいか決定できないないので、Ticket型を返却させるしかないfunc issue() -> Ticket {fatalError("must implement subclass")}}class StudentTicketCounter: TicketCounter {// きちんとStudentDiscountTicketを返したのに、呼び出し元からはTicketとしか認識できないoverride func issue() -> Ticket {return StudentDiscountTicket()}}型情報消失を実際のコードで見ると...21
© GO Inc.// Genericsを使うことで型情報が明確になったclass TicketCounter {var document: Documentinit(document: Document) {self.document = document}func issue() -> IssuedTicket {fatalError("must inprement subclass!")}}// その代わり、記述が若干長くなるclass StudentTicketCounter: TicketCounter {override func issue() -> StudentDiscountTicket {return StudentDiscountTicket() // 呼び出し側にはStudentDiscountTicket型として返却できる}}型情報表現について、実際のコードで見ると...22
© GO Inc.Protocol4
© GO Inc.Protocolの実装4.a
© GO Inc. 25protocol Ticket {static func issue() -> Self // 自分自身を発行するissueという接点を持つ}protocol ValidationRequiredTicket: Ticket { } // 検証が必要なTicketを表現// 学割チケットを検証が必要なTicketとして表現struct StudentDiscountTicket: ValidationRequiredTicket {static func issue() -> StudentDiscountTicket {return StudentDiscountTicket()}}チケットの表現
© GO Inc.// 申請に必要なドキュメントの表現protocol ApplicationDocument {// 自身が有効かを判定するisValidという関数を持つfunc isValid() -> Bool}struct StudentCard: ApplicationDocument {func isValid() -> Bool { } // 学生証が有効かを判定する}26学生証の表現
© GO Inc. 27申請書の表現 - 1protocol TicketRequest {// 発行したいTicketをIssueTicketTypeという型で持つassociatedtype IssueTicketType: Ticket// チケットの発行を行うissueという関数を持つfunc issue() throws -> IssueTicketType}// TicketRequestでTicketを発行できなかったときのエラーenum TicketRequestIssueError: Error {case validationFailure}
© GO Inc.// 検証が必要なTicketを発行するTicketRequestを表現protocol ValidationRequiredTicketRequest: TicketRequest {// 申請に必要なドキュメントをDocumentという型で持つassociatedtype Document: ApplicationDocumentvar document: Document { get }}28申請書の表現 - 2
© GO Inc. 29// 学割チケットのTicketRequeststruct StudentDiscountTicketRequest: ValidationRequiredTicketRequest {// IssueTicketTypeがStudentDiscountTicket型と宣言typealias IssueTicketType = StudentDiscountTicket// DocumentはStudentCard型と宣言typealias Document = StudentCardlet document: Documentfunc issue() throws -> StudentDiscountTicket {guard document.isValid() else { throw TicketRequestIssueError.validationFailure }return IssueTicketType.issue()}}申請書の表現 - 3
© GO Inc.Protocolの特徴4.b
© GO Inc.● 振る舞いだけを表現できる○ Protocolの宣言は実装を持たないので、責務が分離されたり、UnitTestを書くときに好ましい● 複数の振る舞いを同時に適用できる● 型情報を適切に表現することができるメリット31
© GO Inc.// 学割チケットのTicketRequeststruct StudentDiscountTicketRequest: ValidationRequiredTicketRequest {// IssueTicketTypeがStudentDiscountTicket型と宣言typealias IssueTicketType = StudentDiscountTicket// DocumentはStudentCard型と宣言typealias Document = StudentCardlet document: Documentfunc issue() throws -> StudentDiscountTicket {guard document.isValid() else { throw TicketRequestIssueError.validationFailure }return IssueTicketType.issue()}}型情報表現について、実際のコードで見ると...32
© GO Inc.● 実装は別途記述しなくてはならない○ Protocolは振る舞いしか表現できないので、実装は別途記述しなくてはならない○ 適切じゃない使い方をすると、同じような実装を複数箇所に書く必要が出るデメリット33
© GO Inc.まとめ05
© GO Inc.まとめ - Class● 仕組み上Classは、振る舞いと実装を分離できない○ 振る舞い: 何をしたいのか○ 実装: 具体的にどうやるのか● この問題は振る舞いを中心に開発する際に大きな問題となる● その一方、大きく実装を変更する必要がないものにおいては、実装が必ずついてくるというClassの特徴はメリットともなりうる35
© GO Inc.● Protocolは振る舞いだけを表現できる● そのため、振る舞いを中心に開発する際はメリットが大きい● その一方同じような実装を何度も実装する必要がある場合は、Protocolを利用すると同じようなコードを何度も書かなくてはならず、デメリットが有るまとめ - Protocol36
© GO Inc.● ProtocolかClassのどちらを使うべきか判断に迷ったときは、振る舞いと実装が分離されていないと困るかどうかという観点で考えてみると良いまとめ37
© GO Inc.参考資料05
© GO Inc. 39● Protocol-Oriented Programming in Swift (WWDC15)● Embrace Swift Generics (WWDC22)参考資料
文章・画像等の内容の無断転載及び複製等の行為はご遠慮ください。© GO Inc.