Должен ли я извлечь определенные функции в функцию и почему?

29

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

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

Должен ли я сделать это до того, как написал весь код, или я должен оставить его, пока все не будет сделано, а затем извлечь функции?

dhblah
источник
19
«Я оставляю это, пока все не сделано» обычно синонимично с «Это никогда не будет сделано».
Эйфорическая
2
Это в целом верно, но также помните противоположный принцип YAGNI (который в данном случае не применим, так как он вам уже нужен).
Джоккинг
Просто хотел подчеркнуть, не сосредотачивайтесь так сильно на сокращении строк кода. Вместо этого попробуйте мыслить в терминах абстракций. Каждая функция должна иметь только одну работу. Если вы обнаружите, что ваши функции выполняют более одной работы, то, как правило, вам следует провести рефакторинг метода. Если вы будете следовать этим рекомендациям, будет почти невозможно иметь слишком длинные функции.
Адриан

Ответы:

35

Это книга, на которую я часто ссылаюсь, но я снова здесь: Чистый код Роберта С. Мартина , глава 3, «Функции».

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

Вы предпочитаете читать функцию с +150 строками или функцию, вызывающую 3 +50 строковых функций? Я думаю, что предпочитаю второй вариант.

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

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

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

Наконец, и это не противоречит тому, что я только что написал, спросите себя, действительно ли вам нужно выполнить этот рефакторинг, проверьте ответы на вопрос « Когда проводить рефакторинг ?» (также ищите ТАК вопросы по «рефакторингу», их больше и ответы интересно читать)

Должен ли я сделать это до того, как напишу весь код, или я должен оставить его, пока все не будет сделано, а затем извлечь функции?

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

Jalayn
источник
10
На самом деле Боб Мартин несколько раз показал, что он предпочитает 7 функций с 2–3 строками над одной функцией с 15 строками (см. Сайты sites.google.com/site/unclebobconsultingllc/… ). И именно здесь многие даже опытные разработчики будут сопротивляться. Лично я думаю, что многие из этих «опытных разработчиков» просто не могут согласиться с тем, что они могут улучшить такие базовые вещи, как построение абстракций с функциями после> 10 лет написания кода.
Док Браун
+1 только за ссылку на книгу, которая, по моему скромному мнению, должна быть на полках любой софтверной компании.
Фабио Марколини
3
Я мог бы перефразировать здесь, но фраза из этой книги, которая звучит в моей голове почти каждый день: «Каждая функция должна делать только одну вещь, и делать это хорошо». Это выглядит особенно актуально, поскольку ОП сказал, что «моя основная функция состоит в том, чтобы
делать
Вы абсолютно правы!
Джалайн
Зависит от того, насколько тесно связаны три отдельные функции. Может быть проще следовать за блоком кода, который находится в одном месте, чем за тремя блоками кода, которые многократно зависят друг от друга.
user253751
13

Да, очевидно. Если это легко увидеть и разделить различные «задачи» одной функции.

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

Но могут быть проблемы с этим:

  • Нелегко увидеть, как отделить функцию. Это может потребовать рефакторинга внутренней части функции, прежде чем перейти к разделению.
  • Функция имеет огромное внутреннее состояние, которое передается. Это обычно требует какого-то решения ООП.
  • Трудно сказать, какую функцию следует выполнять. Модульное тестирование и рефакторинг, пока вы не знаете.
Euphoric
источник
5

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

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

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

Pro:

  • Как только вы прочитаете это, у вас будет полное представление обо всех операциях, которые выполняет функция (вы можете прочитать это как книгу);
  • Если вы хотите отладить его, вы можете выполнить его шаг за шагом, не переходя ни к какому другому файлу / части файла;
  • У вас есть свобода доступа к любой переменной, объявленной на любой стадии функции;
  • Алгоритм, в котором реализована функция, полностью содержится в функции (инкапсулирован);

Contra:

  • Требуется много страниц вашего экрана;
  • Это займет много времени, чтобы прочитать это;
  • Нелегко запомнить все разные шаги;

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

Pro:

  • За исключением функций ухода, каждая функция описывает словами (названиями подфункций) различные выполненные шаги;
  • Чтение каждой отдельной функции / подфункции занимает очень короткое время;
  • Понятно, какие параметры и переменные влияют на каждую подфункцию (разделение задач);

Contra:

  • Легко представить, что делает функция типа «sin ()», но не так легко представить, что делают наши подфункции;
  • Алгоритм теперь исчез, теперь он распределяется по подфункциям мая (без обзора);
  • При пошаговой отладке легко забыть вызов функции уровня глубины, из которой вы поступаете (переходя туда-сюда в файлах вашего проекта);
  • Вы можете легко потерять контекст при чтении различных подфункций;

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

Антонелло Церавола
источник
2

Для меня есть четыре причины для извлечения блоков кода в функции:

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

  • Это обратный вызов : это обработчик событий или какой-то код пользователя, который вызывает библиотека или фреймворк. (Я с трудом могу представить это без выполнения функций.)

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

  • Вам нужен самодокументируемый код : поэтому вместо того, чтобы писать строку комментария над блоком кода, обобщающим то, что он делает, вы извлекаете все это в функцию и называете то, что пишете в комментарии. Хотя я не фанат этого, потому что мне нравится выписывать название используемого алгоритма, причину, по которой я выбрал этот алгоритм, и т. Д. Имена функций были бы тогда слишком длинными ...

Calmarius
источник
1

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

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

Джон Ву
источник
0

В стороне: я написал это в ответ на вопрос Даллина (сейчас закрытый), но я все еще чувствую, что это может быть полезно для кого-то, так что здесь идет


Я думаю, что причина распыления функций в 2 раза, и, как упоминает @jozefg, зависит от используемого языка.

Разделение проблем

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

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

Скажем, в JavaScript у вас есть функция getMyData (), которая 1) создает сообщение мыла из параметров, 2) инициализирует ссылку на службу, 3) вызывает службу с сообщением мыла, 4) анализирует результат, 5) возвращает результат. Кажется разумным, я написал эту точную функцию много раз - но на самом деле , что может быть разбит на 3 частных функций только включая код для 3 и 5 (если что) , как ни один из другого кода непосредственно отвечает за получение данных от службы ,

Улучшенный опыт отладки

Если у вас полностью атомарные функции, ваша трассировка стека становится списком задач, в котором перечисляется весь успешно выполненный код, а именно:

  • Получить мои данные
    • Построить Мыло Сообщение
    • Инициализация справочной службы
    • Анализ обработанного ответа - ОШИБКА

было бы гораздо интереснее узнать, что при получении данных произошла ошибка. Но некоторые инструменты еще более полезны для отладки подробных деревьев вызовов, чем, например, Microsoft's Debugger Canvas .

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

К вашему сведению - это одна из тех вещей, которые «делай, как я говорю, а не я», хранить атомарный код бессмысленно, если только вы безжалостно не согласны с ним, ИМХО, а я нет.

Dead.Rabit
источник