Prefissi e suffissi non appartengono ai nomi delle interfacce

3 anni fa Da David Grudl  

L'uso del prefisso I o del suffisso Interface per le interfacce, così come Abstract per le classi astratte, è un antipattern. Non ha posto nel codice pulito. La distinzione dei nomi delle interfacce in realtà oscura i principi OOP, introduce rumore nel codice e causa complicazioni nello sviluppo. Le ragioni sono le seguenti.

Tipo = classe + interfaccia + discendenti

Nel mondo OOP, sia le classi che le interfacce sono considerate tipi. Se uso un tipo nella dichiarazione di una proprietà o di un parametro, dal punto di vista dello sviluppatore non c'è differenza se il tipo su cui si fa affidamento è una classe o un'interfaccia. Questa è una cosa fantastica, grazie alla quale le interfacce sono in realtà così utili. Questo dà senso alla loro esistenza. (Seriamente: a cosa servirebbero le interfacce se questo principio non fosse valido? Provate a pensarci.)

Guardate questo codice:

class FormRenderer
{
	public function __construct(
		private Form $form,
		private Translator $translator,
	) {
	}
}

Il costruttore dice: “Ho bisogno di un form e di un traduttore.” E non gli importa affatto se riceve un oggetto GettextTranslator o DatabaseTranslator. E allo stesso tempo, come utente, non mi importa affatto se Translator è un'interfaccia, una classe astratta o una classe concreta.

Non mi importa affatto? In realtà no, ammetto di essere piuttosto curioso, quindi quando esamino una libreria altrui, do un'occhiata a cosa si nasconde dietro il tipo e ci passo sopra il mouse:

Ah, ora lo so. E finisce qui. La conoscenza se si tratta di una classe o di un'interfaccia sarebbe importante se volessi creare la sua istanza, ma non è questo il caso, ora sto solo parlando del tipo della variabile. E qui voglio essere schermato da questi dettagli. E non voglio assolutamente introdurli nel mio codice! Ciò che si nasconde dietro il tipo fa parte della sua definizione, non del tipo stesso.

E ora guardate il codice successivo:

class FormRenderer
{
	public function __construct(
		private AbstractForm $form,
		private TranslatorInterface $translator,
	) {
	}
}

Questa definizione del costruttore dice letteralmente: “Ho bisogno di un form astratto e di un'interfaccia traduttore.” Ma questa è una sciocchezza. Ha bisogno di un form concreto da renderizzare. Non di un form astratto. E ha bisogno di un oggetto che svolga il ruolo di traduttore. Non ha bisogno di un'interfaccia.

Voi sapete che le parole Interface e Abstract devono essere ignorate. Che il costruttore vuole la stessa cosa dell'esempio precedente. Ma… sul serio? Vi sembra davvero una buona idea introdurre nelle convenzioni di denominazione l'uso di parole che devono essere trascurate?

Crea una falsa idea dei principi OOP. Un principiante deve essere confuso: “Se per tipo Translator si intende 1) un oggetto della classe Translator 2) un oggetto che implementa l'interfaccia Translator o 3) un oggetto che ne eredita, cosa si intende allora per TranslatorInterface?” A questo non si può rispondere ragionevolmente.

Quando scriviamo TranslatorInterface, sebbene anche Translator possa essere un'interfaccia, commettiamo una tautologia. Lo stesso quando dichiariamo interface TranslatorInterface. E così via. Fino a creare uno scherzo da programmatori:

interface TranslatorInterface
{
}

class FormRendererClass
{
	/**
	 * Costruttore
	 */
	public function __construct(
		private AbstractForm $privatePropertyForm,
		private TranslatorInterface $privatePropertyTranslator,
	) {
		// 🤷‍♂️
	}
}

Implementazione eccezionale

Quando vedo qualcosa come TranslatorInterface, è probabile che esista anche un'implementazione con il nome Translator implements TranslatorInterface. Mi costringe a riflettere: cosa rende Translator così eccezionale da avere il diritto unico di chiamarsi Translator? Ogni altra implementazione necessita di un nome descrittivo, ad esempio GettextTranslator o DatabaseTranslator, ma questa è in qualche modo “predefinita”, come suggerisce la sua posizione privilegiata, chiamandosi Translator senza aggettivi.

Questo addirittura rende le persone insicure e non sanno se devono scrivere il typehint per Translator o TranslatorInterface. Nel codice client si mescolano quindi entrambi, sicuramente vi siete già imbattuti molte volte (in Nette ad esempio in relazione a Nette\Http\Request vs IRequest).

Non sarebbe meglio sbarazzarsi dell'implementazione eccezionale e lasciare il nome generico Translator per l'interfaccia? Cioè avere implementazioni concrete con un nome concreto + un'interfaccia generica con un nome generico. Questo ha senso.

L'onere del nome descrittivo ricade quindi puramente sulle implementazioni. Se rinominiamo TranslatorInterface in Translator, la nostra ex classe Translator ha bisogno di un nuovo nome. Le persone tendono a risolvere questo problema chiamandola DefaultTranslator, anch'io sono colpevole. Ma ancora, cosa la rende così eccezionale da chiamarsi Default? Non siate pigri e pensate bene a cosa fa e perché si differenzia dalle altre possibili implementazioni.

E se non riesco a immaginare più implementazioni? E se mi viene in mente solo un modo valido? Allora semplicemente non create l'interfaccia. Almeno per ora.

Ecco, è apparsa un'altra implementazione

Ed eccoci qui! Abbiamo bisogno di una seconda implementazione. Succede comunemente. Non c'è mai stata la necessità di memorizzare le traduzioni in un modo diverso da un unico metodo collaudato, ad esempio in un database, ma ora è emersa una nuova esigenza ed è necessario avere più traduttori nell'applicazione.

Questo è anche il momento in cui vi rendete chiaramente conto di quale fosse la specificità del traduttore unico originale. Era un traduttore basato su database, non predefinito.

Cosa fare?

  1. Trasformiamo il nome Translator in un'interfaccia
  2. Rinominate la classe originale in DatabaseTranslator e implementerà Translator
  3. E create nuove classi GettextTranslator e magari NeonTranslator

Tutte queste modifiche si fanno molto comodamente e facilmente, soprattutto se l'applicazione è costruita secondo i principi della dependency injection. Non è necessario modificare nulla nel codice, solo nella configurazione del container DI cambiamo Translator in DatabaseTranslator. Questo è fantastico!

Una situazione diametralmente diversa si verificherebbe però se insistessimo sui prefissi/suffissi. Dovremmo rinominare i tipi in tutto il codice dell'applicazione da Translator a TranslatorInterface. Tale ridenominazione sarebbe puramente finalizzata al rispetto della convenzione, ma andrebbe contro il senso dell'OOP, come abbiamo mostrato poco fa. L'interfaccia non è cambiata, il codice utente non è cambiato, ma la convenzione richiede di rinominare? Allora è una convenzione errata.

Se inoltre col tempo si scoprisse che una classe astratta sarebbe migliore di un'interfaccia, rinomineremmo di nuovo. Un tale intervento non è affatto banale, ad esempio se il codice è distribuito su più pacchetti o viene utilizzato da terze parti.

Ma tutti fanno così

Non tutti. È vero che nel mondo PHP la distinzione dei nomi delle interfacce e delle classi astratte è stata resa popolare da Zend Framework e poi da Symfony, cioè grandi player. Questo approccio è stato adottato anche da PSR, che paradossalmente pubblica solo interfacce, eppure per ognuna riporta nel nome la parola interfaccia.

D'altra parte, un altro framework significativo, Laravel, non distingue affatto interfacce e classi astratte. Non lo fa ad esempio nemmeno il popolare layer di database Doctrine. E non lo fa nemmeno la libreria standard di PHP (abbiamo così l'interfaccia Throwable o Iterator, la classe astratta FilterIterator, ecc.).

Se guardassimo al mondo al di fuori di PHP, ad esempio C# utilizza il prefisso I per le interfacce, al contrario in Java o TypeScript i nomi non vengono distinti.

Quindi non lo fanno tutti, tuttavia anche se lo facessero, non significa che sia giusto. Adottare acriticamente ciò che fanno gli altri non è ragionevole, perché si possono adottare anche errori. Errori di cui l'altro molto probabilmente si libererebbe volentieri, solo che è troppo impegnativo.

Non riconosco nel codice cos'è un'interfaccia

Molti programmatori obietteranno che per loro prefissi/suffissi sono utili, perché grazie ad essi riconoscono subito nel codice cosa sono le interfacce. Hanno la sensazione che tale distinzione mancherebbe loro. Vediamo un po', riconoscete in questi esempi cosa è una classe e cosa un'interfaccia?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X è sempre una classe, Y è un'interfaccia, è inequivocabile anche senza prefissi/suffissi. Ovviamente lo sa anche l'IDE e nel contesto dato vi suggerirà sempre correttamente.

Ma cosa succede qui:

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

try { ... } catch (A $x) { ... }

In questi casi non lo riconoscete. Come abbiamo detto all'inizio, qui dal punto di vista dello sviluppatore non dovrebbe esserci differenza tra cosa è una classe e cosa un'interfaccia. Il che appunto dà senso alle interfacce e alle classi astratte.

Se qui foste in grado di distinguere una classe da un'interfaccia, neghereste il principio fondamentale dell'OOP. E le interfacce perderebbero il loro senso.

Ci sono abituato

Cambiare le abitudini fa semplicemente male 🙂 Quante volte anche solo l'idea. Ma per non essere ingiusti, molte persone sono invece attratte dai cambiamenti e non vedono l'ora, tuttavia per la maggior parte vale che l'abitudine è una camicia di ferro.

Ma basta guardare al passato, come alcune abitudini siano state spazzate via dal tempo. Probabilmente la più famosa è la cosiddetta notazione ungherese usata dagli anni Ottanta e resa popolare da Microsoft. La notazione consisteva nel fatto che il nome di ogni variabile iniziava con una sigla che simboleggiava il suo tipo di dati. In PHP apparirebbe così echo $this->strName o $this->intCount++. Dalla notazione ungherese si è iniziato ad abbandonare negli anni Novanta e oggi Microsoft nelle sue linee guida scoraggia direttamente gli sviluppatori dal suo utilizzo.

Un tempo era imprescindibile e oggi non manca a nessuno.

Ma perché andare così indietro nel tempo? Forse ricordate che in PHP era consuetudine distinguere i membri non pubblici delle classi con un trattino basso (esempio da Zend Framework). Era ai tempi in cui esisteva già da tempo PHP 5, che aveva i modificatori di visibilità public/protected/private. Ma i programmatori lo facevano per abitudine. Erano convinti che senza i trattini bassi avrebbero smesso di orientarsi nel codice. “Come distinguerei nel codice le variabili pubbliche da quelle private, eh?”

Oggi nessuno usa i trattini bassi. E non mancano a nessuno. Il tempo ha dimostrato brillantemente che le preoccupazioni erano infondate.

Eppure è esattamente la stessa obiezione: “Come distinguerei nel codice un'interfaccia da una classe, eh?”

Ho smesso di usare prefissi/suffissi dieci anni fa. Non tornerei mai indietro, è stata una decisione fantastica. Non conosco nemmeno nessun altro programmatore che vorrebbe tornare indietro. Come ha detto un amico: “Provalo e tra un mese non capirai come hai fatto a fare diversamente.”

Voglio mantenere la coerenza

Posso immaginare che un programmatore si dica: “Usare prefissi e suffissi è davvero un nonsenso, lo capisco, ma ho già costruito il codice così e il cambiamento è molto difficile. E se iniziassi a scrivere il nuovo codice correttamente senza di essi, creerei un'incoerenza, che è forse peggiore della cattiva convenzione.”

In realtà, il vostro codice è già incoerente ora, perché utilizzate la libreria di sistema di PHP, che non ha prefissi e suffissi:

class Collection implements ArrayAccess, Countable, IteratorAggregate
{
	public function add(string|Stringable $item): void
	{
	}
}

E mano sul cuore, dà fastidio? Vi è mai venuto in mente che sarebbe stato più coerente questo?

class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
	public function add(string|StringableInterface $item): void
	{
	}
}

O questo?

try {
	$command = $this->find($name);
} catch (ThrowableInterface $e) {
	return $e->getMessage();
}

Penso di no. La coerenza non gioca un ruolo così significativo come potrebbe sembrare. Al contrario, l'occhio preferisce meno rumore visivo, il cervello la pulizia del design. Quindi modificare la convenzione e iniziare a scrivere nuove interfacce correttamente senza prefissi e suffissi ha senso.

Possono essere eliminati di proposito anche da progetti di grandi dimensioni. Un esempio è il Nette Framework, che storicamente utilizzava i prefissi I nei nomi delle interfacce, che ha iniziato a eliminare gradualmente alcuni anni fa, mantenendo la piena compatibilità con il passato.