如何在JavaScript和Reactjs中使用Control Inversion来简化代码处理

如何在JavaScript和Reactjs中使用Control Inversion来简化代码处理


控制反转是一个相当容易理解的编程原理,同时可以显着改善您的代码。 本文将演示如何在JavaScript和Reactjs中应用控件反转。


如果您已经编写了在多个地方使用的代码,那么您将熟悉这种情况:


  1. 您创建一段可重用的代码(它可以是一个函数,React组件,React钩子等)并共享(用于协作或在开源中发布)。
  2. 有人要求您添加新功能。 您的代码不支持建议的功能,但是如果您进行了少量更改,则可以。
  3. 您可以在代码及其关联的逻辑中添加新的自变量/ prop /选项,以使此新功能正常运行。
  4. 重复步骤2和3几次(或多次)。
  5. 现在,您的可重用代码很难使用和维护。

是什么使代码成为使用和维护的噩梦? 有几个方面会使您的代码有问题:


  1. 封装大小和/或性能:在设备上运行的代码过多会导致性能下降。 有时,这可能导致人们只是拒绝使用您的代码。
  2. 难以维护:以前,您的可重用代码只有很少的选择,并且专注于做好一件事情,但是现在它可以做很多不同的事情,您需要全部记录下来。 此外,人们会开始问您有关如何在某些用例上使用您的代码的问题,这些用例可能与您已经添加了支持的用例不相上下。 您甚至可能拥有两个几乎完全相同的用例,但它们之间略有不同,因此您将不得不回答有关在给定情况下最佳使用情况的问题。
  3. 实现复杂性 :每次这不仅是另一个if ,代码逻辑的每个分支都与现有的逻辑分支共存。 实际上,当您尝试维护甚至没有人使用过的参数/选项/道具的组合时,可能会出现这种情况,但是您仍然需要考虑任何可能的选项,因为您不确切知道是否有人会使用这些组合。
  4. 复杂的API :添加到可重用代码中的每个新参数/选项/属性都很难使用,因为现在您拥有庞大的自述文件或记录所有可用功能的站点,人们必须学习所有这些内容才能有效使用您的代码。 使用它并不方便,因为API的复杂性会渗透到使用它的开发人员的代码中,这会使它的代码复杂化。

结果,每个人都受苦。 值得注意的是,最终计划的实施是发展的重要组成部分。 但是,如果我们更多地考虑抽象的实现(请参阅“ AHA编程” ),那就太好了。 有没有一种方法可以减少可重用代码的问题,并仍然获得使用抽象的好处?


控制反转


控制反转是真正简化抽象的创建和使用的原理。 这是维基百科所说的:


...在传统编程中,表达程序目的的用户代码在可重用的库中被调用以解决常见问题,但是随着控件的倒置,它是一种调用自定义或特定于任务的代码的环境,

可以这样考虑:“减少抽象的功能,并使其成为可能,以便用户可以实现所需的功能。” 这似乎是完全荒谬的,因为我们使用抽象来隐藏复杂且重复的任务,从而使我们的代码更“干净”和“简洁”。 但是,如上所述,传统的抽象方法并不总是简化代码。


什么是代码管理反转?


首先,这是一个非常人为的示例:


 //   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', ''] 

现在,让我们玩一个典型的“抽象生命周期”,在此抽象中添加新的用例,并“无意间改进”它以支持这些新的用例:


 //   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'] 

因此,我们的程序仅适用于六个用例,但实际上我们支持任何可能的功能组合,并且多达25种这样的组合(如果我计算正确的话)。


通常,这是一个相当简单的抽象。 但是可以简化。 通常会发生这样的情况:对于实际支持的用例,添加新功能的抽象可能会大大简化。 不幸的是,一旦抽象开始支持某种东西(例如,执行{ filterZero: true, filterUndefined: false } ),我们就害怕删除此功能,因为它可能会破坏依赖它的代码。


我们甚至为实际上没有的用例编写测试,这仅仅是因为我们的抽象支持这些方案,并且将来我们可能需要这样做。 当这些用例或其他用例对我们而言不必要时,我们不会移除它们的支持,因为我们只是忘记了它,或者认为它将来可能对我们有用,或者只是我们害怕破坏某些东西。


好的,现在让我们对该函数编写更详细的抽象,并应用控件反转方法来支持我们需要的所有用例:


 //   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'] 

太好了! 事实证明要容易得多。 我们只是对函数进行了控制,将负责确定哪个元素落入新数组的责任从filter功能转移到了调用过滤器功能的功能。 注意, filter函数本身仍然是有用的抽象,但是现在它更加灵活。


但是,这种抽象的先前版本是如此糟糕吗? 可能不是。 但是自从我们将控制权移交给我们之后,我们现在可以支持更多独特的用例:


 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}, // ] 

试想一下,如果需要在不应用控件反转的情况下增加对此用例的支持? 是的,那太荒谬了。


API错误?


我听到人们对使用控制反转的API的最普遍的抱怨之一是:“是的,但是现在比以前更难使用了。” 举个例子:


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

是的,其中一个选项显然比另一个选项更易于使用。 但是,控制反转的好处之一是,您可以使用使用控制反转的API重新实现旧的API。 这通常很简单。 例如:


 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 === '') ), ) } 

酷吧? 这样,我们可以在应用了控件反转的API之上创建抽象,从而创建更简单的API。 而且,如果我们的“简单” API中没有足够的用例,那么我们的用户可以使用与创建高级API时所使用的相同的构建块,来为更复杂的任务开发解决方案。 他们不需要让我们向filterWithOptions添加新功能并等待其实现。 他们已经拥有可以独立开发所需其他功能的工具。


而且,仅针对粉丝:


 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}, // ] 

您可以为经常发生的任何情况创建特殊功能。


现实生活中的例子


所以,这在简单的情况下可行,但是这个概念适合现实生活吗? 好吧,很可能您一直在使用控制反转。 例如,函数Array.prototype.filter应用逆控制。 类似于Array.prototype.map函数。


您可能已经熟悉了多种模式,它们只是控制反转的一种形式。


这是我最喜欢展示的两种模式,分别是“复合成分”“状态还原剂” 。 下面是如何应用这些模式的简要示例。


复合成分


假设您要创建一个Menu组件,该组件具有一个用于打开菜单的按钮和一个单击该按钮时将显示的菜单项列表。 然后,当选择该项目时,它将执行一些操作。 通常,要实现此目的,他们只需创建道具:


 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')}, ]} /> ) } 

这使我们可以在菜单项中配置很多东西。 但是,如果我们想在“删除”菜单项之前插入一行怎么办? 我们是否应该为与items相关的对象添加特殊选项? 好吧,我不知道,例如: precedeWithLine ? 马马虎虎的想法。


也许创建一种特殊的菜单项,例如{contents: <hr />} 。 我认为这会起作用,但是随后我们将不得不处理没有onSelect 。 老实说,这是一个非常尴尬的API。


当您考虑如何为试图做一些不同的事情的人创建一个好的API时,而不是尝试使用if语句时,请尝试反转控件。 如果我们将菜单可视化的责任转移给用户怎么办? 我们使用反应的优势之一:


 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> ) } 

要注意的重要一点是,用户没有可见的组件状态。 状态在这些组件之间隐式共享。 这是组件模式的核心价值。 利用这个机会,我们将对渲染的控制权交给了我们的组件用户,现在添加一条额外的行(或其他内容)是一种简单而直观的操作。 没有额外的文档,没有额外的功能,没有额外的代码或测试。 每个人都赢。


您可以在此处阅读有关此模式的更多信息。 感谢Ryan Florence ,他教了我这一点。


状态还原器


我想出了这种模式来解决设置组件逻辑的问题。 您可以在我的博客The State Reducer Pattern上了解有关此情况的更多信息,但主要要点是我有一个名为Downshift的搜索/自动完成/输入库,该库的用户之一正在开发多选组件版本,因此,他希望菜单即使在选择一个项目后也保持打开状态。


Downshift背后的逻辑表明,做出选择后,菜单应该关闭。 需要更改其功能的库用户建议添加prop closeOnSelection 。 我拒绝了这个提议,因为一旦我走上了导致破产的道路,并想避免这样做。


相反,我制作了API,以便用户自己可以控制状态更改的发生方式。 可以将状态缩减器视为每次组件状态更改时都会调用的函数的状态,并使应用程序开发人员能够影响即将发生的状态更改。


一个使用Downshift库,以便在用户单击所选项目后不关闭菜单的示例:


 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} /> 

添加此道具后,我们开始收到更少的请求来添加此组件的新设置。 该组件变得更加灵活,并且开发人员可以根据需要轻松配置它。


渲染道具


值得一提的是“渲染道具”模式。 这种模式是使用控制反转的理想示例,但我们尤其不需要它。 在此处了解更多信息: 为什么我们不再需要Render Props了


警告


控制反转是解决关于将来如何使用代码的误解的好方法。 但是在结束之前,我想给您一些建议。


让我们回到牵强的例子:


 //   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', ''] 

如果这是我们filter功能所需的全部呢? 而且,我们从来没有遇到过需要过滤除nullundefined以外的任何内容的情况? 在这种情况下,为单个用例添加控件倒置只会使代码复杂化,而不会带来太多好处。


与任何抽象一样,请小心应用AHA编程的原理,并避免草率抽象!


结论


我希望这篇文章对您有用。 我展示了如何在反应中应用控制反转的概念。 这个概念当然不仅适用于React(正如我们在filter函数中看到的那样)。 下次您发现要在应用程序的coreBusinessLogic函数中添加另一个if ,请考虑如何反转控件并将逻辑转移到使用它的位置(或者,如果它在多个地方使用,则可以为此创建更专业的抽象)具体情况)。


如果需要,可以使用CodeSandbox上的文章中的示例作为参考


祝您好运,感谢您的关注!


PS。 如果您喜欢这篇文章,则可能会喜欢这个话题: youtube Kent C Dodds-Simply React

Source: https://habr.com/ru/post/zh-CN485012/


All Articles