Чи приватні методи з єдиним посиланням поганий стиль?


139

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

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



7
Усі відповіді ґрунтуються на думці. Оригінальні автори завжди вважають, що їх код для равіолі є більш читабельним; наступні редактори називають це равіолі.
Френк Хілеман

5
Я пропоную вам прочитати чистий код. Він рекомендує цей стиль і має безліч прикладів. Він також має рішення для проблеми "стрибки навколо".
Кет

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

1
Чи не вся структура СТРУТів базується на одноразових методах Getter / Setter, які є методами одноразового використання?
Zibbobz

Відповіді:


203

Ні, це не поганий стиль. Насправді це дуже хороший стиль.

Приватні функції не повинні існувати просто через повторне використання. Це, безумовно, одна вагома причина для їх створення, але є й інша: розкладання.

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

Якщо ви розділите цю функцію на більш дрібні шматки, вона все одно "виконує" стільки ж роботи, скільки раніше, але на менші шматочки. Він викликає інші функції, які повинні мати описові назви. Основна функція читається майже як книга: зробіть A, потім зробіть B, потім C і т.д. Функції, які вона викликає, можуть називатися лише в одному місці, але тепер їх менше. Будь-яка конкретна функція обов'язково є пісочницею від інших функцій: вони мають різні області застосування.

Коли ви розкладаєте велику проблему на менші проблеми, навіть якщо ті менші проблеми (функції) використовуються / вирішуються лише один раз, ви отримуєте кілька переваг:

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

  • Місцевість довідки. Тепер стає неможливим оголосити та використовувати змінну, а потім наклеїти її та знову використовувати 100 рядків пізніше. Ці функції мають різні сфери застосування.

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

  • Модульність. Тепер, коли у вас є приватні функції, ви можете знайти один або декілька з них, які можна було б витягнути в окремий клас, незалежно від того, використовується він лише тут або є багаторазовим. До попереднього пункту цей окремий клас, ймовірно, буде простішим і для тестування, оскільки він потребує публічного інтерфейсу.

Ідея розділити великий код на більш дрібні шматки, які легше зрозуміти та перевірити, є ключовим моментом книги «Чистий код дядька Боба» . На момент написання цієї відповіді книзі виповнилося дев'ять років, але сьогодні так само актуально, як і тоді.


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

14
Іноді методи розщеплення можуть бути хорошими, однак утримання речей у черзі також може мати переваги. Якщо, наприклад, перевірка кордону може бути розумно виконана як всередині, так і зовні блоку коду, наприклад, зберігання цього блоку коду в рядку дозволить легко зрозуміти, чи проводиться перевірка кордону один раз, більше одного разу чи взагалі . Перетягування блоку коду у власну підпрограму ускладнить констатацію таких речей.
supercat

7
Куля "читабельність" може бути позначена "Абстракція". Йдеться про абстракцію. "Шматки розміру" роблять одне - одну річ низького рівня, яку вам не цікавлять деталі, що містять гострий смак, коли ви читаєте чи переглядаєте публічний метод. Абстрагуючи його функцією, ви можете переступити над нею і зосередитись на великій схемі речей / того, що публічний метод робить на більш високому рівні.
Матьє Гіндон

7
"Тестування" кулі є сумнівним ІМО. Якщо ваші приватні члени хочуть пройти тестування, вони, ймовірно, належать до свого класу, як громадські члени. Звичайно, перший крок до вилучення класу / інтерфейсу - це вилучення методу.
Матьє Гіндон

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

36

Це, мабуть, чудова ідея!

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

function step1(){
  // ...
  step2(zarb, foo, biz);
}

function step2(zarb, foo, biz){
  // ...
  step3(zarb, foo, biz, gleep);
}

function step3(zarb, foo, biz, gleep){
  // ...
}

Тепер ви фактично додали рядки джерела та значно знизили загальну читабельність. Особливо, якщо ви зараз передаєте безліч параметрів між кожною функцією для відстеження стану. Yikes!

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

function foo(){
  f = getFrambulation();
  g = deglorbFramb(f);
  r = reglorbulate(g);
}

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

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

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


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

34
@FrankHileman Справедливо кажучи, я ніколи не бачив, щоб будь-який розробник компліментував код свого попередника.
Т. Сар

4
@TSar попередній розробник - джерело всіх проблем!
Френк Хілеман

18
@TSar Я ніколи навіть не вітав попереднього розробника, коли я був попереднім розробником.
IllusiveBrian

3
Приємна річ у розкладанні - це те, що потім можна складати foo = compose(reglorbulate, deglorbFramb, getFrambulation);
:;

19

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

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

  • Абстракції є герметичними . Хоча приємно робити вигляд, як виконується операція або як зберігаються дані, не має значення, але є випадки, коли це відбувається, і важливо їх ідентифікувати.

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

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

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

Розкладайте функції на основі їх внутрішньої згуртованості та зв'язку, а не абсолютної довжини.


6
Ігноруємо читабельність, давайте розглянемо аспект повідомлення про помилку: якщо користувач повідомляє, що сталася помилка MainMethod(досить легко дістатись, як правило, символи включаються, і у вас повинен бути файл перенасичення символів), який становить 2k рядків (номери рядків ніколи не включаються в звіти про помилки), і це NRE, який може статися за 1 тис. цих рядків, і я буду проклятий, якщо я розглядаю це. Але якщо вони скажуть мені, що це сталося, SomeSubMethodAі це 8 рядків, а виняток був NRE, я, ймовірно, знайду і виправлю це досить просто.
410_Gone

2

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

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

  2. Сам код.

Якщо код набагато складніше, ніж опис, заміна вбудованого коду на виклик функції може полегшити розуміння оточуючого коду. З іншого боку, деякі поняття можна виразити читабельніше на комп'ютерній мові, ніж на людській. Я вважав би w=x+y+z;, наприклад, більш читаним, ніж w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z);.

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


2
Назва вашої функції вводить в оману. На w = x + y + z немає нічого, що вказує на будь-який вид управління переповненням, тоді як назва методу сама по собі начебто означає інше. Кращою назвою вашої функції було б, наприклад, "addAll (x, y, z)", або навіть просто "Sum (x, y, z)".
Т. Сар

6
Оскільки мова йде про приватні методи , це питання, можливо, не стосується C. У будь-якому разі, це не моє значення - ви можете просто назвати ту саму функцію «сума», не потребуючи вирішення припущення про підпис методу. За вашою логікою, будь-який рекурсивний метод повинен називатися, наприклад, "DoThisAssumingStackDoesntOverflow". Те саме стосується і "FindSubstringAssumingStartingPointIsInsideBounds".
Т. Сар

1
Іншими словами, основні припущення, якими б вони не були (два числа не переповнюватимуться, стек не переллється, індекс був переданий правильно), не повинні міститись у назві методу. Ці речі повинні бути задокументовані, звичайно, якщо потрібно, але не на підписі методу.
Т. Сар

1
@TSar: Я трохи пильнував ім'я, але ключовим моментом є те, що (перехід на усереднення, тому що це кращий приклад) програміст, який бачить "(x + y + z + 1) / 3" в контексті, буде Набагато краще дізнатися, чи це підходящий спосіб обчислення середнього за кодом виклику, ніж той, хто дивиться або на визначення, або на сайт виклику int average3(int n1, int n2, int n3). Розбиття «середньої» функції означало б, що той, хто хотів би зрозуміти, чи підходить вона до вимог, повинен буде шукати в двох місцях.
supercat

1
Якщо ми, наприклад, візьмемо C #, у вас буде лише один "Середній" метод, який може бути прийнятий для прийняття будь-якої кількості параметрів. Насправді, у c # він працює над будь-якою колекцією номерів - і я впевнений, що це легше зрозуміти, ніж брати участь у грубій математиці.
Т. Сар

2

Це врівноважувальний виклик.

Краще - краще

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

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

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

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

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

Поки не стане занадто добре

Чи можна перестаратися з даванням імен невеликим шматкам коду і закінчитися занадто багатьма, занадто маленькими приватними методами? Звичайно. Один або більше з наступних симптомів вказують на те, що методи стають занадто маленькими, щоб бути корисними:

  • Занадто багато перевантажень, які насправді не представляють альтернативної підписи за тією ж суттєвою логікою, а скоріше одного фіксованого стека викликів.
  • Синоніми, що використовуються просто для позначення одного і того ж поняття неодноразово ("розважальне називання")
  • Багато недбалих прикраси іменування , такі як XxxInternalабо DoXxx, особливо , якщо там немає єдиної схеми в поданні тих.
  • Незграбні назви майже довші, ніж сама реалізація, як LogDiskSpaceConsumptionUnlessNoUpdateNeeded

1

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

Але іноді у мене є великий публічний метод, який можна розбити на менші кроки

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

Чому б не розкласти на приватні методи? Ось кілька причин:

  • Щільне зчеплення і тестабельність. Зменшивши розмір свого публічного методу, ви поліпшили його читабельність, але весь код все ще щільно з'єднаний між собою. Ви можете перевірити окремі приватні методи (використовуючи розширені функції рамки тестування), але ви не можете легко протестувати публічний метод незалежно від приватних методів. Це суперечить принципам одиничного тестування.
  • Розмір і складність класу. Ви зменшили складність методу, але ви збільшили складність класу. Публічний метод легше читати, але зараз його важче читати, оскільки він має більше функцій, які визначають його поведінку. Мої переваги - це невеликі класи з однією відповідальністю, тому довгий метод - знак того, що клас робить занадто багато.
  • Не може бути використаний легко. Часто буває так, що як частина коду дозріває багаторазове використання. Якщо ваші кроки є приватними методами, їх не можна повторно використовувати ніде, не попередньо витягнувши їх якось. Крім того, він може заохочувати копіювання-вставку, коли необхідний крок в іншому місці.
  • Розщеплення таким чином, ймовірно, буде довільним. Я можу стверджувати, що розділення довгого публічного методу не потребує такої великої думки чи дизайну, як якщо б ви розділили відповідальність на класи. Кожен клас повинен бути обґрунтований належним іменем, документацією та тестами, тоді як приватний метод не так багато розглядає.
  • Приховує проблему. Тож ви вирішили розділити свій публічний метод на невеликі приватні методи. Тепер проблем немає! Ви можете продовжувати додавати все більше і більше кроків, додаючи все більше приватних методів! Навпаки, я думаю, що це основна проблема. Він встановлює шаблон для додавання складності класу, за яким будуть подаватися подальші виправлення помилок та реалізація функцій. Незабаром ваші приватні методи виростуть, і їх доведеться розколоти.

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

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


1
Я мав би не погодитися. Занадто велика або передчасна абстракція призводить до зайво складного коду. Приватний метод може бути першим кроком на шляху, який потенційно може потребувати взаємозв'язку та абстрагування, але ваш підхід тут пропонує блукати навколо місць, які, можливо, ніколи не потребують відвідування.
WillC

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

Я згоден з таким підходом. Насправді, коли ви слідуєте TDD, це природна прогресія. Інша людина каже, що це занадто багато передчасного абстрагування, але мені здається, що набагато простіше швидко знущатися над функціоналом, який мені потрібен в окремому класі (за інтерфейсом), ніж реально реалізовувати його в приватному методі, щоб тест пройшов .
Вічний21
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.