Чи я занадто "розумний", щоб його читали молодші розробники? Занадто багато функціонального програмування в моєму 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;
}

Тепер, щоб бути більш перевіреним, я взяв усі ці дані, якщо заяви, і зробив їх окремими, окремими функціями, а потім переглядаю їх.

Тепер тестування - це одне, але це читається, і мені цікаво, чи я роблю тут речі менш читаними.

// 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 Functors, currying або потрійні заяви, я міг би навіть зрозуміти, що робить останнє рішення? Чи краще іноді робити речі "неправильним, легшим" способом?


13
як хтось, хто має лише 10 років самонавчання в JS, я вважав би себе молодшим, і ти втратив мене вBabel ES6
RozzA

26
OMG - працює в промисловості з 1999 року і кодує з 1983 року, і ви є найбільш шкідливим видом розробників. Те, що ви вважаєте "розумним", насправді називають "дорогим" і "важким для обслуговування" та "джерелом помилок", і йому немає місця в бізнес-середовищі. Перший приклад простий, легкий для розуміння, і він працює, тоді як другий приклад складний, важкий для розуміння і не доказується правильний. Будь ласка, перестаньте робити подібні речі. НЕ краще, хіба що, можливо, в якомусь академічному сенсі, який не стосується реального світу.
користувач1068

15
Я просто хочу тут процитувати Брайана Кернінгана: "Усі знають, що налагодження в першу чергу важче, ніж написання програми в першу чергу. Тож якщо ти настільки розумний, як ти можеш бути, коли пишеш її, як ти коли-небудь налагодиш її? " - en.wikiquote.org/wiki/Brian_Kernighan / "Елементи стилю програмування", 2-е видання, глава 2.
MarkusSchaber

7
@ Логістична прохолода - це не головна мета, ніж простота. Заперечення тут полягає в безперервній складності, яка є ворогом коректності (безумовно, першочерговою проблемою), оскільки це робить код важче міркувати і, швидше за все, містити несподівані кутові випадки. Враховуючи раніше заявлений скептицизм тверджень, що насправді простіше перевірити, я не бачив жодного переконливого аргументу щодо цього стилю. Аналогічно правилу безпеки найменших привілеїв, можливо, може існувати правило, яке говорить про те, що потрібно робити обережність у використанні потужних мовних функцій, щоб робити прості речі.
sdenham

6
Ваш код схожий на молодший код. Я б очікував, що старший напише перший приклад.
sed

Відповіді:


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
Дякую за пояснення , що має три вкладених лямбда - вираження для збору відображений список (на мій недосвідчений очей) може бути трохи занадто розумним.
Thorbjørn Ravn Andersen

2
Коментарі не для розширеного обговорення; ця розмова переміщена до чату .
янніс

Може, вільний стиль буде добре працювати у вашому другому прикладі?
user1068

225

Якщо ви сумніваєтесь, це, мабуть, занадто розумно! Другий приклад вводить випадкову складність з виразами типу foo ? parseFoo(foo) : x => x, і в цілому код є більш складним, що означає, що його важче дотримуватися.

Передбачувана вигода, яку ви можете перевірити на шматки окремо, можна було досягти більш простим способом, просто розбившись на окремі функції. А у другому прикладі ви з'єднуєте інакше окремі ітерації, тому ви отримуєте меншу ізоляцію.

Якими б не були ваші почуття щодо функціонального стилю загалом, це очевидно приклад, коли він робить код складнішим.

Я знаходжу трохи попереджувального сигналу в тому, що ви асоціюєте простий і простий код з "розробниками-початківцями". Це небезпечний менталітет. На мій досвід, все навпаки: розробники-початківці схильні до надмірно складного та розумного коду, тому що йому потрібно більше досвіду, щоб можна було побачити найпростіше і зрозуміліше рішення.

Порада проти "розумного коду" насправді не стосується того, використовує чи не в коді передові поняття, які новачок може не зрозуміти. Скоріше йдеться про написання коду, який є складнішим або складнішим, ніж потрібно . Це ускладнює дотримання коду як для всіх , так і для початківців та експертів, а також, можливо, також і для себе за кілька місяців.


156
"Новачки-розробники схильні до надмірно складного та розумного коду, оскільки для отримання необхідного найпростішого та найяснішого рішення потрібен більший досвід", не може з вами більше погодитися. Відмінна відповідь!
Боніфасіо

23
Надмірно складний код також досить пасивно-агресивний. Ви навмисно виробляєте код, який мало хто може прочитати чи налагодити легко ... що означає безпеку роботи для вас та повний пекло для всіх інших у вашій відсутності. Ви також можете написати свою технічну документацію повністю латиною.
Іван

14
Я не думаю, що розумний код - це завжди показ речі. Іноді це виглядає природно, і сміливо виглядає лише при повторному огляді.

5
Я видалив фразу про "демонстрацію", оскільки це звучало більше, ніж задумано.
ЖакБ

11
@BaileyS - я думаю, що підкреслює важливість перегляду коду; те, що почуває себе природним і прямим для кодера, особливо коли поступово розвивається таким чином, може легко здатися перекрученим для рецензента. Код потім не проходить перевірку, поки не буде відновлений / переписаний для видалення згортки.
Майлз

21

Ця моя відповідь приходить трохи пізно, але я все одно хочу передзвонити. Тільки тому, що ви використовуєте новітні методи ES6 або використовуєте найпопулярнішу парадигму програмування, не обов'язково означає, що ваш код правильніший або код цього молодшого неправильно. Функціональне програмування (або будь-яка інша техніка) слід використовувати, коли це фактично потрібно. Якщо ви спробуєте знайти найдрібніший шанс втягнути новітні методи програмування у кожну проблему, ви завжди будете готові до надмірно розробленого рішення.

Зробіть крок назад і спробуйте вербалізувати проблему, яку ви намагаєтеся вирішити на секунду. По суті, ви просто хочете, щоб функція addPagesперетворила різні частини apiDataв набір пар ключових значень, а потім додала їх у PagesList.

І якщо це все є на нього, навіщо використовуючи identity functionз 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 programmingі синтаксиси ES6 є потужними, але якщо ви вирішите проблему неправильно за допомогою цих методів, ви отримаєте рівний код Messier.

Якщо ви не поспішаєте з проблемою і застосовуєте правильні прийоми в потрібних місцях, ви можете мати код, який є функціональним за своєю суттю, хоча все-таки є дуже організованим та підтримуваним усіма членами команди. Ці характеристики не є взаємовиключними.


2
+1 за вказівку на таке широко розповсюджене ставлення (не обов'язково стосується ОП): "Тільки тому, що ви використовуєте новітні методи ES6 або використовуєте найпопулярнішу парадигму програмування, не обов'язково означає, що ваш код є більш правильним, або код цього молодшого є неправильним. "
Джорджіо

+1. Лише невелике педантичне зауваження, ви можете використовувати оператор розповсюдження (...) замість _.concat, щоб усунути цю залежність.
YoTengoUnLCD

1
@YoTengoUnLCD Ах, хороший улов. Тепер ви знаєте, що я та моя команда все ще продовжуємо навчатись нашому lodashвикористанню. Цей код можна використовувати spread operatorабо навіть [].concat()якщо хтось хоче зберегти форму коду недоторканою.
b0nyb0y

Вибачте, але цей перелік коду для мене все ще набагато менш очевидний, ніж оригінальний "молодший код" у пості ОП. В основному: ніколи не використовуйте потрійного оператора, якщо можете уникнути цього. Це занадто напружено. У "справжній" функціональній мові, if-заяви будуть виразами, а не висловлюваннями, а отже, більш читабельними.
Olle Härstedt

@ OlleHärstedt Umm, це цікава заява, яку ви зробили. Вся справа в тому, що парадигма функціонального програмування або будь-яка парадигма там ніколи не прив’язана до якоїсь конкретної "реальної" функціональної мови, тим більше, до її синтаксису. Таким чином, диктувати, якими умовними конструкціями слід або ніколи не слід користуватися просто не має сенсу. A ternary operatorє таким же дійсним, як і звичайне ifтвердження, подобається вам це чи ні. Спори між читаністю if-elseі ?:табором ніколи не закінчується, так що я не буду вдаватися в нього. Все, що я скажу, це, з навченими очима, такі лінії навряд чи є "занадто напруженими".
b0nyb0y

5

Другий фрагмент не є більш перевіреним, ніж перший. Було б досить просто встановити всі необхідні тести для будь-якого з двох фрагментів.

Справжня різниця між двома фрагментами - це зрозумілість. Я можу досить швидко прочитати перший фрагмент і зрозуміти, що відбувається. Другий фрагмент, не так вже й багато. Це набагато менш інтуїтивно зрозуміло, а також істотно довше.

Це полегшує обслуговування першого фрагмента, що є цінною якістю коду. Я знаходжу дуже мало значення у другому фрагменті.


3

TD; DR

  1. Чи можете ви пояснити свій код Молодшому розробнику за 10 хвилин чи менше?
  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 (), (що могло бути, оскільки я не знайомий з вашим доменом), я не розумію, чому цей розділ потребує спеціального тестування.

Взагалі, тестування всієї функції повинно працювати добре.

Відмова від відповідальності : Якщо для перевірки всіх 8 можливостей трьох if()тверджень є спеціальні причини , тоді так, розділіть тести. Або, якщо PagesList.add()чутливий, так, розділіть тести.

Структура: чи варто розбиватися на три частини (як Галія)

Ось у вас найкращий аргумент. Особисто я не думаю, що оригінальний код є "занадто довгим" (я не фанатик SRP). Але, якби було ще кілька if (apiData.pages.blah)розділів, то SRP значить, що це некрасива голова, і її варто було б розділити. Особливо, якщо застосовується DRY і функції можуть використовуватися в інших місцях коду.

Моя одна пропозиція

YMMV. Щоб зберегти рядок коду та деяку логіку, я можу об'єднати if і let в один рядок: напр

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

Це не вдасться, якщо apiData.pages.arrayOfBars є числом або рядком, але так буде і оригінальний код. І мені це зрозуміліше (і завищена ідіома).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.