Prefixos e sufixos não pertencem a nomes de interface

há 2 anos De David Grudl  

O uso do sufixo I prefix or Interface para interfaces, e Abstract para classes abstratas, é um antipadrão. Ele não pertence em código puro. Distinguir nomes de interfaces na verdade embaça os princípios OOP, acrescenta ruído ao código e causa complicações durante o desenvolvimento. Aqui estão as razões.

Tipo = Classe + Interface + Descendentes

No mundo OOP, tanto as classes como as interfaces são consideradas tipos. Se eu usar um tipo ao declarar uma propriedade ou um parâmetro, da perspectiva do desenvolvedor, não há diferença se o tipo em que eles estão confiando é uma classe ou uma interface. Isso é o mais legal que torna as interfaces tão úteis, na verdade. É o que dá significado a sua existência. (A sério: para que seriam as interfaces se este princípio não fosse aplicado? Tente pensar sobre isso).

Dê uma olhada neste código:

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

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

Eu realmente não me importo? Na verdade, não, admito que estou bastante curioso, então quando estou explorando a biblioteca de outra pessoa, dou uma olhada no que está por trás do tipo e fico pairando sobre ela:

Ah, estou vendo. E é o fim da história. Saber se é uma classe ou uma interface seria importante se eu quisesse criar uma instância dela, mas não é o caso, estou apenas falando do tipo da variável agora. E é aqui que eu quero ficar fora desses detalhes de implementação. E certamente não os quero embutidos em meu código! O que está por trás de um tipo é parte de sua definição, não o tipo em si.

Agora olhe para o outro código:

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

Esta definição de construtor diz literalmente: "Preciso de uma forma abstracta e uma interface de tradutor " Mas isso é tolice. Precisa de uma forma concreta para renderizar. Não uma forma abstrata. E precisa de um objeto que aja como um tradutor. Ele não precisa de uma interface.

Você sabe que as palavras Interface e Abstract devem ser ignoradas. Que o construtor quer a mesma coisa que no exemplo anterior. Mas… realmente? Parece realmente uma boa idéia introduzir nas convenções de nomenclatura o uso de palavras a serem ignoradas?

Afinal, ela cria uma falsa idéia dos princípios do OOP. Um principiante deve ser confundido: “Se o tipo Translator significa ou 1) um objeto da classe Translator 2) um objeto implementando a interface Translator ou 3) um objeto herdando deles, então o que se entende por TranslatorInterface?” Não há uma resposta razoável a isto.

Quando utilizamos TranslatorInterface, mesmo que Translator possa ser uma interface, estamos cometendo uma tautologia. O mesmo acontece quando declaramos interface TranslatorInterface. Gradualmente, uma piada de programação vai acontecer:

interface TranslatorInterface
{
}

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

Implantação extraordinária

Quando eu vejo algo como TranslatorInterface, é provável que haja uma implementação chamada Translator implements TranslatorInterface. Isso me faz pensar: o que torna Translator tão especial a ponto de ter o privilégio único de ser chamado de Tradutor? Qualquer outra implementação precisa de um nome descritivo, como GettextTranslator ou DatabaseTranslator, mas este é uma espécie de “padrão”, como sugere seu status preferido de ser chamado de Translator sem o rótulo.

Mesmo as pessoas ficam confusas e não sabem se devem digitar Translator ou TranslatorInterface. Então ambos se confundem no código do cliente, tenho certeza que você já se deparou com isso muitas vezes (em Nette, por exemplo, na conexão com Nette\Http\Request vs IRequest).

Não seria melhor se livrar da implementação especial e manter o nome genérico Translator para a interface? Ou seja, ter implementações específicas com um nome específico + uma interface genérica com um nome genérico. Isso faz sentido.

O ônus de um nome descritivo recai então puramente sobre as implementações. Se renomearmos TranslatorInterface para Translator, nossa antiga classe Translator precisa de um novo nome. As pessoas tendem a resolver este problema chamando-o DefaultTranslator, até eu sou culpado disso. Mas novamente, o que a torna tão especial que é chamada de Default? Não seja preguiçoso e pense bem no que ele faz e por que é diferente de outras implementações possíveis.

E se eu não puder imaginar várias implementações possíveis? E se eu só puder pensar em uma maneira válida? Então simplesmente não crie a interface. Pelo menos por enquanto.

Eh, há outra implementação

E está aqui! Precisamos de uma segunda implementação. Isso acontece o tempo todo. Nunca houve necessidade de armazenar traduções de mais de uma forma comprovada, por exemplo, em um banco de dados, mas agora há uma nova exigência e precisamos ter mais tradutores na aplicação.

Isto também é quando você percebe claramente quais eram as especificidades do tradutor único original. Era um tradutor de banco de dados, sem default.

O que tem?

  1. Faça do nome Translator a interface
  2. Renomeie a classe original para DatabaseTranslator e implementará Translator
  3. E você cria novas classes GettextTranslator e talvez NeonTranslator

Todas estas mudanças são muito convenientes e fáceis de fazer, especialmente se a aplicação for construída de acordo com os princípios da injeção de dependência. **Não há necessidade de alterar nada no código***, basta alterar Translator para DatabaseTranslator na configuração do recipiente DI. Isso é ótimo!

Entretanto, uma situação diametralmente diferente surgiria se insistíssemos em prefixar/sufixar. Teríamos que renomear os tipos de Translator para TranslatorInterface no código em toda a aplicação. Tal renomeamento seria puramente para fins de convenção, mas iria contra o espírito do OOP, como acabamos de demonstrar. A interface não mudou, o código do usuário não mudou, mas a convenção requer renomeação? Então é uma convenção com falhas.

Além disso, se com o tempo se revelasse que uma classe abstrata seria melhor do que a interface, nós renomearíamos novamente. Tal ação pode não ser nada trivial, por exemplo, quando o código é espalhado por vários pacotes ou utilizado por terceiros.

Mas todo mundo faz isso

Nem todos o fazem. É verdade que no mundo PHP, o Zend Framework, seguido pelo Symfony, os grandes jogadores, popularizou a interface de distinção e nomes de classes abstratos. Esta abordagem foi adotada pelo PSR, que ironicamente publica apenas interfaces, mas inclui a palavra interface no nome de cada uma delas.

Por outro lado, outra grande estrutura, Laravel, não distingue interfaces e classes abstratas. Mesmo a popular camada de banco de dados Doctrine não faz isto, por exemplo. E nem a biblioteca padrão em PHP (portanto, temos as interfaces Throwable ou Iterator, a classe abstrata FilterIterator, etc.).

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

Portanto, nem todos o fazem, mas mesmo que o fizessem, isso não significa que seja uma coisa boa. Não é prudente adotar sem sentido o que os outros estão fazendo, porque você pode estar adotando erros também. Erros dos quais o outro preferiria se livrar, é apenas uma mordida grande demais hoje em dia.

Eu não sei no Código o que é a interface

Muitos programadores argumentarão que os prefixos/sufixos são úteis para eles porque lhes permitem saber quais interfaces estão imediatamente no código. Eles sentem que perderiam tal distinção. Então, vejamos, você pode dizer o que é uma classe e o que é uma interface nestes exemplos?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X is always a class, Y é uma interface, é inequívoca mesmo sem prefixos/postfixos. É claro que a IDE também sabe disso e sempre lhe dará a dica correta em um determinado contexto.

Mas e aqui?

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

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

Nesses casos, você não saberá. Como dissemos logo no início, do ponto de vista de um desenvolvedor não deve haver diferença entre o que é uma classe e o que é uma interface. Que é o que dá sentido às interfaces e às classes abstratas.

Se você fosse capaz de distinguir entre uma classe e uma interface aqui, isso negaria o princípio básico do OOP. E as interfaces se tornariam inúteis.

Estou acostumado a isso

A mudança de hábitos só dói 🙂 Muitas vezes a idéia de que dói. Não vamos culpar as pessoas que se sentem atraídas pelas mudanças e esperam ansiosamente por elas, pois a maioria das vezes, como se aplica o provérbio tcheco, esse hábito é uma camisa de ferro.

Mas basta olhar para o passado, como alguns hábitos foram varridos pelo tempo. Talvez o mais famoso seja a chamada notação húngara utilizada desde os anos oitenta e popularizada pela Microsoft. A notação consistia em iniciar o nome de cada variável com uma abreviação que simbolizava seu tipo de dado. Em PHP ela se pareceria com echo $this->strName ou $this->intCount++. A notação húngara começou a ser abandonada nos anos 90, e hoje as diretrizes da Microsoft desencorajam diretamente os desenvolvedores de usá-la.

Costumava ser uma característica essencial e hoje ninguém sente falta dela.

Mas por que ir a tão longo tempo atrás? Você deve se lembrar que em PHP, era costume distinguir os membros não públicos de classes com um sublinhado (amostra da Zend Framework). Isto era quando havia o PHP 5, que tinha modificadores de visibilidade pública/protegida/privada. Mas os programadores o faziam por hábito. Eles estavam convencidos de que sem sublinhados deixariam de entender o código. “Como eu distinguiria as propriedades públicas das privadas no código, huh?”

Hoje ninguém usa underscores. E ninguém sente falta deles. O tempo provou perfeitamente que os medos eram falsos.

Mas é exatamente o mesmo que a objeção, "Como eu distinguiria uma interface de uma classe em código, hein?

Eu parei de usar prefixos/pós-fixos há dez anos. Eu jamais voltaria atrás, foi uma grande decisão. Eu também não conheço nenhum outro programador que queira voltar. Como disse um amigo: “Tente e em um mês você não vai entender que alguma vez o fez de maneira diferente”.

Quero Manter a Consistência

Posso imaginar um programador dizendo: “Usar prefixos e sufixos é realmente um absurdo, eu entendo, é que já tenho meu código construído dessa maneira e mudá-lo é muito difícil. E se eu começar a escrever um novo código corretamente sem eles, acabarei com uma inconsistência, o que talvez seja ainda pior do que uma má convenção”.

Na verdade, seu código já é inconsistente porque você está usando a biblioteca do sistema PHP:

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

E de coração, será que isso importa? Você já pensou que isso seria mais consistente?

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 coerência não desempenha um papel tão grande quanto poderia parecer. Pelo contrário, o olho prefere menos ruído visual, o cérebro prefere a clareza do design. Portanto, ajustar a convenção e começar a escrever novas interfaces corretamente sem prefixos e sufixos, faz sentido.

Eles podem ser removidos propositalmente mesmo de grandes projetos. Um exemplo é o Nette Framework, que historicamente utilizava prefixos I em nomes de interface, que começou a ser eliminado gradualmente há alguns anos, mantendo total retrocompatibilidade.