Я слишком «умен», чтобы быть доступным для чтения младшим разработчикам? Слишком много функционального программирования в моем 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

}

Вот моя забота. Для меня дно более организовано. Сам код разбит на более мелкие куски, которые можно тестировать изолированно. НО я думаю: если бы мне пришлось читать это как начинающий разработчик, не привыкший к таким понятиям, как использование функторов идентификации, карри или троичных выражений, смог бы я понять, что делает последнее решение? Лучше ли иногда делать вещи «неправильно, проще»?

Брайан Бойко
источник
13
как человек, у которого только 10 лет самообучения в JS, я бы считал себя младшим, а ты потерял меня наBabel ES6
RozzA
26
OMG - работает в индустрии с 1999 года, а с 1983 года занимается кодированием, и вы - самый вредный из разработчиков. То, что вы считаете «умным», на самом деле называют «дорогим», «трудно обслуживаемым» и «источником ошибок», и ему нет места в бизнес-среде. Первый пример прост, легок для понимания и работает, в то время как второй пример сложен, труден для понимания и недопустимо корректен. Пожалуйста, прекратите делать такие вещи. Это НЕ лучше, разве что в каком-то академическом смысле это не относится к реальному миру.
user1068
15
Я просто хочу процитировать Брайана Кернингхана здесь: «Все знают, что отладка в два раза сложнее, чем писать программу. Итак, если вы настолько умны, насколько это возможно, когда вы пишете ее, как вы будете ее отлаживать? " - en.wikiquote.org/wiki/Brian_Kernighan / «Элементы стиля программирования», 2-е издание, глава 2.
MarkusSchaber
7
@Logister Coolness - не более главная цель, чем простота. Возражение здесь заключается в необоснованной сложности, которая является врагом правильности (безусловно, главной задачей), потому что она затрудняет рассуждение кода и с большей вероятностью содержит неожиданные угловые случаи. Учитывая мой ранее высказанный скептицизм в отношении утверждений о том, что на самом деле это проще для тестирования, я не видел убедительных аргументов в пользу этого стиля. По аналогии с правилом наименьших привилегий в отношении безопасности, возможно, может быть практическое правило, которое говорит, что следует опасаться использования мощных языковых функций для выполнения простых вещей.
Сденхэм
6
Ваш код выглядит как младший код. Я ожидаю, что старший напишет первый пример.
СЭД

Ответы:

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).
Артс
8
Спасибо за объяснение того, что наличие трех вложенных лямбда-выражений для сбора сопоставленного списка (на мой неопытный взгляд) может быть слишком умным.
Турбьерн Равн Андерсен
2
Комментарии не для расширенного обсуждения; этот разговор был перемещен в чат .
Яннис
Может быть, свободный стиль будет хорошо работать во втором примере?
user1068
225

Если вы сомневаетесь, это, вероятно, слишком умно! Второй пример вводит случайную сложность с подобными выражениями foo ? parseFoo(foo) : x => x, и в целом код является более сложным, что означает, что ему труднее следовать.

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

Что бы вы ни думали о функциональном стиле в целом, это, безусловно, пример, когда он делает код более сложным.

Я нахожу немного предупреждающего сигнала в том, что вы ассоциируете простой и понятный код с «начинающими разработчиками». Это опасный менталитет. По моему опыту все наоборот: разработчики-новички склонны к чрезмерно сложному и умному коду, потому что для того, чтобы увидеть самое простое и понятное решение, требуется больше опыта.

Совет против «умного кода» на самом деле не о том, использует ли код передовые концепции, которые новичок может не понять. Скорее речь идет о написании кода, который является более сложным или запутанным, чем необходимо . Это усложняет выполнение кода для всех , как для новичков, так и для экспертов, а также, возможно, и для вас самих через несколько месяцев.

JacquesB
источник
156
«Начинающие разработчики склонны к чрезмерно сложному и умному коду, потому что для того, чтобы увидеть самое простое и ясное решение, требуется больше опыта», не могу с вами больше согласиться. Отличный ответ!
Бонифачо
23
Слишком сложный код также довольно пассивно-агрессивный. Вы сознательно создаете код, который немногие могут легко прочитать или отладить ... что означает безопасность работы для вас и настоящий ад для всех остальных в ваше отсутствие. Вы также можете написать свою техническую документацию полностью на латыни.
Иван
14
Я не думаю, что умный код - это всегда хвастовство. Иногда это кажется естественным и выглядит смешно только при втором осмотре.
5
Я убрал фразу о "хвастовстве", поскольку это звучало более осуждающе, чем предполагалось.
JacquesB
11
@BaileyS - я думаю, что это подчеркивает важность проверки кода; то, что кажется программисту естественным и понятным, особенно при постепенном развитии, может легко показаться запутанным рецензенту. Затем код не проходит проверку, пока не будет реорганизован / переписан для удаления свертки.
Майлз
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 programmingи синтаксис ES6 является мощным, но если вы неправильно решите проблему с помощью этих методов, вы получите еще более грязный код.

Если вы не спешите с проблемой и применяете правильные методы в нужных местах, вы можете получить код, который будет функциональным по своей природе, но при этом будет очень организованным и поддерживаемым всеми членами команды. Эти характеристики не являются взаимоисключающими.

b0nyb0y
источник
2
+1 за указание на это широко распространенное отношение (не обязательно относится к ОП): «То, что вы используете новейшие методы ES6 или самую популярную парадигму программирования, не обязательно означает, что ваш код более корректен, или код этого младшего неверен ".
Джорджио
+1. Просто небольшое педантичное замечание, вы можете использовать оператор распространения (...) вместо _.concat, чтобы удалить эту зависимость.
YoTengoUnLCD
1
@YoTengoUnLCD Ах, хороший улов. Теперь вы знаете, что я и моя команда все еще на пути к тому, чтобы отучиться от некоторых наших lodashцелей. Этот код может использовать spread operator, или даже [].concat()если кто-то хочет сохранить форму кода без изменений.
b0nyb0y
Извините, но этот листинг кода для меня все же гораздо менее очевиден, чем оригинальный «младший код» в посте ОП. По сути: никогда не используйте троичный оператор, если вы можете избежать этого. Это слишком напряженно. В «реальном» функциональном языке операторы if были бы выражениями, а не утверждениями, и, следовательно, более читабельными.
Олле Харстедт
@ OlleHärstedt Хмм, это интересное заявление, которое вы сделали. Дело в том, что парадигма функционального программирования или любая другая парадигма никогда не привязана к какому-либо конкретному «реальному» функциональному языку, тем более его синтаксису. Таким образом, диктовать, какие условные конструкции следует или не следует использовать никогда, просто не имеет никакого смысла. А ternary operatorтак же, как и обычное ifутверждение, нравится вам это или нет. Споры между читаемостью if-elseи ?:лагерем никогда не заканчивается, так что я не буду вдаваться в него. Все, что я скажу, с натренированными глазами, такие линии вряд ли "слишком напряжены".
b0nyb0y
5

Второй фрагмент не более тестируем, чем первый. Было бы достаточно просто настроить все необходимые тесты для любого из двух фрагментов.

Реальная разница между этими двумя фрагментами - понятность. Я могу довольно быстро прочитать первый фрагмент и понять, что происходит. Второй фрагмент, не так уж и много. Это гораздо менее интуитивно, а также значительно дольше.

Это облегчает поддержку первого фрагмента, что является ценным качеством кода. Я нахожу очень мало значения во втором фрагменте.

Дауд ибн Карим
источник
3

TD; DR

  1. Можете ли вы объяснить свой код младшему разработчику за 10 минут или меньше?
  2. Через два месяца вы можете понять свой код?

Детальный анализ

Ясность и удобочитаемость

Оригинальный код впечатляюще прост и понятен для любого уровня программиста. Это стиль, знакомый всем .

Удобочитаемость в значительной степени основана на знакомстве, а не на математическом подсчете токенов . ИМО, на данном этапе у вас слишком много 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 является Число или Строка, но так же как и исходный код. И мне это понятнее (и излишне употребляемая идиома).

user949300
источник