Criando uma biblioteca Java amigável ao usuário
1. Visão geral
Java é um dos pilares do mundo de código aberto. Quase todos os projetos Java usam outros projetos de código aberto, já que ninguém quer reinventar a roda. No entanto, muitas vezes acontece que precisamos de uma biblioteca para sua funcionalidade, mas não temos idéia de como usá-la. Nós encontramos coisas como:
-
O que há com todas essas classes "* Service"?
-
Como instanciar isso, são necessárias muitas dependências. O que é “latch“?
-
Oh, eu juntei, mas agora começa a lançarIllegalStateException. O que estou fazendo de errado?
O problema é que nem todos os designers de bibliotecas pensam em seus usuários. A maioria pensa apenas em funcionalidade e recursos, mas poucos consideram como a API será usada na prática e como o código dos usuários será exibido e testado.
Este artigo vem com alguns conselhos sobre como salvar nossos usuários de algumas dessas dificuldades - e não, não é através da escrita de documentação. Certamente, um livro inteiro poderia ser escrito sobre esse assunto (e alguns foram); esses são alguns dos pontos principais que aprendi enquanto trabalhava em várias bibliotecas.
Vou exemplificar as idéias aqui usando duas bibliotecas:charlesejcabi-github
2. Limites
Isso deveria ser óbvio, mas muitas vezes não é. Antes de começar a escrever qualquer linha de código, precisamos ter uma resposta clara para algumas perguntas: que entradas são necessárias? qual é a primeira classe que meu usuário verá? precisamos de alguma implementação do usuário? qual é a saída? Uma vez que essas perguntas são respondidas com clareza, tudo fica mais fácil, pois a biblioteca já possui um forro, uma forma.
2.1. Entrada
Este é talvez o tópico mais importante. Temos que ter certeza de que está claro o que o usuário precisa fornecer à biblioteca para que ela faça seu trabalho. Em alguns casos, isso é uma questão muito trivial: pode ser apenas uma String que representa o token de autenticação de uma API, mas também pode ser uma implementação de uma interface ou uma classe abstrata.
Uma prática muito boa é obter todas as dependências por meio de construtores e mantê-las curtas, com alguns parâmetros. Se precisarmos de um construtor com mais de três ou quatro parâmetros, o código deve ser claramente refatorado. E se os métodos forem usados para injetar dependências obrigatórias, é provável que os usuários acabem com a terceira frustração descrita na visão geral.
Além disso, devemos sempre oferecer mais de um construtor, oferecer alternativas aos usuários. Deixe-os trabalhar comStringeInteger ou não os restrinja aFileInputStream, trabalhe comInputStream, para que eles possam enviar talvezByteArrayInputStream quando teste de unidade etc.
Por exemplo, aqui estão algumas maneiras pelas quais podemos instanciar um ponto de entrada da API do Github usando o jcabi-github:
Github noauth = new RtGithub();
Github basicauth = new RtGithub("username", "password");
Github oauth = new RtGithub("token");
Objetos de configuração simples, sem confusão e sem sombra para inicializar. E faz sentido ter esses três construtores, porque você pode usar o site do Github enquanto está desconectado, conectado ou um aplicativo pode se autenticar em seu nome. Naturalmente, algumas funcionalidades não funcionarão se você não estiver autenticado, mas você sabe disso desde o início.
Como segundo exemplo, aqui está como trabalharíamos com Charles, uma biblioteca de rastreamento da Web:
WebDriver driver = new FirefoxDriver();
Repository repo = new InMemoryRepository();
String indexPage = "http://www.amihaiemil.com/index.html";
WebCrawl graph = new GraphCrawl(
indexPage, driver, new IgnoredPatterns(), repo
);
graph.crawl();
Também é bastante autoexplicativo, eu acredito. No entanto, enquanto escrevo isso, percebo que na versão atual há um erro: todos os construtores exigem que o usuário forneça uma instância deIgnoredPatterns. Por padrão, nenhum padrão deve ser ignorado, mas o usuário não precisa especificar isso. Decidi deixá-lo assim aqui, para que você veja um exemplo contrário. Presumo que você tentaria instanciar um WebCrawl e se perguntaria “O que há com esseIgnoredPatterns ?!”
A variável indexPage é a URL de onde o rastreamento deve iniciar, o driver é o navegador a ser usado (não é possível usar o padrão para nada, pois não sabemos qual navegador está instalado na máquina em execução). A variável repo será explicada abaixo na próxima seção.
Portanto, como você vê nos exemplos, tente mantê-lo simples, intuitivo e auto-explicativo. Encapsule a lógica e as dependências de forma que o usuário não coça a cabeça ao olhar para seus construtores.
Se você ainda tiver dúvidas, tente fazer solicitações HTTP para a AWS usandoaws-sdk-java: você terá que lidar com um chamado AmazonHttpClient, que usa um ClientConfiguration em algum lugar, então precisa levar um ExecutionContext em algum lugar no meio. Finalmente, você pode executar sua solicitação e obter uma resposta, mas ainda não tem idéia do que é um ExecutionContext, por exemplo.
2.2. Resultado
Isso é principalmente para bibliotecas que se comunicam com o mundo exterior. Aqui devemos responder à pergunta “como a saída será tratada?”. Novamente, uma pergunta bastante engraçada, mas é fácil errar.
Veja novamente o código acima. Por que precisamos fornecer uma implementação de repositório? Por que o método WebCrawl.crawl () não retorna apenas uma lista de elementos WebPage? Claramente, não é trabalho da biblioteca lidar com as páginas rastreadas. Como deveria saber o que gostaríamos de fazer com eles? Algo assim:
WebCrawl graph = new GraphCrawl(...);
List pages = graph.crawl();
Nada poderia ser pior. Uma exceção OutOfMemory pode acontecer do nada se o site rastreado tiver, digamos, 1000 páginas - a biblioteca carrega todas elas na memória. Existem duas soluções para isso:
-
Continue retornando as páginas, mas implemente algum mecanismo de paginação no qual o usuário precisaria fornecer os números inicial e final. Or
-
Peça ao usuário para implementar uma interface com um método chamado export (List
), que o algoritmo chamaria toda vez que um número máximo de páginas fosse alcançado
A segunda opção é de longe a melhor; mantém as coisas mais simples dos dois lados e é mais testável. Pense em quanta lógica teria que ser implementada no lado do usuário se fôssemos com o primeiro. Assim, um Repositório para páginas é especificado (para enviá-las em um banco de dados ou gravá-las em disco, talvez) e nada mais precisa ser feito depois de chamar o método crawl ().
A propósito, o código da seção Entrada acima é tudo o que precisamos escrever para obter o conteúdo do site (ainda na memória, como diz a implementação do repo, mas é a nossa escolha - fornecemos essa implementação nós corremos o risco).
Para resumir esta seção: nunca devemos separar completamente o nosso trabalho do trabalho do cliente. Devemos sempre pensar no que acontece com o resultado que criamos. Assim como um motorista de caminhão, deve ajudar a desembalar as mercadorias, em vez de simplesmente jogá-las fora na chegada ao destino.
3. Interfaces
Sempre use interfaces. O usuário deve interagir com nosso código apenas através de contratos estritos.
Por exemplo, na bibliotecajcabi-github, a classe RtGithub si é a única que o usuário realmente vê:
Repo repo = new RtGithub("oauth_token").repos().get(
new Coordinates.Simple("eugenp/tutorials"));
Issue issue = repo.issues()
.create("Example issue", "Created with jcabi-github");
O snippet acima cria um tíquete noeugenp/tutorials repo. Instâncias de repositório e problema são usadas, mas os tipos reais nunca são revelados. Não podemos fazer algo assim:
Repo repo = new RtRepo(...)
O exposto acima não é possível por um motivo lógico: não podemos criar um problema diretamente em um repositório do Github, podemos? Primeiro, precisamos fazer o login, pesquisar no repositório e só então podemos criar um problema. Claro, o cenário acima poderia ser permitido, mas então o código do usuário ficaria poluído com muito código clichê: aqueleRtRepo provavelmente teria que pegar algum tipo de objeto de autorização através de seu construtor, autorizar o cliente e obter para o repo certo etc.
As interfaces também oferecem facilidade de extensibilidade e compatibilidade com versões anteriores. Por um lado, nós, como desenvolvedores, devemos respeitar os contratos já liberados e, por outro, o usuário pode estender as interfaces que oferecemos - ele pode decorá-las ou escrever implementações alternativas.
Em outras palavras, abstraia e encapsule o máximo possível. Ao usar interfaces, podemos fazer isso de maneira elegante e não restritiva - reforçamos as regras de arquitetura, dando ao programador liberdade para aprimorar ou alterar o comportamento que expomos.
Para finalizar esta seção, lembre-se: nossa biblioteca, nossas regras. Devemos saber exatamente como o código do cliente vai ficar e como ele vai fazer o teste de unidade. Se não soubermos disso, ninguém e nossa biblioteca simplesmente contribuirão na criação de código difícil de entender e manter.
4. Terceiros
Lembre-se de que uma boa biblioteca é uma biblioteca leve. Seu código pode resolver um problema e ser funcional, mas se o jar adicionar 10 MB à minha construção, então fica claro que você perdeu os planos do seu projeto há muito tempo. Se você precisar de muitas dependências, provavelmente está tentando cobrir muita funcionalidade e deve dividir o projeto em vários projetos menores.
Seja o mais transparente possível, sempre que possível, não se vincule a implementações reais. O melhor exemplo que vem à mente é: use SLF4J, que é apenas uma API para registro - não use log4j diretamente, talvez o usuário queira usar outros registradores.
Bibliotecas de documentos que passam por seu projeto transitivamente e certifique-se de não incluir dependências perigosas, comoxalan ouxml-apis (por que elas são perigosas não cabe a este artigo detalhar).
O ponto principal aqui é: mantenha sua construção leve, transparente e sempre saiba com o que está trabalhando. Isso poderia poupar mais agitação aos usuários do que você imagina.
5. Conclusão
O artigo descreve algumas idéias simples que podem ajudar um projeto a permanecer em linha com relação à usabilidade. Uma biblioteca, sendo um componente que deve encontrar seu lugar em um contexto maior, deve ter uma funcionalidade poderosa, mas oferecer uma interface suave e bem criada.
É um passo fácil, além de prejudicar o design. Os colaboradores sempre saberão como usá-lo, mas alguém novo que primeiro põe os olhos nele pode não. A produtividade é a mais importante de todas e, seguindo esse princípio, os usuários devem poder começar a usar uma biblioteca em questão de minutos.