Prefixos e sufixos não pertencem aos nomes de interface

há 3 anos De David Grudl  

O uso do prefixo I ou do sufixo Interface em interfaces, bem como Abstract em classes abstratas, é um antipadrão. Não tem lugar em código limpo. A diferenciação dos nomes de interface, na verdade, obscurece os princípios da OOP, introduz ruído no código e causa complicações durante o desenvolvimento. As razões são as seguintes.

Tipo = classe + interface + descendentes

No mundo da OOP, tanto classes quanto interfaces são consideradas tipos. Se eu usar um tipo na declaração de uma propriedade ou parâmetro, do ponto de vista do desenvolvedor, não há diferença se o tipo em que ele se baseia é uma classe ou uma interface. Isso é ótimo, e é o que torna as interfaces tão úteis. Isso dá sentido à sua existência. (Sério: para que serviriam as interfaces se este princípio não fosse válido? Tente pensar sobre isso.)

Veja este código:

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

O construtor diz: “Preciso de um formulário e um tradutor.” E não importa se ele recebe um objeto GettextTranslator ou DatabaseTranslator. E, ao mesmo tempo, como usuário, não me importa se Translator é uma interface, uma classe abstrata ou uma classe concreta.

Não me importa mesmo? Na verdade não, confesso que sou bastante curioso, então quando estou explorando uma biblioteca de terceiros, dou uma olhada no que está por trás do tipo e passo o mouse sobre ele:

Ah, agora eu sei. E isso termina aqui. O conhecimento se é uma classe ou interface seria importante se eu quisesse criar sua instância, mas não é o caso, agora estou apenas falando sobre o tipo da variável. E aqui quero estar protegido desses detalhes. E certamente não quero introduzi-los no meu código! O que está por trás do tipo faz parte de sua definição, não do tipo em si.

E agora veja o próximo código:

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

Esta definição do construtor diz literalmente: “Preciso de um formulário abstrato e uma interface de tradutor.” Mas isso é um disparate. Ele precisa de um formulário concreto para renderizar. Não um formulário abstrato. E precisa de um objeto que desempenhe o papel de tradutor. Não precisa de uma interface.

Você sabe que as palavras Interface e Abstract devem ser ignoradas. Que o construtor quer o mesmo que no exemplo anterior. Mas… sério? Você realmente acha uma boa ideia introduzir o uso de palavras em suas convenções de nomenclatura que devem ser ignoradas?

Isso cria uma falsa impressão sobre os princípios da OOP. Um iniciante deve ficar confuso: “Se o tipo Translator significa 1) um objeto da classe Translator, 2) um objeto que implementa a interface Translator ou 3) um objeto que herda deles, o que então significa TranslatorInterface?” Não há resposta razoável para isso.

Quando escrevemos TranslatorInterface, embora Translator também possa ser uma interface, cometemos uma tautologia. O mesmo quando declaramos interface TranslatorInterface. E assim por diante. Até que surja uma piada de programador:

interface TranslatorInterface
{
}

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

Implementação excepcional

Quando vejo algo como TranslatorInterface, é provável que também exista uma implementação com o nome Translator implements TranslatorInterface. Isso me faz pensar: o que torna Translator tão especial a ponto de ter o direito exclusivo de se chamar Translator? Qualquer outra implementação precisa de um nome descritivo, por exemplo, GettextTranslator ou DatabaseTranslator, mas esta é de alguma forma “padrão”, como sugere sua posição privilegiada, ao se chamar Translator sem adjetivo.

Isso até deixa as pessoas inseguras e sem saber se devem escrever o typehint para Translator ou TranslatorInterface. No código cliente, ambos acabam se misturando, certamente você já se deparou com isso muitas vezes (no Nette, por exemplo, em conexão com Nette\Http\Request vs IRequest).

Não seria melhor se livrar da implementação excepcional e manter o nome genérico Translator para a interface? Ou seja, ter implementações concretas com nomes concretos + uma interface genérica com um nome genérico. Isso faz sentido, afinal.

O fardo do nome descritivo então recai puramente sobre as implementações. Se renomearmos TranslatorInterface para Translator, nossa antiga classe Translator precisa de um novo nome. As pessoas tendem a resolver esse problema chamando-a de DefaultTranslator, eu também sou culpado disso. Mas, novamente, o que a torna tão excepcional a ponto de se chamar Default? Não seja preguiçoso e pense cuidadosamente sobre o que ela faz e por que difere de outras implementações possíveis.

E se eu não conseguir imaginar múltiplas implementações? E se apenas uma maneira válida me ocorrer? Então simplesmente não crie a interface. Pelo menos por enquanto.

Eis que surge outra implementação

E aqui está! Precisamos de uma segunda implementação. Isso acontece comumente. Nunca houve a necessidade de armazenar traduções de outra forma senão uma comprovada, por exemplo, em um banco de dados, mas agora surgiu um novo requisito e é necessário ter mais tradutores na aplicação.

Este também é o momento em que você percebe claramente qual era a especificidade do tradutor único original. Era um tradutor de banco de dados, nenhum padrão.

O que fazer?

  1. Transformamos o nome Translator em uma interface
  2. Renomeamos a classe original para DatabaseTranslator e ela implementará Translator
  3. E criamos novas classes GettextTranslator e talvez NeonTranslator

Todas essas mudanças são feitas de forma muito confortável e fácil, especialmente se a aplicação for construída de acordo com os princípios da injeção de dependência. Não é necessário alterar nada no código, apenas na configuração do contêiner de DI, mudamos Translator para DatabaseTranslator. Isso é ótimo!

Uma situação diametralmente diferente ocorreria, no entanto, se insistíssemos em prefixar/sufixar. Teríamos que renomear os tipos de Translator para TranslatorInterface em todo o código da aplicação. Tal renomeação seria puramente proposital para cumprir a convenção, mas iria contra o sentido da OOP, como mostramos há pouco. A interface não mudou, o código do usuário não mudou, mas a convenção exige renomear? Então é uma convenção errada.

Além disso, se com o tempo se mostrasse que uma classe abstrata seria melhor do que uma interface, renomearíamos novamente. Tal intervenção pode não ser trivial, por exemplo, se o código estiver distribuído em vários pacotes ou for usado por terceiros.

Mas todo mundo faz assim

Nem todo mundo. É verdade que no mundo PHP, a diferenciação de nomes de interfaces e classes abstratas foi popularizada pelo Zend Framework e depois pelo Symfony, ou seja, grandes players. Essa abordagem foi adotada também pela PSR, que paradoxalmente publica apenas interfaces e, ainda assim, inclui a palavra interface no nome de cada uma.

Por outro lado, outro framework significativo, o Laravel, não diferencia interfaces e classes abstratas de forma alguma. Nem a popular camada de banco de dados Doctrine faz isso. E nem a biblioteca padrão do PHP faz isso (temos interfaces Throwable ou Iterator, classe abstrata FilterIterator, etc.).

Se olharmos para o mundo fora do PHP, por exemplo, C# usa o prefixo I para interfaces, enquanto em Java ou TypeScript os nomes não são diferenciados.

Portanto, nem todo mundo faz isso, mas mesmo que fizessem, não significa que seja bom. Adotar sem pensar o que os outros fazem não é sensato, porque você pode adotar também os erros. Erros dos quais o outro talvez gostaria muito de se livrar, mas é muito difícil.

Não consigo distinguir no código o que é uma interface

Vários programadores argumentarão que os prefixos/sufixos são úteis para eles, pois graças a eles reconhecem imediatamente no código o que são interfaces. Eles sentem que sentiriam falta dessa distinção. Vamos ver, você consegue distinguir nestes exemplos o que é classe e o que é interface?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X é sempre uma classe, Y é uma interface, é inequívoco mesmo sem prefixos/postfixos. Claro, o IDE também sabe disso e no contexto dado sempre fornecerá as sugestões corretas.

Mas e aqui:

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

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

Nestes casos, você não consegue distinguir. Como dissemos no início, aqui não deve haver diferença do ponto de vista do desenvolvedor entre o que é uma classe e o que é uma interface. O que justamente dá sentido às interfaces e classes abstratas.

Se você fosse capaz de distinguir classe de interface aqui, isso negaria um princípio fundamental da OOP. E as interfaces perderiam o sentido.

Estou acostumado com isso

Mudar hábitos simplesmente dói 🙂 Às vezes, até a ideia. Mas, para não sermos injustos, muitas pessoas, pelo contrário, são atraídas por mudanças e as aguardam com expectativa, mas para a maioria, o hábito é uma camisa de ferro.

Mas basta olhar para o passado, como alguns hábitos foram levados pelo tempo. Provavelmente a mais famosa é a chamada notação húngara usada desde os anos oitenta e popularizada pela Microsoft. A notação consistia em que o nome de cada variável começasse com uma abreviação simbolizando seu tipo de dados. Em PHP, ficaria assim echo $this->strName ou $this->intCount++. A notação húngara começou a ser abandonada nos anos noventa e hoje a Microsoft em suas diretrizes desencoraja diretamente os desenvolvedores de usá-la.

Antigamente era indispensável e hoje ninguém sente falta.

Mas por que ir tão longe no passado? Talvez você se lembre que em PHP era costume distinguir membros não públicos de classes com um sublinhado (exemplo do Zend Framework). Isso foi na época em que o PHP 5 já existia há muito tempo, com modificadores de visibilidade public/protected/private. Mas os programadores faziam isso por hábito. Estavam convencidos de que sem os sublinhados deixariam de se orientar no código. “Como eu distinguiria no código variáveis públicas de privadas, hein?”

Hoje ninguém usa sublinhados. E ninguém sente falta deles. O tempo provou brilhantemente que os receios eram infundados.

No entanto, é exatamente o mesmo que a objeção: “Como eu distinguiria no código interfaces de classes, hein?”

Eu parei de usar prefixos/postfixos há dez anos. Nunca voltaria atrás, foi uma ótima decisão. Também não conheço nenhum outro programador que gostaria de voltar. Como disse um amigo: “Experimente e em um mês você não entenderá como um dia fez diferente.”

Quero manter a consistência

Consigo imaginar um programador dizendo: “Usar prefixos e sufixos é realmente um disparate, eu entendo, mas já tenho meu código construído assim e a mudança é muito difícil. E se eu começasse a escrever o novo código corretamente sem eles, criaria uma inconsistência, que talvez seja ainda pior do que uma convenção ruim.”

Na verdade, seu código já é inconsistente agora, porque você usa a biblioteca do sistema PHP, que não tem prefixos nem postfixos:

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

E, sinceramente, isso incomoda? Já lhe ocorreu que seria mais consistente assim?

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

Ou isto?

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

Acho que não. A consistência não desempenha um papel tão significativo quanto poderia parecer. Pelo contrário, o olho prefere menos ruído visual, o cérebro a clareza do design. Portanto, ajustar a convenção e começar a escrever novas interfaces corretamente sem prefixos e sufixos faz sentido.

É possível removê-los propositalmente mesmo de grandes projetos. Um exemplo é o Nette Framework, que historicamente usava prefixos I nos nomes das interfaces, dos quais começou a se livrar gradualmente e com total preservação da compatibilidade retroativa há alguns anos.