Чтеність та ремонтопридатність, особливий випадок написання вкладених викликів функцій


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 #.


17
@gnat: ви посилаєтесь на загальне запитання, в той час як мене особливо цікавить згаданий випадок вкладених функцій викликів та наслідки у разі аналізу дамп-збоїв, але, дякую за посилання, він містить досить цікаву інформацію.
Домінік

9
Зауважте, що якщо цей приклад було застосовано до C ++ (як ви згадуєте про це, що використовується у вашому проекті), це не лише питання стилю, оскільки порядок оцінки HXта GXвикликів може змінюватися в одноколірці, як порядок оцінки аргументів функції не визначений. Якщо ви з якихось причин залежите від порядку побічних ефектів (свідомо чи несвідомо) у викликах, цей "стиль рефакторингу" може в кінцевому підсумку мати більше, ніж просто читабельність / обслуговування.
dfri

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()дзвінок проходив би по власній лінії, цей стиль в основному переносить вас на виклик, який ображає вас.

Тому, будь ласка, не використовуйте проблеми 1 і 2 як привід для вирішення проблеми 4. Використовуйте хороші імена, коли ви можете їх думати. Уникайте безглуздих імен, коли не можете.

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

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()
;

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


20
@Steve, і я не кажу тобі не робити. Я прошу значущого імені. Я часто бачив проміжний стиль, зроблений бездумно. Неправильні імена спалюють мій мозок набагато більше, ніж рідкісний код на рядок. Я не дозволяю міркуванням про ширину чи довжину мотивувати мене зробити свій код густим або мої імена короткими. Я дозволяю їм мотивувати мене більше розкладатися. Якщо добрих імен просто не відбудеться, розгляньте цю роботу, щоб уникнути безглуздого шуму.
candied_orange

6
Я додаю до вашого допису: У мене є невелике правило: якщо ви не можете його назвати, це може бути знаком того, що він не є чітко визначеним. Я використовую його для сутностей, властивостей, змінних, модулів, меню, класів помічників, методів тощо. У багатьох ситуаціях це крихітне правило виявило серйозний недолік у дизайні. Таким чином, хороша назва не лише сприяє читабельності та ремонтопридатності, але допомагає перевірити дизайн. Звичайно, є винятки з кожного простого правила.
Аліреза

4
Розширена версія виглядає некрасиво. Там занадто багато пробілів, які зменшуютьефективне відтворення всіх видів, що поетапно розподілено, тобто нічого іншого.
Mateen Ulhaq

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. це може бути проблемою, хоча, якщо функції добре відомі, часто очевидно, що потрібно скільки аргументів. Зокрема, як я вже сказав, для функцій інфікування відразу зрозуміло, що вони беруть два аргументи. (Крім того, синтаксис дужок-кортежів у більшості мов використовує загострює цю проблему; мовою, яка віддає перевагу Currying, це автоматично зрозуміліше F (G1 (H1 b1) (H2 b2)) (G2 c1).).
приблизно

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

8
Я виявив, що код, який легко налагоджувати, - це звичайно код, який не потребує налагодження.
Роб К

25

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

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

Ввівши значущі імена, ви можете подивитися, чи виглядає одна з форм природною, чи є золота середина, для якої потрібно зйомки.

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


17

Як завжди, коли мова йде про читабельність, невдача знаходиться в крайності . Ви можете приймати будь-які хороші поради щодо програмування, перетворити їх на релігійне правило і використовувати його для створення абсолютно нечитабельного коду. (Якщо ви не вірите мені на це, перевірити ці два IOCCC переможців borsanyi і Ґорен і подивляться на те, як по- різному вони використовують функції для візуалізації коди абсолютно нечитаною Підказка :. Borsanyi використовує рівно одну функцію, Гірничо багато, багато іншого ...)

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


14

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

Чи можливо мати код, який є доступним, але не читається? І навпаки, якщо код надзвичайно читабельний, чому він стане незрозумілим через його читабельність? Я ніколи не чув, щоб жоден програміст, який відбивав ці фактори один від одного, мав вибрати одного чи іншого!

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

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

Я б сказав, що такі судження мають значення:

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

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

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

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


3
+1 до "Чи можливо мати код, який можна підтримувати, але не читати?". Це теж була моя перша думка.
РонДжон

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, і не має значення, який стиль ви використовуєте.

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

Читання означає, що програміст може утримувати логіку (вхід, вихід та алгоритм) у своїй голові. Чим більше це робить, тим менше РОЗУМНИК може зрозуміти це. Прочитайте про цикломатичну складність.


1
Я згоден з усім, що ви говорите про читабельність. Але я не згоден , що розтріскування логічної операції на окремі методи, обов'язково робить його більш зручним для читання , ніж розтріскування його на окремі рядки (як методи , які можуть , коли зловживає, зробити просту логіку менш читабельним, і зробити всю програму більш метушнею) - якщо ви занадто далеко розіб’єте речі в методах, ви закінчуєте емуляцію макросів мовної збірки і втрачаєте уявлення про те, як вони інтегруються в ціле. Крім того, у цьому окремому методі ви все ще зіткнетеся з тією ж проблемою: вкладіть виклики або розбийте їх на проміжні змінні.
Стів

@Steve: Я не казав завжди робити це, але якщо ви думаєте про використання 5 рядків, щоб отримати єдине значення, є хороший шанс, що функція буде кращою. Що стосується кількох рядків проти складних ліній: якщо це функція з хорошим іменем, обидві будуть працювати однаково добре.
jmoreno

1

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

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

Ви можете написати однорядковий вираз і все одно вказати там, де проблема - просто переглянувши слід стека.

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.

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

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