Protocol Oriented Programming (Swift)

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:

Utilizziamo la keyword protocol. Strutture, classi e enumerazioni possono conformarsi ad un protocol (o più protocol) in questo modo:

oppure indicando una extension (anche in un file separato):

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:

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:

Il polimorfismo ci permette di definire un metodo nap come il seguente:

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à ownerhome agli animali domestici. Modifichiamo quindi la struttura delle classi per aggiungere queste proprietà a CatDog 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à ownerhome 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:

Molti Protocol definiti nella libreria standard Swift usano i suffissi TypeIngAble 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:

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 CarnivoreType. Possiamo definire il protocol OmnivoreType facendo uso dell’ereditarietà:

Ricordate, con il Protocol-Oriented Programming non sostituiamo il paradigma Object-Oriented, ma lo estendiamo!

Immaginiamo ora di definire due funzioni:

Dato che OmnivoreType eredita da HerbivoreType 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 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 😁.

Nella seconda parte vedremo alcune caratteristiche dei protocol non esaminate finora davvero potenti.

Stay tuned e … happy coding!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *