Protocol oriented programming is a programming paradiagm promoted by Swift developers. Coming from a Java or Objective-C background, interfaces are a core language feature used by practically every developer. Protocols are very similar to interfaces, and it is quite easy to dismiss “protocol oriented programming” as just interface oriented programming.
In many ways, protocols are basically interfaces (or traits in Rust), so the core ideas are the same. However, one of the biggest differences is the addition of the ability to retrofit conformance of existing types to new protocols/traits and generic language features. It may muddle the argument that protocol oriented programming is unique due to other language features not named protocol.
Taking a step back, the use of interfaces is primarily applicable when code is interacting between two separate parties (say a framework/library and application code). Using interfaces allows flexibility for method inputs in comparison to concrete implementations such as classes or structs. While interfaces are extremely helpful, the end goal is integrating the code. Protocol oriented programming expands on the toolset to make code integrations easier.
Here’s an example of how protocol oriented programming iterates over interface based programming.
Type Extensions
Type extensions allow anyone to add methods to an existing type with a few restrictions.
Suppose there’s an Entity
protocol which declares a name
function which
returns the name of the entity. Also, there’s a simple function which prints
hello with the entity’s name. Both exist in a third party library.
protocol Entity {
func name() -> String
}
func sayHello(subject: Entity) {
print("Hello \(subject.name())")
}
If you replace protocol
with interface
(and some language syntax changes),
there does not seem to be any difference between protocols and interfaces.
However, let’s say you have an existing Person
type which you wish to pass to
the sayHello
function.
class Person {
func firstName() -> String {
return "Jane"
}
func lastName() -> String {
return "Doe"
}
}
Now, instead of having to create a wrapper type, you can simply add an extension
which makes Person
conform to the Entity
protocol.
extension Person: Entity {
func name() -> String {
return "\(self.firstName()) \(self.lastName())"
}
}
It could be argued that this is just language syntax sugar and writing a wrapper type is easier. Ultimately it depends on the complexity of the protocol and how you use the underlying instance. If you need to convert between types often and depending on how your compiler can treat the wrapping type, there are drawbacks to wrapper types.
Generics
Generics allow expressive function and type declarations which empower the compiler to better type check the code as well as potentially generate better performing code (aka “generic specialization”).
Following the above examples, the sayHello
function can be changed to use a
generic type.
func sayHello<T>(subject: T) where T: Entity {
print("Hello \(subject.name())")
}
While the above example is a trivial use of generics, it allows some compilers
to determine if it should create a specific version of the function for every
specific type of Entity
which is used. So there could be a version of
sayHello
for the Person
type. If there was a Robot
type which conformed to
the Entity
protocol, the compiler may generate machine code for both Person
and Robot
. The type specific machine code could be faster by directly invoking
type specific methods rather than going through the Entity
protocol via a
lookup table (e.g. vtables).
In a more realistic generic function, the function could guarantee that two parameters are the same type while implementing the same interface(s). A common example is a comparision function which usually is used in more performance demanding code.
Library-Application Integration Reversal
But perhaps you do not care about the convenience of type extensions or the
improvements from generics because you control your application code. Afterall,
you can easily just open up your class and add the : Entity
to cause your
Person
type to conform to Entity
.
Applications call library code and it is generally presumed that libraries should be the party that uses interfaces as library inputs. In reality, the integating code goes both ways. The library code may return concrete or interface types (which cannot be changed by the application code author). Then, the library’s concrete types and interfaces are used like other types which were created in the application code base.
In some sense, the library’s type definitions have polluted the application’s code base. If the library were to change its types/method names or make other breaking changes, the changes could drastically affect the application code base.
For instance, suppose you are writing a web application and are getting query
parameters and header values off the HTTP request. The web framework has a
standard Request
type which provides access to all the properties of an HTTP
request. Your function needs to retrieve the name parameter off the request
like:
func sayHello(req: Request) {
print("Hello \(req.getQueryParameter("name"))")
}
The Request
library type is now part of your application code. While the above
is simple, imagine library types which might be passed throughout functions
defined in the application.
The use of type extensions and interfaces/protocols defined in the application code base can negate the impact.
You could declare a EntityParameter
protocol, and use type extensions to
retroactively conform Request
like:
protocol EntityParameter {
func name() -> String?
}
extension Request: EntityParameter {
func name() -> String? {
return self.getQueryParameter("name")
}
}
func sayHello(subject: EntityParameter) {
print("Hello \(subject.name())")
}
let incomingRequest: Request = // given by the framework
sayHello(subject: incomingRequest)
Now your function can take a type which implements EntityParameter
. The
dependence on the library code is less. Furthermore, the function is more easily
testable since you can easily create a stub which implements EntityParameter
.
The goal of the code above could be accomplished with interfaces and wrapper
types (e.g. a new application type which delegates to the Request
type and
which implements EntityParmeter
). I would argue that type extensions
require less code. Again, generics can help ensure expressiveness with type
safety while improving performance.
Iteratively improving code integrations
In the end, conceptually, “protocol oriented programming” may not be significantly different than programming with interfaces in mind. However, the addition of type extensions and generics make programming with protocols/interfaces more pleasant and can help separately developed code bases evolve together.