I prefissi e i suffissi non appartengono ai nomi delle interfacce

2 anni fa Da David Grudl  

L'uso del suffisso I prefix or Interface per le interfacce e Abstract per le classi astratte è un antipattern. Non appartiene al codice puro. La distinzione dei nomi delle interfacce confonde i principi dell'OOP, aggiunge rumore al codice e causa complicazioni durante lo sviluppo. Ecco i motivi.

Tipo = Classe + Interfaccia + Discendenti

Nel mondo OOP, sia le classi che le interfacce sono considerate tipi. Se utilizzo un tipo quando dichiaro una proprietà o un parametro, dal punto di vista dello sviluppatore non c'è alcuna differenza tra il tipo su cui si sta facendo affidamento e una classe o un'interfaccia. Questo è l'aspetto positivo che rende le interfacce così utili. È ciò che dà significato alla loro esistenza. (Seriamente: a cosa servirebbero le interfacce se non si applicasse questo principio? Provate a pensarci).

Date un'occhiata a questo codice:

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

Il costruttore dice: "Ho bisogno di un modulo e di un traduttore " e non gli importa se ottiene un oggetto GettextTranslator o un oggetto DatabaseTranslator. E allo stesso tempo, a me come utente non interessa se Translator è un'interfaccia, una classe astratta o una classe concreta.

Non mi interessa davvero? In realtà no, ammetto di essere piuttosto curioso, quindi quando esploro la libreria di qualcun altro, do una sbirciatina a cosa c'è dietro il tipo e ci passo sopra:

Oh, capisco. E questa è la fine della storia. Sapere se si tratta di una classe o di un'interfaccia sarebbe importante se volessi crearne un'istanza, ma non è questo il caso, ora sto solo parlando del tipo di variabile. È qui che voglio rimanere fuori da questi dettagli di implementazione. E di certo non li voglio incorporati nel mio codice! Ciò che sta dietro a un tipo fa parte della sua definizione, non del tipo stesso.

Ora guardate l'altro codice:

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

Questa definizione di costruttore dice letteralmente: "Ho bisogno di una forma astratta e di una interfaccia di traduttore. " Ma è una sciocchezza. Ha bisogno di una forma concreta da rendere. Non una forma astratta. E ha bisogno di un oggetto che agisca come traduttore. Non ha bisogno di un'interfaccia.

Si sa che le parole Interface e Abstract devono essere ignorate. Che il costruttore vuole la stessa cosa dell'esempio precedente. Ma… davvero? Sembra davvero una buona idea introdurre nelle convenzioni di denominazione l'uso di parole da ignorare?

Dopo tutto, crea una falsa idea dei principi OOP. Un principiante deve essere confuso: “Se il tipo Translator significa 1) un oggetto della classe Translator 2) un oggetto che implementa l'interfaccia Translator o 3) un oggetto che eredita da essa, allora cosa si intende per TranslatorInterface?”. Non esiste una risposta ragionevole a questa domanda.

Quando usiamo TranslatorInterface, anche se Translator può essere un'interfaccia, commettiamo una tautologia. Lo stesso accade quando dichiariamo interface TranslatorInterface. A poco a poco, si verifica uno scherzo della programmazione:

interface TranslatorInterface
{
}

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

Implementazione eccezionale

Quando vedo qualcosa come TranslatorInterface, è probabile che ci sia un'implementazione chiamata Translator implements TranslatorInterface. Mi chiedo: cosa rende Translator così speciale da avere il privilegio unico di essere chiamato Translator? Ogni altra implementazione ha bisogno di un nome descrittivo, come GettextTranslator o DatabaseTranslator, ma questa è una sorta di “default”, come suggerisce il suo stato preferito di essere chiamata Translator senza l'etichetta.

Anche le persone si confondono e non sanno se digitare il suggerimento per Translator o per TranslatorInterface. Poi entrambi si confondono nel codice client, sono sicuro che vi siete imbattuti in questo molte volte (in Nette, per esempio, nel collegamento con Nette\Http\Request e IRequest).

Non sarebbe meglio eliminare l'implementazione speciale e mantenere il nome generico Translator per l'interfaccia? Cioè, avere implementazioni specifiche con un nome specifico + un'interfaccia generica con un nome generico. Questo ha senso.

L'onere di un nome descrittivo ricade quindi esclusivamente sulle implementazioni. Se rinominiamo TranslatorInterface in Translator, la nostra precedente classe Translator ha bisogno di un nuovo nome. Si tende a risolvere il problema chiamandola DefaultTranslator, anche io sono colpevole di questo. Ma ancora una volta, cosa la rende così speciale da essere chiamata Default? Non siate pigri e pensate bene a cosa fa e perché è diversa dalle altre possibili implementazioni.

E se non riuscissi a immaginare diverse possibili implementazioni? Se riesco a pensare a un solo modo valido? Quindi non create l'interfaccia. Almeno per ora.

C'è un'altra implementazione

Ed è qui! Abbiamo bisogno di una seconda implementazione. Succede sempre. Non c'è mai stata la necessità di memorizzare le traduzioni in più di un modo collaudato, ad esempio in un database, ma ora c'è un nuovo requisito e dobbiamo avere più traduttori nell'applicazione.

Questo è anche il momento in cui ci si rende conto di quali fossero le caratteristiche del traduttore unico originale. Si trattava di un traduttore di database, senza default.

Cosa c'entra?

  1. Si rende il nome Translator l'interfaccia
  2. Rinominare la classe originale in DatabaseTranslator e questa verrà implementata. Translator
  3. Si creano nuove classi GettextTranslator e forse NeonTranslator

Tutte queste modifiche sono molto comode e facili da fare, soprattutto se l'applicazione è costruita secondo i principi della dependency injection. Non è necessario modificare nulla nel codice, basta cambiare Translator in DatabaseTranslator nella configurazione del contenitore DI. È fantastico!

Tuttavia, una situazione diametralmente diversa si verificherebbe se insistessimo sulla prefissazione/suffissazione. Dovremmo rinominare i tipi da Translator a TranslatorInterface nel codice dell'applicazione. Tale rinominazione sarebbe puramente convenzionale, ma andrebbe contro lo spirito dell'OOP, come abbiamo appena dimostrato. L'interfaccia non è cambiata, il codice utente non è cambiato, ma la convenzione richiede una ridenominazione? Allora è una convenzione sbagliata.

Inoltre, se col tempo si scoprisse che una classe astratta è migliore dell'interfaccia, dovremmo rinominarla di nuovo. Un'azione del genere potrebbe non essere affatto banale, per esempio quando il codice è distribuito su più pacchetti o è utilizzato da terzi.

Ma tutti lo fanno

Non tutti lo fanno. È vero che nel mondo PHP, il framework Zend, seguito da Symfony, i grandi player, hanno reso popolare la distinzione dei nomi delle interfacce e delle classi astratte. Questo approccio è stato adottato da PSR, che per ironia della sorte pubblica solo interfacce, ma include la parola interfaccia nel nome di ciascuna di esse.

D'altra parte, un altro importante framework, Laravel, non distingue tra interfacce e classi astratte. Anche il popolare livello di database Doctrine non lo fa, per esempio. E nemmeno la libreria standard di PHP (quindi abbiamo le interfacce Throwable o Iterator, la classe astratta FilterIterator, ecc.)

Se guardiamo al mondo esterno a PHP, C# usa il prefisso `I' per le interfacce, mentre in Java o TypeScript i nomi non sono diversi.

Quindi non tutti lo fanno, ma anche se lo facessero, non significa che sia una buona cosa. Non è saggio adottare senza criterio ciò che fanno gli altri, perché si potrebbero adottare anche gli errori. Errori di cui l'altro preferirebbe sbarazzarsi, perché oggi è un boccone troppo grosso.

Non so quale sia l'interfaccia nel codice

Molti programmatori sostengono che i prefissi/suffissi sono utili perché consentono loro di sapere subito quali sono le interfacce nel codice. Ritengono che una distinzione del genere non sarebbe stata apprezzata. Vediamo, quindi, se siete in grado di dire cosa è una classe e cosa è un'interfaccia in questi esempi.

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X is always a class, Y è un'interfaccia, è inequivocabile anche senza prefissi/postfissi. Naturalmente, anche l'IDE lo sa e vi darà sempre il suggerimento corretto in un determinato contesto.

Ma che dire di questo caso:

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

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

In questi casi, non lo saprete. Come abbiamo detto all'inizio, dal punto di vista dello sviluppatore non dovrebbe esserci alcuna differenza tra una classe e un'interfaccia. È questo che dà alle interfacce e alle classi astratte il loro senso.

Se si potesse distinguere tra una classe e un'interfaccia, si negherebbe il principio fondamentale dell'OOP. E le interfacce diventerebbero prive di significato.

Ci sono abituato

Cambiare abitudini fa male 🙂 Spesso fa male anche l'idea di farlo. Non biasimiamo le persone che sono attratte dai cambiamenti e non vedono l'ora di farli, perché per la maggior parte di loro, come dice il proverbio ceco, l'abitudine è una camicia di ferro.

Ma basta guardare al passato, a come alcune abitudini sono state spazzate via dal tempo. Forse la più famosa è la cosiddetta notazione ungherese, utilizzata dagli anni Ottanta e resa popolare da Microsoft. La notazione consisteva nell'iniziare il nome di ogni variabile con un'abbreviazione che simboleggiava il suo tipo di dati. In PHP sarebbe come echo $this->strName o $this->intCount++. La notazione ungherese ha iniziato a essere abbandonata negli anni Novanta e oggi le linee guida di Microsoft scoraggiano direttamente gli sviluppatori dall'utilizzarla.

Un tempo era una caratteristica essenziale e oggi nessuno ne sente la mancanza.

Ma perché tornare a tanto tempo fa? Forse ricorderete che in PHP era consuetudine distinguere i membri non pubblici delle classi con un trattino basso (sample from Zend Framework). Questo accadeva quando esisteva PHP 5, che aveva i modificatori di visibilità pubblico/protetto/privato. Ma i programmatori lo facevano per abitudine. Erano convinti che senza underscore avrebbero smesso di capire il codice. “Come faccio a distinguere le proprietà pubbliche da quelle private nel codice, eh?”.

Oggi nessuno usa gli underscore. E nessuno ne sente la mancanza. Il tempo ha dimostrato perfettamente che quei timori erano falsi.

Eppure è esattamente come l'obiezione: “Come faccio a distinguere un'interfaccia da una classe nel codice, eh?”.

Ho smesso di usare i prefissi/postfissi dieci anni fa. Non tornerei mai indietro, è stata una grande decisione. Non conosco nessun altro programmatore che voglia tornare indietro. Come ha detto un amico, “provate e tra un mese non capirete più di aver fatto diversamente”.

Voglio mantenere la coerenza

Posso immaginare un programmatore che dice: “L'uso di prefissi e suffissi è davvero insensato, lo capisco, è solo che il mio codice è già costruito in questo modo e cambiarlo è molto difficile. E se cominciassi a scrivere correttamente nuovo codice senza di essi, finirei per avere incoerenza, il che è forse anche peggio di una cattiva convenzione”.

In realtà, il vostro codice è già incoerente perché state usando la libreria di sistema di PHP:

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

E, mano sul cuore, che importanza ha? Avete mai pensato che questo sarebbe stato più coerente?

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();
}

Non credo. La coerenza non gioca un ruolo così importante come potrebbe sembrare. Al contrario, l'occhio preferisce meno rumore visivo, il cervello preferisce la chiarezza del design. Quindi, modificare le convenzioni e iniziare a scrivere correttamente le nuove interfacce 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.