Per alcuni il Protocol Oriented Programming è solo un reinventare l’acqua calda, asserendo che le classi astratte e le interfacce non sono nulla di nuovo o che si tratta semplicemente di una buzzword coniata da Apple.
In effetti è stata proprio Apple nel World Wide Developers Conference (WWDC) del 2015 a dire che con Swift hanno “realizzato il primo linguaggio protocol-oriented”. (tradotto testualmente da “we made the first protocol-oriented programming language”).
Cos’è il Protocol Oriented Programming? È davvero così rivoluzionario? Quali sono i suoi punti deboli?
Parto dal presupposto che conosci abbastanza bene il paradigma della programmazione orientata agli oggetti.
Il nuovo paradigma introdotto in Swift (e adottato all’interno della libreria standard) si focalizza su ciò che un oggetto può fare (sui tratti), invece che su ciò che è. Comprendere il Protocol Oriented Programming e adottarlo, ci aiuterà a migliorare il nostro codice e a renderlo non solo più mantenibile, ma anche piacevole da lavorarci :).
Non preoccuparti se all’inizio ti sentirai un po’ confuso, anche per me spostare il mio approccio mentale dall’OOP al Protocol-Oriented non è stato immediato. Si tratta di un modo completamente differente di pensare alla struttura del codice.
Cerchiamo prima di tutto di comprendere cos’è un Protocol e le sue differenze con le interfacce di altri linguaggi di programmazione.
Introduzione ai Protocol
Se non sei nuovo in Swift, avrai notato come i vari framework dichiarano diversi Protocol. Ad esempio in UIKit
avrai utilizzato il protocol UITableViewDataSource
.
Un Protocol in Swift è simile ad una interfaccia in altri linguaggi OOP, si comporta come un contratto che definisce i metodi, le proprietà e altri requisiti che un tipo deve soddisfare. Ma i Protocol in Swift offrono molto di più di quello che le interfacce permettono in altri linguaggi di programmazione, approfondirò questi dettagli in un prossimo articolo.
Definire un Protocol
La sintassi per definire un protocol non è niente di nuovo:
protocol MyProtocol {
//definizione...
}
Utilizziamo la keyword protocol
. Strutture, classi e enumerazioni possono conformarsi ad un protocol (o più protocol) in questo modo:
class MyClass: MyProtocol, MySecondProtocol, MyThirdProtocol {
//...
}
oppure indicando una extension (anche in un file separato):
extension MyClass: MyProtocol {
//implementazione protocol
}
extension MyClass: MySecondProtocol {
}
Io personalmente utilizzo spesso le extension per mantenere il mio codice più ordinato e facile da mantenere.
Facciamo un esempio pratico. Dobbiamo definire un protocol che rappresenta un animale domestico: deve avere un nome, un età che può essere modificata, un metodo sleep, una variabile statica che descrive il nome latino del nostro animale. La definizione del nostro protocol sarà la seguente:
protocol PetType {
var name: String { get }
var age: Int { get set }
func sleep()
static var latinName: String { get }
}
Quando dichiariamo una proprietà, dobbiamo anche specificare se può essere gettable, settable o entrambi. Possiamo anche definire variabili statiche. In questo caso è giusto che il nome latino sia statico, dato che può appartenere a diversi animali domestici dello stesso tipo.
Proviamo a definire due struct conformi al protocol appena definito:
struct Cat: PetType {
let name: String
var age: Int
static let latinName: String = "Felis catus"
func sleep() { print("Cat: Zzzz") }
}
struct Dog: PetType {
let name: String
var age: Int
static let latinName: String = "Canis familiaris"
func sleep() { print("Dog: Zzzz") }
}
Il polimorfismo ci permette di definire un metodo nap come il seguente:
func nap(pet: PetType){
pet.sleep()
}
I protocol in Swift hanno anche ulteriori caratteristiche uniche come gli associatedType, le implementazioni di default e altre ancora che esamineremo in un articolo a parte. In questo articolo il mio obiettivo è quello di introdurre il Protocol-Oriented Programming.
Verifica dei tratti, invece dei tipi
Nel paradigma orientato agli oggetti, quando i livelli di ereditarietà crescono, ci si ritrova ad un certo punto con delle classi che contengono metodi che sono rilevanti solo per un paio di sottoclassi. Vediamo come il Protocol-Oriented ci permette di superare questo limite.
In OOP ci troviamo spesso a creare classi base e classi derivate per raggruppare insieme un oggetto con capacità simili. Immaginiamo di dover raggruppare un gruppo di felini nel regno animale con le classi:
Immaginiamo ora di voler aggiungere ulteriori animali e ulteriori caratteristiche. Il nostro class domain diventerà abbastanza complicato (se non impossibile ad un certo punto) da gestire.
Ad esempio, vogliamo aggiungere le proprietà owner
e home
agli animali domestici. Modifichiamo quindi la struttura delle classi per aggiungere queste proprietà a Cat e Dog per esempio. Ma non sono gli unici animali che possono essere tenuti in casa, dovremmo includere pesci, roditori, uccelli, … Ci renderemo conto che diventerebbe complicato (o impossibile) ristrutturare la gerarchia di classi in un modo tale da non avere ridondanza delle proprietà owner
e home
ad ogni animale addomesticabile nella gerarchia. Diventerebbe impossibile aggiungere queste proprietà selettivamente alle giuste classi.
Ancora peggio se vogliamo scrivere una funzione che mostra la proprietà home
di ogni anime domestico. O si fa in modo che la funzioni accetti ogni tipo di animale, oppure andrebbe scritta una implementazione separata della stessa funzione per ogni tipo che ha la proprietà home
.
Complichiamo ancora le cose. Finora ci siamo concentrati su ciò che gli oggetti sono, non su ciò che fanno. Vogliamo caratterizzare gli animali che possono volare o no, quelli che vivono in acqua, terra, sotto terra e così via.
Usando l’ereditarietà il nostro progetto sarà complicato e non facilmente mantenibile.
Proviamo con un approccio diverso.
Protocol Oriented Programming
Immaginiamo di definire la struttura per un piccione con i protocol:
struct Pigeon: Bird, FlyingType, OmnivoreType, Domesticatable
Molti Protocol definiti nella libreria standard Swift usano i suffissi Type, Ing, Able per indicare che un protocol definisce un tratto, una caratteristica di un tipo concreto. Useremo anche noi questa convezione.
Pigeon
è una struttura. Bird
è un protocol che definisce i requisiti fondamentali della classe (in senso zoologico) Uccelli. La struttura è conforme anche ai protocol FlyingType
, OmnivoreType
e Domesticatable
. Ognuno di essi ci dice qualcosa della struttura Pigeon in quanto a capacità e caratteristiche.
Questa definizione di Pigeon
ci mostra cosa Pigeon
è e fa invece di dirci semplicemente che eredita da un certo tipo di uccello o altro.
Ora diventa molto semplice definire una funzione che mostra la proprietà home
definita nel protocol Domesticatable
:
protocol Domesticatable {
var homeAddress: String? { get }
}
func printHmeAddress(animal: Domesticatable) {
if let address = animal.homeAddress {
print(address)
}
}
Cambiare il modo di pensare dall’approccio Object-Oriented dove si pensa per gerarchia ereditata a quello Protocol-Oriented dove ci si focalizza sulle caratteristiche non è facile.
Cerchiamo di espandere ulteriormente il nostro esempio. Definiamo i protocol OmnivoreType
, HerbivoreType
e CarnivoreType
. Possiamo definire il protocol OmnivoreType
facendo uso dell’ereditarietà:
protocol HerbivoreType {
var favouritePlant: String { get }
}
protocol CarnivoreType {
var favouriteMeat: String { get }
}
protocol OminovoreType: HerbivoreType, CarnivoreType {}
Ricordate, con il Protocol-Oriented Programming non sostituiamo il paradigma Object-Oriented, ma lo estendiamo!
Immaginiamo ora di definire due funzioni:
func printFavouriteMeat(forAnimal animal: CarnivoreType){
print(animal.favouriteMeat)
}
func printFavouritePlant(forAnimal animal: HerbivoreType){
print(animal.favouritePlant)
}
Dato che OmnivoreType
eredita da HerbivoreType
e CarnivoreType
, entrambi i metodi accettano il nostro Pigeon
.
Usare i protocol per comporre i nostri oggetti ci permette di semplificare molto il nostro codice. Invece di pensare a complicate strutture di ereditarietà, possiamo comporre i nostri oggetti definendone certe caratteristiche. La cosa bella è che definire nuovi oggetti diventa molto semplice.
Immaginiamo, per assurdo, un animale che può volare, nuotare e che vive sulla terra. Diventerebbe un po’ complicato da modellare con un’architettura basata sull’ereditarietà. Con i protocolli ci basterebbe dire che l’oggetto deve conformarsi ai protocol FlyingType
, LandType
e SwimmingType
.
Ogni volta che stai per creare una classe base o derivata, chiedi a te stesso: sto semplicemente provando a incapsulare una certa caratteristica in quella classe? Se la risposta è affermativa usa un Protocol.
Un protocol viene definito con pochi requisiti, solitamente sono sufficienti uno o due requisiti. Non esitiamo a definire protocol con giusto una proprietà o un metodo. Man mano che il nostro progetto cresce e i requisiti cambiano, ringrazierai te stessi 😁.
happy coding!