Однорядкові функції, які викликаються лише один раз


120

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

Він міг би виконати запит, перевірити деякі значення, зробити щось, що стосується регулярного виразів ... нічого незрозумілого або "хакі".

Обґрунтуванням цього слід було б уникати важкочитаних оцінок:

if (getCondition()) {
    // do stuff
}

де getCondition()однолінійна функція.

Моє запитання просто: чи це хороша практика? Мені це здається нормальним, але я довго не знаю ...


9
Якщо ваш фрагмент коду не є мовою OOP з неявним приймачем (наприклад, це), це, безумовно, було б поганою практикою, оскільки getCondition (), ймовірно, покладається на глобальний стан ...
Ingo,

2
Він, можливо, мав на увазі, що getCondition () може мати аргументи?
user606723

24
@Ingo - деякі речі дійсно мають глобальний стан. Поточний час, імена хостів, номери портів і т. Д. - всі дійсні "Глобали". Помилка дизайну - це створення Глобального з чогось, що по суті не є глобальним.
Джеймс Андерсон

1
Чому ти просто не нарікаєш getCondition? Якщо він настільки малий і нечасто використовується, як ви кажете, то давання йому імені нічого не досягає.
davidk01

12
davidk01: читабельність коду.
wjl

Відповіді:


240

Залежить від цього одного рядка. Якщо рядок є читабельним і стислим сам по собі, функція може не знадобитися. Спрощений приклад:

void printNewLine() {
  System.out.println();
}

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

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

99
+1. Чарівне слово тут - «Код самодокументування».
Конаміман

8
Хороший приклад того, що дядько Боб назвав "умовними умовами".
Ентоні Пеграм

12
@Aditya: Ніщо не говорить про те, що taxPayerце глобально в цьому сценарії. Можливо, цей клас є TaxReturnі taxPayerє атрибутом.
Адам Робінсон

2
Крім того, це дозволяє документувати функцію таким чином, щоб її можна було взяти, наприклад, javadoc і бути загальнодоступною.

2
@dallin, Чистий код Боб Мартіна показує безліч прикладів напівреального життя. Якщо у вас є занадто багато функцій в одному класі, можливо, ваш клас занадто великий?
Péter Török

67

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

startIgnition();
petrolFlag |= 0x006A;
engageChoke();

У цьому випадку, безумовно, краще перенести середню лінію в розважливу функцію.


15
Це 0x006A прекрасно: DA добре відома константа вуглекислого палива з добавками a, b і c.
Coder

2
+1 Я все за самодокументування коду :) до речі, я думаю, що погоджуюсь підтримувати постійний рівень абстракції через блоки, але не зміг пояснити, чому. Ви б не хотіли це розширити?
vemv

3
Якщо ви просто говорите, що petrolFlag |= 0x006A;без будь-якого прийняття рішень, то краще просто сказати petrolFlag |= A_B_C; без додаткової функції. Імовірно, його engageChoke()слід викликати лише у тому випадку, якщо він petrolFlagвідповідає певним критеріям, і це чітко повинно сказати: «Мені тут потрібна функція». Лише незначна нітка, ця відповідь в основному помітна на іншому :)
Tim Post

9
+1 для вказівки, що код у методі повинен знаходитися на одному рівні абстракції. @vemv, це добра практика, оскільки це полегшує розуміння коду, уникаючи необхідності вашого розуму стрибати вгору і вниз між різними рівнями абстракції під час читання коду. Прив’язання перемикачів рівня абстракції до методу викликів / повернень (тобто структурних «стрибків») - хороший спосіб зробити код більш плавним та чистим.
Péter Török

21
Ні, це цілком складене значення. Але той факт, що вам це цікаво, просто підкреслює суть: якби код сказав mixLeadedGasoline(), вам би не довелося!
Кіліан Фот

37

Я думаю, що у багатьох випадках така функція є гарним стилем, але ви можете розглядати локальну булеву змінну як альтернативу у випадках, коли вам не потрібно використовувати цю умову десь в інших місцях, наприклад:

bool someConditionSatisfied = [complex expression];

Це дасть підказку для зчитування коду та вбереже від введення нової функції.


1
Буль особливо корисний, якщо назва функції умови було б важким або, можливо, оманливим (наприклад IsEngineReadyUnlessItIsOffOrBusyOrOutOfService).
dbkk

2
Я відчув цю пораду поганою ідеєю: бул та стан відволікають увагу від основної діяльності функції. Також стиль, що надає перевагу локальним змінним, ускладнює рефакторинг.
Вовк

1
@Wolf OTOH, я віддаю перевагу цьому функціональному виклику, щоб зменшити "глибину абстракції" коду. IMO, перехід до функції - це більш широкий контекстний перемикач, ніж явний і прямий пару рядків коду, особливо якщо він просто повертає булевий.
Кейч

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

23

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

Слідуючи прикладу Петра, якщо це

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

стає цим

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isMutant() 
        && (taxPayer.getNumberOfThumbs() > 2 
        || (taxPayer.getAge() > 123 && taxPayer.getEmployer().isXMan()));
}

Ви робите одну редакцію, і вона оновлюється повсюдно. Мудрість ремонту, це плюс.

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


2
Я вже усвідомлюю переваги збереження єдиного пункту редагування - саме тому питання говорить "... зателефонував один раз". Тим не менш, це здорово знати про такі оптимізації компілятора, я завжди думав, що вони буквально слідують цим інструкціям.
vemv

Розщеплення методу тестування простої умови має на увазі, що умова може бути змінена шляхом зміни методу. Такі наслідки корисні, коли це правда, але можуть бути небезпечними, коли це не так. Наприклад, припустимо, що код повинен ділити речі на легкі об'єкти, важкі зелені об'єкти та важкі незелені об'єкти. Об'єкти мають швидке властивість "здається зеленим", яке є надійним для важких предметів, але може повертати помилкові позитиви з легкими предметами; вони також мають функцію "mjeraSpectralResponse", яка повільна, але надійно працюватиме для всіх об'єктів.
supercat

Той факт, що seemsGreenз легкими предметами буде ненадійним, буде неактуальним, якщо код ніколи не піклується про те, чи легкі об’єкти зелені. Якщо визначення "легкої ваги" зміниться, однак, так що деякі незелені об'єкти, для яких повертається істина seemsGreen, не повідомляються як "легкі", то така зміна визначення "легкої ваги" може порушити код, який тестує на об'єкти будучи "зеленим". У деяких випадках тестування на зеленість та вагу разом у коді може зробити зв’язок між тестами більш чітким, ніж якщо це окремі методи.
supercat

5

Крім читабельності (або доповнення до неї) це дозволяє записувати функції на належному рівні абстракції.


1
Боюся, я не розумію, що ви мали на увазі ...
vemv

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

4

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

Однак іноді це просто щось, що вам не потрібно. Коли вираз легко прочитати в будь-якому випадку та / або просто з’являється в одному місці, тоді не загортайте його.


3

Я думаю, якщо у вас їх є лише кілька, то це добре, але проблема виникає, коли їх у вашому коді багато. І коли компілятор працює або інтерпітер (залежно від мови, якою ви користуєтесь), він переходить до цієї функції в пам'яті. Тож скажемо, що у вас є 3 з них, я не думаю, що комп'ютер помітить, але якщо ви почнете мати 100 з цих дрібниць, то система повинна реєструвати функції в пам'яті, які викликаються лише один раз, а потім не знищуються.


Відповідно до відповіді Стівена, це може не завжди статися (хоча довіряти
сліпій

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

+1 для вирівнювання планет :), але твоє останнє речення для мене звучить абсолютно глухо, ти справді це робиш?
vemv

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

3

Я точно це зробив нещодавно у програмі, яку я переробляв, щоб явно визначити власне значення коду, не коментуючи:

protected void PaymentButton_Click(object sender, EventArgs e)
    Func<bool> HaveError = () => lblCreditCardError.Text == string.Empty && lblDisclaimer.Text == string.Empty;

    CheckInputs();

    if(HaveError())
        return;

    ...
}

Просто цікаво, що це за мова?
vemv

5
@vemv: Мені схоже на C #.
Скотт Мітчелл

Я також віддаю перевагу додатковим ідентифікаторам над коментарями. Але чи справді це відрізняється від введення локальної змінної для збереження ifкороткого? Цей локальний (лямбда) підхід робить PaymentButton_Clickфункцію (в цілому) важче читати. lblCreditCardErrorУ вашому прикладі , як видається, є членом, так і HaveErrorє (приватний) предикат , який дійсний для об'єкта. Я схильний би спростувати це, але я не програміст на C #, тому я протистояв.
Вовк

@ Вовк Ей, чоловіче. Я це писав зовсім недавно :) Я напевно роблю речі зовсім по-іншому зараз. Насправді, дивлячись на вміст етикетки, щоб побачити, чи виникла помилка, змушує мене мріяти ... Чому я просто не повернув bool від CheckInputs()???
joshperry

0

Переміщення цього рядка у добре названий метод полегшує читання вашого коду. Багато інших вже згадували про це ("код самодокументування"). Інша перевага переходу його до методу полягає в тому, що це полегшує тестування одиниці. Коли це виділено у власний метод та перевірено блок, ви можете бути впевнені, що якщо / коли буде виявлена ​​помилка, її не буде у цьому методі.


0

Хороших відповідей уже багато, але є особливий випадок, який варто згадати .

Якщо ваш однорядковий оператор потребує коментаря , і ви зможете чітко визначити (що означає: ім'я) його призначення, розгляньте можливість вилучення функції при вдосконаленні коментаря в API doc. Таким чином ви полегшите швидше і зрозуміти виклик функції.

Цікаво, що те ж саме можна зробити, якщо наразі нічого не робити, але коментар, що нагадує про необхідні розширення (у найближчому майбутньому 1) ), тож це,

def sophisticatedHello():
    # todo set up
    say("hello")
    # todo tear down

міг також змінитися в цьому

def sophisticatedHello():
    setUp()
    say("hello")
    tearDown()

1) ви повинні бути впевнені в цьому (див. Принцип YAGNI )


0

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

someCondition = lambda p: True if [complicated expression involving p] else False
#I explicitly write the function with a ternary to make it clear this is a a predicate
if (someCondition(p)):
    #do stuff...

ІМХО - це хороший компроміс, оскільки він дає перевагу для читабельності, коли складний вираз не засмічує ifумову, уникаючи захаращування простору імен глобального / пакету невеликими мітками, що викидають. Додатковою перевагою є те, що функція "визначення" знаходиться саме там, де її використовується, що дозволяє легко змінювати та читати визначення.

Це не повинні бути лише предикатними функціями. Мені подобається включати неодноразову плиту котла в такі маленькі функції (Це особливо добре працює для генерації пітонічного списку, не захаращуючи синтаксис дужок). Наприклад, наступний спрощений приклад під час роботи з PIL в python

#goal - I have a list of PIL Image objects and I want them all as grayscale (uint8) numpy arrays
im_2_arr = lambda im: array(im.convert('L')) 
arr_list = [im_2_arr(image) for image in image_list]

Чому "лямбда p: Істинно, якщо [складний вираз із залученням p] else False" замість "lambda p: [складний вираз із залученням p]"? 8-)
Ганс-Пітер Штерр

@hstoerr це в коментарі прямо під цим рядком. Мені подобається чітко знати, що ми деякіCondition - це предикат. Хоча це абсолютно непотрібно, я пишу безліч наукових сценаріїв, читаних людьми, які не так багато кодують, я особисто вважаю, що більш доречно мати додаткову лайливість, а не збивати з пантелику своїх колег, бо вони не знають того [] == Falseчи іншого подібна пітонічна еквівалентність, що не завжди "інтуїтивно зрозуміла". Це, в основному, спосіб позначити, що деякаУмова насправді є присудком.
катастрофа

Просто, щоб усунути мою очевидну помилку, [] != Falseале []це Неправдиво, як коли вона кидається на
булінг

@crasic: коли я не чекаю, що мої читачі знають, що [] оцінює значення False, я вважаю за краще робити len([complicated expression producing a list]) == 0, а не використовувати те, True if [blah] else Falseщо читач все ще вимагає знати, що [] оцінює значення False.
Лі Лі Раян
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.