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

57

Мой стиль кодирования для вызовов вложенных функций следующий:

var result_h1 = H1(b1);
var result_h2 = H2(b2);
var result_g1 = G1(result_h1, result_h2);
var result_g2 = G2(c1);
var a = F(result_g1, result_g2);

Недавно я перешел в отдел, где очень популярен следующий стиль кодирования:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

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

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

С другой стороны, чем больше обработки вы помещаете в строку, тем больше логики вы получаете на одной странице, что повышает удобочитаемость.

Мой страх верен или я что-то упускаю, и вообще, что предпочтительнее в коммерческой среде? Читаемость или ремонтопригодность?

Я не знаю, уместно ли это, но мы работаем на C ++ (STL) / C #.

Dominique
источник
17
@gnat: вы обращаетесь к общему вопросу, в то время как меня особенно интересует упомянутый случай вызовов вложенных функций и последствия в случае анализа аварийного дампа, но спасибо за ссылку, он содержит довольно интересную информацию.
Доминик
9
Обратите внимание , что если этот пример должны были быть применен к C ++ (как вы упоминаете это используются в проекте) , то это не только вопрос стиля, как того от оценки HXи GXвызовов могут изменяться в однострочнике, а порядок вычисления аргументов функции не указан. Если вы по какой-то причине зависите от порядка побочных эффектов (сознательно или неосознанно) в вызовах, этот «рефакторинг стиля» может в конечном итоге повлиять не только на читаемость / обслуживание.
18:00
4
Имя переменной, result_g1которое вы на самом деле используете, или это значение действительно представляет что-то с разумным именем; например percentageIncreasePerSecond. Это было бы моим тестом, чтобы выбрать между двумя
Ричард Тингл
3
Независимо от ваших чувств к стилю кодирования, вы должны следовать уже принятому соглашению, если только оно явно не ошибочно (кажется, что это не так).
n00b
4
@ t3chb0t Вы можете голосовать так, как вам нравится, но имейте в виду, что вы должны поощрять хорошие, полезные, тематические вопросы на этом сайте (и не поощрять плохие), что цель повышения или понижения вопроса заключается в чтобы указать, является ли вопрос полезным и понятным, поэтому голосование по другим причинам, таким как использование голосования в качестве средства для критики некоторых примеров кода, размещенного для помощи контексту вопроса, как правило, не помогает поддерживать качество сайта. : softwareengineering.stackexchange.com/help/privileges/vote-down
Бен Коттрелл,

Ответы:

111

Если вы были вынуждены расширить один лайнер, как

 a = F(G1(H1(b1), H2(b2)), G2(c1));

Я не буду винить тебя. Это не только трудно читать, но и трудно отлаживать.

Почему?

  1. Это плотно
  2. Некоторые отладчики только выдвигают на первый план все сразу
  3. Это свободно от описательных имен

Если вы расширите его с промежуточными результатами, вы получите

 var result_h1 = H1(b1);
 var result_h2 = H2(b2);
 var result_g1 = G1(result_h1, result_h2);
 var result_g2 = G2(c1);
 var a = F(result_g1, result_g2);

и это все еще трудно читать. Почему? Это решает две проблемы и вводит четвертую:

  1. Это плотно
  2. Некоторые отладчики только выдвигают на первый план все сразу
  3. Это свободно от описательных имен
  4. Это загромождено неописательными именами

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

 var temperature = H1(b1);
 var humidity = H2(b2);
 var precipitation = G1(temperature, humidity);
 var dewPoint = G2(c1);
 var forecast = F(precipitation, dewPoint);

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

Если вы делаете это с бессмысленными именами, такими как, result_thisи result_thatпотому что вы просто не можете думать о хороших именах, то я бы действительно предпочел, чтобы вы избавили нас от бесполезного загромождения имен и расширили его, используя старые добрые пробелы:

int a = 
    F(
        G1(
            H1(b1), 
            H2(b2)
        ), 
        G2(c1)
    )
;

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

  1. Это плотно
  2. Некоторые отладчики только выдвигают на первый план все сразу
  3. Это свободно от описательных имен
  4. Это загромождено неописательными именами

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

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

введите описание изображения здесь

Если этого недостаточно, представьте, что вам G2()позвонили более чем в одном месте, и тогда это произошло:

Exception in thread "main" java.lang.NullPointerException
    at composition.Example.G2(Example.java:34)
    at composition.Example.main(Example.java:18)

Я думаю, это хорошо, что, поскольку каждый G2()вызов будет на отдельной линии, этот стиль приведет вас непосредственно к вызову-нарушителю в main.

Поэтому, пожалуйста, не используйте проблемы 1 и 2 в качестве предлога, чтобы увязать нас с проблемой 4. Используйте добрые имена, когда вы можете думать о них. Избегайте бессмысленных имен, когда вы не можете.

Гонки Легкости в комментарии Orbit правильно указывают на то, что эти функции являются искусственными и имеют мертвые плохие имена. Итак, вот пример применения этого стиля к некоторому дикому коду:

var user = db.t_ST_User.Where(_user => string.Compare(domain,  
_user.domainName.Trim(), StringComparison.OrdinalIgnoreCase) == 0)
.Where(_user => string.Compare(samAccountName, _user.samAccountName.Trim(), 
StringComparison.OrdinalIgnoreCase) == 0).Where(_user => _user.deleted == false)
.FirstOrDefault();

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

var user = db
    .t_ST_User
    .Where(
        _user => string.Compare(
            domain, 
            _user.domainName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(
        _user => string.Compare(
            samAccountName, 
            _user.samAccountName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(_user => _user.deleted == false)
    .FirstOrDefault()
;

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

candied_orange
источник
20
@ Стив, и я не говорю тебе не делать этого. Я прошу осмысленное имя. Все часто я видел промежуточный стиль, сделанный бездумно. Плохие имена сжигают мой мозг гораздо больше, чем разреженный код на строку. Я не позволяю соображениям ширины или длины побуждать меня к тому, чтобы сделать мой код плотным или мои имена короткими. Я позволил им мотивировать меня разлагаться больше. Если хороших имен не произойдет, подумайте об этой работе, чтобы избежать бессмысленного шума.
candied_orange
6
Я добавляю к вашему сообщению: у меня есть небольшое эмпирическое правило: если вы не можете назвать его, это может быть признаком того, что он не четко определен. Я использую его для сущностей, свойств, переменных, модулей, меню, вспомогательных классов, методов и т. Д. Во многих ситуациях это крошечное правило выявило серьезный недостаток в дизайне. Таким образом, хорошее именование не только способствует удобочитаемости и удобству обслуживания, но и помогает проверить дизайн. Конечно, есть исключения из каждого простого правила.
Алиреза
4
Расширенная версия выглядит некрасиво. Там слишком много пустого пространства, которое сокращает его эффективность, так как ничего не значит.
Матин Улхак
5
@MateenUlhaq Единственный лишний пробел - пара строк и отступы, и все они аккуратно размещены на значимых границах. Вместо этого ваш комментарий помещает пробелы в не имеющие смысла границы. Я предлагаю вам немного поближе и более открытый взгляд.
jpmc26
3
В отличие от @MateenUlhaq, в данном конкретном примере я нахожусь в стороне от пробелов с такими именами функций, но с реальными именами функций (длиной более двух символов, верно?) Это может быть то, что я бы выбрал.
Гонки Легкости с Моникой
50

С другой стороны, чем больше обработки вы помещаете в строку, тем больше логики вы получаете на одной странице, что повышает удобочитаемость.

Я совершенно не согласен с этим. Просто глядя на ваши два примера кода, это неверно:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

слышно читать. «Читаемость» не означает плотность информации; это означает «легко читать, понимать и поддерживать».

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

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

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

Дэвид Арно
источник
37
Хотя я согласен, что F(G1(H1(b1), H2(b2)), G2(c1))это трудно читать, это не имеет ничего общего с тем, чтобы быть слишком плотным. (Не уверен, что вы хотели сказать это, но это можно интерпретировать таким образом.) Вложение трех или четырех функций в одну строку может быть легко читаемым, особенно если некоторые функции являются простыми инфиксными операторами. Проблема заключается в неописательных именах, но эта проблема еще хуже в многострочной версии, где вводится еще больше неописательных имен . Добавление только шаблона практически никогда не способствует удобочитаемости.
оставил около
23
@leftaroundabout: Для меня трудность заключается в том, что неясно, G1принимает ли 3 параметра или только 2 и G2является ли другим параметром F. Я должен щуриться и считать скобки.
Матье М.
4
@MatthieuM. это может быть проблемой, хотя, если функции хорошо известны, часто очевидно, сколько аргументов требуется. В частности, как я уже сказал, для инфиксных функций сразу становится ясно, что они принимают два аргумента. (Кроме того, синтаксис в круглых скобках, используемый большинством языков, усугубляет эту проблему; на языке, который предпочитает Curry, он автоматически становится понятнее:. F (G1 (H1 b1) (H2 b2)) (G2 c1))
leftaroundabab
5
Лично я предпочитаю более компактную форму, если вокруг нее есть стилизация, как в моем предыдущем комментарии, потому что она гарантирует меньше состояний для ментального отслеживания - result_h1не может быть повторно использована, если она не существует, и расположение между 4 переменными очевидно.
Изката
8
Я обнаружил, что код, который обычно легко отлаживать, - это код, который не нуждается в отладке.
Роб К
25

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

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

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

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

Deduplicator
источник
17

Как всегда, когда дело доходит до читабельности, неудачи в крайности . Вы можете получить любой хороший совет по программированию, превратить его в религиозное правило и использовать его для создания совершенно нечитаемого кода. (Если вы мне не верите, посмотрите на этих двух победителей IOCCC, борсаний и горенов, и посмотрите, как по-разному они используют функции, чтобы сделать код совершенно нечитаемым. Подсказка: Борсани использует ровно одну функцию, намного, гораздо больше ...)

В вашем случае двумя крайностями являются: 1) использование только выражений с одним выражением и 2) объединение всего в большие, краткие и сложные выражения. Любой из этих подходов делает ваш код нечитаемым.

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


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

double d = sqrt(square(x1 - x0) + square(y1 - y0));

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

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

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

var a = F(G1(H1(b1), H2(b2)), G2(c1));

мне кажется слишком сложным, хотя это на одну операцию меньше, чем вычисление расстояния . Конечно, это является прямым следствием меня ничего не зная о том F(), G1(), G2(), H1(), или H2(). Я мог бы решить по-другому, если бы я знал о них больше. Но в этом-то и проблема: рекомендуемая сложность утверждения сильно зависит от контекста и задействованных операций. И вы, как программист, должны взглянуть на этот контекст и решить, что включить в одно утверждение. Если вы заботитесь о читабельности, вы не можете переложить эту ответственность на какое-то статическое правило.

cmaster
источник
14

@ Доминик, я думаю, что в анализе вашего вопроса вы делаете ошибку, что «удобочитаемость» и «ремонтопригодность» - это две разные вещи.

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

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

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

Я бы сказал, что на решение влияют следующие моменты:

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

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

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

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

Стив
источник
3
+1 к «Возможно ли иметь код, который поддерживается, но не читается?». Это была моя первая мысль тоже.
RonJohn
4

Оба субоптимальны. Учитывайте комментарии.

// Calculating torque according to Newton/Dominique, 4th ed. pg 235
var a = F(G1(H1(b1), H2(b2)), G2(c1));

Или конкретные функции, а не общие:

var a = Torque_NewtonDominique(b1,b2,c1);

При принятии решения о том, какие результаты следует изложить, следует учитывать стоимость (копия против ссылки, l-значение против r-значения), читаемость и риск в отдельности для каждого оператора.

Например, нет никакой дополнительной выгоды от перемещения простых преобразований единиц / типов в их собственные строки, потому что они легко читаются и крайне маловероятны:

var radians = ExtractAngle(c1.Normalize())
var a = Torque(b1.ToNewton(),b2.ToMeters(),radians);

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

Питер
источник
В отношении стоимости передачи аргумента: есть два правила оптимизации. 1) Не надо. 2) (только для экспертов) Пока нет .
RubberDuck
1

Читаемость является основной частью ремонтопригодности. Сомневаться во мне? Выберите большой проект на языке, который вы не знаете (желательно, как на языке программирования, так и на языке программистов), и посмотрите, как вы будете проводить его рефакторинг ...

Я бы поставил удобочитаемость где-то между 80 и 90 ремонтопригодностью. Остальные 10-20 процентов - насколько это поддается рефакторингу.

Тем не менее, вы эффективно передаете 2 переменные вашей конечной функции (F). Эти 2 переменные создаются с использованием 3 других переменных. Было бы лучше передать b1, b2 и c1 в F, если F уже существует, тогда создайте D, который выполняет композицию для F и возвращает результат. На этом этапе нужно просто дать D хорошее имя, и не имеет значения, какой стиль вы используете.

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

Читаемый означает, что программист может держать логику (вход, выход и алгоритм) в своей голове. Чем больше, тем МЕНЬШЕ программист может понять. Читайте о цикломатической сложности.

jmoreno
источник
1
Я согласен со всем, что вы говорите о читабельности. Но я не согласен , что растрескивание логической операции на отдельные методы, обязательно делает его более удобным для чтения , чем растрескивание его на отдельные строки (как методы , которые могут , когда злоупотребляют, сделать простую логику менее читаемой, и сделать всю программу более загромождена) - если вы слишком сильно разберетесь с методами, вы в конечном итоге будете эмулировать макросы на ассемблере и упускать из виду их интеграцию в целом. Кроме того, в этом отдельном методе вы все равно столкнетесь с той же проблемой: вложите вызовы или разбейте их на промежуточные переменные.
Стив
@ Steve: я не говорил всегда делать это, но если вы думаете об использовании 5 строк для получения одного значения, есть хороший шанс, что функция будет лучше. Что касается нескольких строк против сложной строки: если это функция с хорошим именем, обе будут работать одинаково хорошо.
Jmoreno
1

Независимо от того, находитесь ли вы в C # или C ++, пока вы находитесь в отладочной сборке, возможное решение заключается в переносе функций

var a = F(G1(H1(b1), H2(b2)), G2(c1));

Вы можете написать oneline выражение, и, тем не менее, указать, где проблема, просто посмотрев трассировку стека.

returnType F( params)
{
    returnType RealF( params);
}

Конечно, если вы вызываете одну и ту же функцию несколько раз в одной и той же строке, вы не можете знать, какую функцию, однако вы все равно можете ее идентифицировать:

  • Глядя на параметры функции
  • Если параметры идентичны, а функция не имеет побочных эффектов, то два идентичных вызова становятся 2 одинаковыми вызовами и т. Д.

Это не серебряная пуля, но не так уж плохо на полпути.

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

type CallingGBecauseFTheorem( T b1, C b2)
{
     return G1( H1( b1), H2( b2));
}

var a = F( CallingGBecauseFTheorem( b1,b2), G2( c1));
Разработчик игр
источник
1

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

Вышеупомянутое утверждение плотное, но «самодокументированное»:

double d = sqrt(square(x1 - x0) + square(y1 - y0));

Когда разбивается на этапы (проще для тестирования, конечно) теряет весь контекст, как указано выше:

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

И очевидно, что использование имен переменных и функций, в которых четко указана их цель, неоценимо.

Даже блоки «если» могут быть хорошими или плохими в самодокументировании. Это плохо, потому что вы не можете легко заставить первые 2 условия проверить третье ... все они не связаны:

if (Bill is the boss) && (i == 3) && (the carnival is next weekend)

Этот имеет больше «коллективного» смысла и легче создавать условия тестирования:

if (iRowCount == 2) || (iRowCount == 50) || (iRowCount > 100)

И это утверждение - просто случайная строка символов, рассматриваемая с точки зрения самодокументирования:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

Глядя на вышеприведенное утверждение, ремонтопригодность по-прежнему является серьезной проблемой, если функции H1 и H2 обе изменяют одни и те же «переменные состояния системы» вместо объединения в одну функцию «H», потому что кто-то в конечном итоге изменит H1, даже не думая, что существует H2 функция, чтобы смотреть и может сломать H2.

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

Ozymandias
источник