HackIT-2017 - Форум по кибербезопасности - защити свой код, проект, бизнес. Харьков, 23 сентября
×Закрыть

Делайте return, как только нашли ответ

Работая с React-проектами, я столкнулся с повторяющейся проблемой: render-функции, которые сложно понять. Я хотел бы рассказать о наиболее частой причине данного усложнения — вложенные if-else операторы в render-функциях, и как этого можно избежать.

Что такое render-функция?

Оттолкнемся от React-документации. Render-функция должна возвращать React Element или нулевое значение. В идеале она должна быть чистой и использовать только this.props и this.state.

Зачем нужен условный оператор в render-функции?

В чистой render-функции условный оператор обычно основан на состоянии/параметрах компонента. Есть две основные причины if-проверок:

  1. Есть ли у нас все необходимые данные?
    if (this.props.user) {
     // Есть данные, отображаем компонент юзера
    } else {
     // Нет данных юзера
    }
  2. Различные варианты отображения на основании state/props.
    if (this.props.showWarning) {
     // Дополнительно нужно отобразить компонент с предупреждением
    }

Но нельзя сказать, что наличие условных операторов в render-функции само по себе плохо или хорошо. Проблема заключается именно в организации таких проверок.

Плохая организация

Давайте перейдем к примеру:

render() {
 let content;
 
 if (this.props.users) {
   if (this.props.users.length) {
     content = (
       <List>
         {this.props.users
            .map(user => <UserCard key={user.id} user={user} />)}
       </List>
     );
   } else {
     content = <Warning>Empty</Warning>;
   }
 } else {
   content = <Loading />;
 }
 
 return content;
}

Выглядит сложновато. Попробуем упростить:

render() {
 if (!this.props.users) {
   return <Loading />;
 }
 
 if (!this.props.users.length) {
   return <Warning>Empty</Warning>;
 }
 
 return (
   <List>
     {this.props.users
        .map(user => <UserCard key={user.id} user={user} />)}
   </List>
 );
}

Какие преимущества мы получаем?

Читабельность

Основная задача нашей функции — это отображение списка пользователей. И после простого рефакторинга мы с легкостью можем сосредоточиться именно на этой части. Почему? Потому что теперь в ней нет никаких вложений и нам не нужно задумываться: «Все ли данные представлены?». Мы сделали все необходимые проверки в верхней части, поэтому ошибок быть не должно. Это снижает когнитивную нагрузку и позволяет сосредоточиться на оптимистичной части кода (happy path). Мы также можем сказать, что логика визуализации пользователей является изолированной и визуально выделяется, и то же можно сказать о каждой защитной проверке.

Меньше ошибок и погрешностей

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

Недостатки?

Нет никаких недостатков! Но не все со мной согласятся. Часто люди видят проблему в том, что количество операций возврата растет, а у функции должна быть одна точка выхода. Но если копнуть, кто и почему это придумал, то в основном все дороги ведут к правилу Single Entry — Single Exit.

Single Entry — Single Exit

Здесь проблема заключается в том, что принцип Single Entry — Single Exit используется для предотвращения входа в функцию оператором GO-TO. Суть отдельного выхода состоит в том, что функция возвращается в единое место, а не прыгает при помощи GO-TO в какую-то другую часть приложения, никогда не достигнув возвратного значения. Кроме того, нигде не сказано, как много возвратных значений у вас может быть. Принцип Single Entry — Single Exit сформулировал Эдсгер В. Дейкстра, который решительно выступает против практики использования операторов GO-TO.

Что еще?

В книге Code complete написано следующее:

«Минимизируйте число возвратов из каждого метода. Тяжело понять логику метода, если при чтении его последних строк вы не подозреваете о возможности выхода из него где-то вверху. По этой причине используйте операторы возврата благоразумно — только если они улучшают читабельность».

Но, кроме этого, в книге также сказано:

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

Как раз в нашем случае множественный возврат уместен из-за улучшения читабельности.

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

«Возврат из метода сразу после получения ответа вместо установки возвращаемого значения внутри вложенных операторов ifthenelse»

Выход из метода сразу же после нахождения возвращаемого значения часто помогает облегчить чтение кода и снизить вероятность ошибок. Альтернативный вариант — установка возвращаемого значения и следование к выходу из метода через заторы операторов — может оказаться более сложным".


Есть еще два похожих паттерна, которые мне удалось найти, — Guard Clause и Bouncer Pattern. Они выглядят примерно так же, как описанный случай.

Оставляйте свое мнение в комментариях!

Лучшие комментарии пропустить

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

а можно переписать в более функциональном стиле:


//assume:
export function resolve(expr) {
    return {
        as: val => (input => expr(input) ? val : null)
    };
}

export function stateResolver(...conditions) {
    return val => {
        for (let i = 0; i < conditions.length; i++) {
            const resolved = conditions[i](val);

            if (resolved !== null) {
                return resolved;
            }
        }
        return null;
    };
}

// react
const userList = stateResolver(
    resolve((props) => !props.users).as(<Loading />),
    resolve((props) => !props.users.length).as(<Warning>Empty</Warning>),
    resolve((props) => props.users).as(<List>
     {this.props.users
        .map(user => <UserCard key={user.id} user={user} />)}
   </List>)
);

нет ifов, тернарок, ретурн стейтментов, код читабелен, просто и главное чистые функции — никакого сайд еффекта

89 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Просто оставлю это здесь www.amazon.com/...​aftsmanship/dp/0132350882
Кто в теме, тот поймет. Кто не в теме, просто будет холиварить в коментах дальше.

И из статьи видно, что этот подход как бы повышает читабельность, а из комментариев — использовать его везде и во всех языках. Например, в scala советую использовать return только в исключительных случаях. Например, в функция wtf будет всегда возращать «wtf»:

def using(block: => String) = block

def wtf(returnArg: Boolean, arg: String) {
  using {
    if (returnArg) {
      return arg
    }
    else {
      return "hello world"
    }
  }
  return "wtf"
}

И не скажеш, что следующий код на type script совсем уж четабельный и понятный:

function using(block: () => string): string {
  return block()
}

function wtf(returnArg: boolean, arg: string): string {
  using(() => {
    if (returnArg) {
      return arg
    }
    else {
      return "hello world"
    }
  })
  return "wtf"
}

Так что если вы активно пользуете лямбды и функциональщину, то я не советую использовать этот подход.

P.S.а пример то надуманный, его можно переписать в следующем читабельном виде:

render() {
  let content;
  if (!this.props.users) {
   content = <Loading />;
  }
  else if (!this.props.users.length) {
   content = <Warning>Empty</Warning>;
  }
  else {
    content = (
     <List>
       {this.props.users
          .map(user => <UserCard key={user.id} user={user} />)}
     </List>
    );
  }
  if (debugMode) {
    log(content);
  }
  return content;
}

Как раз для таких проблем higher order components придумали

Но вот только если каждую мелочь делать через HOC то вы сами себя запутаете, в таком примере это overkill
jsfiddle.net/simonr/yLmkht0e

“Why Many Return Statements Are a Bad Idea in OOP” ->
www.yegor256.com/...​rn-statements-in-oop.html

идея правильная, но я ее развиваю дальше. Особенно, при большом количестве if\else
каждый набор if\else — это какое-то состояние, поэтому я создаю список состояний, некий enumeration, своими названиями каждое состояние описывает себя. Далее короткой функцией преобразовываю if\else в состояние. Потом в самом render использую switch () {case state1: case state2: }. Который четко разбивает функцию на независимые блоки и return там смотрится гораздо лучше

По такому способу напевне будуть заморочки з вкладеними if-else
if (...) {
...
if (...) {
...
}
}
така логіка не мапиться на switch/case

Хто вам таке сказав? О_о

Да, вложенное смотрится уже чуть хуже. Но тут есть два варианта:
1) выделить вложенное в отдельное состояние, тогда прийдется делать переиспользуемые куски, будет как конструктор, которым будут пользоваться сразу несколько case-ов
2) оставить вложенным, но только если вложенность не более одно уровня, иначе выигрыша нет перед обычным if else
Я еще не встречал компонента, который бы не вкладывался в такие рамки. А если он не вкладывается, то он уже слишком сложен, тут при любом подходе будет проблема и его надо только разбивать

Это отличный подход, когда все кейсы приблизительно равноправны.
А когда у вас есть четно выделенный «happy path» где сосредоточенна основная часть кода, и пару исключительный случаев, то как по мне лучше просто оградить нормальный флоу от исключений

Да, так и есть. В моем случае один компонент очень вместительный, так как у нас responsive design, как минимум есть ветки под каждый размер экрана, так же могут быть ветки при сильной смене дизайна в зависимости от состояния системы. Компонент является по сути контейнером для stateless подкомпонентов (по классике реакта) . Мы делаем баланс между читабельность. и масштабируемостью. Если подлогика растет, ее можно выделить в отдельный подкомпонент, особенно, если он может быть переиспользован. Но в целом структура с выделенными состояниями остается. Исключения скорее всего были бы выделены в подкомпонент.

уровень сложности изложенного материала — тимлид в плариуме

это похвала или оскорбление?

вот тут еще хорошо на эту тему рассказано — о “Line of sight in code”
medium.com/...​ight-in-code-186dd7cdea88

Автор, не узагальнюйте рішення із специфічного випадку із світу javascript на програмування в цілому (в плані назви).
Коли вам доведеться дебагати в gdb/cdb велику програму, можливо, ви подивитесь по-іншому на наявність змінних замість `f(g(), h())` і `return f(3)`

Ну сама по себе практика описанная в посте не является специфичной для одного только JS.

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

Если из за ограничений gdb/cdb дебагера вы не можете так писать, то значит это неправильно для всех случаев? Проблема дебагера и есть специфичный случай. Странная логика.

Логування, дебагінг, error codes vs exceptions.. Є багато випадків, коли це важливо (напевно рівно 0 з них — в JS фронтенді). Я про те, що ви узагальнюєте на широкий випадок, а ідеоматично правильно (для вас) так робити — у вашому вузькому випадку (React UI).

ам, на сколько я знаю хорошей практикой является когда в методе есть только 1 return )

Я цього не заперечував

Но только если это не мешает readability. Да и как по мне нужно стараться минимизировать количество return, но не доводить это правило до экстрима, считая что больше одного return это code smell.

Проблема точно заслуживает отдельной темы

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

думаю автор просто опустил момент что это должна быть более высокоуровневая компонента, и упростил для примера

а можно переписать в более функциональном стиле:


//assume:
export function resolve(expr) {
    return {
        as: val => (input => expr(input) ? val : null)
    };
}

export function stateResolver(...conditions) {
    return val => {
        for (let i = 0; i < conditions.length; i++) {
            const resolved = conditions[i](val);

            if (resolved !== null) {
                return resolved;
            }
        }
        return null;
    };
}

// react
const userList = stateResolver(
    resolve((props) => !props.users).as(<Loading />),
    resolve((props) => !props.users.length).as(<Warning>Empty</Warning>),
    resolve((props) => props.users).as(<List>
     {this.props.users
        .map(user => <UserCard key={user.id} user={user} />)}
   </List>)
);

нет ifов, тернарок, ретурн стейтментов, код читабелен, просто и главное чистые функции — никакого сайд еффекта

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

ну не знаю, лично для меня в разы читабельнее, а джуну раз показать как оно работает — он тоже поймет, причем если заменить функции отвечающее за условие именоваными, а не как в примере стрелки, то будете читать вообще как нормальный текст, а не как код (как в примере ТС). что типа:

const noUsers = props => !props.user;
const usersIsEmpty = props => !props.users.length;
const users = props => props.users;

const userList = stateResolver(
    resolve(noUsers).as(<Loading />),
    resolve(usersIsEmpty).as(<Warning>Empty</Warning>),
    resolve(users).as(props => <List>
     {this.props.users
        .map(user => <UserCard key={user.id} user={user} />)}
   </List>)
);

куда более читабельнее??? тут нет кода, тут есть только описание

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

кстати тут хорошо отображенна реальность — это композиция 3 разных компонентов, у ТС это та же композиция — только не очевидная, и вы сразу заметили что это код смел — а вот у ТС это не так очевидно

а хипстеры так не пишут обычно, ибо тут мало фич используется (кроме функций ничего и нет)

Если читать как текст, то возможно и понятнее. Но в плане восприятия синтаксиса имхо заметно хуже. Функция stateResolver которая принимает на вход результат вызова метода as результата вызова функции resolve...

Тогда как онструкция вида:

if (!this.props.users) {
   return <Loading />;
 }
вообще не нуждается в пояснении. Она понятна всем кто писал хоть десяток строк кода.

Да и вообще, чем конкретно плох if .. return ?

если вам надо читать с учетом синтаксиса то уже чтото пошло не так.

насчет stateResolver-a — я его вчера на работе написал для мапинга ошибок с сервера, на ошибки клиента — проблема была в том что сам процесс ресолвинга был неодназначен и около порядка 6 ошибок, и там stateResolver смотрится куда круче. если честно то не понимаю почему его нет в каком-то lodash. насчег его неясности — думаю когда вы впервые увидели функции .map, .reduce, .filter — тоже могли удивлятся зачем, ведь можно пройтись циклом и не городить функций, но в реальности с этими функциями лаконичнее.

по поводу if...return — если такое имеется то это лучше заменить на return expr ? val1 : val2 — чем меньше return-ов тем проще вышлядить код. обычно несколько return-ов там где алгоритмы, самое типичное алгоритмы связанные с поиском.

а вот если в вашей функции есть более одного if...return и эта функция не решает какойто специфический алгоритм то это code smell c вероятностью в 95%. почему? потому что в рамках одной функции у вас более одной функции. самый типичный кейс:

function (input) {
    if (!input) {
        return null;
    }

    //... a lot of logic
    return result;
}

функция делает валадицаю входных параметров и собственно реализует свою логику — то есть это две функции на самом деле, что с этим делать?

function calc(input) {
    //... a lot of logic
    return result;
}

function (input) {
   return input
       ? calc(input)
       : null;
}

теперь 1 return statement и тернарная операция, в теле первоначальной функции оставили только валидацию input и вызов собственно калькуляции если надо. то есть мы разбили ее так чтобы каждая функция выполняла только одну задачу, одна функция — одна задача, проще чем одна функция — много задач.

еще множество return-ов плохи тем что не всегда очевидна логика функции, непонятно где закончится — обычно это проблема появляется со временем. когда дебажил какуюто библиотеку для node.js там одна функция имела 5 return-ов в зависимости от входных параметров, и еще кучу логику для каждого return. был бы там stateResolver — было бы проще, но пришлось вникать и разбиратся в императивной логике, а она там была не тривиальна — да я разобрался но потратил около получаса времени на это (что очень много), кстати изначально я просто ее исходник смотрел, функция была большая и смотрел на ее конец сразу чтобы знать что она возращает, когда прошелсяс по ней дебагом понял что там до фига других return-ов ранее, я долго матюкался, и понял что надо избегать множественных выходов из функции. но это лирика.

возвращаясь к примеру ТС. там главная проблема в том что та компонента занимается не своим делом, она обрабатывает два дополнительных состоянии, и если в случае с пустым массивом я могу понять — то в случае отсутствием массива пользователей я не могу. если взять это во внимание то функция перепишется прекрасно с одним return:


    render() {
        return this.props.users.length 
            ? <List>
                {this.props.users
                    .map(user => <UserCard key={user.id} user={user} />)}
            </List>
            : <Warning>Empty</Warning>;
    }

там проблема как раз с третим состоянием, которое явно выходит из зоны отвественности той компоненты — когда нет this.props.users

и тут уже тернарка не такое хорошее решение (более одной тернарки читается трудно), можно разбить на чтото такое:


    renderUsers() {
        return this.props.users.length 
            ? <List>
                {this.props.users
                    .map(user => <UserCard key={user.id} user={user} />)}
            </List>
            : <Warning>Empty</Warning>;
    }

    render() {
        return this.props.users 
            ? this.renderUsers()
            : <Loading />;
    }

но тут уже появляются 2 функции, но как по мне все равно лаконичнее чем у ТС.

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

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

в итоге — стоит ли использовать в данном примере мой stateResolver? лично я считаю что компонента должна отображать массив пользователей, а значит он должен существовать, потому я бы вынес часть с на уровень выше, а в компоненте оставил бы тернарку с одним return. stateResolver — когда надо хендлить более 2 состояний, особенно если эти состояния определяются не тривиальным способом, или для каких-то мепингов, чтобы не заводить объект через который будете маппить, и не городить огромные свит кейсы.

как-то так, все прочитанное вами выше, результат моего опыта, так что вы вполне можете быть несогласны :)

хех почти мини статья получилась ))

Работать с кодом, который не находится в тернарном условии намного проще, и приятнее. Так же тут уже будет нужна декомпозиция.

Как по мне сделать кастомный stateResolver это запутать всю команду, и повысить уровень вхождения для новоприбывших. Решения иногда нужно принимать не для себя тк «мне так нравится» а для команды, и на благо проекта.
Как минимум зачем? Ведь код в примере читается легко, и работать с ним удобно. Я вижу только одну цель — запутать других.

Да и если уже делать декомпозицию, то делать в стиле реакта с помощью recompose/branch. Да и выглядит это все как крайности.
jsfiddle.net/simonr/yLmkht0e

Работать с кодом, который не находится в тернарном условии намного проще, и приятнее.

в том то и дело что с условным кодом не надо работать — его надо избегать всеми силами, ибо любое условие повышает количество багов — в этом прелестб тернарки — у вас не получится туда запихать много логики, а в if еще как.

Как по мне сделать кастомный stateResolver это запутать всю команду

как я говорил — не понимаю почему его нет в lodash — может надо сделать им PR

и на благо проекта.

вот я как раз такое и принимаю — чтотбы код был читабелен, предсказуем, прост. stateResolver это дает — прямая связь между одним условием и одним состоянием.

Да и если уже делать декомпозицию, то делать в стиле реакта с помощью recompose/branch.

этот «стиль react-а» такие же велосипеды как у меня, просто они вынесли это в либу, и тут они молодцы.

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

я просто подал это как пример решение в фукнкциональном стиле — вы можете использовать библиотеку recompose — но это абсолютно тот же подход как и мой

Проблема не в коде а в джунах. Страно в функциональном языке не использовать функциональный подход.

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

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

Оно то так, но есть ли смысл возводить это в абсолют. Тут наверное больше вопрос вкуса или религии...

Имхо, декларативность даёт DRY и уменьшает вероятность выстрелить себе в ногу.

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

Делайте return, как только нашли ответ
противоречит функциональной парадигме. В примере
stateResolver
тоже возвращает результат как только получил его.
И я честно не представляю как выстрелить себе в ногу с помощью
function doSomething(x,y,z){ if(x===null){ return null; } ................................................. }

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

А где там декларативность? Код предложенный автором статьи читается без включения головного мозга — настолько он тривиален и идиоматичен. Антон вместо этого предлагает запилить по сути микро DSL. Ну да, применение резолвера читается как книга. Но, пардон, а код самого резолвера типа не считается?

Следуя этой логике код всего приложения должен читаться так же легко как и тела его функций, зачем же разбивать его на функции? А ощущение необходимости включать мозг только потому что количество информации к количеству символов больше. Впрочем jump to definition тут поможет.

Вопрос риторический, потому что вы следуете какой-то «очень своей» логике.

а код самого резолвера типа не считается

если вы его юзаете в более чем одном месте — то вы просто будете знать как он себя ведет — и будете смотреть на него как на .map или .reduce

Мне кажется, мы говорим о немного ортогональных вещах. Вопрос не в том, что нельзя понть как работает резолвер. Вопрос в том, что, на мой взгляд, использовать функции высшего порядка («потому что могу») там, где за глаза хватает примитивных как грабли guard clauses — это спорное решение.

ну функции высшего порядка не такие страшные как термин «функции высшего порядка».

и конкретно в данном примере, я бы сам сделал по другому, как именно описал выше.

это была приведено в качестве альтернативы, и в целом можно было бы тут это заюзать, если в команде все пишут в функциональном стиле, а если нет, то все равно подход ТС как по мне плох

ну функции высшего порядка не такие страшные как термин «функции высшего порядка».

Речь не о том, что они страшные, речь о том, что из пушки не стреляют по воробьям. Скажем, в каком-то условном эрланге никто, будучи в здравом уме, не стал бы решать задачу из статьи при помощи фвп именно потому, что читаться будет значительно хуже (при том, что во всю эту функциональщину эрланг умеет явно не хуже джс), а функция рендера могла бы выглядеть как-то так:

render([]) ->
  ...рендерим предупреждение...;
render(Users) when is_list(Users) ->
  ...рендерим соббсно список...;
render(_) ->
  ...рендерим ошибку....
Это тупо проще читать, дебажить итд...
речь о том, что из пушки не стреляют по воробьям
и конкретно в данном примере, я бы сам сделал по другому, как именно описал выше.
Страно в функциональном языке не использовать функциональный подход.

В любом языке странно писать код ради кода, когда стоило бы писать код для решения задачи.

правда в том что если код не писать ради кода, то дальше hello world-a мы не сдвинемся из-за сложноти сопровождения.

Думаете, тс руководит в плариуме командой, которая пишет helloworld’ы?

Так он и в реакте пишет, так что пишет код для кода однозначно

Экскаватор заводить некогда, копаем лопатами!

когда нужна одна ямка для саженца, вы почему-то заводите экскаватор и копаете котлован.
вопрос не в целесообразности инструмента вообще, вопрос в целесообразности применения инструмента в конкретной ситуации.

Ну так-то оно, что и говорить, когда конечно. Но при условии тысяч одинаковых ямок по всему проекту проще таки заморочиться. И вообще, смысл тут на dou истину выяснять, когда можно похоливарить. Мне, например, вот, EMACS очень нравится.

Наскільки зручно дебажити такий код відносно іф-елс?

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

я про те, що з таким підходом, степ-овер/інто незручний, брейкпоінти незручні, локальні змінні незручні...

это дело привычки, никакой разницы в сложности, как я уже писал, нет :)

ну я его вообще никогда не дебажил, обычно прочитать хватает, а сам код предельно прост, если что то и дебажить не должно быть проблемой

за все время пользования реактом никогда не было проблем с условной логикой в рендер функции, обычно тернарные операторы спасают на 100%:

    render() {
        return <div className="cool-user-list">
            <List>
                {this.props.users
                    .map(user => <UserCard key={user.id} user={user} />)}
            </List>
            
            {this.props.isLoading === true
                ? <Loading />
                : null}

            {this.props.error !== null
                ? <div className="cool-user-list-error">
                    {this.props.error.message}
                </div>
                : null}
        </div>;
    }

как по мне ничего сложного тут нет

Я согласен с вашим примером, тернарки тут отлично смотрятся.
Но это не совсем тот же код что в статье

— там нет общего контейнера
— List компонент не рендерится если нет пользователей
— Там не ошибки а сообщение что нет пользователей, и опять таки нет общего контейнера

Понимаю что в контексте примеров это незначительная разница, но если перейти к реальным примерам, это разные случаи.

Пример очень упрощен, если добавится какая либо необходимость что то сделать для контейнера UserCard, как пример преобразавать props и прям в jsx это делать не очень хотелось бы. То в вашем примере мы это будем делать даже если мы не хотим рендерить контейнер которому нужен этот мапинг
jsfiddle.net/simonr/bmspsfsz/1

jsfiddle.net/simonr/td42kvwj

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

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

Да и мы уже отошли от сути
jsfiddle.net/simonr/mvj96g1x
Можно еще и permissions вынести в отдельный уровень, но тогда вообще далеко от темы

Ну так же не всегда можно что то вынести на верх, иногда loading для юзеров должен быть какой то особенный, ведь если он одинаковый на всем сайте, то зачем его вообще тут разруливать. Просто подписаться на request/response и все.
Тоже самое и с error, ошибок вообще в примере не было :( там был кастомный warning про то что нет юзеров

По поводу композиции, да все круто но тут нужно еще сильнее разбивать на компоненты, даже банальный мепинг для props потребует отдельного компонента или hoc recompose/mapProps
jsfiddle.net/simonr/4u9nk60h

Хм. Взагалі то, концепція «Single Entry — Single Exit» актуальна для мов з керуванням пам’яттю вручну (без GC). При супроводі можливі ситуації, коли наступні виправлення, що включають return, не будуть вивільняти всі ресурси. Або, перед кожним таким поверненням треба було б копіпастити код для вивільнення ресурсів.
Для GC-based мов повернення посеред функції виглядає нормально. Проте, якщо довжина функції велика, то теж можливі проблему при супроводі:

Тяжело понять логику метода, если при чтении его последних строк вы не подозреваете о возможности выхода из него где-то вверху
Проте, якщо довжина функції велика

...то тут краще зробити декомпозицію у будь-якому разі, згодні?

ага, еще одно название «ранний выход».
а в React доступимо разделять render() на несколько функций?
иногда создавать отдельный компонент ради структуры, которая всего лишь трижды используется(но в одном и том же компоненте), выглядит как overhead

Добрый день, да можно в основном есть 3 способа

— вынести в отдельный компонент
— создать метод в компоненте
— просто функция в том же файле

вот пример с методом в компоненте jsfiddle.net/simonr/0jL52u9z

ну, я скорее насчет «допустимости».
тоже может повлиять на читаемость: if + methodCall() или if + 5 и более строк инлайн

Всеми руками за то что бы выносить в отдельный метод, если там +5 строк, я бы даже сказал +3 строки, а в компонент или метод, уже нужно смотреть конкретный случай.

Переоткрытия...

Кент Бек «Шаблоны реализации корпоративных приложений»
Глава 7, «Поведение», Сторожевой пункт, стр 88 (в русском издании)

це не перевідкриття, це капітан очевидність. Я можу з десяток таких істин написати сходу:

  1. перевіряй об’єкт на null перед тим як викликати функцію
  2. не використовуй string для enum
  3. використовуй switch замість багатьох if else операторів
  4. чисть зуби перед сном...
перевіряй об’єкт на null перед тим як викликати функцію

не используйте null/замените на функтор/монаду

1 не істина.
2 enum — а взагалі він треба?
3 не істина. бо може краще рознести перевірки по іншому. багато варіантів.

Я можу з десяток таких

поки що назвали одну з 4ьох :)
тобто сходу у вас не вийшло назвати 3. а ви про десяток :)

пункт 4 поки тримається, ніхто не став став апелювати)

використовуй switch замість багатьох if else операторів

Просто как интересный факт в питоне, к примеру, нет switch.

1. Никогда не проверяй, используй язык без null
2. Забей на енумы, используй discriminated unions
3. Используй паттерн матчинг

Я бы с радостью написал бы статью по пункту 3 :)

Но попробую вкратце.

Если много if/else или switch кейсов, то я бы предпочел Table-driven methods.

Есть две основные реализации

— Lookup table
проблема — i.imgur.com/9zMxPvZ.png
решение — i.imgur.com/4y18ZG9.png

— Dispatch table
проблема — i.imgur.com/RioqcGS.png
решение — i.imgur.com/w9mCvxF.png

А в чем принципиальная разница между вашей реализацией dispatch table и корным switch statement? По сути, вы реализовали аналог switch, который не проваливается дальше и не имеет default. В данном случае, когда из каждого кейса будет возврат, обычный switch имхо, будет предпочтительнее, поскольку он может обратботать неизвестное значение в дефолтном блоке.

В случае, если в вашу функцию будет передан неизвестный или новый тип, будет ошибка.

неизвестное значение в дефолтном блоке.

вообще-то неизвестное значение — нельзя обработать :)
а реакций на неизвестное значение всего две — игнорировать, как будто вообще не было никакого значения(или назначать известное константное) или ошибка.

дефолтный блок не для неизвестных значений, а для — известных И обычных, типичных.

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

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

Но если предлагается писать таблицу, которая фактически является самописным недо-свичем, не вижу пойнта.

зависит все конечно от конкретной задачи, алгоритма который описывается. То что в одном случае будет велосипедом, в другом может оказаться лучшим решением.
Например табличное описание бывает хуже патерна стратегия. Что не означает что всякий свич надо заменять патерном стратегия

Основной плюс в том что в таблицу мы можем что то добавить не только там где она определенна. Всегда нужно знать меру, я неправильно выразился говоря (я бы предпочел), я не против switch, и табличный метод подходит не всегда, и чаще switch понятнее. Hо когда все начинает выходить за рамки контроля, табличный подход, как вариант для развития.

Вот я выдрал упрощенный пример из доклада slides.com/simonradionov/deck-5-5

Переоткрытия...
Кент Бек «Шаблоны реализации корпоративных приложений»
Глава 7, «Поведение», Сторожевой пункт, стр 88 (в русском издании)

В интернетах всегда все всё знают. А открываешь исходники — один говнокод.

В интернетах всегда все всё знают.

вообще-то нет. даже на доу в обсуждениях частенько выясняется — не знают.

но, то такое. главное — чтобы на собеседовании знали.

А открываешь исходники — один говнокод.

во-первых — чьи? тех кого сами же на работу взяли, или нарыли где-то в инете? ну так ройте другой код, или набирайте других. тех что не только в интернетах все знают :)

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

Коротко и в тему! Люблю статьи, после которых в голове маленькая неясность сменяется ответом с четким обоснованием :)

Подписаться на комментарии