プロトコル指向プログラミング

AppleはWWDC 2015で、Swiftが最初のプロトコル指向プログラミング言語であると発表しました( ビデオセッション「Protocol-Oriented Programming in Swift」 )。

このセッションおよび他の多くのセッション( Swift in PracticeUIKitアプリのプロトコルおよび値指向プログラミング )では、プロトコルの使用の良い例を示していますが、プロトコル指向プログラミングとは正式には定義していません。

プロトコル指向プログラミング(POP)に関する多くの記事がインターネット上にあり、プロトコルの使用例が示されていますが、その中でさえPOPの明確な定義は見つかりませんでした。

プロトコルの使用例を分析し、コードをプロトコル指向と呼ぶことができるように従うべき原則を策定しようとしました。

POPを示すコード例を見てみると、次の言語ツールがPOPで重要な役割を果たしていることがわかります: protocolextensionsconstraint
彼らが私たちに与える機会を見てみましょう。

プロトコル


プロトコルの使用は、いくつかのシナリオに分けることができます。

タイプとしてのプロトコル


OOPからのインターフェイスコントラクトプログラミングからのコントラクトは 、概念に似ています。 オブジェクトの機能を説明します。 プロパティのタイプ、関数の結果のタイプ、異種コレクションの要素のタイプとして使用できます。 言語の制限により、関連するタイプまたは自己要件を持つプロトコルはタイプとして使用できません。

パターンタイプとしてのプロトコル


一般化プログラミング概念に似ています

また、オブジェクトの機能を説明する役割も果たしますが、「型としてのプロトコル」とは異なり、一般化された関数の型要件として使用されます。 関連するタイプが含まれる場合があります。
関連型 -概念モデリング型に関連する補助型( ウィキペディアの定義)。
プロトコルをタイプとして使用する場合、およびタイプの制限としていいえ、さらに-両方のシナリオでプロトコルを使用することが必要な場合がある明確な行はありません。 ユースケースを強調してみることができます:


特性としてのプロトコル


特性(タイプ) -実装された機能のセットを提供するエンティティ。 クラス/構造/列挙型のビルディングブロックのセットとして機能します。

特性コンセプトの説明はここにあります

この概念は、継承を置き換えるように設計されています。 OOPでは、クラスの役割の1つは再利用可能なコードの単位です。 再利用自体は継承を通じて行われます。

同時に、クラスはインスタンスの作成に使用されるため、その機能は完全でなければなりません。 これらの2つのクラスの役割はしばしば競合します。 さらに、各クラスにはクラス階層内の特定の場所があり、コードの再利用の単位は任意の場所に適用できます。 解決策として、より軽いエンティティを使用することが提案されています-コードの再利用の単位としての特性、およびクラスは、特性から継承されたロジックを呼び出すための接続要素の役割を割り当てられます。

Swiftは、プロトコルとプロトコル拡張を通じてこの概念を実装します。 プロトコルに固有の必要な機能を「接続」するには、作成するタイプにこのプロトコルへの通信を追加する必要があります-機能を継承するための基本クラスを作成する必要はありません。

特性にはどのような特性があり、プロトコルとの類似性があります。


ご覧のとおり、プロトコルはSwiftが登場するずっと前に説明された特性の概念と完全に一致しています。

マーカーとしてのプロトコル


タイプの「属性」として使用されます。この場合、プロトコルにはメソッドが含まれていません。 例は、CoreDataのNSFetchRequestResultです。 彼は、NSNumber、NSDictionary、NSManagedObject、NSManagedObjectIDをマークしました。 この場合のプロトコルは、クラスの機能については説明していませんが、CoreDataがこれらのクラスをクエリ結果のタイプとしてサポートしているという事実を説明しています。 NSFetchRequestResultプロトコルでマークされていないタイプを結果として指定すると、アセンブリ段階でエラーが発生します。

プロトコルマーカーの存在の確認は、ロジックの分岐にも使用できます。

 if object is HighPrioritized { ... } 

拡張機能


拡張機能は、既存のタイプまたはプロトコルに機能を追加できる言語ツールです。

拡張機能を使用すると、次のことができます。


制約


型の制限。 以下がサポートされています。プロトコルに準拠し、クラスから継承し、タイプを持っています。 制約は、ジェネリック型が持つメソッドのセットを決定するために使用されます。 満足できない型の制約を引数として渡すと、コンパイラはエラーをスローします。

使用場所:


関連タイプの定数は、プロトコル自体と、プロトコルが渡されるメソッド、およびプロトコル拡張の両方で指定できるため、定数をどこに追加するかという疑問が生じます。 いくつかの推奨事項:

  1. プロトコルがアプリケーション固有であり、1つの実装を持つ場合は、関連するタイプではなく特定のタイプを使用する可能性を検討する価値があります。
  2. プロトコルがアプリケーション固有であり、いくつかの実装がある場合(テスト用の偽物を考慮に入れて)-このプロトコルが使用される場所で複製しないように、プロトコル自体にそれらを配置する方が便利です。
  3. プロトコルを再利用する計画がある場合、プロトコルにはこれらの定数のみを含める必要があります。これらの定数がないと、プロトコルの存在が意味を成さず、その上にメインロジックが構築されます。 他のすべての定数は、特殊なケースの説明と見なされ、メソッドおよび拡張機能に配置される必要があります。

原則


最高のPOP教材は、デイブ・アブラハムズが主催する「プロトコル指向プログラミングのスウィフト」セッションです。 視聴することを強くお勧めします。 ほとんどの原則は、そこからの例のおかげで形成されています。

  1. 「クラスから始めないでください。 プロトコルで開始します。 "。 これは、上記のセッションからのデイブアブラハムスによる声明です。 解釈する方法は2つあります。

    • 実装ではなく、契約の説明(オブジェクトが消費者に提供する必要がある機能の説明)から始めます
    • クラスではなく、プロトコルで再利用可能なロジックを記述します。 プロトコルをコードの再利用の単位として使用し、クラスを一意のロジックの場所として使用します。 別の方法で、この原則を説明することができます- 変化するものをカプセル化します
      「テンプレートメソッド」パターンは、優れた類似物です。 彼のアイデアは、一般的なアルゴリズムを実装の詳細から分離することです。 基本クラスには共通のアルゴリズムが含まれ、子クラスはアルゴリズムの特定のステップを再定義します。 POPでは、一般的なアルゴリズムはプロトコル拡張に含まれ、プロトコルは使用されるアルゴリズムのステップとタイプ、およびクラス内のステップの実装を決定します。
  2. 拡張機能による構成。 多くの人が「継承よりも合成を好む」というフレーズを聞いています。 OOPでは、オブジェクトに異なる機能セット(多態的な動作)が必要な場合、この機能をパーツに分割し、クラスの階層を編成できます。各クラスは祖先から機能を継承して独自に追加するか、バインダーで使用されるインスタンスの階層に関係のないクラスに分割します教室。 拡張機能を使用してプロトコルに適合性を追加する機能を使用すると、補助クラスを作成せずに構成を使用できます。 これは、さまざまなデリゲートに一致するviewControllerを追加するときによく使用されます。 クラス自体にプロトコル適合性を追加することの利点は、より適切に編成されたコードです。

     extension MyTableViewController: UITableViewDelegate { //    UITableViewDelegate } extension MyTableViewController: UITableViewDataSource { //    UITableViewDataSource } extension MyTableViewController: UITextFieldDelegate { //    UITextFieldDelegate } 

  3. 継承の代わりにプロトコルを使用します。 Dave Abrahamsはプロトコルをスーパークラスと比較しました。これは、プロトコルが複数の継承を許可するためです。
    クラスに多くのロジックが含まれている場合は、ログに記録できる機能の別のセットに分類する価値があります。

    もちろん、Cocoaのようなサードパーティのフレームワークを使用している場合、継承は避けられません。
  4. 遡及モデリングを使用します。

    同じセッション「Swiftのプロトコル指向プログラミング」の興味深い例です。 CoreGraphicsを使用してレンダリングするためのRendererプロトコルを実装するクラスを作成する代わりに、このプロトコルへの準拠が拡張機能を介してCGContextクラスに追加されます。 プロトコルを実装する新しいクラスを追加する前に、プロトコル準拠に適応できるタイプ(クラス/構造/列挙型)があるかどうかを検討する必要がありますか?
  5. プロトコルでオーバーライドできるメソッドを含めます( 要件によりカスタマイズポイントが作成されます )。

    特定のクラスのプロトコル拡張で定義された一般的なメソッドを再定義する必要がある場合は、このメソッドの署名をプロトコル要件に転送してください。 他のクラスは編集する必要はありません。 拡張機能のメソッドを引き続き使用します。 違いは言葉遣いにあります-現在は「拡張メソッド」ではなく「デフォルトの実装メソッド」です

POPとOOPの違い


抽象化


OOPでは、クラスは抽象データ型の役割を果たします。 POPでは、プロトコル。
Appleによると、抽象化としてのプロトコルの利点(スライド: 「より良い抽象化メカニズム」 ):


翻訳
  • 値型(およびクラス)のサポート
  • 静的な型関係(および動的なディスパッチ)のサポート
  • キャストなし
  • 遡及モデリングのサポート
  • オブジェクトデータを課しません(基本クラスフィールド)
  • 初期化に負担をかけないでください(基本クラス)
  • 実装するものが明確になります


カプセル化


-システム内のプロパティで、クラス内でそれらと連携するデータとメソッドを組み合わせることができます。
プロトコルにはデータ自体を含めることはできません。このデータが提供するプロパティの要件のみを含めることができます。 OOPの場合と同様に、必要なデータはクラス/構造に含める必要がありますが、関数はクラスと拡張の両方で定義できます。

多型


POP / swiftは、2種類のポリモーフィズムをサポートしています。


サブタイプポリモーフィズムの場合、関数に渡される特定のタイプはわかりません。このタイプのメソッドの実装は、実行時に検出されます( 動的ディスパッチ )。 パラメトリック多相性を使用する場合-パラメータの型はそれぞれコンパイル時に既知であり、そのメソッド( Static dispatch )。 使用される型はアセンブリ段階で既知であるという事実により、コンパイラは、主にインライン関数を使用することで、コードをより最適化できます。

継承


OOP継承は、親クラスから機能を借用するために使用されます。
POPでは、拡張機能を介して機能を提供するプロトコルに対応を追加することにより、必要な機能が取得されます。 同時に、クラスに限定されず、プロトコルにより構造を拡張し列挙する機能があります。

プロトコルは他のプロトコルから継承できます。これは、親プロトコルの要件を独自の要件に追加することを意味します。

実際にPOPを使用する方法を見てみましょう。

例1


最初の例は、 WWDC 2015-Session 411 Swift in Practiceで発表されたSegueHandlerのアップグレード版です。

RootViewControllerがあり、DetailViewControllerおよびAboutViewControllerの遷移処理を行う必要があるとします。 prepareの典型的な実装(for:sender :)

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { switch segue.identifier { case "DetailViewController": guard let vc = segue.destination as? DetailViewController else { fatalError("Invalid destination view controller type.") } // configure vc case "AboutViewController": guard let vc = segue.destination as? AboutViewController else { fatalError("Invalid destination view controller type.") } // configure vc default: fatalError("Invalid segue identifier.") } } 

idがDetailViewControllerとAboutViewControllerで同じ名前のコントローラークラスを持つ2つの遷移しか持てないことがわかっていますが、不明なseque.identifierチェックとsegue.destinationの型変換を行う必要があります。

このメソッドのコードを改善してみましょう。 可能な遷移の説明から始めましょう-enumはこれに最適です:

 enum SegueDestination { case detail(DetailViewController) case about(AboutViewController) } 

(注:SegueDestinationはRootViewController内で宣言されます)

私たちの目標は、遷移を処理するための汎用ヘルパーメソッドを記述することです。 これを行うには、遷移を記述する関連するタイプでSegueHandlerTypeプロトコルを定義します。 関連付けられた型の要件-セグエIDとコントローラー型の無効な組み合わせの場合にnilを返す失敗可能な初期化子を提供する必要があります。

 protocol SegueHandlerType { associatedtype SegueDestination: SegueDestinationType } protocol SegueDestinationType { init?(segueId: String, controller: UIViewController) } 

プロトコルが定義され、移行インスタンスを返すsegueDestination(forSegue :)メソッドを追加します。

 extension SegueHandlerType { func segueDestination(forSegue segue: UIStoryboardSegue) -> SegueDestination { guard let id = segue.identifier else { fatalError("segue id should not be nil") } guard let destination = SegueDestination(segueId: id, controller: segue.destination) else { fatalError("Wrong segue Id or destination controller type") } return destination } } 

RootViewControllerにSegueHandlerTypeを実装させましょう(この些細なコードが目を引く可能性が低いように、別のファイルに入れてください):

 // file RootViewController+SegueHandler.swift extension RootViewController.SegueDestination: SegueDestinationType { init?(segueId: String, controller: UIViewController) { switch (segueId, controller) { case ("DetailViewController", let vc as DetailViewController): self = .detail(vc) case ("AboutViewController", let vc as AboutViewController): self = .about(vc) default: return nil } } } extension RootViewController: SegueHandlerType { } 

SegueHandlerTypeのrelatedtypeとRootViewControllerのenumは同じ名前であるため、RootViewControllerのSegueHandlerTypeの実装は空であることに注意してください。 異なる名前があり、列挙がRootViewController内で定義されていない場合、typealiasを使用してプロトコルに関連付けられたタイプを指定する必要があります。

 extension RootViewController: SegueHandlerType { typealias SegueDestination = RootControllerSegueDestination } 

例の最後の部分-これで準備をリファクタリングできます(for:sender :):

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { switch segueDestination(forSegue: segue) { case .detail(let vc): // configure vc case .about(let vc): // configure vc } } 

コードはずっときれいですよね?

もちろん、コードの結果として、さらに多くがありましたが、メインロジック( "// configure vc"コメントの後ろに隠されているロジック)と補助コードを分離することができました。 長所-コードが読みやすくなり、補助SegueHandlerTypeを再利用できます。

例2


UITableViewで要素のリストを表示するための典型的なタスクを検討してください。
初期データとして、 Catモデルと、 CatRepositoryプロトコルに対応するTestCatRepositoryがあります。

 struct Cat { var name: String var photo: UIImage? } protocol CatRepository { func getCats() -> [Cat] } 

テーブルとセルコントローラークラスがプロジェクトに追加されます: CatListTableViewControllerCatTableViewCell

一般化リストプロトコルを説明してみましょう。 他のテーブルをプロジェクトに追加する計画があると想像してください。これには、いくつかのセクションが含まれます。 プロトコル要件:


作成された要件を考慮して、プロトコルを記述します。

 protocol ListViewType: class { associatedtype CellView associatedtype SectionIndex associatedtype ItemIndex func refresh(section: SectionIndex, count: Int) var updateItemCallback: (CellView, ItemIndex) -> () { get set } } 

猫に関する情報を表示するためのセルの要件を説明しましょう。

 protocol CatCellType { func setName(_: String) func setImage(_: UIImage?) } 

このプロトコルへの通信をCatTableViewCellクラスに追加します。

メインプロトコルであるListViewTypeをCatListTableViewControllerに追加する必要があります。 CatTableViewCellの1つのタイプのみを使用しているため、関連付けられたタイプのCellViewとして使用します。 テーブルにはセクションが1つしかなく、要素の数は事前にわかりません。SectionIndexとItemIndexとして、それぞれVoidとIntを使用します。

CatListTableViewControllerの完全な実装:

 class CatListTableViewController: UITableViewController, ListViewType { var itemsCount = 0 var updateItemCallback: (CatTableViewCell, Int) -> () = { _, _ in } func refresh(section: Void, count: Int) { itemsCount = count tableView.reloadData() } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return itemsCount } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "CatCell", for: indexPath) as! CatTableViewCell updateItemCallback(cell, indexPath.row) return cell } } 

現在の目標は、CatRepositoryとListViewTypeをリンクすることです。 ただし、アルゴリズムを特定のCatモデルに関連付けたくありません。 これを行うために、モデルタイプがrelatedtypeでレンダリングされる一般化されたプロトコルを区別します。

 protocol RepositoryType { associatedtype Model func getItems() -> [Model] } protocol ConfigurableViewType { associatedtype Model func configure(using model: Model) } 

新しいプロトコルへのコンプライアンスを追加します。

 extension CatRepository { func getItems() -> [Cat] { return getCats() } } extension TestCatRepository: RepositoryType { } extension CatCellType where Self: ConfigurableViewType { func configure(using model: Cat) { setName(model.name) setImage(model.photo) } } extension CatTableViewCell: ConfigurableViewType { } 

ListViewTypeリストのRepositoryTypeによって提供されるオブジェクトを表示するメソッドを実装する準備がすべて整いました。 アルゴリズムは複数のセクションをサポートしませんが、インデックスとしてIntを使用します。 拡張機能に制限を追加します。

 extension ListViewType where SectionIndex == (), ItemIndex == Int { ... } 

CatListTableViewControllerはこれらの制限に準拠しています。
しかし、これらはすべての制限ではありません-ListViewType.CellViewはConfigurableViewTypeでなければならず、そのモデルタイプはRepositoryType.Modelでなければなりません:

 func setup<Repository: RepositoryType>(repository: Repository) where CellView: ConfigurableViewType, CellView.Model == Repository.Model { ... } 

そして、クラスはこれらの制限を満たしています。

完全な拡張コード:

 extension ListViewType where SectionIndex == (), ItemIndex == Int { func setup<Repository: RepositoryType>(repository: Repository) where CellView: ConfigurableViewType, CellView.Model == Repository.Model { let items = repository.getItems() refresh(section: (), count: items.count) updateItemCallback = { cell, index in let item = items[index] cell.configure(using: item) } } } 

メインロジックの準備ができました。AppDelegateでこの関数を使用します。

 let catListTableView = window!.rootViewController as! CatListTableViewController let repository = TestCatRepository() catListTableView.setup(repository: repository) 

完全なサンプルコードはこちらにあります

この例では、ロジックは、全体として機能する多くの小さな独立した部分に分割されています。

ロジックのほとんどはクラスではなく拡張機能にあるため、一見どのクラスがどの責任を負うのかは明確ではありません。したがって、疑問が生じます。この例はどのアーキテクチャに起因するのでしょうか?使用される機能は、ListViewType拡張機能にあります。このロジックは、このプロトコルに準拠しているため、CatListTableViewControllerクラスで使用できます。CatListTableViewControllerのコンシューマーは、これをその機能と見なします。

 catListTableView.setup(repository: repository) 

したがって、CatListTableViewController-MVCのコントローラーの役割。したがって、アプリケーションアーキテクチャはMVCですが、コードの構成は変わっています。

おわりに


プロトコル指向プログラミングは、汎用プログラミングと特性の概念に依存しています。
POPを使用すると、コードの再利用性が向上し、構造化されたコードが改善され、コードの重複が減少し、クラス継承階層の複雑さが回避され、コードの接続性が向上します。

ソース:

  1. WWDC 2015「Swiftのプロトコル指向プログラミング」
  2. WWDC 2015「Swift in Practice」
  3. WWDC 2016「UIKitアプリのプロトコルと価値指向プログラミング」
  4. タイプ消去
  5. 特性:構成可能な行動単位

Source: https://habr.com/ru/post/J358804/


All Articles