Являются ли закрытые методы с одной ссылкой плохим стилем?


139

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

Есть ли консенсус по этому вопросу? Лучше ли иметь длинные публичные методы или разбивать их на более мелкие части, даже если каждая часть не может быть повторно использована?



7
Все ответы основаны на мнении. Оригинальные авторы всегда думают, что их код равиоли более читабелен; последующие редакторы называют это равиоли.
Фрэнк Хилман

5
Я предлагаю вам прочитать Чистый код. Он рекомендует этот стиль и имеет много примеров. У него также есть решение проблемы «прыжков вокруг».
Кэт

1
Я думаю, что это зависит от вашей команды и от того, какие строки кода содержатся.
Надеемся, что

1
Разве вся структура STRUTS не основана на одноразовых методах Getter / Setter, все из которых являются одноразовыми?
Зиббобз

Ответы:


203

Нет, это не плохой стиль. На самом деле это очень хороший стиль.

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

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

Если вы разделите эту функцию на более мелкие части, она все равно «выполняет» такую ​​же работу, как и раньше, но на более мелкие части. Он вызывает другие функции, которые должны иметь описательные имена. Основная функция читается почти как книга: выполните A, затем B, затем C и т. Д. Функции, которые она вызывает, могут вызываться только в одном месте, но теперь они меньше. Любая конкретная функция обязательно изолирована от других функций: они имеют разные области действия.

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

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

  • Местность ссылки. Теперь становится невозможным объявить и использовать переменную, затем оставить ее и использовать снова 100 строк спустя. Эти функции имеют разные области применения.

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

  • Модульность. Теперь, когда у вас есть частные функции, вы можете найти одну или несколько из них, которые можно извлечь в отдельный класс, независимо от того, используется ли он только здесь или может использоваться повторно. К предыдущему пункту этот отдельный класс, вероятно, также будет легче тестировать, так как для него потребуется открытый интерфейс.

Идея разбить большой код на более мелкие части, которые легче понять и протестировать, является ключевым моментом книги Дяди Боба «Чистый код» . На момент написания этого ответа книге было девять лет, но сегодня она так же актуальна, как и тогда.


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

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

7
Пуля «читабельность» может быть помечена как «Абстракция». Это про абстракцию. «Куски размером с кусочек» делают одну вещь , одну вещь низкого уровня, которую вы не особо заботите о мельчайших деталях, когда читаете или проходите через публичный метод. Абстрагируя его от функции, вы перешагиваете через нее и сосредотачиваетесь на большой схеме вещей / на том, что публичный метод делает на более высоком уровне.
Матье

7
«Испытательная» пуля сомнительна ИМО. Если ваши частные участники хотят пройти тестирование, то они, вероятно, принадлежат к своему классу как публичные участники. Конечно, первым шагом к извлечению класса / интерфейса является извлечение метода.
Матье

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

36

Это, наверное, отличная идея!

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

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

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

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

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

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

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(достаточно легко получить, обычно символы включены, и у вас должен быть файл разыменования символов), то есть 2 000 строк (номера строк никогда не включаются в отчеты об ошибках), и это NRE, которое может произойти на 1k этих строк, ну, будь я проклят, если буду вникать в это. Но если мне скажут, что это произошло, SomeSubMethodAи в нем 8 строк, а исключением было NRE, то я, вероятно, найду и исправлю это достаточно просто.
410_Появился

2

ИМХО, ценность извлечения блоков кода исключительно как средство разрушения сложности, часто связана с различием в сложности между:

  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. В любом случае, это не моя точка зрения - вы могли бы просто вызвать ту же функцию «sum», не прибегая к предположению о сигнатуре метода. По вашей логике любой рекурсивный метод должен называться, например, «DoThisAssumingStackDoesntOverflow». То же самое касается "FindSubstringAssumingStartingPointIsInsideBounds".
Т. Сар

1
Другими словами, основные предположения, какими бы они ни были (два числа не будут переполнены, стек не переполнится, индекс был передан правильно) не должны присутствовать в имени метода. Разумеется, эти вещи должны быть задокументированы, если это необходимо, но не в сигнатуре метода.
Т. Сар

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

1
Если мы возьмем, например, C #, у вас будет только один метод «Средний», который можно использовать для принятия любого количества параметров. На самом деле, в c # он работает с любым набором чисел - и я уверен, что это гораздо проще для понимания, чем грубая математика в коде.
Т. Сар

2

Это балансирующая задача.

Тоньше лучше

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

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

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

Последующее обслуживание также помогает, потому что дополнительное именование помогает сделать самодокументирование кода .

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

Пока не станет слишком хорошо

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

  • Слишком много перегрузок, которые на самом деле не представляют альтернативные сигнатуры для той же самой важной логики, а скорее для одного фиксированного стека вызовов.
  • Синонимы используются только для повторного обращения к одному и тому же понятию («занимательное именование»)
  • Множество небрежных именных украшений, таких как XxxInternalили DoXxx, особенно если нет единой схемы их введения.
  • Корявые имена почти длиннее самой реализации, вроде LogDiskSpaceConsumptionUnlessNoUpdateNeeded

1

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

Но иногда у меня есть большой публичный метод, который можно разбить на более мелкие шаги

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

Почему бы не разложить на частные методы? Вот несколько причин:

  • Тесная связь и тестируемость. Уменьшая размер вашего публичного метода, вы улучшили его читабельность, но весь код все еще тесно связан между собой. Вы можете выполнить модульное тестирование отдельных закрытых методов (используя расширенные возможности инфраструктуры тестирования), но вы не можете легко протестировать открытый метод независимо от закрытых методов. Это идет вразрез с принципами модульного тестирования.
  • Размер класса и сложность. Вы уменьшили сложность метода, но вы увеличили сложность класса. Открытый метод легче читать, но класс теперь труднее читать, потому что он имеет больше функций, которые определяют его поведение. Я предпочитаю маленькие классы с одной ответственностью, поэтому длинный метод является признаком того, что класс делает слишком много.
  • Не может быть легко использован повторно. Часто случается, что по мере того, как основная часть кода созревает, полезно многократное использование. Если ваши шаги в частных методах, они не могут быть использованы где-либо еще без предварительного извлечения их. Кроме того, это может стимулировать копирование-вставку, когда в другом месте необходим шаг.
  • Расщепление таким образом, вероятно, будет произвольным. Я бы сказал, что разделение длинного публичного метода не требует столько размышлений или проектного рассмотрения, как если бы вы разбивали обязанности на классы. Каждый класс должен иметь правильное имя, документацию и тесты, в то время как закрытый метод не требует особого внимания.
  • Скрывает проблему. Итак, вы решили разделить ваш публичный метод на маленькие приватные методы. Теперь нет проблем! Вы можете продолжать добавлять все больше и больше шагов, добавляя все больше и больше частных методов! Наоборот, я думаю, что это серьезная проблема. Он устанавливает шаблон для добавления сложности к классу, за которым последуют последующие исправления ошибок и реализации функций. Вскоре ваши личные методы будут расти, и их придется разделить.

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

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


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

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

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