[Из песочницы] Стрелочный ад, или новый круг старой проблемы

Истоки


Когда-то давно JavaScript разработчики очень любили использовать анонимные функции (собственно и сейчас многие их любят использовать), и всё бы ничего, и код становится короче, и не надо придумывать очередное название для функции, но рано или поздно это превращается в «водопад функций» прочитать который вы сможете разве только в хорошем IDE и то немного поломав голову.



Вот вам один пример:


Пример 1:


$http.request('GET', '/api').then(function (data) {
  return data.arr.map(function (item) {
    item.props.filter(function (prop) {
      return !!prop;
    }).map(function (prop) {
      prop.a = prop.a * 2;
      return prop;
    })
  }).filter(function (item) {
    return item.props.some(function (prop) {
      return prop.a > 10;
    });
  });
}, function (error) {
  throw new TypeError(error);
});

Это очень простой пример, в моей практике я встречал «монстров» на 100 — 200, а то и 300+ строк кода, которые приходилось рефакторить, прежде чем понять что за магию они делают.


В своё время переход на «шаблон функционального дизайна», в котором каждая функция объявляется отдельно и используются только ссылки на них сделал код красивым, аккуратным и самое главное легко читаемым, что очень важно, когда вы работаете с ним не один.


Этот пример, например, преобретает следующий вид:


Пример 2:


$http.request('GET', '/api').then(requestSuccessHandler, requestErrorHandler);

function requestSuccessHandler(result) {
  return result.arr
               .map(transformResultArr)
               .filter(filterResultArr);

  function transformResultArr(item) {
    item.props
        .filter(checkProperty)
        .map(transformProperty)
  }

  function filterResultArr(item) {
    return item.props.some(filterProperty);
  }

  function checkProperty(prop) {
    return !!prop;
  }

  function transformProperty(prop) {
    prop.a = prop.a * 2;
    return prop;
  }

  function filterProperty(prop) {
    return prop.a > 10;
  }
}

function requestErrorHandler(error) {
  throw new TypeError(error);
}

Кода хоть и больше, но зато с таким вариантом проще работать, он легче читается и дебажится.


Но всё хорошее длилось не долго.


Стрелочные функции


Прежде чем перейдём к проблеме, давайте посмотрим что-же такое стрелочный функции и что они нам дали со своим приходом.


Стрелочный функции это короткая запись анонимных функций. В принципе это всё, но на практике это оказывается очень полезно. Например, если вам надо выбрать из массива все элементы имеющие поле id. Раньше это было бы записано так:


Пример 3:


arr.filter(function(item) {
  return !!item.id;
});

Или так:


Пример 4:


arr.filter(filterID);

function filterID(item) {
  return !!item.id;
}

Но со стрелочными функциями эта запись станет неприлично короткой:


Пример 5:


arr.filter(item => !!item.id);

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


Проблема появляется не сразу.


Стрелки во всём


Недавно я перешёл на новый проект, в котором с самого начала активно используется ECMAScript 2015, и как водится везде по максимуму используются новые фичи языка. Самой заметной из них является повсеместное использование стрелочных функций. И всё было бы хорошо, если бы это не были водопады функций на 100+ строк и 4+ уровней вложенности.


Для наглядности вернёмся к первому примеру, в этом проекте он бы выглядел примерно так:


Пример 5:


$http.request('GET', '/api').then(data => {
  return data.arr.map(item => {
    item.props.filter(prop => !!prop)
        .map(prop => {
          prop.a = prop.a * 2;
          return prop;
        })
  }).filter(item => item.props.some(prop => prop.a > 10));
}, error => {
  throw new TypeError(error)
});

В общем то он стал короче (и я сам кстати люблю, когда код короткий). Местами он стал понятней. Но это только если его сравнивать с первым примером. На практике такой код вырастает в огромных не читаемых монстров, которые быстро пишутся, но потом часами дебажатся и дорабатываются.


Но я не говорю, что использовать стрелки плохо, просто их надо использовать в меру.


Как же всё сделать лучше?


На пути создания хороших практик с ECMAScript 2015 мы будем ещё долго спотыкаться и падать, но для начала давайте начнём объединять лучшие старые и новые практики.


Например соединим примеры 2 и 5:


Пример 6:


$http.request('GET', '/api').then(requestSuccessHandler, requestErrorHandler);

function requestSuccessHandler(result) {
  return result.arr
               .map(transformResultArr)
               .filter(filterResultArr);

  function transformResultArr(item) {
    item.props
        .filter(prop => !!prop)
        .map(transformProperty)
  }

  function filterResultArr(item) {
    return item.props.some(prop => prop.a > 10);
  }

  function transformProperty(prop) {
    prop.a = prop.a * 2;
    return prop;
  }
}

function requestErrorHandler(error) {
  throw new TypeError(error);
}

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

Комментарии (7)

  • 11 июля 2016 в 13:05 (комментарий был изменён)

    +2

    Стрелочный функции это короткая запись анонимных функций. В принципе это всё

    Ну нет. Гораздо важнее, что они сохраняют контекст (точнее, не создают свой собственный, что теоретически делает их быстрее там, где они поддерживаются нативно).


    А так да, именованные функции, особенно если они длиннее 1–2 строк, проще читать и поддерживать (если они нормально названы, конечно:)).

  • 11 июля 2016 в 13:24 (комментарий был изменён)

    0

    Как насчёт такого варианта?


    class $my_app {
    
        itemsRaw() {
            return $mol_http.resource( '/api' ).json().arr
        }
    
        itemsNormal() {
            var items = this.itemsRaw()
            items.forEach( item => {
                if( !item.prop ) return
                item.prop *= 2
            } )
            return items
        }
    
        itemsFiltered() {
            return this.itemsNormal().filter( item => {
                return item.props.some( prop => prop.a > 10 )
            } )
        }
    
    }
    • 11 июля 2016 в 13:29

      0

      Точнее так:


      class $my_app {
          itemsRaw() {
              return $mol_http.resource( '/api' ).json().arr
          }
          itemsNormal() {
              var items = this.itemsRaw()
              items.forEach( item => item.prop.forEach( prop => {
                  if( !prop ) return
                  prop.a *= 2
              } ) )
              return items
          }
          itemsFiltered() {
              return this.itemsNormal().filter( item => {
                  return item.props.some( prop => {
                      return prop.a > 10
                  } )
              } )
          }
      }
      • 11 июля 2016 в 13:43

        0

        Или даже так:


        class $my_app {
        
            itemsFiltered() {
                return this.itemsNormal().filter( item => this.itemCheck( item ) )
            }
        
            itemCheck( item ) {
                return item.props.some( prop => prop.a > 10 )
            }
        
            itemsNormal() {
                return this.itemsRaw().map( item => this.itemNormalize( item ) )
            }
        
            itemNormalize( item ) {
                item.prop.forEach( prop => {
                    if( !prop ) return
                    prop.a *= 2
                } )
                return item
            }
        
            itemsRaw() {
                return $mol_http.resource( '/api' ).json().arr
            }
        
        }
        • 11 июля 2016 в 13:52

          0

          Зачем создавать класс, если здесь нет локальных свойств? Просто чтоб сгруппировать несколько функций? Да и методы должны быть глаголами
          • 11 июля 2016 в 14:05

            0

            Например, за этим:


            class $my_app_prefixed extends $my_app {
            
                itemsNormal() {
                    return [ this.itemPrefix() ].concat( super.itemsNormal() )
                }
            
                itemPrefix() {
                    return {
                        prop : [
                            { a : 10 }
                        ]
                    }
                }
            
            }

            Это процедуры (itemNormalize) и конвертеры (itemCheck) должны быть глаголами. Геттеры же (itemsFiltered, itemsNormalized, itemsRaw) вполне могут именоваться и существительными.

  • 11 июля 2016 в 13:34

    0

    Мое мнение: стрелочные функции хороши тогда, когда они укладываются в одно выражение (арифметическое или логическое).
    Ну и конечно в случае, если принципиально важно сохранить this.
    Во всем остальном лучше традиционные.

© Habrahabr.ru