Метод вилучення та основні припущення


27

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

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

Єдине рішення, яке я бачив для цього, - це whereпункт Haskell, який дозволяє визначити невеликі функції, які використовуються лише у функції "батьків". В основному, це виглядає приблизно так:

len x y = sqrt $ (sq x) + (sq y)
    where sq a = a * a

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

Отже, моє запитання - чи стикаєтесь ви з цим, і чи бачите ви, що це проблема? Якщо це так, як ти зазвичай це вирішуєш, особливо в "основних" мовах OOP, як-от Java / C # / C ++?

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

Оновлення: Якщо ви дотримувались цього питання та обговорення під ним, вам може сподобатися ця стаття Джона Кармака з цього питання , зокрема:

Крім усвідомлення фактичного коду, який виконується, функції вбудовування також мають перевагу в тому, що він не дозволяє викликати функцію з інших місць. Це звучить смішно, але в цьому є сенс. Оскільки база коду зростає з роками використання, з’явиться багато можливостей скористатися ярликом і просто зателефонувати до функції, яка виконує лише ту роботу, яку ви вважаєте за потрібну. Можливо, існує функція FullUpdate (), яка викликає PartialUpdateA (), і PartialUpdateB (), але в певному конкретному випадку ви можете зрозуміти (або подумати), що вам потрібно зробити лише PartialUpdateB (), і ви будете ефективними, уникаючи іншого робота. З цього випливає багато і багато помилок. Більшість помилок є результатом того, що стан виконання не є таким, яким ви вважаєте, що це таке.




@gnat питання, яке ви пов’язали, обговорює, чи потрібно взагалі витягувати функції, а я не сумніваюся. Натомість я ставлю під сумнів найоптимальніший метод зробити це.
Макс Янков

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

1
@Doval з мого досвіду, це дійсно так. Коли навколо цього, як ви описуєте, є клопітні помічники, витяг нового класу згуртованості
gnat

Відповіді:


29

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

Ваша турбота обгрунтована. Є ще одне рішення.

Зробіть крок назад. Що принципово є метою методу? Методи виконують лише одну з двох речей:

  • Виробляють значення
  • Викликати ефект

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

Ви зазначаєте, що методи викликаються в "контексті". Що це за контекст?

  • Значення аргументів
  • Стан програми поза методом

По суті, на що ви вказуєте: правильність результату методу залежить від контексту, в якому він викликається .

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

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

Вирішення цієї проблеми полягає в тому, щоб передумови та постумови були явними в програмі . Наприклад, у C # ви можете використовувати Debug.Assertабо Код контрактів, щоб висловити передумови та постумови.

Наприклад: я працював над компілятором, який проходив кілька "етапів" компіляції. Спочатку код буде лексикований, потім проаналізований, потім типи будуть вирішені, потім ієрархії спадкування перевірятимуться на цикли тощо. Кожен біт коду був дуже чутливим до його контексту; Наприклад, було б катастрофічно запитати "чи цей тип конвертований у цей тип?" якщо графік базових типів ще не був відомий як ациклічний! Тому кожен біт коду чітко задокументував свої передумови. Ми assertв методі, який перевіряв конвертованість типів, що ми вже пройшли перевірку "базових типів ацилових", і тоді читачеві стало зрозуміло, де метод можна викликати і де його не можна викликати.

Звичайно, існує маса способів, коли хороший дизайн методу пом'якшує визначену вами проблему:

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

+1 за відповідь, яка пояснює проблему з точки зору передумов / постумов.
QuestionC

5
Я хочу додати, що часто можна (і гарна ідея!) Делегувати перевірку до і після умов до типової системи. Якщо у вас є функція, яка бере stringі зберігає її в базі даних, ви загрожуєте ін'єкцією SQL, якщо ви забудете її очистити. Якщо, з іншого боку, ваша функція займає a SanitisedString, і єдиний спосіб отримати це SantisiedString- зателефонувавши Sanitise, то ви виключили помилки інжекцій SQL за конструкцією. Я все частіше шукаю способи змусити компілятор відкинути неправильний код.
Бенджамін Ходжсон

+1 Одне, що важливо зауважити, - це роздрібнення великого методу на менші шматки: це зазвичай не корисно, якщо тільки попередні умови та постулати не будуть більш спокійними, ніж були б спочатку, і в кінцевому підсумку ви зможете оплатити витрати, повторно зробивши чеки, які ви в іншому випадку вже зробили. Це не зовсім «безкоштовний» процес рефакторингу.
Мехрдад

"Що це за контекст?" просто для уточнення, я здебільшого мав на увазі приватний стан об'єкта, до якого використовується цей метод. Я думаю, що він включений до другої категорії.
Макс Янков

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

13

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

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


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

Нещодавно я просто опинився в ситуації, яка була б ідеальною для цього з точки зору архітектури. Я написав програмний рендерінг із класом візуалізації та методом публічного відтворення, який мав багато контексту, який він використовував для виклику інших методів. Я задумав створити для цього окремий клас RenderContext, однак просто видався надзвичайно марнотратним виділяти цей проект і розміщувати цей проект. github.com/golergka/tinyrenderer/blob/master/src/renderer.h
Макс Янков

6

Багато мов дозволяють вкладати такі функції, як Haskell. Java / C # / C ++ є фактично відносними переживаючими в цьому плані. На жаль, вони настільки популярні, що люди думають: "Це має бути поганою ідеєю, інакше моя улюблена" основна "мова дозволила б це".

Java / C # / C ++ в основному вважають, що клас повинен бути єдиною групою методів, які вам коли-небудь потрібні. Якщо у вас так багато методів, що ви не можете визначити їх контекст, слід скористатися двома загальними підходами: сортувати їх за контекстом або розділити їх за контекстом.

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

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


Вкладені функції ... хіба це не те, чого досягають лямбда-функції в C # (і Java 8)?
Артуро Торрес Санчес

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

Ці приклади Python, безумовно, можливі в C #. Наприклад, факторний . Вони можуть бути більш багатослівними, але вони на 100% можливі.
Артуро Торрес Санчес

2
Ніхто не сказав, що це неможливо. ОП навіть згадав про використання лямбда у своєму питанні. Просто, якщо ви витягнете метод заради читабельності, було б непогано, якби він був більш читабельним.
Карл Білефельдт

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

4

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

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

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

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


1

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

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

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

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

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

Тепер мені подобаються 80-лінійні функції великої дупи, якщо вони все ще виконують єдину і чітку відповідальність і не мають 8 рівнів вкладених блоків. Вони призводять до того, що в системі є менше речей для тестування та осмислення, навіть якщо менші, нарізані версії цих великих функцій були лише приватними деталями впровадження, які ніхто інший не може зателефонувати ... Це, як правило, відчуває, що в системі відбувається менше взаємодій. Мені навіть подобається дуже скромне дублювання коду, якщо це не складна логіка (скажімо, лише 2-3 рядки коду), якщо це означає менше функцій. Мені подобаються міркування Кармака про те, щоб зробити цю функцію неможливою для виклику в іншому місці вихідного файлу. Там '

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

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


0

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

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

Ви вже згадали про інше рішення - визначте помічника всередині основної функції. Це може бути дещо незвичайна ідіома в деяких мовах, але я не думаю, що це буде заплутано (якщо тільки ваші ровесники не бентежать лямбдами взагалі). Це працює лише в тому випадку, якщо ви можете легко визначити функції або функціональні об'єкти. Я б не намагався цього робити в Java 7, наприклад, оскільки анонімний клас вимагає введення 2 рівнів введення навіть для найменшої "функції". Це максимально близько до letабо whereпункту, який ви можете отримати; ви можете посилатися на локальні змінні перед визначенням, і помічник не може бути використаний поза цим діапазоном.

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