Como usar a Inversão de controle em JavaScript e Reactjs para simplificar o tratamento de código

Como usar a Inversão de controle em JavaScript e Reactjs para simplificar o tratamento de código


Inversão de controle é um princípio de programação bastante fácil de entender, que, ao mesmo tempo, pode melhorar significativamente seu código. Este artigo demonstrará como aplicar a Inversão de controle no JavaScript e no Reactjs.


Se você já escreveu um código usado em mais de um local, está familiarizado com esta situação:


  1. Você cria um fragmento de código reutilizável (pode ser uma função, componente React, gancho React etc.) e o compartilha (para colaboração ou publicação em código aberto).
  2. Alguém pede para você adicionar novas funcionalidades. Seu código não suporta a funcionalidade proposta, mas poderia se você fizesse uma pequena alteração.
  3. Você adiciona um novo argumento / prop / opção ao seu código e sua lógica associada para manter esse novo recurso funcionando.
  4. Repita as etapas 2 e 3 várias vezes (ou várias, muitas vezes).
  5. Agora, seu código reutilizável é difícil de usar e manter.

O que exatamente faz do código um pesadelo para usar e manter? Existem vários aspectos que podem tornar seu código problemático:


  1. Tamanho e / ou desempenho do pacote: apenas mais código para executar nos dispositivos pode levar a um desempenho ruim. Às vezes, isso pode levar as pessoas a simplesmente se recusarem a usar seu código.
  2. Difícil de manter: Anteriormente, seu código reutilizável tinha apenas algumas opções e estava focado em fazer uma coisa bem, mas agora pode fazer várias coisas diferentes, e você precisa documentar tudo. Além disso, as pessoas começarão a fazer perguntas sobre como usar seu código para determinados casos de uso, que podem ou não ser comparáveis ​​aos casos de uso para os quais você já adicionou suporte. Você pode até ter dois casos de uso quase idênticos que são ligeiramente diferentes; portanto, você terá que responder a perguntas sobre o que é melhor usar em uma determinada situação.
  3. Complexidade de implementação : cada vez que essa não é apenas outra if , cada ramo da lógica do seu código coexiste com os ramos da lógica existentes. De fato, situações são possíveis quando você está tentando manter uma combinação de argumentos / opções / adereços que ninguém usa, mas ainda precisa considerar todas as opções possíveis, pois não sabe exatamente se alguém irá ou não usar essas combinações.
  4. API sofisticada : cada novo argumento / opção / suporte que você adiciona ao seu código reutilizável dificulta o uso, porque agora você tem um README enorme ou um site onde todas as funcionalidades disponíveis estão documentadas e as pessoas precisam aprender tudo isso para um uso eficaz seu código Não é conveniente usá-lo, porque a complexidade da sua API penetra no código do desenvolvedor que a usa, o que complica seu código.

Como resultado, todo mundo sofre. Vale ressaltar que a implementação do programa final é uma parte essencial do desenvolvimento. Mas seria ótimo se pensássemos mais sobre a implementação de nossas abstrações (leia sobre "programação AHA" ). Existe uma maneira de reduzir os problemas com código reutilizável e ainda colher os benefícios do uso de abstrações?


Inversão de controle


A inversão do controle é um princípio que realmente simplifica a criação e o uso de abstrações. Aqui está o que a Wikipedia diz sobre isso:


... na programação tradicional, o código do usuário que expressa o objetivo do programa é chamado em bibliotecas reutilizáveis ​​para resolver problemas comuns, mas com inversão de controle, é um ambiente que chama código personalizado ou específico da tarefa,

Pense da seguinte maneira: "Reduza a funcionalidade de sua abstração e faça com que seus usuários possam implementar a funcionalidade de que precisam". Isso pode parecer um absurdo completo, pois usamos abstrações para ocultar tarefas complexas e repetitivas e, assim, tornar nosso código mais "limpo" e "limpo". Mas, como vimos acima, as abstrações tradicionais nem sempre simplificam o código.


O que é Inversão de Gerenciamento em código?


Para começar, aqui está um exemplo bem elaborado:


 //   Array.prototype.filter   function filter(array) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (element !== null && element !== undefined) { newArray[newArray.length] = element } } return newArray } // : filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] 

Agora, vamos reproduzir um "ciclo de vida de abstração" típico, adicionando novos casos de uso a essa abstração e "melhorando-os sem pensar" para suportar esses novos casos de uso:


 //   Array.prototype.filter   function filter( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if ( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ) { continue } newArray[newArray.length] = element } return newArray } filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterNull: false}) // [0, 1, 2, null, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterUndefined: false}) // [0, 1, 2, undefined, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterZero: true}) // [1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterEmptyString: true}) // [0, 1, 2, 3, 'four'] 

Portanto, nosso programa funciona com apenas seis casos de uso, mas, na verdade, suportamos qualquer combinação possível de funções, e existem até 25 combinações desse tipo (se calculado corretamente).


Em geral, essa é uma abstração bastante simples. Mas isso pode ser simplificado. Muitas vezes acontece que a abstração na qual a nova funcionalidade foi adicionada pode ser bastante simplificada para os casos de uso que ela realmente suporta. Infelizmente, assim que a abstração começa a oferecer suporte a algo (por exemplo, executando { filterZero: true, filterUndefined: false } ), temos medo de remover essa funcionalidade devido ao fato de poder quebrar o código que depende dela.


Até escrevemos testes para casos de uso que realmente não temos, simplesmente porque nossa abstração suporta esses cenários, e talvez seja necessário fazer isso no futuro. E quando esses ou outros casos de uso se tornam desnecessários para nós, não removemos o suporte deles, pois simplesmente o esquecemos ou pensamos que pode ser útil para nós no futuro, ou simplesmente temos medo de quebrar alguma coisa.


Ok, vamos agora escrever uma abstração mais elaborada para essa função e aplicar o método de inversão de controle para suportar todos os casos de uso que precisamos:


 //   Array.prototype.filter   function filter(array, filterFn) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (filterFn(element)) { newArray[newArray.length] = element } } return newArray } filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null && el !== undefined, ) // [0, 1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined) // [0, 1, 2, null, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null) // [0, 1, 2, undefined, 3, 'four', ''] filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined && el !== null && el !== 0, ) // [1, 2, 3, 'four', ''] filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined && el !== null && el !== '', ) // [0, 1, 2, 3, 'four'] 

Ótimo! Acabou muito mais fácil. Acabamos de transformar o controle sobre a função, passando a responsabilidade de decidir qual elemento cai na nova matriz, da função de filter para a função que chama a função de filtro. Observe que a função de filter ainda é uma abstração útil por si só, mas agora é muito mais flexível.


Mas a versão anterior dessa abstração foi tão ruim? Provavelmente não. Mas desde que mudamos o controle, agora podemos oferecer suporte a casos de uso muito mais exclusivos:


 filter( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], animal => animal.legs === 0, ) // [ // {name: 'dolphin', legs: 0, mammal: true}, // {name: 'salmon', legs: 0, mammal: false}, // ] 

Imagine se você precisasse adicionar suporte para este caso de uso sem aplicar a inversão de controle? Sim, isso seria ridículo.


API ruim?


Uma das reclamações mais comuns que ouvi de pessoas sobre APIs que usam inversão de controle é: "Sim, mas agora é mais difícil de usar do que antes". Veja este exemplo:


 //  filter([0, 1, undefined, 2, null, 3, 'four', '']) //  filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null && el !== undefined, ) 

Sim, uma das opções é claramente mais fácil de usar que a outra. Mas um dos benefícios da inversão de controle é que você pode usar uma API que usa inversão de controle para reimplementar sua API antiga. Isso geralmente é bem simples. Por exemplo:


 function filterWithOptions( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { return filter( array, element => !( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ), ) } 

Legal, né? Dessa maneira, podemos criar abstrações sobre a API na qual a inversão de controle é aplicada e, assim, criar uma API mais simples. E se nossa API "mais simples" não tiver casos de uso suficientes, nossos usuários poderão aplicar os mesmos blocos de construção que usamos para criar nossa API de alto nível para desenvolver soluções para tarefas mais complexas. Eles não precisam nos pedir para adicionar uma nova função ao filterWithOptions e esperar que ela seja implementada. Eles já possuem ferramentas com as quais podem desenvolver independentemente as funcionalidades adicionais necessárias.


E, apenas para o fã:


 function filterByLegCount(array, legCount) { return filter(array, animal => animal.legs === legCount) } filterByLegCount( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], 0, ) // [ // {name: 'dolphin', legs: 0, mammal: true}, // {name: 'salmon', legs: 0, mammal: false}, // ] 

Você pode criar funcionalidades especiais para qualquer situação que ocorra com frequência.


Exemplos da vida real


Então, isso funciona em casos simples, mas esse conceito é adequado para a vida real? Bem, provavelmente você está constantemente usando a inversão de controle. Por exemplo, a função Array.prototype.filter aplica controle inverso. Como a função Array.prototype.map .


Existem vários padrões com os quais você já deve estar familiarizado e que são apenas uma forma de inversão de controle.


Aqui estão dois dos meus padrões favoritos que mostram estes são "Componentes compostos" e "Redutores de estado" . Abaixo estão breves exemplos de como esses padrões podem ser aplicados.


Componentes compostos


Suponha que você deseje criar um componente de Menu que tenha um botão para abrir um menu e uma lista de itens de menu que serão exibidos quando você clicar no botão. Então, quando o item for selecionado, ele executará alguma ação. Geralmente, para implementar isso, eles simplesmente criam adereços:


 function App() { return ( <Menu buttonContents={ <> Actions <span aria-hidden></span> </> } items={[ {contents: 'Download', onSelect: () => alert('Download')}, {contents: 'Create a Copy', onSelect: () => alert('Create a Copy')}, {contents: 'Delete', onSelect: () => alert('Delete')}, ]} /> ) } 

Isso nos permite configurar muitas coisas nos itens de menu. Mas e se quisermos inserir uma linha antes do item de menu Excluir? Devemos adicionar uma opção especial a objetos relacionados a items ? Bem, eu não sei, por exemplo: precedeWithLine ? Idéia mais ou menos.


Talvez crie um tipo especial de item de menu, por exemplo {contents: <hr />} . Acho que funcionaria, mas teríamos que lidar com casos em que não há onSelect . E para ser sincero, essa é uma API muito incômoda.


Quando você pensa em como criar uma boa API para pessoas que estão tentando fazer algo um pouco diferente, em vez de buscar a instrução if , tente inverter o controle. E se transferirmos a responsabilidade pela visualização do menu para o usuário? Usamos um dos pontos fortes da reação:


 function App() { return ( <Menu> <MenuButton> Actions <span aria-hidden></span> </MenuButton> <MenuList> <MenuItem onSelect={() => alert('Download')}>Download</MenuItem> <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem> <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem> </MenuList> </Menu> ) } 

Um ponto importante a ser observado é que não há estado dos componentes visíveis para o usuário. O estado é implicitamente compartilhado entre esses componentes. Este é o valor principal do padrão de componente. Usando essa oportunidade, demos algum controle sobre a renderização ao usuário de nossos componentes e, agora, adicionar uma linha extra (ou outra coisa) é uma ação simples e intuitiva. Sem documentação adicional, sem funções adicionais, sem código ou testes extras. Todo mundo ganha.


Você pode ler mais sobre esse padrão aqui . Agradeço ao Ryan Florence , que me ensinou isso.


Redutor de estado


Eu vim com esse padrão para resolver o problema de definir a lógica do componente. Você pode ler mais sobre essa situação no meu blog , The State Reducer Pattern , mas o ponto principal é que eu tenho uma biblioteca de pesquisa / preenchimento automático / digitação chamada Downshift , e um dos usuários da biblioteca estava desenvolvendo uma versão de componente de múltipla escolha , por causa do qual ele queria que o menu permanecesse aberto mesmo depois de selecionar um item.


A lógica por trás do Downshift sugeria que, após a escolha, o menu deveria ser fechado. Um usuário da biblioteca que precisava alterar sua funcionalidade sugeriu adicionar prop closeOnSelection . Recusei esta oferta, pois já havia percorrido o caminho que levava ao apropalapse e queria evitá-la.


Em vez disso, criei a API para que os próprios usuários pudessem controlar como as alterações de estado ocorrem. Pense no redutor de estado como o estado de uma função que é chamada toda vez que o estado do componente é alterado e oferece ao desenvolvedor de aplicativos a capacidade de influenciar a alteração de estado que está prestes a ocorrer.


Um exemplo de uso da biblioteca Downshift para que não feche o menu após o usuário clicar no item selecionado:


 function stateReducer(state, changes) { switch (changes.type) { case Downshift.stateChangeTypes.keyDownEnter: case Downshift.stateChangeTypes.clickItem: return { ...changes, //     Downshift   //       isOpen  highlightedIndex //      isOpen: state.isOpen, highlightedIndex: state.highlightedIndex, } default: return changes } } // ,   // <Downshift stateReducer={stateReducer} {...restOfTheProps} /> 

Depois de adicionarmos esse suporte, começamos a receber MUITO menos solicitações para adicionar novas configurações para este componente. O componente ficou mais flexível e ficou mais fácil para os desenvolvedores configurá-lo conforme necessário.


Render Props


Vale mencionar o padrão "render adereços" . Esse padrão é um exemplo ideal de inversão de controle, mas especialmente não precisamos dele. Leia mais sobre isso aqui: por que não precisamos mais dos Render Props .


Advertência


A inversão do controle é uma excelente maneira de contornar o problema do equívoco sobre como o seu código será usado no futuro. Mas antes de terminar, gostaria de lhe dar alguns conselhos.


Vamos voltar ao nosso exemplo rebuscado:


 //   Array.prototype.filter   function filter(array) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (element !== null && element !== undefined) { newArray[newArray.length] = element } } return newArray } // : filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] 

E se isso é tudo o que precisamos da função de filter ? E nunca enfrentamos uma situação em que precisaríamos filtrar qualquer coisa, exceto null e undefined ? Nesse caso, adicionar inversão de controle para um único caso de uso simplesmente complicaria o código e não traria muitos benefícios.


Como em qualquer abstração, tenha cuidado ao aplicar o princípio da programação da AHA e evite abstrações precipitadas!


Conclusões


Espero que o artigo tenha sido útil para você. Eu mostrei como você pode aplicar o conceito de Inversão de controle em uma reação. É claro que esse conceito se aplica não apenas ao React (como vimos com a função de filter ). Na próxima vez que você perceber que está adicionando outra if à função coreBusinessLogic do seu aplicativo, pense em como você pode inverter o controle e transferir a lógica para onde ele é usado (ou, se for usado em vários lugares, é possível criar uma abstração mais especializada para isso caso específico).


Se quiser, você pode brincar com um exemplo de um artigo no CodeSandbox .


Boa sorte e obrigado pela atenção!


PS. Se você gostou deste artigo, poderá gostar desta conversa: youtube Kent C Dodds - Simply React

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


All Articles