Desenvolvimento

25 set, 2014

Por que Adapters e Facades são sensacionais

Publicidade

Este artigo é sobre uma grande ideia que implementei no trabalho que envolveu os padrões Adapter e Facade. Depois de usá-los da maneira como os utilizei, tenho um respeito muito mais profundo por eles e quero que vocês compartilhem esse respeito, então vamos dar uma olhada em algumas maneiras sensacionais de usá-los e boas razões pra isso!

As armadilhas do PouchDB

Tudo começou quando eu fui parar num projeto em que estávamos criando um aplicativo web offline (que já tinha sido 90% desenvolvido no momento em que fui alocado) e tentávamos resolver alguns problemas de desempenho relacionados a armazenamento/recuperação/manipulação de dados locais, os quais tinham sido armazenados no IndexedDB utilizando uma biblioteca chamada PouchDB. Bem no meio do processo de debug dos problemas, cheguei à conclusão de que eu odeio o PouchDB. Não me entendam mal: não é uma biblioteca horrível (apesar de o seu objetivo ser imitar o CouchDB para o frontend), mas ela tem alguns aspectos com os quais tenho dificuldade de lidar:

  • Inferno dos callbacks: Tudo era assíncrono, e o PouchDB lida com isso por meio de callbacks (as versões mais recentes implementam promises, mas não sabíamos disso; além do mais, isso exigiria um monte de testes de regressão para nos assegurarmos de que não havia nenhuma alteração significativa no PouchDB). Não demorou muito para nos depararmos com callbacks aninhados em vários níveis de profundidade, pois temos dados hierárquicos que usam IDs para se referir a objetos pais/filhos, então acabamos fazendo chamadas semi-recursivas em tudo quanto é lugar.
  • A API é feia: Não estamos falando sobre o protocolo HTTP, então quando vejo dois métodos diferentes para salvar dados com os nomes de put e post, fico meio frustrado (eles fizeram isso para imitar o CouchDB). E o método para deletar algo se chama remove em vez de delete. A API não parece muito consistente, e não é nada conveniente ter dois métodos diferentes para salvar. Cada operação também enviava um objeto err e um objeto response para o callback, então a gente sempre tinha que colocar if (! err)… dentro de cada callback. O objeto response também era um pouco inconsistente naquilo que continha. Também considero o uso de callbacks parte do projeto de API, que é um outro aspecto que me incomoda. Claro que é ligeiramente melhor do que usar a API IndexedDB nativa, eu acho.
  • Lento: O PouchDB acrescenta um pouco de sua própria funcionalidade à mistura, o que pode causar lentidão. Isso é um dos componentes que causam a relativa lentidão do IndexedDB. As bibliotecas alternativas e opções de armazenamento offline provavelmente serão mais rápidas.

É lógico, temos um prazo a cumprir, então não dá para simplesmente substituir o PouchDB por outra coisa, porque isso exigiria pesquisar outras soluções para testar se são mais fáceis de usar e mais rápidas. Depois, teríamos que revisar todo o aplicativo e alterar completamente todos os códigos que tenham usado o PouchDB – e foram muitos.

Melhorando as coisas com o padrão Adapter/Facade

Nossa melhor opção para corrigir pelo menos alguns dos problemas foi implementar uma camada de abstração que pudesse atuar como Facade e Adapter. Era um Facade porque simplificava a interface, e um Adapter porque a camada de abstração nos permitia alternar para a biblioteca enquanto ainda estivéssemos usando a mesma API para interagir com a nova biblioteca. Com essa Facade instalada, pudemos usar imediatamente a nova API na qual estávamos aplicando mudanças, e só depois atualizamos o resto do aplicativo para utilizá-lo. Essa abordagem melhorou demais a situação:

  • Promises: Todos os métodos que criamos usavam promises em vez de exigir callbacks. Isso acabou com nosso inferno de callbacks e nos ajudou a organizar nosso código mais logicamente. Também ajudou a deixar as coisas consistentes com as nossas chamadas AJAX que já usavam promises, portanto tudo o que era assíncrono usava promises.
  • API mais simples: Um único método para salvar tudo! As promises dividem os erros em funções separadas em vez de sempre exigir que se verifique se há erros em cada callback. Isso deixou as respostas mais consistentes e normalizadas. Também acrescentou características convenientes: frequentemente tentávamos recuperar um grupo de registros usando uma lista de IDs, então, em vez de precisar chamar get para cada registro, implementamos a capacidade de passar um array de IDs para get e obter de volta um array de registros.
  • Mais fácil de mudar: Os problemas de velocidade que vêm com o PouchDB ainda não estão totalmente resolvidos. Conseguimos otimizar nosso próprio código para obter melhoras substanciais de desempenho, mas ainda encontramos problemas nesse quesito. No entanto, se tivermos a oportunidade de fazer algumas pesquisas e descobrir que existem alternativas mais rápidas para serem implementadas, precisaremos apenas mexer no nosso Adapter sem tocar em nenhum outro código.

É claro que não basta eu dizer tudo isso para vocês sem mostrar alguns exemplos de código. Aqui está um exemplo do que fizemos com nosso método get para nos permitir requisitar um ou mais “documentos” (em vez de apenas um) e usar promises em vez de simples callbacks. Sei que muitos vão questionar a nossa escolha de usar jQuery para promises, mas isso serve aos nossos propósitos e não exige que se carregue uma biblioteca adicional.

Database.prototype.get = function (ids) {
    var docs = [];
    var self = this;
 
    // Just get a single doc if it's not an array of IDs
    if (!_.isArray(ids)) {
        return this._getSingle(ids);
    }
 
    // Otherwise we need to grab all of the docs
    return _.reduce(ids, function(memo, id, index) {
        // Start a new `_getSingle` when the previous one is done
        return memo.then(function() {
            return self._getSingle(id);
        }).then(function(doc) {
            // Assign the retrieved doc to it's rightful place
            docs[index] = doc;
        });
 
    // Use an already-resolved promise to get the 'memo' started
    }, $.Deferred().resolve().promise()).then(function() {
        // Make sure the user gets the docs when we're done
        return docs;
    });
};
 
Database.prototype._getSingle = function(id) {
    var dfd = $.Deferred();
 
    this.db.get(id, function(err, doc) {
        if (err) {
            // Reject when we have an error
            dfd.reject(err);
        } else {
            // We got ourselves a doc! Resolve!
            dfd.resolve(doc);
        }
    });
 
    // Make sure the user get's a promise
    return dfd.promise();
};

Vale ressaltar que a função reduce realmente vem a calhar para executar várias operações assíncronas sequencialmente. Você pode achar que seria melhor tentar rodar várias chamadas de _getSingle em paralelo, mas o PouchDB já coloca as operações em fila mesmo, então não ganhamos nada com isso. Usar _.reduce acaba dificultando um pouco entender o código se você não estiver acostumado com o padrão, mas você se acostuma. Também é muito bom porque, se um deles falhar, o resto não vai nem tentar recuperar os registros.

Em todo caso, deixamos nosso método get mais poderoso e flexível ao acrescentar (alguns) dos benefícios das promises (teríamos todos os benefícios se usássemos as promises “reais”). Fizemos uma coisa parecida com nosso método save, que nos permitiu salvar um ou mais documentos – os quais podem ser novos ou já salvos anteriormente – sem a necessidade de saber qual método chamar no PouchDB, e novamente acrescentamos promises:

Database.prototype.save = function (doc) {
    var dfd = $.Deferred();
    var arg = doc;
    var method;
 
    // Determine method and arguments to use
    if (_.isArray(doc)) {
        method = "bulkDocs";
        arg = {docs: doc};
    }
    else if (doc._id) {
        method = "put";
    }
    else {
        method = "post";
    }
 
    // Save the doc(s) with the proper method/args
    this.db[method](arg, function (err, response) {
        if (err) {
            // Uh oh... error. REJECTED!
            dfd.reject(err);
        }
        else {
            // Yay it worked! RESOLVED!
            dfd.resolve(response);
        }
    });
 
    return dfd.promise();
};

Nesse caso, a realidade é que o PouchDB tinha seu próprio método para lidar com vários documentos ao mesmo tempo. Sendo assim, utilizamos esse método e, caso recebêssemos apenas um doc, determinávamos se era necessário usar put ou post. Uma vez tendo determinado qual método usar e tendo formatado os argumentos de acordo com ele, seguíamos em frente e executávamos a operação.

Outras grandes oportunidades para Adapter e/ou Facade

Um único exemplo do uso de Adapter e Facades é ótimo, mas não significa que eles sejam úteis em muitas situações, certo? Então, criar um Adapter para praticamente qualquer biblioteca relativamente pequena pode ser uma boa ideia, especialmente se há uma boa chance de você querer/precisar migrar para uma nova biblioteca que venha a substituí-la. Na verdade, tenho outro exemplo interessante de uma coisa que estou querendo fazer e que é um pouco diferente.

Venho usando o Socket.IO por um tempo e o acho o máximo, mas há inúmeros relatos de bugs e problemas com ele. Todo mundo parece estar migrando para o SockJS. Não me incomodo em migrar para o SockJS, a não ser por um problema gritante: ele não tem um monte de funcionalidades que acabei amando no Socket.IO. Não posso simplesmente continuar com o Socket.IO (a menos que corrijam seus problemas), mas mudar meus apps para usar o SockJS demandaria um monte de refatoração e alterações. A solução? Adicionar uma camada de Adapter que me dá a API do Socket.IO por cima do SockJS. Isso pode acabar sendo uma tarefa difícil e extensa, provavelmente mais ainda do que simplesmente mudar meus apps diretamente, mas, se seu conseguir terminá-la, pode vir a ser extremamente útil em projetos futuros também.

Este exemplo é interessante, pois não estou implementando um Adapter apenas para modificar a API da biblioteca que já estou usando, mas em vez disso estou pegando a API de uma biblioteca que uso atualmente e aplicando-a à biblioteca para qual estou migrando. Se você gosta da API da sua biblioteca, mas precisa trocar de biblioteca por um motivo ou outro, pode ser uma ótima maneira de fazer a alteração de um jeito mais simples. Isso também funciona bem se você não gosta necessariamente da biblioteca da API, mas não tem tempo de criar imediatamente um Adapter para a biblioteca que você está usando atualmente e utilizá-lo em todo o código.

Conclusão

Bom, isso é tudo. Existe um motivo para Adapter e Facade serem mencionados em livros/artigos/etc. de design patterns. Na verdade, eles são o motivo pelo qual muitas bibliotecas existem! Mas não precisamos deixar apenas os autores das bibliotecas escrevê-los: há muitas situações nas quais adicionar uma camada entre seu aplicativo e suas bibliotecas pode ser útil, então não tenha vergonha. Alguns frameworks, como Aura, até criam Adapters em torno dos utilitários DOM, caso você queira usar alguma coisa diferente do jQuery ou, mais tarde, decidir mudar para outra coisa. Essa é uma ótima prática que demanda bastante trabalho no começo, mas certamente ajuda a diminuir o trabalho no futuro se você precisar fazer alterações. Apenas certifique-se de pensar bem na sua API para que ela não vire justamente a peça que precisa mudar mais pra frente.

***

Artigo traduzido pela Redação iMasters com autorização do autor. Publicado originalmente em http://www.joezimjs.com/javascript/adapters-facades-awesome/