Separare l’interfaccia dall’implementazione, una delle regole d’oro dell’OOP. Come applicarla in un linguaggio tipizzato dinamicamente come Python? Un’introduzione allo Structural Subtyping e all’utilizzo dei Protocol
Per coding-to-interfaces si intende una best practice della OOP che consiste nel separare la definizione di una classe (interfaccia) dalla sua effettiva implementazione. Con questa separazione si ottiene un codice fortemente disaccoppiato, che ci rende reattivi al cambiamento consentendoci di modificare le implementazioni senza compromettere il “contratto” sottoscritto con gli utilizzatori della classe. Quali strumenti ci offre Python, un linguaggio tipizzato dinamicamente, per implementare questa metodologia?
Per determinare se una classe implementa una particolare interfaccia possiamo utilizzare due approcci: il Nominal Subtyping e lo Structural Subtyping.
Il Nominal Subtyping si basa essenzialmente sul concetto classico di ereditarietà: se una classe A eredita da una classe B, è un sottotipo di B, e le istanze di A possono essere usate dove sono attese istanze di B.
Per Structural Subtyping si intende una metodologia standard dei linguaggi tipizzati dinamicamente per cui un oggetto con determinate proprietà è trattato a runtime indipendentemente dalla sua classe reale. Per esemplificare, una classe B è un sottotipo strutturale di una classe A quando B implementa le stesse proprietà e gli stessi metodi di A, con tipi e signature compatibili. Non è necessaria un ereditarietà diretta tra le due classi, il check della compatibilità avviene per inferenza. Lo Structural Subtyping può essere visto come l’equivalente statico del duck typing (se cammina come un’anatra e starnazza come un’anatra, deve essere un’anatra).
Rispetto ad altri linguaggi, in Python non esiste il concetto di Interfaccia pura. Prima dell’avvento di Python 3.x, entrambe le metodologie potevano essere applicate solo implicitamente, tramite l’utilizzo di convenzioni.
La prima formalizzazione del concetto di interfaccia è stata introdotta in Python 3.x tramite le Abstract Base Classes (PEP 3119). Una classe base astratta (ABC) è una classe ristretta che non può essere istanziata. Può essere usata solamente come classe base per creare classi concrete. I casi d’uso di una ABC sono due: Interfaccia (ABC)
A partire da Python 3.8 (ma anche da versioni precedenti tramite il package typing_extensions) è stato introdotto il concetto di Protocol (PEP 544), che implementa un supporto esplicito allo Structural Subtyping. Un Protocol viene definito includendo la classe typing.Protocol (un’istanza di abc.ABCMeta) nella lista delle classi base, tipicamente al termine della lista stessa. Esso definisce le proprietà ed i metodi che le classi aderenti a quel protocollo dovranno implementare. Quando una classe derivata include il Protocol nella lista delle sue classi base, si parla di utilizzo esplicito del Protocol. Quando ne implementa proprietà e metodi senza utilizzare l’ereditarietà diretta, si parla al contrario di utilizzo implicito. L’utilizzo implicito è particolarmente interessante perché consente di definire interfacce anche per codice di terze parti, su cui non si ha un controllo diretto.
Nell’ambito dell’analisi statica del codice, le due modalità di utilizzo sono assolutamente equivalenti. Sia l’utilizzo esplicito che quello implicito di un Protocol danno luogo agli stessi risultati durante una validazione. Un Protocol può essere validato a runtime proprio come le classi ABC. Per fare ciò è necessario decorare il Protocol con @runtime_checkable affinché si possano utilizzare i metodi isinstance e issubclass per la verifica.
Nominal e Structural Subtyping presentano entrambi vantaggi e svantaggi. Nell’ottica del coding-to-interfaces, l’utilizzo dei Protocol a nostro giudizio risulta più flessibile perché consente:
Carlo Bertini è uno sviluppatore full stack e appassionato di tecnologia. Attualmente lavora come senior software engineer presso Fiscozen. Nel tempo libero ama ascoltare musica dal vivo e prendersi cura dei suoi gatti.