📜 ⬆️ ⬇️

Protocol-Oriented Programming

At WWDC 2015, Apple announced that Swift is the first protocol-oriented programming language ( video session "Protocol-Oriented Programming in Swift" ).

At this session and a number of others ( Swift in Practice , Protocol and Value Oriented Programming in UIKit Apps ) Apple demonstrates good examples of using protocols, but does not give a formal definition of what Protocol-Oriented Programming is.

There are many articles on Protocol-Oriented Programming (POP) on the Internet that demonstrate examples of using protocols, but I haven’t found a clear definition of POP in them either.
')
I tried to analyze examples of using protocols and formulate principles that should be followed, so that the code could be called protocol-oriented.

By looking at code examples demonstrating POP, you can determine that the key language features in POP are protocol , extensions, and constraints .
Let's see what opportunities they give us.

Protocol


Using the protocol can be divided into several scenarios:

Protocol as a type


Similar to the concept of the interface of the PLO and the contract of contract programming . Used to describe the functionality of the object. It can be used as a property type, as a function result type, as an element of a heterogeneous collection. Due to language restrictions, protocols having associated types or Self-requirements cannot be used as types.

Protocol as a type template


Similar to the concept of a concept of generalized programming .

It also serves to describe the functionality of an object, but, unlike “protocol as a type,” it is used as a type requirement in generic functions. May contain associated types.
associated types - auxiliary types that have some relevance to the concept-modeling type (definition from wikipedia ).
There is no clear line in which case to use the protocol as a type, and in which - as a restriction on the type, moreover - sometimes it is required to use the protocol in both scenarios. You can try to isolate use cases:


Protocol as trait


Trait (type) - an entity that provides a set of implemented functionality. Serves as a set of building blocks for classes / structures / enums.

Description of the concept of traits can be found here .

This concept is designed to replace inheritance. In OOP, one of the roles of classes is the unit of the code being reused. The reuse itself is through inheritance.

At the same time, the class is used to create instances, so its functionality must be complete. These two class roles often conflict. Plus, each class has a certain place in the class hierarchy, and the unit of code reuse can be used in an arbitrary place. As a solution, it is proposed to use more lightweight entities, traits, in the role of code reuse units, while classes will be assigned the role of a connecting element to enable logic inherited from traits.

In swift, this concept is implemented through protocols and protocol extensions. In order to “connect” the necessary functions defined for the protocol, you need to add a correspondence to this protocol to the created type - there is no need to create a base class for inheriting functionality.

What trait properties and protocol analogy has:


As we can see, the protocols fully comply with the concept of traits, described long before the advent of Swift.

Protocol as a marker


Used as an “attribute” of a type, in this case the protocol does not contain any methods. As an example, NSFetchRequestResult from CoreData. They are labeled NSNumber, NSDictionary, NSManagedObject, NSManagedObjectID. The protocol in this case does not describe the functionality of classes, but the fact that CoreData supports these classes as a type of query result. If you specify a type unlabeled with the NSFetchRequestResult protocol as a result, then at the assembly stage you will get an error.

Check for the presence of the protocol-marker can be used for branching logic:

 if object is HighPrioritized { ... } 

Extensions


Extension is a language tool that allows you to add functionality to an existing type or protocol.

With the help of extensions we can do:


Constraints


Restrictions on type. The following are supported: protocol compliant, inherited from class, has type. Constraints are used to determine the set of methods that a generic type has. If you pass an unsatisfying type as an argument, the compiler will generate an error.

Where are used:


Since the associatedtypes collections can be specified both on the protocol itself and for the methods to which the protocol is transferred, and for the protocol extensions, the question arises where to add the constraints. A few recommendations:

  1. If the protocol is application specific and will have one implementation, it is worth considering the possibility of using specific types instead of associated ones.
  2. If the protocol is application-specific and will have several implementations (taking into account fake tests), it is more convenient to put them in the protocol itself so as not to duplicate to the places where this protocol is used.
  3. If there are plans to reuse the protocol, the protocol should contain only those frameworks without which the existence of the protocol does not make sense and on which the main logic is built. All other constraints should be considered as a description of special cases and placed on methods and extensions.

Principles


The best material on POP is the “Protocol-Oriented Programming in Swift” session conducted by Dave Abrahams. I strongly recommend it for viewing. Most of the principles are shaped by examples from it.

  1. “Don't start with a class. Start with a protocol. ” This is the statement of Dave Abrahams from the above session. It can be interpreted in two ways:

    • start not with the implementation, but with the description of the contract (description of the functionality that the object will be required to provide to consumers)
    • Describe reusable logic in protocols, not classes. Use the protocol as a unit of code reuse, and the class as a place for unique logic. Otherwise, you can describe this principle - encapsulate what varies .
      The Template Method method can be a good analogue. His idea is to separate the general algorithm from the implementation details. The base class contains the general algorithm, and the children override certain steps of the algorithm. In POP, the general algorithm will be contained in the protocol extension, the protocol will determine the steps of the algorithm and the types used, and the implementation of the steps in the class.
  2. Composition through extensions. Many have heard the phrase “prefer composition over inheritance” . In OOP, when a different set of functionality is required from an object (polymorphic behavior), this functionality can either be divided into parts and a hierarchy of classes is organized, where each class inherits the functionality from its ancestor and adds its own, or divided into unrelated classes by hierarchy, instances of which are used in a binder. classroom. Using the ability to add conformance to the protocol through an extension, we can use composition without resorting to creating auxiliary classes. Often used in this way when viewController is added to match various delegates. The advantage over adding conformance to protocols in the class itself is better organized code:

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

  3. Instead of inheritance, use protocols. Dave Abrahams compared the protocols with superclasses because they allow to achieve the similarity of multiple inheritance.
    If your class contains a lot of logic, you should try to break it up into separate sets of functionality that you can put into protocols.

    Of course, if third-party frameworks like Cocoa are used, inheritance cannot be avoided.
  4. Use Retroactive modeling.

    An interesting example from the same session “Protocol-Oriented Programming in Swift”. Instead of writing a class that implements the Renderer protocol for rendering using CoreGraphics, the CGContext class via extension adds conformance to this protocol. Before adding a new class that implements the protocol, it is worth considering whether there is a type (class / structure / enum) that can be adapted to match the protocol?
  5. Include protocols that you can override ( Requirements create customization points ) in the logs.

    If you need to override the generic method defined in the protocol extension for a particular class, then transfer the signature of this method to protocol requirements. Other classes do not have to edit, because will continue to use the method from the extension. The difference will be in the wording - now it is the “default implementation method” instead of the “extension method” .

Differences POP from OOP


Abstraction


In OOP, the role of an abstract data type is played by a class. In the POP protocol.
Advantages of the protocol as an abstraction, according to Apple (slide: “A Better Abstraction Mechanism” ):


Transfer
  • Support for value types (and classes)
  • Support static type relationships (and dynamic dispatch)
  • Non monolithic
  • Retroactive modeling support
  • Does not impose object data (fields of the base class)
  • Does not burden initialization (base class)
  • Makes it clear what to implement


Encapsulation


- the property of the system, allowing to combine data and methods working with them in the class.
The protocol cannot contain the data itself; it can contain only the requirements for the properties that these data would provide. As in OOP, the necessary data should be included in the class / structure, but functions can be defined both in the class and in extensions.

Polymorphism


POP / swift supports 2 types of polymorphism:


In the case of polymorphism of subtypes, we do not know the specific type that is passed to the function - finding the implementation of methods of this type will be carried out at runtime ( Dynamic dispatch ). When using parametric polymorphism - the type of the parameter is known at compile time, respectively, and its methods ( Static dispatch ). Due to the fact that at the assembly stage the types used are known, the compiler is able to better optimize the code — first of all, through the use of substitution (inline) functions.

Inheritance


Inheritance in OOP is used to borrow functionality from the parent class.
In POP, obtaining the necessary functionality occurs by adding correspondences to the protocols that provide functions via extensions. At the same time, we are not limited to classes; we have the opportunity to expand structures and enums with protocols.

Protocols can be inherited from other protocols - this means adding requirements from the parent protocols to your own requirements.

Let's see how you can use POP in practice.

Example 1


The first example is an upgraded version of SegueHandler, presented at WWDC 2015 - Session 411 Swift in Practice .

Imagine that we have a RootViewController and we need to do the processing of transitions to DetailViewController and AboutViewController. A typical implementation of 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.") } } 

We know that we can have only 2 transitions - with id DetailViewController and AboutViewController with the same controller classes, however, we have to do a check for the unknown seque.identifier and type conversion segue.destination.

Let's try to improve the code of this method. Let's start with a description of possible transitions - enum is perfect for this:

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

(Note: SegueDestination is declared inside the RootViewController)

Our goal is to write a universal helper method for conversion processing. To do this, we define the SegueHandlerType protocol with an associated type describing the transition. The associated type requirement is that it must provide a failable initializer that returns nil in case of a non-valid combination of segue id and controller type:

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

The protocol is defined, now we add the segueDestination (forSegue :) method for it that returns a transition instance:

 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 } } 

Let's make RootViewController implement SegueHandlerType (we will put it into a separate file so that this trivial code will rarely catch the eye):

 // 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 { } 

I want to note that the associated type in SegueHandlerType and enum in RootViewController have the same name, so the implementation of SegueHandlerType for RootViewController is empty. In the case of different names, and if our enum were not defined inside the RootViewController, we would need to specify the type associated with the protocol using typealias:

 extension RootViewController: SegueHandlerType { typealias SegueDestination = RootControllerSegueDestination } 

The final part of the example - now we can refactor prepare (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 } } 

The code has become much cleaner, right?

Of course, as a result, the code became more - but we managed to separate the main logic (the one that is hidden behind the comments "// configure vc") and the auxiliary code. Pros - the code has become easier to read, and the auxiliary SegueHandlerType can be reused.

Example 2


Consider a typical task to display a list of items in UITableView.
As the source data we have the Cat model and TestCatRepository , which corresponds to the CatRepository protocol:

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

Table and cell controller classes have been added to the project: CatListTableViewController , CatTableViewCell .

Let's try to describe a generalized protocol list. Imagine that we have plans to add other tables to the project, which, in particular, may contain several sections. Protocol requirements:


Taking into account the requirements made, we write our protocol:

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

Let's describe the cell requirements for displaying cat information:

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

Add compliance with this protocol to the CatTableViewCell class.

Our main protocol, ListViewType, must be added to CatListTableViewController. We use only one type of cell - CatTableViewCell, so we use it as the associatedtype CellView. There is only one section in the table and the number of elements is not known in advance - we use Void and Int as SectionIndex and ItemIndex, respectively.

Full implementation of 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 } } 

Now our goal is to connect CatRepository and ListViewType. However, I do not want to associate the algorithm with a specific Cat model. To do this, we select generalized protocols, where the type of the model is in the associatedtype:

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

Add compliance with new protocols:

 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 { } 

Everything is ready to implement the method of displaying objects provided by the RepositoryType in the ListViewType list. The algorithm will not support multiple sections, but uses Int as an index. Add restrictions on the extension:

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

Our CatListTableViewController complies with these limitations.
But these are not all limitations - ListViewType.CellView should be ConfigurableViewType, and its Model type should be RepositoryType.Model:

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

And these restrictions correspond to our class.

Full extension code:

 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) } } } 

The main logic is ready, we use this function in AppDelegate:

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

.

, .

extensions, , , . , : ? ListViewType. CatListTableViewController , . CatListTableViewController , :

 catListTableView.setup(repository: repository) 

CatListTableViewController — Controller MVC. — MVC, .

Conclusion


Protocol-Oriented Programming Generic Programming Traits.
POP , , , , .

Sources:

  1. WWDC 2015 «Protocol-Oriented Programming in Swift»
  2. WWDC 2015 «Swift in Practice»
  3. WWDC 2016 «Protocol and Value Oriented Programming in UIKit Apps»
  4. type erasure
  5. Traits: Composable Units of Behaviour

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


All Articles