web-dev-qa-db-ja.com

Jr. devsが読めないほど私は「賢い」のでしょうか? JSの関数型プログラミングが多すぎますか?

私はシニアフロントエンド開発者で、Babel ES6でコーディングしています。アプリの一部でAPI呼び出しが行われ、API呼び出しから返されるデータモデルに基づいて、特定のフォームに入力する必要があります。

これらのフォームは二重にリンクされたリストに保存されます(データの一部が無効であるとバックエンドが言った場合、ユーザーを混乱させた1つのページにすばやく戻してから、単にリスト。)

とにかく、ページを追加するために使用される関数がたくさんあり、私はあまりにも賢いのではないかと思っています。これは基本的な概要にすぎません。実際のアルゴリズムははるかに複雑で、さまざまなページやページタイプが含まれていますが、例を示します。

これは、初心者プログラマーが処理する方法だと私は思います。

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ステートメントをすべて取り、それらを個別のスタンドアロン関数にしてから、それらをマッピングします。

さて、テスト可能というのは1つですが、()もそうです。ここでは、可読性を下げているのでしょうか。

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

}

ここに私の懸念があります。 meへ下部はより整理されています。コード自体は、分離してテスト可能な小さなチャンクに分割されます。しかし、私が考えているのは、Identityファンクタ、カレー、または3項ステートメントを使用するなどの概念に慣れていない、ジュニア開発者としてそれを読まなければならない場合、後者のソリューションが何をしているかを理解することさえできるでしょうか?時々「間違った、簡単な」方法で物事を行う方が良いですか?

134
Brian Boyko

コードで、複数の変更を加えました。

  • pagesのフィールドにアクセスするための割り当ての構造化を解除することは良い変更です。
  • parseFoo()関数などの抽出は、おそらく良い変更です。
  • ファンクタを導入することは…非常に混乱します。

ここで最も混乱する部分の1つは、関数型プログラミングと命令型プログラミングをどのように混合するかです。ファンクタを使用すると、実際にデータを変換するのではなく、それを使用して可変リストをさまざまな関数に渡します。それは非常に有用な抽象化のようには見えません。すでにそのための変数があります。抽象化されている可能性のあるもの(存在する場合はその項目を解析するだけ)がコードに明示的に存在しますが、今は隅々まで考えなければなりません。たとえば、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リストの可変性は、複数の呼び出しに分散するのではなく、1つの関数にまとめられます。非ローカル変異を回避することは、Monadのようなおかしな名前の抽象化よりも、関数型プログラミングのはるかに大きな部分です。

だから、はい、あなたのコードは賢すぎました。賢いコードを書くのではなく、露骨な賢さの必要性を回避するための賢い方法を見つけるために、賢さを適用してください。最高のデザインはlook派手ではありませんが、それらを見た人には明白に見えます。また、プログラミングを簡素化するために優れた抽象化があり、最初に心の中で解く必要のある余分なレイヤーを追加するのではありません(ここで、ファンクターは変数と同等であり、効果的に省略できることを理解します)。

どうか、疑わしい場合は、コードを単純で愚かにしてください(KISSの原則)。

320
amon

疑問がある場合は、おそらく賢すぎるでしょう。 2番目の例はfoo ? parseFoo(foo) : x => xのような式で偶然の複雑さを導入しており、コード全体がより複雑であるため、追跡するのがより困難です。

チャンクを個別にテストできるとされている利点は、個々の関数に分割するだけで簡単に実現できます。 2番目の例では、別の方法で別々の反復を結合するため、実際にはless分離が得られます。

一般的に関数スタイルについてのあなたの気持ちが何であれ、これは明らかにそれがコードをより複雑にする例です。

単純でわかりやすいコードを「初心者の開発者」に関連付けているという点で、警告信号が少し見つかります。これは危険な考え方です。私の経験ではそれは逆です。初心者の開発者は、最も単純で最も明確なソリューションを見るにはより多くの経験を必要とするため、過度に複雑で巧妙なコードになりがちです。

「賢いコード」に対するアドバイスは、コードが初心者には理解できないかもしれない高度な概念を使用しているかどうかについての本当の意味ではありません。むしろ、それは必要以上に複雑または複雑であるコードを記述することについてです。これにより、コードはeverybodyでも、初心者でもエキスパートでも、またおそらく数か月後には自分でもフォローすることが難しくなります。

224
JacquesB

私のこの答えは少し遅れますが、私はまだ入りたいと思います。あなたが最新のES6技術を使用している、または最も人気のあるプログラミングパラダイムを使用しているからといって、必ずしもコードがより正確であること、またはそのジュニアのコードを意味するわけではありません間違っている。関数型プログラミング(またはその他の手法)は、実際に必要なときに使用する必要があります。最新のプログラミング手法をすべての問題に詰め込む最も小さなチャンスを見つけようとすると、常に過度に設計されたソリューションになってしまいます。

一歩下がって、解決しようとしている問題を少しの間口頭で表現してください。基本的に、関数addPagesapiDataのさまざまな部分を一連のキーと値のペアに変換し、それらすべてをPagesListに追加するだけです。

それがすべてである場合、なぜidentity functionternary 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 programmingとES6の構文は強力ですが、これらの手法を使って問題を間違った方法でスライスすると、さらに厄介なコードになってしまいます。

問題に突入せず、適切なテクニックを適切な場所に適用することで、コードが自然に機能し、すべてのチームメンバーが非常に整理され、保守しやすくなります。これらの特性は相互に排他的ではありません。

21
b0nyb0y

2番目のスニペットはnotで、最初のものよりもテスト可能です。 2つのスニペットのどちらか一方に必要なすべてのテストを設定するのは、かなり簡単です。

2つのスニペットの本当の違いは、わかりやすさです。最初のスニペットをすぐに読んで、何が起こっているのかを理解できます。 2番目のスニペット、それほどではありません。直感的ではなく、かなり長くなります。

これにより、最初のスニペットの保守が容易になります。これは、コードの貴重な品質です。 2番目のスニペットではほとんど価値がありません。

TD; DR

  1. 10分以内にコードをジュニア開発者に説明できますか?
  2. 今から2か月後、コードを理解できますか?

詳細分析

明快さと読みやすさ

元のコードは、どのレベルのプログラマーにとっても、非常に明確で理解しやすいものです。みんなに馴染みやすいスタイルです

読みやすさは、主に親しみに基づいており、トークンの数学的カウントではありません。 IMO、現時点では、書き換えにES6が多すぎます。おそらく2、3年以内に、私の答えのこの部分を変更します。 :-)ところで、私は@ 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()でのトリッキーな状況(ドメインに慣れていないために発生する可能性があります)がない限り、このセクションで特別なテストが必要な理由はわかりません。

一般に、関数全体をテストすると問題なく動作します。

免責事項:3つのif()ステートメントの8つの可能性すべてをテストする特別な理由がある場合は、テストを分割してください。または、PagesList.add()が機密である場合、はい、テストを分割します。

構造:(ガリアのように)3つの部分に分割する価値はありますか

ここにあなたは最高の議論があります。個人的には、元のコードは「長すぎる」とは思わない(私はSRP狂信者ではない)。しかし、いくつかのif (apiData.pages.blah)セクションがある場合、SRPは醜い頭をもたげ、分割する価値があります。特にDRYが適用され、関数がコードの他の場所で使用される可能性がある場合。

私の1つの提案

YMMV。コード行といくつかのロジックを保存するには、ifletを1行に結合します:eg

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

ApiData.pages.arrayOfBarsがNumberまたはStringの場合、これは失敗しますが、元のコードも失敗します。そして、私にはそれはより明確です(そして使いすぎたイディオム)。

3
user949300