API Unit: Tudo num só lugar


Uma única técnica que resolve muitos problemas.

Unsplash image Photo by NordWood Themes on Unsplash

Introdução

Quando você utiliza uma Classe muitas vezes, já deve ter memorizado em qual Unit a Classe foi implementada.

Mas e se Library (Lib) que você está utilizando é nova ou foi completamente remodelada?

E se você está tentando fazer um trabalho que nunca fez antes?

Como saber as Classes existentes na Lib?

Há basicamente 3 opções:

  1. Abrir todas as Units da Lib e ler o código;
  2. Ler a documentação, se houver;
  3. Abrir apenas uma Unit que contenha todas as Interfaces e Classes;

Esse artigo é sobre a última opção, que eu chamo de “API Unit”.

API Unit

API Unit é um conceito em fase de experimentação.

A ideia é simples: Disponibilizar ao desenvolvedor, todas as Interfaces e Classes de uso público em apenas 1 Unit.

Não estou dizendo que deveríamos codificar tudo num único arquivo. Isso seria insano. Mas se houvesse uma maneira de continuarmos codificando em módulos mas, ao mesmo tempo, termos tudo disponível para uso em apenas 1 única Unit, não acha que seria interessante?

Já vi projetos onde os autores utilizavam arquivos *.inc para ir montando a “verdadeira” Unit — o FPC e Lazarus utilizam essa técnica — mas eu acho confuso de administrar, além de dificultar a leitura do código.

Não fui por esse caminho e continuei pensando…

Então, tive uma ideia que chamei de API Unit, meio que “por acaso”, após a publicação desse artigo. Eu estava pensando em outras possibilidades para nomear Interfaces e Classes, algo que já havia explicado em outro artigo.

Por quê?

Bem, a técnica de nomenclatura que expliquei anteriormente continua válida, porém pode haver Libs mais complexas com nomes de Classes bem especializadas. Esses nomes tem a tendência de serem maiores e adicionar o nome da Lib como prefixo — conforme explicado no artigo — deixando o nome da Classe ainda mais verboso.

A linguagem Object Pascal não tem uma sintaxe para “renomear” uma Unit dentro do código. Algo simples de se fazer em linguagens como Python, C# e Lua, por exemplo. Nessas linguagens, após o “import” do arquivo, o desenvolvedor pode escolher um alias para a “unidade”, ou seja, ele pode renomear dinamicamente o arquivo que irá conter as Interfaces, Classes e todo o resto.

Eu mesmo já solicitei essa feature para os desenvolvedores do Free Pascal, porém o pedido foi ignorado devido haver outras prioridades.

Como isso não é possível (hoje), voltei para os meus pensamentos.

Seria possível utilizar a nomenclatura mais simples (nomes curtos para Units, Interfaces e Classes) sem haver colisão de nomes?

Meus requisitos foram:

  1. Classes com nomes curtos e simples;
  2. Havendo colisão de nomes entre Classes, a Unit seria utilizada como prefixo;
  3. Units devem ter o nome mais curto possível, porém legível e sem ambiguidades;
  4. O usuário da Lib deveria ter o controle da nomenclatura utilizada em seus sistemas;

Vamos analisar cada item:

Classes com nomes curtos e simples deveria ser a norma utilizada em todos os sistemas. Mais simples de ler, mas fácil de entender.

Ao invés de escrevermos TAcCar ou TAcmeCar, iríamos utilizar apenas TCar. Mas, conforme explicado no artigo sobre nomenclatura, há grandes possibilidades de haver uma colisão de nomes caso duas ou mais unidades tenham utilizado o mesmo identificador TCar e isso nos leva ao segundo requisito.

Havendo colisão de nomes entre Classes, a Unit seria utilizada como prefixo e o problema estaria resolvido. No entanto, é muito verboso declarar um método assim:

function TDriver.Drive(
  ACar: Acme.Interfaces.Cars.ICar): IDriver;

O argumento ACar é um tipo de Interface declarado em alguma Lib externa chamada Acme. E foi preciso prefixar a Interface ICar por que o seu projeto, hipoteticamente, também tem uma interface com o mesmo nome em outra Unit.

Imagine agora esse mesmo método com mais 4 argumentos, sendo cada um deles prefixado com a Unit. O método ficaria muito mais verboso e, talvez, difícil de entender.

Units devem ter o nome mais curto possível, porém legível e sem ambiguidades mas a linguagem não nos dá essa opção. Se fosse possível criar um alias no Object Pascal poderíamos fazer isso:

uses
  Acme.Interfaces.Cars.ICar as Acme;

function TDriver.Drive(ACar: Acme.ICar): IDriver;

Nesse ponto você pode estar pensando que voltamos a utilizar a mesma técnica anterior, ou seja, o nome da Lib é o prefixo.

Ora, afinal o identificador Acme.ICar não é tão diferente de IAcmeCar, não é?

Sim, é verdade, porém com o uso (hipotético) de alias é o usuário que tem o controle de qual nome utilizar como prefixo em todo o projeto. Algo que lhe dá liberdade, fazendo muita diferença.

O usuário da Lib deveria ter o controle da nomenclatura utilizada em seus sistemas e não deixar que a Lib “imponha” um nome que, muitas vezes, é ambíguo, mal escolhido, confuso, verboso ou completamente sem sentido.

É claro que identificadores fixos como IAcmeCar podem ser redeclarados, porém se houver muitas Classes e Interfaces, poderia ser uma tarefa bem dispendiosa.

Então, o uso de alias na linguagem Object Pascal seria uma solução inovadora, porém não a temos.

E é aí que entra a ideia da API Unit.

Se todos os identificadores estivessem declarados em apenas uma única Unit, representando toda a Lib, poderíamos simular um alias utilizando um arquivo físico.

Veja aqui um bom exemplo do que é uma API Unit.

No link acima podemos ver que todas as Interfaces e Classes da Lib estão declaradas (apenas um alias para os identificadores em outras Unit ) em apenas uma única Unit e isso nos dá grandes possibilidades que irei explicar mais adiante.

No entanto, sempre haverá vantagens de desvantagens.

Desvantagens

Vou começar pelas desvantagens, pois as vantagens são bem maiores.

Você irá deixar o código do desenvolvedor da Lib um pouquinho mais verboso por que, para cada Interface ou Classe existente, você terá que declarar um alias.

Por exemplo, veja nesse link que a Classe TDataStream tem um alias declarado como TDataStreamAlias = TDataStream;.

A princípio, o identificador TDataStreamAlias não faz sentido para a Unit na qual ele foi declarado, mas será importante para montarmos a API Unit.

Todas as Interfaces e Classes para os usuários devem estar numa única Unit e isso é meio chato e talvez um pouco trabalhoso de se fazer.

O desenvolvedor terá um pequeno retrabalho adicionando as Interfaces e Classes de outras Unit na Unit de API. O desenvolvedor poderia até esquecer de fazer isso. No entanto se a Lib possuir testes automatizados, não haverá esquecimento, pois os próprios testes irão utilizar apenas a API Unit e para testar uma Classe ela deve estar declarada lá.

Vantagens

Agora vamos falar um pouco sobre as vantagens em utilizarmos essa simples técnica.

Se toda a Lib pode ser utilizada através de apenas 1 Unit você pode criar um alias apenas renomeando o arquivo original.

Por exemplo. Se a Lib Acme do exemplo acima tivesse uma API Unit ela poderia se chamar Acme.API.pas. Mas se você não gostou do nome, se ele ainda é verboso ou se você já tem uma Unit com esse nome no seu projeto, bastaria renomear a Unit para, digamos, LibAcme.pas.

Então, qualquer colisão de nome entre Interfaces e Classes poderia ser resolvida apenas adicionando o “novo” nome da Lib como, por exemplo, LibAcme.ICar. Simples, curto, fácil de entender e totalmente sob o controle do usuário da Lib.

Se o desenvolvedor da Lib quiser ser ainda mais “legal” com o usuário, ele poderia criar um .inc com todos os identificadores e declarar a Acme.API assim:

unit Acme.API;

{$include api.inc}

end.

Então o usuário nem mesmo precisaria renomear o arquivo original. Bastaria criar um novo arquivo, por exemplo LibAcme.pas, no mesmo diretório e incluir a api.inc no corpo da nova Unit.

Como é fácil renomear a Unit que contém a API o desenvolvedor não precisaria mais se preocupar em adicionar prefixos de 2~3 letras em Interfaces e Classes afim de diminuir a colisão de nomes nos projetos que utilizam a Lib. Qualquer nome simples pode ser utilizado, pois qualquer conflito será fácil de resolver adicionando a Unit quando for necessário.

O usuário não precisa memorizar as Units da Lib pois tudo está num único só lugar. Simples e conveniente.

O desenvolvedor da Lib poderia incluir mais Interfaces e Classes porém apenas para o uso “privado” da Lib sem que o usuário precise saber da existência desses novos artefatos. O usuário já estaria acostumado a utilizar apenas uma única Unit, então ele não iria ter “acesso” a outros identificadores que não estão listados lá. É claro que ele ainda poderia utilizar as Units reais da Lib, porém isso poderia ser informado na documentação que isso seria considerado um “hack” ou quebra de encapsulamento e o usuário estaria por conta própria.

Ao definir um alias para cada Interface ou Classe — exemplo TDataStreamAlias— não seria necessário redeclarar os identificadores na API utilizando as Units reais como prefixo.

Compare as duas opções abaixo:

unit James.API;

type
  {1} TDataStream = TDataStreamAlias;
  {2} TDataStream = James.Data.Clss.TDataStream;

Com certeza a primeira linha e menos verbosa.

Esse é o motivo de haver esses identificadores que são apenas alias. Além de ser menos verboso, o desenvolvedor poderia mover a TDataStream para outra Unit e não precisaria alterar a API.

Quando a API começar a ficar grande demais será um sinal — ou um “cheiro” — de que a Lib pode estar fazendo coisas demais. Então a API Unit é, também, uma restrição. Mas isso é bom. Menos é mais, lembre-se disso.

Conclusão

Esse artigo tentou demostrar que utilizando apenas uma única técnica simples e um pouco mais trabalho, é possível melhorar muito a qualidade do código, resolvendo problemas de escopo, nomenclatura e legibilidade.

Estou utilizando API Unit em alguns projetos privados e Open Sources (aqui e aqui) e a legibilidade tem melhorado.

Penso em utilizar o mesmo conceito para delimitar API’s (módulos) em grandes sistemas, mas a referência circular entre Units pode ser um problema. Então o design do projeto tem que levar isso em conta desde o início.

Se vai valer a pena… o tempo dirá.

Até logo.

Posts Relacionados

  • Eliminando Métodos Privados

  • Classes Aninhadas

  • Injeção de Dependência sem XML, Atributos/Anotações ou Frameworks

  • Nomeando Classes em Libraries

  • Versionando e Organizando seus Pacotes

  • Xavier Package

  • Inter-process Communication

  • Porquê eu escolhi Delphi e então, Object Pascal

  • Redefinindo Classes

  • Git-work Project