繁体   English   中英

Javascript:如何用更多功能模式替换嵌套的if / else?

[英]Javascript: How can I replace nested if/else with a more functional pattern?

我的React应用程序代码库中重复了以下模式:

const {items, loading} = this.props
const elem = loading
  ? <Spinner />
  : items.length
    ? <ListComponent />
    : <NoResults />

虽然这比嵌套实际的 if/else子句更清晰,但我试图拥抱更优雅和功能性的模式。 我已经阅读过关于使用像Either monad这样的东西,但我所做的所有努力最终看起来更冗长,更少可重复使用(这个伪代码可能不起作用,因为我试图记住以前的尝试):

import {either, F, isEmpty, prop} from 'ramda'
const isLoading = prop('loading')
const renderLoading = (props) => isLoading(props) ? <Spinner /> : false
const loadingOrOther = either(renderLoading, F)
const renderItems = (props) => isEmpty(props.items) ? <NoResults /> : <ListComponent />
const renderElem = either(loadingOrOther, renderItems)
const elems = renderElem(props)

我可以使用哪种模式更干/可重复使用?

谢谢!

虽然这比嵌套实际的 if/else子句更清晰

 render () { const {items, loading} = this.props return loading ? <Spinner /> : items.length ? <ListComponent items={items} /> : <NoResults /> } 

你发布了不完整的代码,所以我填补了一些空白,以获得更具体的例子。

查看代码,我发现很难读取条件的位置以及返回值的位置。 条件分散在各种缩进级别的各行中 - 同样,返回值也没有视觉一致性。 实际上,直到您进一步阅读程序才能看到loading return loading甚至是一个条件并不明显? 在这种情况下选择要呈现的组件是一个平坦的决策,代码的结构应该反映出来。

使用if/else会在此处生成一个非常易读的示例。 没有嵌套,您可以看到返回的各种类型的组件,整齐地放在相应的return语句旁边。 这是一个简单而扁平的决策,只需简单, 详尽的案例分析。

我在这里强调详尽无遗的一句话,因为至少为你的决定提供ifelse选择分支是很重要的。 在您的情况下,我们有第三个选项,所以else if使用另一个选项。

render () {
  const {items, loading} = this.props
  if (loading)
    return <Spinner />
  else if (items.length)
    return <ListComponent items={items} />
  else
    return <NoResults />
}

如果你看一下这段代码并尝试“修复”它,因为你正在考虑“拥抱更优雅和功能性的模式” ,你会误解“优雅”和“功能”。

嵌套三元表达式没有任何优雅。 功能编程不是用最少量的击键来编写程序,导致程序过于简洁且难以阅读。

if/else语句就像我使用的那样,并不是那么“功能性”,因为它们涉及不同的语法。 当然,它们比三元表达式更冗长,但它们正如我们所希望的那样运行,并且它们仍然允许我们声明功能行为 - 不要让语法单独强迫你做出关于编码风格的愚蠢决定。

我同意不幸的是, if是JavaScript中的语句而不是表达式 ,那就是你给予的工作。 您仍然能够通过这种约束生成优雅且功能强大的程序。


备注

个人认为依赖于真实的价值观。 我宁愿把你的代码写成

render () {
  const {items, loading} = this.props
  if (loading)                              // most important check
    return <Spinner />
  else if (items.length === 0)              // check of next importance
    return <NoResults />
  else                                      // otherwise, everything is OK to render normally
    return <ListComponent items={items} />
}

与代码相比,这不太可能吞下错误。 例如,假装你的组件以某种方式具有loading={false} items={null} prop值 - 你可以说你的代码会优雅地显示NoResults组件; 我认为组件处于非加载状态且没有项目是错误的,我的代码会产生错误以反映: Cannot read property 'length' of null

这告诉我在这个组件的范围之上发生了一个更大的问题 - 即这个组件有load = true 或者某些项目数组(空或其他); 没有其他道具组合是可以接受的。

我认为你的问题并不是关于if语句与三元组的关系。 我认为您可能正在寻找不同的数据结构,允许您以强大的DRY方式抽象条件。

有一些数据类型可以派上用场抽象。 例如,您可以使用AnyAll monoid来抽象相关条件。 你可以使用Either或者Maybe

您还可以查看ifElsecondwhenifElseifElse 你已经看过总和类型了。 这些都是特定环境中强大而有用的策略。

但根据我的经验,这些策略确实超出了观点。 在视图中,我们实际上希望可视化层次结构,以便了解它们将如何呈现。 所以三元组是一个很好的方法。

人们可以不同意“功能”的含义。 有人说功能编程是关于纯度或参考透明度; 其他人可能会说它只是“用功能编程”。 不同的社区有不同的解释。

因为FP对不同的人意味着不同的东西,所以我将专注于一个特定的属性,即声明性代码。

声明性代码在一个地方定义一个算法或一个值,并且不会强制改变或改变单独的部分。 声明要求写明的东西什么,而不是通过势在必行不同的代码路径名称分配值。 您的代码目前是声明性的,这很好! 声明性代码提供了保证:例如“此函数肯定会返回,因为return语句位于第一行”。

这是错误的观念,即三元组是嵌套的,而if语句则是扁平的。 这只是格式化的问题。

return ( 
  condition1
    ? result1
  : condition2
    ? result2
  : condition3
    ? result3
    : otherwise
)

将条件放在自己的行上,然后嵌套响应。 您可以根据需要多次重复此操作。 最后的“else”就像任何其他结果一样缩进,但它没有条件。 它可以根据您的需要扩展到尽可能多的情况。 我已经看到并用这样的许多扁平三元组写了视图,我发现更容易完全遵循代码,因为路径没有分开。

你可能会说if声明的可读性,但我认为再次可读意味着不同的事情不同的人。 所以要打开它,让我们考虑一下我们正在强调的内容

当我们使用三元时,我们强调只有一种可能的方式来声明或返回某些东西。 如果函数只包含表达式,那么我们的代码更可能作为公式读取,而不是公式的实现。

当我们使用if语句时,我们强调单独的,分开的步骤来产生输出。 如果您更愿意将您的观点视为单独的步骤,那么如果陈述有意义。 如果您希望将视图视为具有基于上下文的不同表示的单个实体,则三元组和声明性代码会更好。

总而言之,您的代码已经正常运行。 可读性和易读性是主观的,专注于您想要强调的内容。 不要觉得表达式中的多个条件是代码气味,它只是代表UI的复杂性,解决这个问题的唯一方法(如果需要解决)就是改变UI的设计。 UI代码被允许是复杂的,并且让代码诚实且代表所有潜在状态并不是一件容易的事。

求和类型和模式匹配

您可以使用sum类型和模式匹配来避免if/else语句。 由于Javascript不包含这些功能,您必须自己实现它们:

const match = (...patterns) => (...cons) => o => {
  const aux = (r, i) => r !== null ? cons[i](r)
   : i + 1 in patterns ? aux(patterns[i + 1](o), i + 1)
   : null;

  return aux(patterns[0](o), 0);
};

match需要一堆模式函数和构造函数和数据。 除非匹配,否则针对数据测试每个模式函数。 然后使用成功模式函数的结果调用相应的构造函数,并返回最终结果。

为了使match能够识别模式匹配是否不成功,模式必须实现一个简单的协议:每当模式不匹配时,该函数必须返回null 如果模式匹配但相应的构造函数是一个无效的构造函数,它必须只返回一个空的Object 这是微调框的模式函数:

({loading}) => loading ? {} : null

由于我们使用解构赋值来模仿模式匹配,因此我们必须将每个模式函数包装在try/catch块中,以避免在解构期间出现未捕获的错误。 因此,我们不是直接调用模式函数,而是使用特殊的应用程序:

const tryPattern = f => x => {
  try {
    return f(x);
  } catch (_) {
    return null;
  }
};

最后,这是一个微调框的构造函数。 它不需要参数并返回一个JSX微调器元素:

const Spinner = () => <Spinner />;

让我们把它们放在一起看看它是如何工作的:

 // main function const match = (...patterns) => (...cons) => x => { const aux = (r, i) => r !== null ? cons[i](r) : i + 1 in patterns ? aux(patterns[i + 1](x), i + 1) : null; return aux(patterns[0](x), 0); }; // applicator to avoid uncaught errors during destructuring const tryPattern = f => x => { try { return f(x); } catch (_) { return null; } }; // constructors const Spinner = () => "<Spinner />"; const NoResult = () => "<NoResult />"; const ListComponent = items => "<ListComponent items={items} />"; // sum type const List = match( tryPattern(({loading}) => loading ? {} : null), tryPattern(({items: {length}}) => length === 0 ? {} : null), tryPattern(({items}) => items !== undefined ? items : null) ); // mock data props1 = {loading: true, items: []}; props2 = {loading: false, items: []}; props3 = {loading: false, items: ["<Item />", "<Item />", "<Item />"]}; // run... console.log( List(Spinner, NoResult, ListComponent) (props1) // <Spinner /> ); console.log( List(Spinner, NoResult, ListComponent) (props2) // <NoResult /> ); console.log( List(Spinner, NoResult, ListComponent) (props3) // <ListComponent /> ); 

现在我们有一个List sum类型,它有三个可能的构造函数: SpinnerNoResultListComponent 输入( props )确定最终使用的构造函数。

如果List(Spinner, NoResult, ListComponent)对您来说仍然过于费力,并且您不希望显式List的各个状态,则可以在求和类型定义期间传递构造函数:

const List = match(
  tryPattern(({loading}) => loading ? {} : null),
  tryPattern(({items: {length}}) => length === 0 ? {} : null),
  tryPattern(({items}) => items)
) (
  Spinner,
  NoResult,
  ListComponent
);

现在你可以简单地以干燥的方式调用List(props1)等。

如果没有模式匹配,则match静默返回null。 如果您想要保证至少有一个模式成功匹配,您也可以抛出错误。

由于ifElse具有ifElse函数,您可以使用它以可重用的无点样式编写条件。

可运行的示例(使用字符串而不是<Tags>因此它可以作为堆栈代码运行)。

 const { compose, ifElse, always, prop, isEmpty } = R; const renderItems = ifElse(isEmpty, always('noResults'), always('listComponent')); const renderProps = ifElse( prop('loading'), always('spinner'), compose(renderItems, prop('items')) ); // usage: const elem = renderProps(this.props); // test console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] })); console.log(renderProps({ loading: false, items: [] })); console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] })); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script> 

当然,另一种选择是使用箭头函数和条件运算符将条件分成两个函数。 与上面的示例一样,这为您提供了可重用的renderItems函数:

 const renderItems = list => list.length ? 'listComponent' : 'noResults'; const renderProps = props => props.loading ? 'spinner' : renderItems(props.items); // usage: const elem = renderProps(this.props); // test console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] })); console.log(renderProps({ loading: false, items: [] })); console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] })); 

您不必为此安装额外的包:

content() {
  const {items, loading} = this.props
  if (loading) {
    return <Spinner />;
  }
  return items.length ? <ListComponent /> : <NoResult />;
}

render() {
  return this.content();
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM