Los prefijos y sufijos no pertenecen a los nombres de interfaz

hace 2 años por David Grudl  

Usar el sufijo I prefix or Interface para interfaces, y Abstract para clases abstractas, es un antipatrón. No pertenece al código puro. Distinguir los nombres de las interfaces en realidad desdibuja los principios de la programación orientada a objetos, añade ruido al código y causa complicaciones durante el desarrollo. He aquí las razones.

Tipo = Clase + Interfaz + Descendientes

En el mundo de la programación orientada a objetos, tanto las clases como las interfaces se consideran tipos. Si utilizo un tipo al declarar una propiedad o un parámetro, desde la perspectiva del desarrollador, no hay diferencia entre si el tipo en el que se basa es una clase o una interfaz. En realidad, eso es lo que hace que las interfaces sean tan útiles. Es lo que da sentido a su existencia. (En serio: ¿para qué servirían las interfaces si no se aplicara este principio? Intenta pensarlo).

Echa un vistazo a este código:

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

El constructor dice: "Necesito un formulario y un traductor ". Y no le importa si obtiene un objeto GettextTranslator o un objeto DatabaseTranslator. Y al mismo tiempo, a mí como usuario no me importa si Translator es una interfaz, una clase abstracta o una clase concreta.

¿Realmente no me importa? En realidad, no, admito que soy bastante curioso, así que cuando estoy explorando la biblioteca de otra persona, echo un vistazo a lo que hay detrás del tipo y paso el ratón por encima:

Ah, ya veo. Y ahí se acaba la historia. Saber si es una clase o una interfaz sería importante si quisiera crear una instancia de ella, pero ese no es el caso, ahora sólo estoy hablando del tipo de la variable. Y aquí es donde quiero mantenerme al margen de estos detalles de implementación. Y ciertamente no los quiero incrustados en mi código. Lo que hay detrás de un tipo es parte de su definición, no el tipo en sí.

Ahora mira el otro código:

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

Esta definición de constructor dice literalmente: "Necesito una forma abstracta y una interfaz de traductor " Pero eso es una tontería. Necesita una forma concreta para traducir. No una forma abstracta. Y necesita un objeto que actúe como traductor. No necesita una interfaz.

Sabe que las palabras Interface y Abstract deben ser ignoradas. Que el constructor quiere lo mismo que en el ejemplo anterior. Pero… ¿en serio? ¿Realmente parece una buena idea introducir en las convenciones de nomenclatura el uso de palabras a ignorar?

Después de todo, crea una falsa idea de los principios de la programación orientada a objetos. Un principiante debe estar confundido: “Si el tipo Translator significa 1) un objeto de la clase Translator 2) un objeto que implementa la interfaz Translator o 3) un objeto que hereda de ellos, entonces ¿qué significa TranslatorInterface?” No existe una respuesta razonable a esta pregunta.

Cuando utilizamos TranslatorInterface, aunque Translator pueda ser una interfaz, estamos cometiendo una tautología. Lo mismo ocurre cuando declaramos interface TranslatorInterface. Poco a poco, se producirá una broma de programación:

interface TranslatorInterface
{
}

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

Implementación excepcional

Cuando veo algo como TranslatorInterface, es probable que exista una implementación llamada Translator implements TranslatorInterface. Esto me hace preguntarme: ¿qué hace que Translator sea tan especial como para tener el privilegio único de llamarse Translator? Todas las demás implementaciones necesitan un nombre descriptivo, como GettextTranslator o DatabaseTranslator, pero ésta es una especie de “defecto”, como sugiere su condición preferida de llamarse Translator sin la etiqueta.

Incluso la gente se confunde y no sabe si teclear Translator o TranslatorInterface. Entonces ambos se mezclan en el código cliente, estoy seguro de que te has encontrado con esto muchas veces (en Nette por ejemplo en la conexión con Nette\Http\Request vs IRequest).

¿No sería mejor deshacerse de la implementación especial y mantener el nombre genérico Translator para la interfaz? Es decir, tener implementaciones específicas con un nombre específico + una interfaz genérica con un nombre genérico. Eso tiene sentido.

La carga de un nombre descriptivo recae entonces exclusivamente en las implementaciones. Si renombramos TranslatorInterface a Translator, nuestra antigua clase Translator necesita un nuevo nombre. La gente tiende a resolver este problema llamándola DefaultTranslator, incluso yo soy culpable de esto. Pero de nuevo, ¿qué la hace tan especial como para llamarla Default? No seas perezoso y piensa bien qué hace y por qué es diferente de otras posibles implementaciones.

¿Y si no puedo imaginar varias implementaciones posibles? ¿Y si sólo se me ocurre una forma válida? Pues no crees la interfaz. Al menos por ahora.

Eh, hay otra implementación

¡Y está aquí! Necesitamos una segunda implementación. Ocurre todo el tiempo. Nunca ha habido necesidad de almacenar las traducciones de más de una forma probada, por ejemplo, en una base de datos, pero ahora hay un nuevo requisito y necesitamos tener más traductores en la aplicación.

Aquí es también cuando te das cuenta claramente de cuáles eran las particularidades del traductor único original. Era un traductor de base de datos, no por defecto.

¿Qué pasa con él?

  1. Hacer que el nombre Translator la interfaz
  2. Renombra la clase original a DatabaseTranslator y se implementará Translator
  3. Y creas nuevas clases GettextTranslator y tal vez NeonTranslator

Todos estos cambios son muy convenientes y fáciles de hacer, especialmente si la aplicación está construida según los principios de inyección de dependencia. No necesitas cambiar nada en el código, sólo cambia Translator por DatabaseTranslator en la configuración del contenedor DI. ¡Estupendo!

Sin embargo, una situación diametralmente diferente surgiría si insistiéramos en prefijar/sufijar. Tendríamos que renombrar los tipos de Translator a TranslatorInterface en el código de toda la aplicación. Tal renombramiento sería puramente por convención, pero iría en contra del espíritu de la programación orientada a objetos, como acabamos de demostrar. ¿La interfaz no ha cambiado, el código de usuario no ha cambiado, pero la convención requiere renombrar? Entonces es una convención defectuosa.

Además, si con el tiempo resultara que una clase abstracta sería mejor que la interfaz, volveríamos a renombrar. Una acción así puede no ser trivial en absoluto, por ejemplo cuando el código está repartido en varios paquetes o es utilizado por terceros.

Pero todo el mundo lo hace

No todo el mundo lo hace. Es cierto que en el mundo PHP, el Zend Framework, seguido por Symfony, los grandes jugadores, popularizaron la distinción interfaz y nombres de clases abstractas. Este enfoque ha sido adoptado por PSR, que irónicamente sólo publica interfaces, pero incluye la palabra interfaz en el nombre de cada una.

Por otro lado, otro importante framework, Laravel, no distingue entre interfaces y clases abstractas. Ni siquiera la popular capa de base de datos Doctrine lo hace, por ejemplo. Y tampoco lo hace la biblioteca estándar de PHP (así que tenemos las interfaces Throwable o Iterator, la clase abstracta FilterIterator, etc.).

Si nos fijamos en el mundo fuera de PHP, C# utiliza el prefijo I para las interfaces, mientras que en Java o TypeScript los nombres no son diferentes.

Así que no todo el mundo lo hace, pero incluso si lo hicieran, no significa que sea algo bueno. No es inteligente adoptar sin pensar lo que hacen los demás, porque puedes estar adoptando errores también. Errores de los que los demás preferirían deshacerse, es un bocado demasiado grande hoy en día.

No sé en el código cuál es la interfaz

Muchos programadores argumentarán que los prefijos/sufijos son útiles para ellos porque les permiten saber inmediatamente en el código qué son las interfaces. Sienten que echarían de menos tal distinción. Veamos, ¿puedes decir qué es una clase y qué es una interfaz en estos ejemplos?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X is always a class, Y es una interfaz, es inequívoco incluso sin prefijos/postfijos. Por supuesto, el IDE también lo sabe y siempre te dará la pista correcta en un contexto dado.

Pero ¿qué pasa aquí?

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

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

En estos casos, no lo sabrás. Como decíamos al principio, desde el punto de vista de un desarrollador no debería haber diferencia entre lo que es una clase y lo que es una interfaz. Que es lo que da sentido a las interfaces y a las clases abstractas.

Si aquí se pudiera distinguir entre una clase y una interfaz, se negaría el principio básico de la programación orientada a objetos. Y las interfaces carecerían de sentido.

Estoy acostumbrado

Cambiar de hábitos duele 🙂 A menudo duele la idea de hacerlo. No culpemos a las personas que se sienten atraídas por los cambios y los esperan con impaciencia, pues la mayoría, como dice el proverbio checo, tiene los hábitos como camisas de hierro.

Pero basta con mirar al pasado, cómo algunos hábitos han sido barridos por el tiempo. Quizá la más famosa sea la llamada notación húngara, utilizada desde los años ochenta y popularizada por Microsoft. La notación consistía en comenzar el nombre de cada variable con una abreviatura que simbolizaba su tipo de dato. En PHP se vería como echo $this->strName o $this->intCount++. La notación húngara empezó a abandonarse en los noventa, y hoy en día las directrices de Microsoft desaconsejan directamente su uso a los desarrolladores.

Antes era una característica esencial y hoy nadie la echa de menos.

Pero, ¿por qué ir tan atrás en el tiempo? Quizá recuerdes que en PHP era costumbre distinguir los miembros no públicos de las clases con un guión bajo (ejemplo de Zend Framework). Esto era antes de que existiera PHP 5, que tenía modificadores de visibilidad public/protected/private. Pero los programadores lo hacían por costumbre. Estaban convencidos de que sin guiones bajos dejarían de entender el código. “¿Cómo iba a distinguir en el código las propiedades públicas de las privadas, eh?”.

Hoy nadie usa guiones bajos. Y nadie los echa de menos. El tiempo ha demostrado perfectamente que los temores eran falsos.

Sin embargo, es exactamente lo mismo que la objeción: “¿Cómo voy a distinguir una interfaz de una clase en el código, eh?”.

Dejé de usar prefijos/postfijos hace diez años. Nunca volvería atrás, fue una gran decisión. Tampoco conozco a ningún otro programador que quiera volver atrás. Como decía un amigo: “Pruébalo y en un mes no entenderás que alguna vez lo hiciste de otra manera”.

Quiero mantener la coherencia

Me imagino a un programador diciendo: “Usar prefijos y sufijos es realmente una tontería, lo entiendo, es sólo que ya tengo mi código construido de esta manera y cambiarlo es muy difícil. Y si empiezo a escribir código nuevo correctamente sin ellos, acabaré con inconsistencia, que es quizás incluso peor que una mala convención.”

De hecho, tu código ya es inconsistente porque estás usando la librería del sistema PHP:

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

Y con la mano en el corazón, ¿importa? ¿Alguna vez pensaste que esto sería más consistente?

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

¿O esto?

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

Creo que no. La coherencia no juega un papel tan importante como podría parecer. Al contrario, el ojo prefiere menos ruido visual, el cerebro prefiere claridad de diseño. Así que ajustar la convención y empezar a escribir correctamente las nuevas interfaces sin prefijos ni sufijos tiene sentido.

Se pueden eliminar a propósito incluso de los grandes proyectos. Un ejemplo es el Nette Framework, que históricamente utilizaba prefijos I en los nombres de las interfaces, que empezó a eliminar gradualmente hace unos años, manteniendo la plena compatibilidad con versiones anteriores.