我是否也“聪明”,无法被小开发人员阅读?我的JS中的函数式编程过多?[关闭]


133

我是一名高级前端开发人员,使用Babel ES6进行编码。我们的应用程序的一部分进行了API调用,根据我们从API调用获得的数据模型,需要填写某些表格。

这些表格存储在一个双向链接的列表中(如果后端说某些数据无效,我们可以通过简单地修改清单。)

无论如何,有很多用于添加页面的函数,我想知道我是否太聪明了。这只是一个基本概述-实际的算法要复杂得多,有大量不同的页面和页面类型,但这将为您提供示例。

我认为,这是新手程序员处理该问题的方式。

export const addPages = (apiData) => {
   let pagesList = new PagesList(); 

   if(apiData.pages.foo){
     pagesList.add('foo', apiData.pages.foo){
   }

   if (apiData.pages.arrayOfBars){
      let bars = apiData.pages.arrayOfBars;
      bars.forEach((bar) => {
         pagesList.add(bar.name, bar.data);
      })
   }

   if (apiData.pages.customBazes) {
      let bazes = apiData.pages.customBazes;
      bazes.forEach((baz) => {
         pagesList.add(customBazParser(baz)); 
      })
   } 

   return pagesList;
}

现在,为了更具可测试性,我采用了所有这些if语句,并将它们分离为独立的函数,然后将它们映射。

现在,可测试是一回事,但可读性也是一回事,我想知道我是否在这里使事情变得不那么可读。

// file: '../util/functor.js'

export const Identity = (x) => ({
  value: x,
  map: (f) => Identity(f(x)),
})

// file 'addPages.js' 

import { Identity } from '../util/functor'; 

export const parseFoo = (data) => (list) => {
   list.add('foo', data); 
}

export const parseBar = (data) => (list) => {
   data.forEach((bar) => {
     list.add(bar.name, bar.data)
   }); 
   return list; 
} 

export const parseBaz = (data) => (list) => {
   data.forEach((baz) => {
      list.add(customBazParser(baz)); 
   })
   return list;
}


export const addPages = (apiData) => {
   let pagesList = new PagesList(); 
   let { foo, arrayOfBars: bars, customBazes: bazes } = apiData.pages; 

   let pages = Identity(pagesList); 

   return pages.map(foo ? parseFoo(foo) : x => x)
               .map(bars ? parseBar(bars) : x => x)
               .map(bazes ? parseBaz(bazes) : x => x)
               .value

}

这是我的担心。对我来说,底层更有条理。代码本身分为较小的块,可以单独进行测试。但是我在想:如果我必须作为一个初级开发人员来阅读它,而不使用诸如使用Identity函子,currying或三元语句之类的概念,我什至能够理解后者的解决方案在做什么?有时候以“错误,容易”的方式做事更好吗?


13
作为只在JS方面有10年自我教学经验的人,我会以为自己是Jr.,而您在Babel ES6
RozzA

26
OMG-自1999年以来一直活跃于行业,自1983年以来一直从事编码,您是目前最有害的开发人员。您认为“聪明”的人实际上被称为“昂贵的”,“难以维护的”和“错误的源头”,并且在商业环境中没有地位。第一个示例很简单,易于理解,并且可以正常工作,而第二个示例很复杂,很难理解,并且无法证明正确。请停止做这种事情。它不是更好,除非在某种学术意义上可能不适用于现实世界。
user1068

15
我只想在这里引用Brian Kerninghan的话:“每个人都知道调试首先要比编写程序难一倍。因此,如果您在编写程序时表现出最大的聪明,那么您将如何调试它呢? ” - en.wikiquote.org/wiki/Brian_Kernighan /,第2版,第2章“的编程风格的要素”
MarkusSchaber

7
@Logister Coolness不再是简单性的主要目标。此处的反对意见是不必要的复杂性,这是正确性的敌人(肯定是首要问题),因为它使代码更难以推理,并且更有可能包含意外的极端情况。考虑到我先前所说的对实际上更容易测试的说法的怀疑,我没有看到任何令人信服的关于这种风格的论点。类似于安全性最低特权规则,也许会有一条经验法则说,应该警惕使用强大的语言功能来完成简单的事情。
sdenham

6
您的代码看起来像初级代码。我希望一个长者写第一个例子。
sed

Answers:


322

在代码中,您进行了多项更改:

  • 销毁对中访问字段的分配pages是一个很好的更改。
  • 提取parseFoo()功能等可能是一个很好的变化。
  • 引入函子非常令人困惑。

这里最令人困惑的部分之一是如何混合功能和命令式编程。使用函子并不需要真正转换数据,而是使用它通过各种功能传递可变列表。这似乎不是一个非常有用的抽象,我们已经有了一些变量。应该应该抽象的东西(如果存在则仅对其进行分析)仍然明确地存在于您的代码中,但是现在我们必须三思而后行。例如,parseFoo(foo)返回函数有点不明显。JavaScript没有静态类型系统来通知您这是否合法,因此,如果没有更好的名称(makeFooParser(foo)?),此类代码实际上很容易出错。在这种混淆中,我看不到任何好处。

我希望看到的是:

if (foo) parseFoo(pages, foo);
if (bars) parseBar(pages, bars);
if (bazes) parseBaz(pages, bazes);
return pages;

但这也不理想,因为从呼叫站点尚不清楚这些项目将被添加到页面列表中。相反,如果解析函数是纯函数,并返回一个我们可以显式添加到页面的(可能为空)列表,那可能会更好:

pages.addAll(parseFoo(foo));
pages.addAll(parseBar(bars));
pages.addAll(parseBaz(bazes));
return pages;

额外的好处:现在将有关将项目为空时的操作的逻辑移到各个解析函数中。如果不合适,您仍然可以引入条件句。pages现在,列表的可变性被整合到一个函数中,而不是将其分散到多个调用中。避免非局部突变是函数编程的一部分,比带有诸如的有趣名称的抽象要大得多Monad

所以是的,您的代码太聪明了。请运用您的聪明才智,而不是编写聪明的代码,而是寻找聪明的方法来避免需要公然的聪明才智。最好的设计看起来并不花哨,但是对任何看到它们的人来说都是显而易见的。良好的抽象可以简化编程,而无需添加我必须首先在脑海中解开的多余层(在这里,要确定函子等效于变量,并且可以有效地消除它)。

请:如果有疑问,请保持您的代码简单而愚蠢(KISS原则)。


2
从对称的角度来看,有什么let pages = Identity(pagesList)不同parseFoo(foo)?鉴于这种情况,我可能会... {Identity(pagesList), parseFoo(foo), parseBar(bar)}.flatMap(x -> x)
ArTs

8
谢谢你的解释具有用于收集映射表(我的外行人)三个嵌套lambda表达式可能是有点聪明了。
托尔比约恩Ravn的安德森

2
评论不作进一步讨论;此对话已转移至聊天
yannis

也许流利的风格在您的第二个示例中会很好?
user1068

225

如果您有疑问,它可能太聪明了!第二个示例使用类似的表达式引入了意外的复杂性foo ? parseFoo(foo) : x => x,并且总体而言,代码更加复杂,这意味着很难遵循。

所谓的好处是可以单独测试各个块,可以通过简单地分解成单个函数来实现。在第二个示例中,您将原本分开的迭代进行耦合,因此实际上得到的隔离较少

无论您对功能样式的总体感觉如何,这显然都是使代码更复杂的示例。

我发现一些警告信号是,您将简单直接的代码与“新手开发人员”相关联。这是一种危险的心态。以我的经验是相反的:新手开发人员倾向于过于复杂和聪明的代码,因为它需要更多的经验才能看到最简单,最清晰的解决方案。

反对“聪明的代码”的建议并不是关于该代码是否使用了新手可能不理解的高级概念。而是要编写比必要的代码更复杂或更复杂的代码。这使得每个人(无论是新手还是专家)都难以遵循该代码,甚至可能几个月后也无法遵循。


156
“新手开发人员倾向于使用过于复杂和聪明的代码,因为它需要更多的经验才能看到最简单,最清晰的解决方案。” 优秀的答案!
Bonifacio

23
过于复杂的代码也是相当被动的。您正在故意生成很少有其他人可以轻松阅读或调试的代码...这对您而言意味着工作安全,而在您不在的情况下对其他所有人完全不利。您也可以完全用拉丁文写技术文档。
伊万(Ivan)

14
我不认为聪明的代码总是炫耀的东西。有时感觉很自然,并且仅在第二次检查时看起来很荒谬。

5
我删除了关于“炫耀”的措辞,因为它听起来比预期的更具判断力。
JacquesB

11
@BaileyS-我认为这强调了代码审查的重要性;对于编码人员来说,自然而直接的感觉,特别是当以这种方式逐渐发展时,对于审阅者来说似乎很容易被弄乱。直到重构/重写以消除卷积,代码才通过审核。
Myles

21

我的答案来得有点晚,但我仍然想插话。仅仅因为您使用的是最新的ES6技术或使用最受欢迎的编程范例,并不一定意味着您的代码更正确,或者您的初级代码是错的。实际需要时,应使用函数式编程(或任何其他技术)。如果您试图找到将最新的编程技术塞入每个问题的最小机会,那么您总是会得到过度设计的解决方案。

退后一步,尝试说出您要解决的问题。本质上,您只需要一个函数addPages即可将中的不同部分apiData转换为一组键值对,然后将其全部添加到中PagesList

如果仅此而已,为什么还要麻烦地使用identity functionwith ternary operator或将其functor用于输入解析?此外,您为什么甚至认为应用适当方法functional programming会产生副作用(通过更改列表)呢?为什么所有这些东西,当您需要的只是这个:

const processFooPages = (foo) => foo ? [['foo', foo]] : [];
const processBarPages = (bar) => bar ? bar.map(page => [page.name, page.data]) : [];
const processBazPages = (baz) => baz ? baz.map(page => [page.id, page.content]) : [];

const addPages = (apiData) => {
  const list = new PagesList();
  const pages = [].concat(
    processFooPages(apiData.pages.foo),
    processBarPages(apiData.pages.arrayOfBars),
    processBazPages(apiData.pages.customBazes)
  );
  pages.forEach(([pageName, pageContent]) => list.addPage(pageName, pageContent));

  return list;
}

这里是一个可运行的jsfiddle )

如您所见,此方法仍然使用functional programming,但要适度。另请注意,由于所有3个转换函数均不会引起任何副作用,因此它们很容易测试。本书中的代码addPages也很琐碎,没有任何假设,新手或专家只需看一眼就能理解。

现在,将此代码与上面的代码进行比较,您看到区别了吗?毫无疑问,functional programmingES6语法功能强大,但是如果使用这些技术以错误的方式切分问题,最终甚至会得到更混乱的代码。

如果您不急于解决问题,并在正确的位置应用正确的技术,则可以拥有在自然界中起作用的代码,而所有团队成员仍然可以很好地组织和维护它们。这些特征不是互斥的。


2
+1指出这种广泛的态度(不一定适用于OP):“仅仅因为您使用的是最新的ES6技术或使用了最流行的编程范例,并不一定意味着您的代码更正确,或那个初中的代码是错误的。”。
Giorgio

+1。只是一小段花言巧语,您可以使用spread(...)运算符代替_.concat来删除该依赖项。
YoTengoUnLCD

1
@YoTengoUnLCD啊,很好。现在您知道我和我的团队仍在学习一些lodash使用上。该代码可以使用spread operator,或者即使[].concat()您想保持代码的形状完整。
b0nyb0y

抱歉,但是此代码清单对我而言仍然不如OP帖子中的原始“初级代码”明显。基本上:如果可以避免,请勿使用三元运算符。太紧张了 在“实际”功能语言中,if语句将是表达式而不是语句,因此更具可读性。
OlleHärstedt17年

@OlleHärstedt嗯,这是您提出的一个有趣的主张。关键是,函数式编程范式或那里的任何范式永远都不会与任何特定的“真实”功能语言联系在一起,更不用说其语法了。因此,规定应该或不应该使用什么条件构造只是没有任何意义。无论您是否喜欢,A ternary operator与常规if语句一样有效。之间的可读性辩论if-else?:训练营是没有止境的,所以我不会进入它。我要说的是,用训练有素的眼睛,像这样的线条很难“太紧”。
b0nyb0y

5

第二个片段是不是比第一次更容易测试。为两个摘要之一设置所有所需的测试将是相当简单的。

这两个摘要之间的真正区别在于可理解性。我可以相当快地阅读第一个代码片段,并了解发生了什么。第二个片段,不是很多。它不那么直观,而且更长。

这使得第一个代码片段易于维护,这是有价值的代码质量。我发现第二个片段的价值很小。


3

TD; DR

  1. 您能在10分钟或更短的时间内向Junior Developer解释您的代码吗?
  2. 从现在开始的两个月后,您能理解您的代码吗?

详细分析

清晰度和可读性

对于任何级别的程序员而言,原始代码都非常清晰易懂。它以大家都熟悉的风格。

可读性主要基于熟悉程度,而不是令牌的一些数学计算。IMO,在此阶段,您的ES6重写过多。也许几年后,我会更改答案的这一部分。:-)顺便说一句,我也喜欢@ b0nyb0y答案,这是一个合理而明确的折衷方案。

可测性

if(apiData.pages.foo){
   pagesList.add('foo', apiData.pages.foo){
}

假设PagesList.add()具有测试(应该进行测试),则这是完全简单的代码,并且本节没有明显的理由需要进行特殊的单独测试。

if (apiData.pages.arrayOfBars){
      let bars = apiData.pages.arrayOfBars;
      bars.forEach((bar) => {
         pagesList.add(bar.name, bar.data);
      })
   }

同样,我认为本部分无需立即进行任何特殊的单独测试。除非PagesList.add()出现异常,包括null或重复项或其他输入。

if (apiData.pages.customBazes) {
      let bazes = apiData.pages.customBazes;
      bazes.forEach((baz) => {
         pagesList.add(customBazParser(baz)); 
      })
   } 

此代码也非常简单。假设customBazParser已经过测试并且没有返回太多的“特殊”结果。再说一次,除非使用PagesList.add()遇到棘手的情况(可能是因为我对您的域不熟悉,否则可能会出现这种情况),我不明白为什么本节需要进行特殊测试。

通常,测试整个功能应该可以正常工作。

免责声明:如果出于特殊原因要测试这三个if()语句的所有8种可能性,那么可以,请拆分测试。或者,如果PagesList.add()敏感,可以,请拆分测试。

结构:是否值得分为三个部分(例如高卢)

在这里,您有最好的说法。就我个人而言,我不认为原始代码太长(我不是SRP的狂热者)。但是,如果还有更多if (apiData.pages.blah)部分,则SRP会抬起头来,这很值得一提。特别是如果应用了DRY,并且该功能可以在代码的其他地方使用。

我的一个建议

YMMV。为了节省一行代码和一些逻辑,我可以将iflet组合成一行: 例如

let bars = apiData.pages.arrayOfBars || [];
bars.forEach((bar) => {
   pagesList.add(bar.name, bar.data);
})

如果apiData.pages.arrayOfBars是数字或字符串,则此操作将失败,但是原始代码也将失败。对我来说,这更清楚了(也是一个过度使用的习惯用法)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.