Padrões elegantes em JavaScript moderno (ciclo da equipe Bill Sourour)

Olá Habr! O conhecido professor de JavaScript Bill Sourour, na época, escreveu vários artigos sobre padrões modernos em JS. Como parte deste artigo, tentaremos revisar as idéias que ele compartilhou. Não que tenham sido alguns padrões únicos, mas espero que o artigo encontre seu leitor. Este artigo não é uma "tradução" do ponto de vista da política da Habr, pois Descrevo meus pensamentos para os quais os artigos de Bill me apontaram.

Rooro


A abreviação significa Receber um objeto, retornar um objeto - obter um objeto, retornar um objeto. Eu forneço um link para o artigo original: link

Bill escreveu que ele criou uma maneira de escrever funções nas quais a maioria delas aceita apenas um parâmetro - um objeto com argumentos de função. Eles também retornam um objeto de resultados. Bill foi inspirado pela reestruturação dessa ideia (um dos recursos do ES6).

Para quem não conhece a desestruturação, darei as explicações necessárias durante a história.

Imagine que temos dados do usuário contendo seus direitos sobre determinadas seções do aplicativo apresentadas no objeto de dados. Precisamos mostrar certas informações com base nesses dados. Para fazer isso, poderíamos oferecer a seguinte implementação:

//   const user = { name: 'John Doe', login: 'john_doe', password: 12345, active: true, rules: { finance: true, analitics: true, hr: false } }; //   const users = [user]; //,     function findUsersByRule ( rule, withContactInfo, includeInactive) { //        active const filtredUsers= users.filter(item => includeInactive ? item.rules[rule] : item.active && item.rules[rule]); //  ()   ( )     withContactInfo return withContactInfo ? filtredUsers.reduce((acc, curr) => { acc[curr.id] = curr; return acc; }, {}) : filtredUsers.map(item => item.id) } //     findUsersByRule( 'finance', true, true) 

Usando o código acima, atingiríamos o resultado desejado. No entanto, existem várias armadilhas ao escrever código dessa maneira.

Em primeiro lugar, a chamada para a função findUsersByRule muito duvidosa. Observe como os dois últimos parâmetros são ambíguos. O que acontece se nosso aplicativo quase nunca precisar de informações de contato (withContactInfo), mas quase sempre precisar de usuários inativos (includeInactive)? Sempre teremos que passar valores lógicos. Agora, enquanto a declaração da função está próxima à sua chamada, isso não é tão assustador, mas imagine que você vê essa chamada em algum lugar em outro módulo. Você precisará procurar um módulo com uma declaração de função para entender por que dois valores lógicos na forma pura são transferidos para ele.

Em segundo lugar, se queremos tornar alguns parâmetros obrigatórios, teremos que escrever algo como isto:

 function findUsersByRule ( role, withContactInfo, includeInactive) { if (!role) { throw Error(...) ; } //...  } 

Nesse caso, nossa função, além de suas responsabilidades de pesquisa, também executará a validação, e nós apenas queremos encontrar usuários por determinados parâmetros. Obviamente, a função de pesquisa pode aceitar funções de validação, mas a lista de parâmetros de entrada será expandida. Este também é um sinal negativo desse padrão de codificação.

A reestruturação envolve a decomposição de uma estrutura complexa em partes simples. Em JavaScript, uma estrutura tão complexa geralmente é um objeto ou uma matriz. Usando a sintaxe de desestruturação, você pode extrair pequenos fragmentos de matrizes ou objetos. Essa sintaxe pode ser usada para declarar variáveis ​​ou sua finalidade. Você também pode gerenciar estruturas aninhadas usando a sintaxe da destruição já aninhada.

Usando a desestruturação, a função do exemplo anterior ficará assim:

 function findUsersByRule ({ rule, withContactInfo, includeInactive}) { //    } findUsersByRule({ rule: 'finance', withContactInfo: true, includeInactive: true}) 

Observe que nossa função parece quase idêntica, exceto que colocamos colchetes em torno de nossos parâmetros. Em vez de receber três parâmetros diferentes, nossa função agora espera um único objeto com propriedades: rule , withContactInfo e includeInactive .

Isso é muito menos ambíguo, muito mais fácil de ler e entender. Além disso, ignorar ou outra ordem de nossos parâmetros não é mais um problema, porque agora eles são nomeados propriedades do objeto. Também podemos adicionar novos parâmetros com segurança à declaração da função. Além disso, desde Como a desestruturação copia o valor passado, suas alterações na função não afetam o original.

O problema com os parâmetros necessários também pode ser resolvido de uma maneira mais elegante.

 function requiredParam (param) { const requiredParamError = new Error( `Required parameter, "${param}" is missing.` ) } function findUsersByRule ({ rule = requiredParam('rule'), withContactInfo, includeInactive} = {}) {...} 

Se não passarmos o valor da regra, a função passada por padrão funcionará, o que gerará uma exceção.

As funções em JS podem retornar apenas um valor, para que você possa usar um objeto para transferir mais informações. Obviamente, nem sempre precisamos da função para retornar muitas informações; em alguns casos, ficaremos satisfeitos com o retorno da primitiva, por exemplo, findUserId retornará naturalmente um identificador por alguma condição.

Além disso, essa abordagem simplifica a composição das funções. De fato, com a composição, as funções devem ter apenas um parâmetro. O padrão RORO adere ao mesmo contrato.

Bill Sourour: “Como qualquer modelo, o RORO deve ser visto como outra ferramenta em nossa caixa de ferramentas. "Nós o usamos onde ele se beneficia, tornando a lista de parâmetros mais compreensível e flexível e o valor de retorno mais expressivo".

Fábrica de gelo


Você pode encontrar o artigo original neste link.

Segundo o autor, este modelo é uma função que cria e retorna um objeto congelado.

Bill pensa. que em algumas situações esse padrão pode substituir as classes ES6 usuais para nós. Por exemplo, temos uma certa cesta de alimentos na qual podemos adicionar / remover produtos.

Classe ES6:

 // ShoppingCart.js class ShoppingCart { constructor({db}) { this.db = db } addProduct (product) { this.db.push(product) } empty () { this.db = [] } get products () { return Object .freeze([...this.db]) } removeProduct (id) { // remove a product } // other methods } // someOtherModule.js const db = [] const cart = new ShoppingCart({db}) cart.addProduct({ name: 'foo', price: 9.99 }) 

Os objetos criados com a new palavra-chave são mutáveis, ou seja, podemos substituir o método de instância de classe.

 const db = [] const cart = new ShoppingCart({db}) cart.addProduct = () => 'nope!' //   JS  cart.addProduct({ name: 'foo', price: 9.99 }) // output: "nope!"     

Também deve ser lembrado que as classes em JS são implementadas na delegação de protótipo, portanto, podemos alterar a implementação do método no protótipo da classe e essas alterações afetarão todas as instâncias já criadas (falei sobre isso em mais detalhes no artigo sobre OOP ).

 const cart = new ShoppingCart({db: []}) const other = new ShoppingCart({db: []}) ShoppingCart.prototype .addProduct = () => 'nope!' //     JS cart.addProduct({ name: 'foo', price: 9.99 }) // output: "nope!" other.addProduct({ name: 'bar', price: 8.88 }) // output: "nope!" 

Concordo, esses recursos podem causar muitos problemas.

Outro problema comum é atribuir um método de instância a um manipulador de eventos.

 document .querySelector('#empty') .addEventListener( 'click', cart.empty ) 

Clicar no botão não esvaziará a cesta. O método atribui uma nova propriedade ao nosso botão chamado db e define essa propriedade como [] em vez de afetar o banco de dados do objeto cart. No entanto, não há erros no console, e seu senso comum dirá que o código deve funcionar, mas não é.

Para fazer esse código funcionar, você deve escrever uma função de seta:

 document .querySelector("#empty") .addEventListener( "click", () => cart.empty() ) 

Ou vincule o contexto a um bind:

 document .querySelector("#empty") .addEventListener( "click", cart.empty.bind(cart) ) 

A Fábrica de Gelo nos ajudará a evitar essas armadilhas.

 function makeShoppingCart({ db }) { return Object.freeze({ addProduct, empty, getProducts, removeProduct, // others }) function addProduct (product) { db.push(product) } function empty () { db = [] } function getProducts () { return Object .freeze([...db]) } function removeProduct (id) { // remove a product } // other functions } // someOtherModule.js const db = [] const cart = makeShoppingCart({ db }) cart.addProduct({ name: 'foo', price: 9.99 }) 

Recursos deste padrão:

  • não é necessário usar a nova palavra-chave
  • não há necessidade de vincular isso
  • carrinho é totalmente portátil
  • variáveis ​​locais podem ser declaradas que não serão visíveis do lado de fora

 function makeThing(spec) { const secret = 'shhh!' return Object.freeze({ doStuff }) function doStuff () { //    secret } } // secret    const thing = makeThing() thing.secret // undefined 

  • padrão suporta herança
  • criar objetos usando o Ice Factory é mais lento e exige mais memória do que o uso de uma classe (em muitas situações, podemos precisar de classes, por isso recomendo este artigo )
  • essa é uma função comum que pode ser atribuída como retorno de chamada

Conclusão


Quando estamos falando sobre a arquitetura do software que está sendo desenvolvido, devemos sempre fazer compromissos convenientes. Não há regras e restrições rígidas nesse caminho; cada situação é única; portanto, quanto mais padrões em nosso arsenal, maior a probabilidade de escolhermos a melhor opção de arquitetura em uma situação específica.

Source: https://habr.com/ru/post/pt472816/


All Articles