Los prefijos y sufijos no pertenecen a los nombres de las interfaces

hace 3 años por David Grudl  

Usar el prefijo I o el sufijo Interface en las interfaces, así como Abstract en las clases abstractas, es un antipatrón. No tiene cabida en el código limpio. Distinguir los nombres de las interfaces en realidad oscurece los principios de la POO, introduce ruido en el código y causa complicaciones en el desarrollo. Las razones son las siguientes.

Tipo = clase + interfaz + descendientes

En el mundo de la POO, tanto las clases como las interfaces se consideran tipos. Si utilizo un tipo al declarar una propiedad o un parámetro, desde el punto de vista del desarrollador no hay diferencia entre si el tipo en el que confía es una clase o una interfaz. Esto es algo genial, gracias a lo cual las interfaces son realmente tan útiles. Esto da sentido a su existencia. (En serio: ¿para qué servirían las interfaces si este principio no se aplicara? Intente reflexionar sobre ello.)

Observe este código:

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

El constructor dice: “Necesito un formulario y un traductor.” Y le da completamente igual si recibe un objeto GettextTranslator o DatabaseTranslator. Y al mismo tiempo, como usuario, me da completamente igual si Translator es una interfaz, una clase abstracta o una clase concreta.

¿Me da completamente igual? En realidad no, confieso que soy bastante curioso, así que cuando examino una librería externa, echo un vistazo a lo que se esconde detrás del tipo y paso el ratón sobre él:

Ah, ya lo sé. Y ahí termina. El conocimiento de si se trata de una clase o una interfaz sería importante si quisiera crear su instancia, pero no es el caso, ahora solo hablo del tipo de la variable. Y aquí quiero estar abstraído de estos detalles. ¡Y mucho menos quiero introducirlos en mi código! Lo que se esconde detrás del tipo es parte de su definición, no del tipo en sí.

Y ahora mire el siguiente código:

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

Esta definición del constructor dice literalmente: “Necesito un formulario abstracto y una interfaz de traductor.” Pero eso es una tontería. Necesita un formulario concreto que tiene que renderizar. No un formulario abstracto. Y necesita un objeto que cumpla la función de traductor. No necesita una interfaz.

Usted sabe que las palabras Interface y Abstract deben ignorarse. Que el constructor quiere lo mismo que en el ejemplo anterior. Pero… ¿en serio? ¿Realmente le parece una buena idea introducir en las convenciones de nombres el uso de palabras que deben pasarse por alto?

Porque crea una idea falsa sobre los principios de la POO. Un principiante debe estar confundido: “Si por el tipo Translator se entiende 1) un objeto de la clase Translator, 2) un objeto que implementa la interfaz Translator o 3) un objeto que hereda de ellos, ¿qué se entiende entonces por TranslatorInterface?” A esto no se puede responder razonablemente.

Cuando escribimos TranslatorInterface, aunque Translator también puede ser una interfaz, cometemos una tautología. Lo mismo cuando declaramos interface TranslatorInterface. Y así sucesivamente. Hasta que surge un chiste de programador:

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 también exista una implementación con el nombre Translator implements TranslatorInterface. Me obliga a reflexionar: ¿qué hace que Translator sea tan excepcional como para tener el derecho único de llamarse Translator? Cualquier otra implementación necesita un nombre descriptivo, por ejemplo GettextTranslator o DatabaseTranslator, pero esta es de alguna manera “predeterminada”, como sugiere su posición preferente al llamarse Translator sin calificativo.

Incluso confunde a la gente y no saben si deben escribir el typehint para Translator o TranslatorInterface. En el código cliente, entonces se mezcla ambos, seguro que se ha encontrado con esto muchas veces (en Nette, por ejemplo, en relación con Nette\Http\Request vs IRequest).

¿No sería mejor deshacerse de la implementación excepcional y dejar el nombre genérico Translator para la interfaz? Es decir, tener implementaciones concretas con un nombre concreto + una interfaz genérica con un nombre genérico. Eso tiene sentido, ¿verdad?

La carga del nombre descriptivo recae entonces puramente 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, yo también soy culpable. Pero de nuevo, ¿qué la hace tan excepcional como para llamarse Default? No sea perezoso y reflexione bien sobre lo que hace y por qué se diferencia de otras posibles implementaciones.

¿Y qué pasa si no puedo imaginar múltiples implementaciones? ¿Qué pasa si solo se me ocurre una forma válida? Entonces simplemente no cree la interfaz. Al menos por ahora.

¡Vaya, apareció otra implementación!

¡Y aquí está! Necesitamos una segunda implementación. Sucede comúnmente. Nunca surgió la necesidad de almacenar traducciones de otra manera que no fuera una forma probada, por ejemplo, en una base de datos, pero ahora ha aparecido un nuevo requisito y es necesario tener más traductores en la aplicación.

Este es también el momento en que se da cuenta claramente de cuál era la especificidad del traductor único original. Era un traductor de base de datos, no uno predeterminado.

¿Qué hacer al respecto?

  1. Hacemos que el nombre Translator sea una interfaz
  2. Renombramos la clase original a DatabaseTranslator y hará que implemente Translator
  3. Y creamos nuevas clases GettextTranslator y quizás NeonTranslator

Todos estos cambios se realizan de manera muy cómoda y fácil, especialmente si la aplicación está construida de acuerdo con los principios de inyección de dependencias. No es necesario cambiar nada en el código, solo en la configuración del contenedor DI cambiamos Translator por DatabaseTranslator. ¡Eso es genial!

Una situación diametralmente diferente ocurriría, sin embargo, si insistiéramos en prefijar/sufijar. Tendríamos que renombrar los tipos en todo el código de la aplicación de Translator a TranslatorInterface. Tal renombramiento sería puramente intencionado para cumplir con la convención, pero iría en contra del sentido de la POO, como mostramos hace un momento. La interfaz no cambió, el código de usuario no cambió, ¿pero la convención requiere renombrar? Entonces es una convención errónea.

Si además, con el tiempo, resultara que una clase abstracta sería mejor que una interfaz, volveríamos a renombrar. Tal intervención no tiene por qué ser trivial en absoluto, por ejemplo, si el código está distribuido en varios paquetes o es utilizado por terceros.

Pero todo el mundo lo hace así

No todo el mundo. Es cierto que en el mundo de PHP, la distinción de nombres de interfaces y clases abstractas fue popularizada por Zend Framework y después por Symfony, es decir, grandes actores. Este enfoque adoptado también por PSR, que paradójicamente publica solo interfaces y, sin embargo, incluye en el nombre de cada una la palabra interfaz.

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

Si miramos fuera del mundo de PHP, por ejemplo, C# utiliza el prefijo I para las interfaces, mientras que en Java o TypeScript los nombres no se distinguen.

Por lo tanto, no todo el mundo lo hace, sin embargo, incluso si lo hicieran, no significa que sea lo correcto. Adoptar sin pensar lo que hacen los demás no es razonable, porque también puede adoptar errores. Errores de los que el otro muy probablemente se desharía él mismo, solo que es demasiado exigente.

No reconozco en el código qué es una interfaz

Muchos programadores argumentarán que los prefijos/sufijos les son útiles, ya que gracias a ellos reconocen inmediatamente en el código qué son interfaces. Tienen la sensación de que les faltaría tal distinción. A ver, ¿reconoce en estos ejemplos qué es una clase y qué una interfaz?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X es siempre una clase, Y es una interfaz, es inequívoco incluso sin prefijos/sufijos. Por supuesto, el IDE también lo sabe y en el contexto dado siempre le sugerirá correctamente.

Pero, ¿y aquí?

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

try {
} catch (A $x) {
}

En estos casos no lo reconocerá. Como dijimos al principio, aquí no debe haber diferencia desde el punto de vista del desarrollador entre qué es una clase y qué una interfaz. Lo cual precisamente da sentido a las interfaces y clases abstractas.

Si aquí fuera capaz de distinguir una clase de una interfaz, negaría el principio fundamental de la POO. Y las interfaces perderían su sentido.

Estoy acostumbrado a ello

Cambiar las costumbres simplemente duele 🙂 Cuántas veces incluso la sola idea. Pero para no ser injustos, a muchas personas, por el contrario, los cambios les atraen y los esperan con ilusión, sin embargo, para la mayoría aplica que la costumbre es una camisa de fuerza.

Pero basta con mirar al pasado, cómo algunas costumbres se las llevó el tiempo. Probablemente la más famosa es la llamada notación húngara utilizada desde los años ochenta y popularizada por Microsoft. La notación consistía en que el nombre de cada variable comenzaba con una abreviatura que simbolizaba su tipo de dato. En PHP se vería así echo $this->strName o $this->intCount++. La notación húngara comenzó a abandonarse en los años noventa y hoy Microsoft en sus directrices disuade directamente a los desarrolladores de usarla.

Antaño era indispensable y hoy nadie la echa de menos.

Pero, ¿por qué ir a un pasado tan lejano? Quizás recuerde que en PHP era costumbre distinguir los miembros no públicos de las clases con un guion bajo (ejemplo de Zend Framework). Fue en la época en que ya existía 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 orientarse en el código. “¿Cómo distinguiría en el código las variables públicas de las privadas, eh?”

Hoy nadie usa guiones bajos. Y nadie los echa de menos. El tiempo ha demostrado perfectamente que las preocupaciones eran infundadas.

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

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

Quiero mantener la consistencia

Puedo imaginar que un programador se diga: “Usar prefijos y sufijos es realmente un sinsentido, lo entiendo, pero ya tengo el código estructurado así y el cambio es muy difícil. Y si empezara a escribir el nuevo código correctamente sin ellos, me surgiría una inconsistencia, que es quizás peor que una mala convención.”

En realidad, su código ya es inconsistente ahora, porque usa la biblioteca del sistema de PHP, que no tiene prefijos ni sufijos:

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

Y sinceramente, ¿molesta? ¿Alguna vez ha pensado 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 consistencia no juega un papel tan importante como podría parecer. Por el contrario, el ojo prefiere menos ruido visual, el cerebro la limpieza del diseño. Por lo tanto, ajustar la convención y empezar a escribir nuevas interfaces correctamente sin prefijos y sufijos tiene sentido.

Se pueden eliminar deliberadamente incluso de proyectos grandes. Un ejemplo es Nette Framework, que históricamente usaba prefijos I en los nombres de las interfaces, de lo cual hace unos años comenzó a eliminando gradualmente y con total preservación de la compatibilidad hacia atrás.