Protocol Oriented Programming: Unveiling the Secrets 10 Years Later

Jim LaiJim Lai
11 min read

I want to fight 10 men

10 years after Swift came out, I still can’t find one article that gets it right. Fine. I’ll do it myself. This will be unlike anything you’ve ever seen.

I’m going to show you what makes Swift POP unique and powerful, mistakes others made, and most importantly, how to fight 10 other design patterns.

What object?

var lp = "pop" { didSet { updateUI() }}

Above is a view model. Let’s check.

Can you build a view using this as model source? Yes.

Does it trigger view update after you change it? Yes.

Is model decoupled from view, i.e.; you can just work on model without worrying about view? Yes.

Then it’s a view model.

Moreover, I can update multiple views at once, making it more flexible. I don’t need to create one extra object per view. What object?

What refactor?

If there’s no extra object, how do you refactor out reusable codes?

Use protocol extension as mixin. Note that we don’t use protocol in terms of interface, we use it in terms of extension as mixin.

This is not at all obvious, it seems. Look at this hot shot on Reddit.

The most common mistake on POP is to take the word literally. Protocol => Interface => Java has it => POP useless.

Swift has language features that Java does not. Language features => Refactor is the approach we will be repeating throughout. Our refactor discussion so far stems from value type, property observer, to protocol extension.

lP Man

The most important thing in a design pattern, is its name. Content is irrelevant. If you say it is magical and infinite scalable, then it is. Clean architecture is a bunch of a bootcamp participant’s ideas of infinite scalability based solely on polymorphism without any comparison, but it got the name right.

Let me introduce, Lai’s POP. Or LP in short, and if you write L in lower case, you get IP MAN.

I’m going to write a series of articles each comparing with some grand design pattern. I’ll state some of the features of LP here.

Nobody has time to implement your shitty protocol

This is the core principle of LP. Time and time again, other patterns ask you to write abstract protocols for new functionalities. This is shitty for at least 3 reasons.

  1. Even yourself don’t know how to implement your protocol after some time

  2. Abstraction and inheritance makes it inefficient and error-prone

  3. For what? To add some functionalities? Let me quote Dr.McCoy from Star Trek: The Voyage Home

What is this, the Dark Ages?

Again, following our language feature => refactor approach, what do we have to add new functionalities?

You can assign function as variables; or another overlooked feature: Extension.

Extension is what powers POP

When Apple says you start with protocol, what it really means is that you start with protocol, so you can write extension.

protocol IPMan: class {
    var ip: String {get}
}
extension IPMan {
    func download() {
        // implement it in this sandbox !!!
    }
}

If you ever see this, whoever writes this is an IP Man, i.e.; he practices the martial art that is POP.

Why is this usage special? Let me break it down.

  • First, we use it to do actual work in terms of mixin. We don’t use it as an interface, or abstract shit like “downloadable“. We have a task to download ip man movie, and we do it right then and there.

    We have to compare this to Kodeco’s classical POP tutorial on this matter:

  • See how it defines a worthless “Flyable“ interface without defaults and conform it in an immutable value type in which you can’t mutate anything so it basically asks you to add two properties that you don’t know what they are used for. This is why never ask people to implement your shitty protocol.

  • Note how I use the keyword class. Again, we don’t use it for interface, we use it for actual functioning code, which in most cases requires a reference type. You can provide computed properties and functions that don’t mutate in value type, but state changes, i.e.; things that mutate are usually the central part of the work. LP revolves around identifying these tasks as self-contained mixins.

  • As an exercise, let’s rewrite this using LP

      protocol Fly: class {
          var name: String {get}
          var amplitude: Double {get set}
          var frequency: Double {get set}
      }
      extension Fly {
          var velocity: Double { 3 * frequency * amplitude}
          func fly() {
              // ... state changes
              // default 
          }
      }
    

    We assume there’s this fly function that we want to refactor out. To implement it we declare a couple of properties. It builds around some functions that you want to reuse and implements it right then and there. Always be self-contained. Because mixin by definition is a piece of code that you can plug and play.

Function oriented

Let’s recap the process. LP asks you to start with a protocol to start with an extension to start with a function that you want to reuse.

Which is the key in the process? The function you want refactored.

Function and refactor, which sets LP apart from everything else. You can’t use LP if you can’t refactor for shit; you can’t use LP if you are busy creating objects and making functions abstract.

It puts function back in place where it should be, which goes in direct opposite of say MVVM.

Put in other words, LP refactors out Control, i.e.; things that mutate; and not as objects, but as mixins; and do so retroactively.

What extension?

Extension is probably the most under-valued feature in Swift. The power to add something retroactively completely changes the way you design and refactor.

Let’s look at an example from Google’s guide to app architectures.

 class NewsRemoteDataSource(
  private val newsApi: NewsApi,
  private val ioDispatcher: CoroutineDispatcher
) {
    /**
     * Fetches the latest news from the network and returns the result.
     * This executes on an IO-optimized thread pool, the function is main-safe.
     */
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        // Move the execution to an IO-optimized thread since the ApiService
        // doesn't support coroutines and makes synchronous requests.
        withContext(ioDispatcher) {
            newsApi.fetchLatestNews()
        }
    }

// Makes news-related network synchronous requests.
interface NewsApi {
    fun fetchLatestNews(): List<ArticleHeadline>
}

Where’s extension? What extension?

Basically for each API endpoint, you write a hard-coded endpoint specific class, and a specific interface, then pass interface as parameter which allows you to change fetch, then writes some io dispatch boilerplate so you can finally call a fetch.

All these are so that you can change fetch implementation by creating another specific class.

Implement “NewsApi“, I dare you.

What might this look like with extension?

class DonnieYen: IPMan {
    // ... call get4K() somewhere
}
extension IPMan { 
    func get4K() {
    // ... default
    }
}

Say at some later time, you want 4K version of IP Man. You extend existing mixin to add new functionality.

Then call it in your code. You know you want 4K in compile time, not runtime. No need for pre-defined runtime setup.

This simplifies Google app architecture to this

 class NewsRemoteDataSource() {
    // newsApi.fetchLatestNews()
    // if your class is just to call function from another class
    // you are doing it wrong
 }

Obviously you can then remove this.

I’ll cover this in more detail in follow-up posts, this is to show you how much effort is wasted on worthless over-engineering. With Google’s case, you are never going to need another version of news api, but it has the audacity to ask you to implement its shitty protocol all the same.

Note the actual working codes are in protocol extension. This is a lot more difficult than it sounds. It’s a sandbox that prevents imperative assignments, well, if you are any good. For example you may usually do a lot of setup work in initializer given that you know the underlying concrete type. With protocol you don’t know underlying class yet, hence you can’t provide default for initializer because Swift requires all class properties be initialized but you don’t know the underlying class.

Recall that LP asks protocol as mixin to be self-contained?

This reduces chances that you introduce side effects via arbitrary assignment, because whatever reference you need, you have to declare it, then underlying class has to provide it. The more you require, then less useful your mixin becomes, because the longer it takes to create required properties.

It’s a skill game. You need to identify a set of core functions that can be reused; implement them in a sandbox where there’s no setup while keeping the protocol compact and self-contained; they need to be mostly independent because you don’t expect caller to setup or call in specific order with extension; in a way you are writing unit tests but they are actual working codes.

But before all that, you need to recognize how powerful a feature protocol extension is.

This guy did it in 2016.

That is 10 years ago. But sadly no one can follow up and put together a comprehensive guide… well, until this post.

But mixin is not special!

Man. I love Reddit comments. The counter-arguments I’ve heard so far are

  • Other language has it

  • Apple is silent about it

  • I’ve heard it from Apple dev slack that he regret it

  • Not as useful

  • I got the strongest grasp of POP, you are doing it all wrong, but I ain’t gonna tell you

Look at how Dart designs mixin.

It’s not on protocol level like Swift. Only Swift has value type, property observer, computed properties, associatedType… etc.

All these add up to provide unique pattern. You can see how useful in follow ups where I compare with other patterns. What do I say in the beginning? What object?

If there’s no object, how useful can your pattern be?

Remove inheritance

A common problem is that people just do the usual inheritance but with protocol and value type. E.g.;

A mutating value type is just reference type. “staffProtocol“ is base class to inherit. “doesWork()“ is polymorphism.

This is good ol’ inheritance with a paint of protocol.

LP asks that you remove inheritance completely from your design thinking.

It’s funny that even some POP supporter doesn’t realize the full potential of POP.

POP absolutely can replace OOP. In fact, avoid inheritance even if you don’t use POP.

For this to work, you need to start from scratch and think in way of LP. Forget everything you’ve learned about inheritance.

How does LP approach this?

We start with extension to start with a function that we want to refactor.

We don’t give a shit whether principle is part of staff so there’s inheritance relationship.

protocol IPMan: class { ... }

extension IPMan {
    func fight10() { ... }
    func fightDummy() { ... }
}
class DonnieYen: IPMan { ... }

Note we put emphasis on functions. What exactly we want to do. Not some runtime polymorphic “work“ function. And we specify that this is a class protocol because it mutates.

If Donnie Yen wants to fight 10 people, then he calls fight10(); if he wants to fight dummy, he calls fightDummy(); Both are reusable as mixin.

This is different than, say Kodeco’s POP approach:

This is actually code smell. Note the implicit hint of inheritance relationship between all kinds of birds, and the lack of extension anywhere.

Also note how these tutorials like to use value type to conform to protocol. You can’t do shit in immutable value type. So you just end up with extra property requirements in a value type that is already property requirement itself.

You don’t see what functions that you want to provide as mixin here. Protocol like “Flyable“ works more in interface sense rather than mixin. Since Swift puts mixin on the level of protocol, you now have to weight how much of it is mixin, and how much of it is interface.

In LP, 99% is mixin. Remove inheritance entirely. Use interface only as secondary.

Move control, not data

Unlike other patterns, Control is front and center in LP. Never hide it.

LP refactors out control, not data. For example, in MVVM you move model outside, then hide whatever control logic like fetch and call it view model as if it were some harmless value type.

That is not the case in Swift. Value type model should not be mixed with reference type state changes.

LP highlights function in extension, and builds around it as mixin. So you are refactoring out control. Meanwhile we apply language features like property observer to simplify view update. And the easiest way to do this is to put model and view references from SDK together.

And by moving, we don’t actually create an object. Because Swift put mixin on protocol level. We eliminate the need for passing references, callbacks, managing life cycles… etc. We have the benefits of having everything in one place, but we also have the benefits of having smaller mixins that are easy to manage and reuse.

With some experience, the development process becomes

  1. Write extensions (controls)

  2. add extensions as mixin

  3. create stored properties (data)

  4. handle interaction with SDK, like update views (view)

Extension does the heavy lifting. Class does routine works like creating stored properties and update views.

By design, whatever you put in extension is reusable as mixin; and by LP you do everything in extension, so your work is insanely reusable, well, if you are any good.

So when Apple says, you start with protocol, it’s technically right.

0
Subscribe to my newsletter

Read articles from Jim Lai directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jim Lai
Jim Lai