Los prefijos y sufijos no pertenecen a los nombres de interfaz
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?
- Hacer que el nombre
Translator
la interfaz - Renombra la clase original a
DatabaseTranslator
y se implementaráTranslator
- Y creas nuevas clases
GettextTranslator
y tal vezNeonTranslator
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.
Para enviar un comentario, inicie sesión