Premesse

Quest’articolo vuole essere il più possibile agnostico rispetto al linguaggio di programmazione, per cui di seguito userò spesso termini come “modulo” ed “elemento” al posto di “classe”, “membro”, “metodo”, ecc. per estendere il discorso anche a linguaggi non a oggetti.

Panoramica sull’incapsulamento

Molti linguaggi di programmazione permettono al programmatore di incapsulare parte delle strutture dati e delle operazioni implementate in modo da renderle visibili solo a determinate condizioni.

Esistono varie tecniche per realizzare l’incapsulamento, tra cui:

  • L’utilizzo di apposite parole chiave, che definiscono la visibilità dell’elemento al quale vengono applicate (e.g. le keywork “public“, “private“, ecc. in linguaggi come C#, java e molti altri);
  • L’uso di particolari convenzioni nella nomenclatura degli elementi che si vogliono incapsulare (e.g. in Go i membri che iniziano con lettera minuscola sono privati a livello di package (come la visibilità di default in java); in python e altri linguaggi per convenzione si considerano protected i membri che iniziano con 1 underscore e privati quelli che iniziano con 2 underscore, ecc.);
  • Altro: ad esempio, in C, se si segue la buona pratica di importare i file header anziché i .c, è possibile distinguere tra elementi pubblici e privati definendo i primi nel .h e i secondi solo nel .c.

Solitamente, l’accesso illecito ad elementi su cui non si ha visibilità è controllato dal compilatore o dall’interprete, e causa rispettivamente un errore in fase di compilazione o esecuzione. Inoltre, molti editor segnalano già durante la scrittura del codice errori di questo tipo.

In alcuni casi, tuttavia, specie se l’incapsulamento è basato su convenzioni, è possibile che la sua violazione non causi un errore, oppure che si possa aggirare il controllo. In python, ad esempio, accedere ai membri protetti di una classe non causa errori, ed è possibile accedere ai membri privati di una classe anteponendo il nome della classe stessa (e.g. a._A__xyz).

Incapsulamento e test automatici

L’incapsulamento ha conseguenze sia positive che negative sullo sviluppo di test automatici.

La principale conseguenza positiva è il fatto che, se si fa un buon uso dell’incapsulamento, gli elementi pubblici sono tutti e solo quelli che costituiscono l’interfaccia “consolidata” del SUT (System Under Test), per cui testando solo quell’interfaccia si ottengono generalmente test a loro volta “consolidati” e che richiedono minore manutenzione. Inoltre, gli sviluppatori si sentono più liberi di fare refactoring del codice, perché sanno di non dover aggiornare i test a meno che non modifichino l’interfaccia pubblica.

Gli svantaggi sono invece la maggior difficoltà nell’effettuare il mocking (mi riferisco in generale a mock, fake, stub, ecc.) di componenti o operazioni e l’impossibilità o maggior difficoltà nel testare elementi non visibili ma che contengono logica complessa e meritevole di test dedicati.

La difficoltà nell’effettuare il mocking in particolare è problematica, perché impatta sia sulla possibilità di isolare il SUT dalle dipendenze (specialmente negli unit test), sia sulla possibilità di “sondare” porzioni dello stato del SUT di cui non si ha visibilità.

Trovare un compromesso

Sia l’incapsulamento che i test automatici migliorano la qualità del software, in un caso aumentando il controllo sulle possibilità di interazione tra i moduli, nell’altro verificando in maniera granulare e ripetibile il comportamento del software. Bisogna quindi trovare un compromesso tra massimo incapsulamento e massima testabilità che consenta di ottenere la massima qualità del software.

Quando possibile, è preferibile mantenere l’incapsulamento ed eventualmente effettuare i test più ad “alto livello” di astrazione. Ciò tuttavia rischia di impedire l’esecuzione di test con una granularità sufficientemente fine, come accennato in precedenza. Di seguito verranno quindi illustrati alcuni metodi per ridurre l’incapsulamento in maniera controllata e senza sacrificarlo troppo.

Il metodo più “pulito” (e utilizzabile in qualsiasi linguaggio) per rendere accessibili elementi che prima non lo erano è estrarre un modulo contenente quegli elementi, e iniettarlo poi nel modulo originario come dipendenza. Spesso, quando ci si trova nella situazione di voler accedere ad un elemento non visibile, è un buon indizio che quell’elemento concettualmente non rientra tra le responsabilità di quel modulo, ma andrebbe invece delegato ad un altro. Gli elementi estratti in questo modo diventano membri pubblici del nuovo modulo, per cui possono essere usati nei test. Gli svantaggi di questa soluzione sono la complessità del refactoring (se si parte da codice esistente) e il fatto che possono comunque esistere elementi non pubblici che non si prestano ad essere estratti in altri moduli. Composizione e dependency injection verranno trattati in altri articoli.

Un altro buon metodo, utilizzabile tuttavia solo quando l’incapsulamento è basato su convenzioni aggirabili (come in python), consiste appunto nell’aggirare i controlli. Questo metodo è diverso dall’ultimo indicato di seguito perché in questo caso il compilatore / interprete e gli strumenti di refactoring continuano a garantire la correttezza sintattica dell’accesso.

Se non è possibile usare i metodi precedenti, un discreto compromesso, che tuttavia è utilizzabile solo in linguaggi ad oggetti e che forniscano la visibilità protetta, consiste nel trasformare i membri privati in protetti (ovvero accessibili anche dalle classi derivate). In questo modo si possono creare mock che eseguono l’override dei membri protetti o, se l’override non è possibile o è necessario chiamare il membro dall’esterno del mock, aggiungono dei membri pubblici che a loro volta chiamano quelli protetti.

Esistono poi delle tecniche specifiche per certi linguaggi. In C#, ad esempio, esiste la visibilità internal, che rende visibile l’elemento all’interno dell’assembly in cui è dichiarato (gli assembly corrispondono, in prima approssimazione, ai progetti (dll, exe…), e si può combinare con la keyword protected per ridurre ulteriormente la visibilità. Esiste un attributo (attributo = annotazione in java, all’incirca), InternalsVisibleToAttribute, che permette di rendere visibili gli elementi internal in altri assembly (e.g. quello che contiene i test). Usando internal protected si può quindi minimizzare la perdita di incapsulamento nel caso si opti per l’aumento di visibilità dei membri privati.

Infine, nei linguaggi che offrono funzionalità di reflection, spesso è possibile aggirare tutti i controlli di visibilità usando appunto la reflection. Bisogna tuttavia tener conto che così facendo si perdono tutti i controlli sintattici sull’accesso all’elemento in questione (e.g. se si chiama un metodo tramite reflection, e il nome del metodo cambia, il compilatore non segnala l’incongruenza). Si tratta quindi di un metodo molto potente ma che aumenta la necessità di manutenzione dei test.

Conclusione

Incapsulamento e test automatici sono entrambi utili per migliorare la qualità del software. Tuttavia, a volte un maggior incapsulamento riduce la testabilità del software, per cui si tratta di trovare un compromesso che complessivamente permetta di massimizzare la qualità del software.

Nell’articolo sono state illustrate varie opzioni, ciascuna con pro e contro, per raggiungere tale compromesso. Ogni situazione specifica richiede un “mix” diverso di quelle tecniche.

Per approfondimenti o consulenze in ambito testing e quality assurance, scrivere a info@dvisentin.com.