primers

Protocol Oriented Programming (HEREINAFTER referred to as POP) is a Programming paradigm of Swift proposed by Apple at WWDC 2015. POP is more flexible than traditional object-oriented programming (OOP). In combination with Swift’s value semantics and the implementation of the Swift standard library, many application scenarios of POP have been discovered over the past year. In addition to introducing the idea of POP, this presentation will introduce some scenarios where POP can be used in everyday development, so that guests can start experimenting with POP in their daily work and improve their code design.

This article is reprinted from Meow God :onevcat.com/2016/11/pop… For the purpose of conveying more information, the copyright of this article belongs to the original author or source organization.

First acquaintance – What is the Swift protocol

Protocol

The Swift standard library has more than 50 complex and varied protocols, and almost all of the actual types satisfy several protocols. Protocol is the Swift base on which the rest of the language is organized and built. This is very different from the object-oriented build approach we are used to.

The simplest but useful Swift protocol is defined as follows:

protocol Greetable {
    var name: String { get }
    func greet()
}
Copy the code

These lines define a protocol called Greetable with a definition for the name property and a definition for the greet method.

A protocol is a definition of a set of properties and/or methods, and if a specific type wants to comply with a protocol, it needs to implement all of those things defined by that protocol. All a protocol really does is “a contract about implementation.”

object-oriented

Before I dive into the concept of the Swift protocol, I want to remind you of object orientation. I’m sure we’re all familiar with the term from textbooks, blogs, etc. So here’s an interesting question that not every programmer thinks about: what is the core idea of object orientation?

Let’s start with a piece of object-oriented code:

class Animal {
    var leg: Int { return 2 }
    func eat() {
        print("eat food.")
    }
    func run() {
        print("run with (leg) legs")
    }
}

class Tiger: Animal {
    override var leg: Int { return 4 }
    override func eat() {
        print("eat meat.")
    }
}

let tiger = Tiger()
tiger.eat() // "eat meat"
tiger.run() // "run with 4 legs"
Copy the code

The parent Animal class defines an Animal’s leg (virtual class is used here, but Swift doesn’t have this concept, so ignore return 2), and the Animal’s eat and run methods, and provides implementations for them. The Tiger subclass rewrites leg (four legs) and eat (meat) as appropriate, whereas for Run, the implementation of the parent class is sufficient, so it doesn’t need to be rewritten.

We see that Tiger and Animal share some code that is encapsulated in the parent class, and other subclasses besides Tiger can also use Animal code. This is really the core idea of OOP – using encapsulation and inheritance to bring together a series of related things. Our predecessors developed the concept of object-oriented programming in order to be able to model real-world objects, but the concept had some flaws. Although we try to model things in this abstract and inherited way, the real thing is often a combination of a set of characteristics, rather than simply being built in a continuous and expanding way. So more and more recently people are finding that object orientation doesn’t abstract things very well a lot of the time, and maybe we need to find a better way to do it.

The dilemma of object-oriented programming

Crosscutting concern

Let’s look at another example. Let’s get away from the animal world this time and go back to Cocoa. Suppose we have a ViewController that inherits from UIViewController, and we add a myMethod to it:

Class ViewCotroller: UIViewController {// inherit // view, isFirstResponder()... Func myMethod() {}}Copy the code

If we have another AnotherViewController that inherits from UITableViewController, we want to add the same myMethod to it:

Class AnotherViewController: UITableViewController {// Inherit // tableView, isFirstResponder()... Func myMethod() {}}Copy the code

This brings us to the first big problem with OOP, which is the difficulty of sharing code between classes with different inheritance relationships. The problem here is called, in the jargon, cross-cutting Concerns. Our focus is myMethod in two inheritance chains (UIViewController -> ViewCotroller and UIViewController -> UITableViewController -> AnotherViewController) on the cross section. Object orientation is a good way to abstract, but certainly not the best way. It doesn’t describe the fact that two different things have the same property. Here, the combination of features is closer to the nature of things than inheritance.

To solve this problem, we have several solutions:

  • Copy & Paste

    This is a bad solution, but there are still many friends who choose this solution, especially when the time limit is too tight to optimize. That’s understandable, but it’s also the beginning of bad code. We should avoid this as much as possible.

  • The introduction of BaseViewController

    Add shared code to a BaseViewController that inherits from UIViewController, or simply add extension to UIViewController. This seems like a slightly more plausible approach, but if you do it over and over again, the so-called Base will quickly turn into a garbage dump. Responsibilities aren’t clear, anything can be thrown into Base, you have no idea what classes are out of Base, and the impact of this “superclass” on your code is unpredictable.

  • Dependency injection

    Pass in an object with myMethod and use the new type to provide this functionality. This is a slightly better approach, but introducing additional dependencies may be something we don’t want.

  • Multiple inheritance

    Of course, Swift does not support multiple inheritance. However, if there is multiple inheritance, we can indeed inherit from multiple parent classes and add myMethod to the appropriate place. Some languages have chosen to support multiple inheritance (such as C++), but it introduces another well-known problem in OOP: the diamond defect.

Diamond defects

In the example above, if we had multiple inheritance, the relationship between ViewController and AnotherViewController might look something like this:

In this topology, we only need to implement myMethod in the ViewController and then inherit and use it in the AnotherViewController. It looks perfect. We’ve avoided duplication. But there is an unavoidable problem with multiple inheritance. What do subclasses do when both parent classes implement the same method? It is difficult to determine which parent class’s methods should be inherited. Because the topology of multiple inheritance is a Diamond, this Problem is also called the Diamond Problem. A language choice like C++ crudely leaves the problem of diamond defects in the hands of the programmer, which is undoubtedly complicated and increases the potential for human error. Most modern languages avoid multiple inheritance.

Dynamic distribution security

Objective-c, as its name suggests, is a typical OOP language that inherits Small Talk’s messaging mechanism. This mechanism is very flexible and is the basis of OC, but it can be dangerous. Consider the following code:

ViewController *v1 = ...
[v1 myMethod];

AnotherViewController *v2 = ...
[v2 myMethod];

NSArray *array = @[v1, v2];
for (id obj in array) {
    [obj myMethod];
}
Copy the code

If we implement myMethod in both ViewController and AnotherViewController, this code is fine. MyMethod will be dynamically sent to array v1 and v2. But what if we had a type that didn’t implement myMethod?

NSObject *v3 = [NSObject new] // v3 does not implement 'myMethod' NSArray *array = @[v1, v2, v3]; for (id obj in array) { [obj myMethod]; } // Runtime error: // unrecognized selector sent to instance blablaCopy the code

The compilation will still pass, but obviously the program will crash at run time. Objective-c is insecure, and the compiler defaults to the fact that you know a method actually has an implementation, which is the price you have to pay for the flexibility of sending messages. From app developers’ point of view, the possibility of crash is too high a price to pay for flexibility. While this is not a problem with OOP paradigms, it certainly hurts in the Objective-C era.

Three big trouble

We can summarize these problems with OOP.

  • Dynamic distribution security
  • Crosscutting concern
  • Diamond defects

First, dynamic distribution in OC puts us at risk of discovering an error at run time, most likely in a live product. Second, crosscutting concerns make it harder to model objects perfectly and make code reuse worse.

Know each other – protocol extension and protocol oriented programming

Use protocols to solve OOP dilemmas

Protocol is not new, nor is it an invention of Swift. In Java and C#, it’s called Interface. Protocol in Swift inherits this concept and carries it forward. Let’s go back to the simple protocol we defined at the beginning and try to implement it:

protocol Greetable {
    var name: String { get }
    func greet()
}
Copy the code
Struct Person: Greetable {let name: String func greet() {print(" hello (name)")}} Person(name: "Wei Wang"). Greet ()Copy the code

The implementation is simple. The Person structure satisfies Greetable by implementing name and greet. When called, we can use the methods defined in Greetable.

Dynamic distribution security

In addition to Person, Greetable can be implemented with other types, such as Cat:

struct Cat: Greetable {
    let name: String
    func greet() {
        print("meow~ (name)")
    }
}
Copy the code

We can now use the protocol as a standard type for dynamic distribution of method calls:

let array: [Greetable] = [ Person(name: "Wei Wang"), Cat(name: "Onevcat ") for obj in array {obj.greet()} // Wei Wang // meow~ onevcatCopy the code

For types that do not implement Greetbale, the compiler returns an error, so there is no message sent by mistake:

struct Bug: Greetable {
    let name: String
}

// Compiler Error: 
// 'Bug' does not conform to protocol 'Greetable'
// protocol requires function 'greet()'
Copy the code

In this way, the problem of dynamic distribution security is solved. If you stay in the Swift world, then all your code is safe.

  • ✅ Dynamic dispatch security
  • Crosscutting concern
  • Diamond defects

Crosscutting concern

Using protocols and protocol extensions, we can share code very well. Going back to the myMethod method in the previous section, let’s see how to use the protocol to do it. First, we can define a protocol that contains myMethod:

protocol P {
    func myMethod()
}
Copy the code

Note that this protocol does not provide any implementation. We still need to provide concrete implementations for the actual type when it complies with this protocol:

// class ViewController: UIViewController
extension ViewController: P {
    func myMethod() {
        doWork()
    }
}

// class AnotherViewController: UITableViewController
extension AnotherViewController: P {
    func myMethod() {
        doWork()
    }
}
Copy the code

How is this different from Copy & Paste’s solution, you might ask? Yes, the answer is no. But hang on, there’s another technology that can solve this problem: protocol extension. The protocol itself is not very powerful, but the compiler guarantees that statically typed languages have similar concepts in many static languages. So what exactly makes Swift such a protocol-first language? What really changed the protocol and attracted so much attention was that when WWDC 2015 and Swift 2 were released, Apple introduced a new feature to the protocol, protocol Extension, which revolutionized the Swift language.

Protocol extension means that we can provide a default implementation for a protocol. For P, you can add an implementation for myMethod in Extension P:

protocol P {
    func myMethod()
}

extension P {
    func myMethod() {
        doWork()
    }
}
Copy the code

With this protocol extension, we simply declare that ViewController and AnotherViewController comply with P, and we can use myMethod directly:

extension ViewController: P { }
extension AnotherViewController: P { }

viewController.myMethod()
anotherViewController.myMethod()
Copy the code

Not only that, we can even add methods in extensions that are not defined in the protocol, in addition to methods that are already defined. Among these additional methods, we can rely on methods defined by the protocol to operate. We’ll see more examples of that later. To sum up:

  • Protocol definition

    • Provides entry to the implementation
    • The type of protocol that follows needs to be implemented
  • Protocol extensions

    • Provide a default implementation for the entry
    • Additional implementations are provided depending on the entry

In this way, crosscutting concerns are simply and safely resolved.

  • ✅ Dynamic dispatch security
  • ✅ crosscutting concerns
  • Diamond defects

Diamond defects

Finally, let’s look at multiple inheritance. An important problem with multiple inheritance is the diamond defect, in which subclasses cannot determine which parent class’s methods to use. In terms of protocol correspondence, this problem remains, but it can only be safely determined. Let’s look at an example of an element with the same name in multiple protocols:

protocol Nameable {
    var name: String { get }
}

protocol Identifiable {
    var name: String { get }
    var id: Int { get }
}
Copy the code

If a type needs to implement both protocols, it must provide a name attribute to satisfy both protocols:

Struct Person: Nameable, Identifiable {let name: String let ID: Int} // The 'name' attribute satisfies both Nameable and Identifiable nameCopy the code

What’s interesting here, and a little confusing, is what if we extended one of these protocols to provide a default name implementation. Consider the following code:

extension Nameable { var name: String { return "default name" } } struct Person: Nameable, Identifiable { // let name: String let ID: Int} // Similarly will use the name in Nameable extensionCopy the code

Such compilation is manageable, because although name is not defined in Person, Person can still contrarily comply with the Nameable name (because it is statically distributed). However, when Nameable and Identifiable have a protocol extension of name, this will not compile:

extension Nameable { var name: String { return "default name" } } extension Identifiable { var name: String { return "another default name" } } struct Person: Nameable, Identifiable { // let name: String let id: Int} // failed to compile, name attribute conflictCopy the code

In this case, Person cannot determine which protocol extension definition of name to use. When implementing two protocols that have elements of the same name, and both provide default extensions, we need to explicitly provide the implementation in the concrete type. Here we implement the name in Person:

extension Nameable {
    var name: String { return "default name" }
}

extension Identifiable {
    var name: String { return "another default name" }
}

struct Person: Nameable, Identifiable {
    let name: String 
    let id: Int
}

Person(name: "onevcat", id: 123).name // onevcat
Copy the code

The behavior here looks very similar to the diamond problem, but there are some essential differences. First, this problem occurs only if the element has the same name and an implementation is provided at the same time, and protocol extension is not necessary for the protocol itself. Second, the implementation we provide in the concrete type must be secure and deterministic. Of course, the diamond defect has not been completely solved, Swift can not deal with multiple protocol conflicts well, which is Swift’s current deficiency.

  • ✅ Dynamic dispatch security
  • ✅ crosscutting concerns
  • ❓ diamond defects

The second half of this article shows and explains some of the example code that I use on a daily basis in conjunction with protocol-oriented thinking and Cocoa development.

Share: technical information on iOS development