Можно ли разделить длинные функции и методы на более мелкие, даже если они не будут вызваны ничем другим? [Дубликат]

    

У этого вопроса уже есть ответ:

    

В последнее время я пытаюсь разделить длинные методы на несколько коротких.

Например: У меня есть функция process_url () , которая разбивает URL-адреса на компоненты и затем назначает их некоторым объектам через их методы. Вместо того, чтобы реализовать все это в одной функции, я только готовлю URL для разделения в process_url () , а затем передаю его функции process_components () , которая затем передает компоненты для функции assign_components () .

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

Продолжая предыдущий пример: , кто-то, просматривающий код, может подумать, что функция process_components () абстрагируется в функцию, потому что она вызывается различными методами и функциями, когда на самом деле он вызывается только process_url () .

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

Является ли описанная мною техника разделения функций неправильной? Каков предпочтительный способ управления большими функциями и методами?

ОБНОВЛЕНИЕ: Моя основная проблема заключается в том, что абстрагирование кода в функцию может означать, что оно может быть вызвано несколькими другими функциями.

СМОТРИ ТАКЖЕ: обсуждения reddit на /r /programming a> (здесь представлена ​​другая перспектива, а не большинство ответов) и /r /readablecode .

157 голосов | спросил sbichenko 24 PMpWed, 24 Apr 2013 20:31:34 +040031Wednesday 2013, 20:31:34

13 ответов


220

Тестирование кода, которое делает много вещей, затруднено.

Отладка кода, который делает много вещей, трудно.

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

У моего коллеги есть фраза, которую он использует, если судить, следует ли разбить данный метод на более мелкие:

  

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

Вы писали:

  

У меня есть функция process_url (), которая разбивает URL-адреса на компоненты и , а затем назначает их некоторым объектам через их методы.

Это должно быть как минимум два метода. Хорошо обернуть их одним общедоступным способом, но работа должна быть двумя разными способами.

ответил 24 PMpWed, 24 Apr 2013 20:41:12 +040041Wednesday 2013, 20:41:12
76

Да, разделение длинных функций является нормальным. Это способ сделать то, что поощряет Роберт К. Мартин в своей книге Чистый код . В частности, вы должны выбирать очень описательные имена для своих функций, как форму самодокументирующего кода.

ответил Scott Whitlock 24 PMpWed, 24 Apr 2013 20:34:36 +040034Wednesday 2013, 20:34:36
40

Как указывали люди, это улучшает читаемость. Человек, читающий process_url () , может более четко увидеть, что является общим процессом для обработки URL-адресов, просто прочитав несколько имен методов.

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

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

  • объектно-ориентированные языки могут позволить определить некоторые частные методы, чтобы гарантировать, что они не используются в другом месте
  • модули на других языках могут указывать, какие функции видны снаружи и т. д.
  • языки с очень высоким уровнем, такие как Python, могут исключить необходимость определения нескольких функций, потому что они в любом случае будут простыми одним лайнером.
  • другие языки, такие как Prolog, могут потребовать (или настоятельно предложить) определение нового предиката для каждого условного перехода.
  • В некоторых случаях обычно определяется вспомогательные функции внутри используемой функции (локальные функции), иногда это анонимные функции (замыкания кода), это часто встречается в функциях обратного вызова Javascript.

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

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

  process_url = lambda url: dict (re.findall ('([^? = & amp;] *) = ([^? = & amp;] *)' , url))
 

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

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

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

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

ответил Trylks 24 PMpWed, 24 Apr 2013 21:26:01 +040026Wednesday 2013, 21:26:01
16

Я бы сказал, это зависит.

Если вы просто раскалываете его ради разделения и называете их именами, такими как process_url_partN и т. д., тогда НЕТ , пожалуйста, не делайте этого. Это просто усложняет последующие действия, когда вам или кому-то еще нужно выяснить, что происходит.

Если вы вынимаете методы с ясными целями, которые могут быть проверены сами по себе и имеют смысл сами по себе (даже если их никто не использует), ДА .


Для вашей цели кажется, что у вас есть две цели.

  1. Разберите URL-адрес и верните список его компонентов.
  2. Сделайте что-нибудь с этими компонентами.

Я бы написал первую часть отдельно и вернул бы довольно общий результат, который можно было бы легко протестировать и потенциально позже использовать. Еще лучше, я бы поискал встроенную функцию, которая уже делает это на вашем языке /фреймворке, и использую это вместо этого, если ваш синтаксический анализ не является особым. Если это супер специальное, я все равно напишу его как отдельный метод, но, вероятно, «свяжу» его как частный /защищенный метод в классе, который обрабатывает второй (если вы пишете объектно-ориентированный код).

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

ответил Svish 25 PMpThu, 25 Apr 2013 15:40:26 +040040Thursday 2013, 15:40:26
15

Я никогда не сталкивался с проблемой того, что другие разработчики разделяли более крупные методы на более мелкие методы, поскольку это образец, который я слежу за собой. Метод «Бог» - это страшная ловушка, в которую попадают, а другие менее опытные или просто не заботятся, как правило, чаще встречаются. Это сказано ...

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

Я живу на C # -land, поэтому у нас есть public , private , protected , internal ) и просмотр эти слова показывают меня за пределами тени сомнения в объеме метода и где я должен искать вызовы. Если он частный, я знаю, что метод используется только в одном классе, и я полностью доверяю рефакторингу.

В мире Visual Studio наличие нескольких решений ( .sln ) усугубляет этот анти-шаблон, поскольку помощники IDE /Resharper «Find Usages» не найдут использование вне открытого решения.

ответил Ryan Rodemoyer 25 PMpThu, 25 Apr 2013 23:37:38 +040037Thursday 2013, 23:37:38
11

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

  function process_url (url) {

    функция foo (a) {
        //...
    }

    функциональная панель (a) {
        //...
    }

    return [foo (url), bar (url)];

}
 

Если ваш язык программирования не поддерживает это, вы можете переместить foo () и bar () из области process_url () (так, чтобы он был виден другим функциям /методам), но считайте это «взломом», который вы создали, потому что ваш язык программирования не поддерживает эту функцию.

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

ответил mjs 25 PMpThu, 25 Apr 2013 18:28:00 +040028Thursday 2013, 18:28:00
9

Если кто-то интересуется какой-то литературой по этому вопросу: это именно то, что Джошуа Кериевский называет «составом метода» в своем «Рефакторинге к рисункам» (Аддисон-Уэсли):

  

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

Я считаю, что правильное вложение методов в соответствии с их «уровнем детализации» здесь важно.

Смотрите отрывок на сайте издателя:

  

Большая часть кода, который мы пишем, не начинается просто. Чтобы сделать его простым, мы должны задуматься над тем, что не является простым в этом вопросе, и постоянно спрашивать: «Как это может быть проще?» Мы можем часто упрощать код, рассматривая совершенно другое решение. Рефакторинги в этой главе представляют различные решения для упрощения методов, переходов состояний и древовидных структур.

     

Метод создания (123) посвящен разработке методов, которые эффективно сообщают, что они делают и как они делают то, что делают. Компонентный метод [Beck, SBPP] состоит из вызовов хорошо названных методов, которые находятся на одном уровне детализации. Если вы хотите, чтобы ваша система была простой, постарайтесь применить Compose Method (123) везде ...

     

http://ptgmedia.pearsoncmg.com/images/ch7_9780321213358/elementLinks/07fig01.jpg

Добавление: Кент Бек ( «Шаблоны реализации» ) относится к нему как «Составленный метод». Он советует вам:

  

[c] ompose методы из вызовов другим методам, каждый из которых находится примерно в   такой же уровень абстракции.   Одним из признаков плохо составленного метода является смесь абстракции   Уровни [.]

Здесь снова появляется предупреждение не смешивать разные уровни абстракции (акцент мой).

ответил Sebastian 1 Mayam13 2013, 11:47:45
5

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

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

  void foo () {

     если (x) {
       y = doXformanysmallthings ();
     }

     z = doYforthings (y);

     если (z! = y & amp; & amp; isFullmoon ()) {
         launchSpacerocket ()
     }
}
 

Перемещение материала на более мелкие функции обычно лучше, чем наличие множества циклов и т. д. внутри функции, которая согласуется с несколькими концептуальными «большими» шагами. (Если вы не можете объединить это в относительно небольшие выражения LINQ /foreach /lambda ...)

ответил Macke 26 PMpFri, 26 Apr 2013 18:31:16 +040031Friday 2013, 18:31:16
4

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

ответил Tom Haley 24 PMpWed, 24 Apr 2013 21:59:44 +040059Wednesday 2013, 21:59:44
4

Я уверен, что это не будет популярным мнением, но все в порядке. Локальность ссылки может быть огромной помощью, чтобы убедиться, что вы и другие понимаете функцию (в данном случае я имею в виду код, а не память).

Как и во всем, это баланс. Вы должны быть более озабочены тем, кто говорит вам «всегда» или «никогда».

ответил Fred 25 PMpThu, 25 Apr 2013 17:33:31 +040033Thursday 2013, 17:33:31
3

Рассмотрим эту простую функцию (я использую Scala-подобный синтаксис, но я надеюсь, что идея будет понятной без каких-либо знаний Scala):

  def myFun ... {
    ...
    если (условие 1) {
        ...
    } else {
        ...
    }
    если (условие 2) {
        ...
    } else {
        ...
    }
    если (условие 3) {
        ...
    } else {
        ...
    }
    //отдых
    ...
}
 

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

  • Это означает, что для проверки этой части вам понадобятся 8 различных тестов. Более того, скорее всего, некоторые комбинации не будут возможны, и тогда вам придется тщательно проанализировать, что они (и не забудьте пропустить некоторые из возможных) - много работы.
  • Очень сложно рассуждать о коде и его правильности. Поскольку каждый из блоков if и его условие могут зависеть от некоторых общих локальных переменных, чтобы знать, что происходит после них, все, кто работает с кодом, должны проанализировать все эти кодовые блоки и 8 возможных вариантов выполнения пути. Здесь очень легко совершить ошибку, и, скорее всего, кто-то, кто обновит код, что-то упустит и представит ошибку.

С другой стороны, если вы структурируете вычисление как

  def myFun ... {
    ...
    val result1 = myHelper1 (...);
    val result2 = myHelper2 (...);
    val result3 = myHelper3 (...);
    //отдых
    ...
}

private def myHelper1 (/* arguments * /): SomeResult = {
    если (условие 1) {
        ...
    } else {
        ...
    }
}
//Аналогично myHelper2 и myHelper3
 

вы можете:

  • Легко проверить каждую из вспомогательных функций, каждая из которых имеет только два возможных пути выполнения.
  • При рассмотрении myFun сразу становится очевидным, если result2 зависит от result1 (просто проверяя, является ли вызов myHelper2 (.. .) использует его для вычисления одного из аргументов. (Предполагая, что помощники не используют какое-либо глобальное состояние.) Также очевидно, что , как они зависимы, что-то очень много сложнее понять в предыдущем случае.Кроме того, после трех вызовов, также ясно, как выглядит состояние вычисления - оно отображается только в result1 , result2 и result3 - нет необходимости проверять, были ли изменены другие локальные переменные.
ответил Petr Pudlák 26 AMpFri, 26 Apr 2013 11:23:22 +040023Friday 2013, 11:23:22
2

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

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

ответил lucasvc 25 AMpThu, 25 Apr 2013 10:42:10 +040042Thursday 2013, 10:42:10
-5

Совершенно допустимо назвать ваши методы следующим образом:

  MyProcedure ()
  MyProcedure_part1 ()
  MyProcedure_part2 ()
конец;
 

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

ответил Colin Nicholls 25 PMpThu, 25 Apr 2013 21:25:16 +040025Thursday 2013, 21:25:16

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132