私もジュニアの開発者が読めるように「賢い」のでしょうか?JSの機能プログラミングが多すぎますか?[閉まっている]


133

私はバベル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ステートメントをすべて取り、それらを分離し、スタンドアロンの機能にした後、それらをマップします。

さて、テスト可能というのは一つのことですが、読みやすいので、ここでは読みにくくしているのでしょうか。

// 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ファンクターの使用、カリー化、または3項ステートメントなどの概念に未使用のジュニア開発者としてそれを読む必要があった場合、後者のソリューションが何をしているのかを理解することさえできますか?時々「間違った、より簡単な」方法で物事を行う方が良いでしょうか?


13
唯一のJSで10年の自己教えを持っている人として、私は自分自身ジュニア検討すると、あなたはで私を失ったBabel ES6
RozzA

26
OMG-1999年から業界で活動し、1983年からコーディングを行っています。あなたは最も有害な開発者です。「賢い」と思うのは、実際には「高価」で「保守が難しい」、「バグの原因」と呼ばれ、ビジネス環境には場所がありません。最初の例はシンプルで理解しやすく、動作しますが、2番目の例は複雑で理解しにくく、証明できるほど正確ではありません。このようなことをやめてください。現実の世界には当てはまらない学術的な意味を除けば、それはましです。
user1068

15
ブライアン・カーニングハンをここで引用したい。「誰もがデバッグは、最初にプログラムを書くよりも2倍難しいことを知っている。だから、あなたがそれを書くときにできる限り賢いなら、どのようにそれをデバッグするだろうか? 」- en.wikiquote.org/wiki/Brian_Kernighan、第2版、第2章/「プログラミングスタイルの要素」
MarkusSchaber

7
@Logister Coolnessは、単純さほど主要な目的ではありません。ここでの異議は、コードの推論を難しくし、予期しないコーナーケースを含む可能性が高いため、正当性の敵(確実に最大の懸念)である必要複雑さです。実際にテストする方が簡単であるという以前の主張に対する懐疑論を考えると、このスタイルに対する説得力のある議論は見ていません。セキュリティに関する最小限の特権のルールと同様に、おそらく、強力な言語機能を使用して簡単なことを行うことに注意する必要があるという経験則があるかもしれません。
-sdenham

6
コードはジュニアコードのように見えます。先輩が最初の例を書くことを期待しています。
sed

回答:


322

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

  • のフィールドにアクセスするための割り当てを破壊するの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リストの可変性は、複数の呼び出しに分散するのではなく、単一の関数にまとめられています。非局所的な突然変異を避けることは、のような面白い名前の抽象化よりも、関数型プログラミングのはるかに大きな部分ですMonad

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

どうか:疑わしい場合は、コードをシンプルで愚かにしてください(KISS原則)。


2
対称性の観点から見ると、何let pages = Identity(pagesList)が違うのparseFoo(foo)ですか?ことを考えると、私はおそらく持っているでしょう... {Identity(pagesList), parseFoo(foo), parseBar(bar)}.flatMap(x -> x)
ARTS

8
マップされたリストを収集するために3つのネストされたラムダ式を(私の訓練されていない目で)持つのは少し賢すぎるかもしれないことを説明してくれてありがとう。
トールビョーンラヴンアンデルセン

2
コメントは詳細なディスカッション用ではありません。この会話はチャットに移動さました
ヤンニス

あなたの2番目の例では流なスタイルがうまくいくかもしれませんか?
user1068

225

疑問がある場合は、おそらくあまりにも賢いです!2番目の例では、などの式を使用して偶発的な複雑性を導入しfoo ? parseFoo(foo) : x => xていますが、全体的にコードはより複雑であるため、追跡が難しくなります。

チャンクを個別にテストできるという目的の利点は、個々の機能に分割するだけで、より簡単な方法で実現できます。そして、2番目の例では、それ以外の場合は別々の反復を結合するため、実際に分離が少なくなります。

一般的な機能スタイルに対するあなたの気持ちがどうであれ、これは明らかにそれがコードをより複雑にする例です。

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

「賢いコード」に対するアドバイスは、コードが初心者が理解できないような高度な概念を使用しているかどうかについてではありません。むしろ、必要以上に複雑または複雑なコードを記述することです。これにより、初心者、専門家、すべての人にとって、そしておそらく数か月後の自分にとって、コードを追跡するのが難しくなります。


156
「初心者の開発者は、非常に複雑で賢いコードを使用する傾向があります。最も単純で明確なソリューションを見るには、より多くの経験が必要です」と、あなたは同意できません。優れた答え!
ボニファシオ

23
過度に複雑なコードも非常に受動的で攻撃的です。あなたは意図的に他の人が簡単に読んだりデバッグしたりできないコードを作成しています...これは、あなたにとって仕事のセキュリティを意味し、あなたが不在である他のすべての人にとっては地獄です。技術文書をすべてラテン語で書くこともできます。
イヴァン

14
巧妙なコードが常に自慢できるものだとは思いません。時々それは自然に感じ、二度目の検査ではとんでもないように見えます。

5
「見せびらかす」という言い回しは、意図したものよりも判断力があると思われるため削除しました。
ジャックB

11
@BaileyS-コードレビューの重要性を強調していると思います。特に徐々にそのように開発された場合、コーダーにとって自然で簡単なことは、レビュー担当者にとって複雑に思われるかもしれません。その後、コードは、畳み込みを削除するためにリファクタリング/リライトされるまでレビューに合格しません。
マイルズ

21

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

一歩下がって、解決しようとしている問題を一瞬言葉で表現してください。本質的には、のaddPagesさまざまな部分apiDataをキーと値のペアのセットに変換し、それらのすべてをに追加する関数が必要ですPagesList

そして、それがそれだけであるのなら、なぜidentity functionwith を使用するのternary operatorfunctor、入力解析に使用するのを悩ますのですか?また、(リストを変更することで)副作用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テクニックを使用している、または最も人気のあるプログラミングパラダイムを使用しているからといって、必ずしもコードが正確であることを意味するわけではありませんが、またはそのジュニアのコードが間違っている。」
ジョルジオ

+1。ちょっとした言い回しです。_。concatの代わりにspread(...)演算子を使用して、その依存関係を削除できます。
-YoTengoUnLCD

1
@YoTengoUnLCDああ、いいキャッチ。今、あなたは私と私のチームがまだ私たちのlodash使用法の一部を学ぼうとしていることを知っています。そのコードはを使用できますが、コードの形状をそのままにしたい場合spread operatorでも使用できます[].concat()
b0nyb0y

申し訳ありませんが、このコードのリストは、OPの投稿にある元の「ジュニアコード」よりもはるかにわかりにくいものです。基本的に:回避できる場合は、三項演算子を使用しないでください。緊張しすぎます。「実際の」関数型言語では、ifステートメントはステートメントであり、ステートメントではないため、より読みやすくなります。
オルレハーステット

@OlleHärstedtUmm、それはあなたがした興味深い主張です。問題は、関数型プログラミングのパラダイム、またはそこにあるパラダイムは、特定の「実際の」関数型言語に縛られることはなく、その構文よりもはるかに少ないということです。したがって、条件付きコンストラクトをどのように使用するか、または「決して」使用しないかを指定するだけでは意味がありません。A ternary operatorは、if好むと好まざるとにかかわらず、通常のステートメントと同じくらい有効です。if-else?:キャンプの間の読みやすさの議論は終わりがありませんので、私はそれには入りません。私が言うすべては、訓練された目で、これらのような線はほとんど「緊張しすぎ」ではありません。
-b0nyb0y

5

第二スニペットがあるではない最初よりもテスト可能。2つのスニペットのいずれか1つに必要なすべてのテストをセットアップすることは、合理的です。

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

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


3

TD; DR

  1. 10分以内にコードをジュニア開発者に説明できますか?
  2. 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()にヌル、重複、またはその他の入力に関する異常な問題がない限り。

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が適用され、コードの他の場所で関数を使用できる場合。

私の提案

YMMV。コードの行といくつかのロジックを保存するには、私は組み合わせ可能性がある場合をしてみましょう 1行に: 例えば

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.