Существуют ли рекомендации относительно того, сколько параметров должна принимать функция?


114

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

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


4
@Ominus: Идея в том, что вы хотите, чтобы ваши классы были сосредоточены. Сосредоточенные классы обычно не имеют такого количества зависимостей / атрибутов, поэтому у конструктора будет меньше параметров. Некоторое модное слово, которое люди добавляют в этот момент, - это высокая сплоченность и принцип единой ответственности . Если вы чувствуете, что они не нарушены и нуждаются в большом количестве параметров, попробуйте использовать шаблон Builder.
c_maker

2
Определенно не следуйте примеру MPI_Sendrecv () , который принимает 12 параметров!
Chrisaycock

6
Проект, над которым я сейчас работаю, использует определенную среду, в которой методы с 10+ параметрами являются обычным явлением. Я вызываю один конкретный метод с 27 параметрами в нескольких местах. Каждый раз, когда я вижу это, я умираю немного внутри.
Пермь

3
Никогда не добавляйте логические переключатели для изменения поведения функции. Разделите функцию вместо этого. Разбейте обычное поведение на новые функции.
Кевин Клайн

2
@ Оминус Что? Всего 10 параметров? Это ничего, гораздо больше нужно . : D
maaartinus

Ответы:


106

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

  1. Функция делает слишком много. Он должен быть разбит на несколько меньших функций, каждая из которых имеет меньший набор параметров.
  2. Там есть еще один объект, скрывающийся там. Вам может понадобиться создать другой объект или структуру данных, которая включает эти параметры. См. Эту статью о шаблоне Parameter Object для получения дополнительной информации.

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

Делая это, вы получаете несколько хороших результатов:

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

5
Абстракция параметров превратилась в шаблон проектирования? Что произойдет, если у вас есть 3 класса параметров. Вы добавили еще 9 перегрузок метода для обработки различных возможных комбинаций параметров? Это звучит как неприятная проблема с объявлением параметров O (n ^ 2). Ой, подождите, вы можете наследовать только 1 класс в Java / C #, так что для его работы на практике потребуется еще немного биологической таблицы (возможно, несколько подклассов). Извините, я не убежден. Игнорирование более выразительных подходов, которые язык может предложить в пользу сложности, просто кажется неправильным.
Эван Плейс,

Если вы не используете шаблон Pattern Object для упаковки переменных в экземпляре объекта и передачи его в качестве параметра. Это работает для упаковки, но может быть заманчиво создать классы разнородных переменных только для удобства упрощения определения метода.
Эван Плейс,

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

@MichaelK Если вы никогда не использовали их, попробуйте поискать «инициализаторы объектов». Это довольно новый подход, который значительно сокращает определение. Теоретически вы можете исключить конструкторы классов, параметры и перегрузки всего за один раз. Однако на практике обычно хорошо поддерживать один общий конструктор и полагаться на синтаксис «инициализатора объекта» для остальных непонятных / нишевых свойств. ИМХО, это ближе всего к выразительности динамически типизированных языков в статически типизированном языке.
Эван Плейс,

@Evain Plaice: С каких это пор языки с динамической типизацией являются выразительными?
ThomasX

41

Согласно «Чистому коду: руководство по гибкому программному обеспечению», ноль - это идеал, один или два приемлемы, три в особых случаях и четыре или больше, никогда!

Слова автора:

Идеальное число аргументов для функции равно нулю (niladic). Далее следует одно (монадическое), за которым следует два (диадическое). По возможности следует избегать трех аргументов (триадных). Более трех (полиадических) требует особого обоснования, и их не следует использовать в любом случае.

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

По моему личному мнению, один параметр лучше, чем никто, потому что я думаю, что более ясно, что происходит.

Как пример, на мой взгляд, второй вариант лучше, потому что более понятно, что метод обрабатывает:

LangDetector detector = new LangDetector(someText);
//lots of lines
String language = detector.detectLanguage();

против

LangDetector detector = new LangDetector();
//lots of lines
String language = detector.detectLanguage(someText);

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


8
"три в особых случаях и четыре или больше, никогда!" BS. Как насчет Matrix.Create (x1, x2, x3, x4, x5, x6, x7, x8, x9); ?
Лукаш Мадон

71
Ноль это идеал? Как, черт возьми, функции получают информацию? Глобальные / экземпляр / статические / какие переменные? YUCK.
Питер С.

9
Это плохой пример. Ответ очевиден: String language = detectLanguage(someText);. В любом из ваших случаев вы передали одинаковое количество аргументов, просто бывает, что вы разбили выполнение функции на две из-за плохого языка.
Матье М.

8
@lukas на языках поддерживает такие фантазии конструкции в виде массивов или (удушье!) списки, как о , Matrix.Create(input);где inputесть, скажем, .NET IEnumerable<SomeAppropriateType>? Таким образом, вам также не нужна отдельная перегрузка, когда вы хотите создать матрицу, содержащую 10 элементов вместо 9.
CVn

9
Нулевые аргументы как «идеальные» - это пустяк, и одна из причин, по которой я считаю, что чистый код слишком переоценен.
user949300

24

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

Например, скажем, у вас есть класс менеджера, который просит класс 3-го класса выполнить задания.

Если вы моделируете правильно,

3rdGradeClass.finishHomework(int lessonId) {
    result = students.assignHomework(lessonId, dueDate);
    teacher.verifyHomeWork(result);
}

Это просто

Если у вас нет правильной модели, метод будет таким

Manager.finishHomework(grade, students, lessonId, teacher, ...) {
    // This is not good.
}

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

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

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


16

Один аспект, который другие ответы не затрагивают, - это производительность.

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

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

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

Обновить:

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

Есть некоторые проблемы с наклоном, хотя.

  1. Встраивание приводит к росту скомпилированного двоичного файла, поскольку один и тот же код дублируется в двоичном виде, если он вызывается из нескольких мест. Это пагубно, когда дело доходит до использования I-кэша.
  2. Компиляторы, как правило, допускают встраивание только до определенного уровня (3 шага IIRC?). Представьте себе вызов встроенной функции из встроенной функции из встроенной функции. Бинарный рост взорвался бы, если бы inlineво всех случаях его считали обязательным.
  3. Существует множество компиляторов, которые либо полностью игнорируют, inlineлибо фактически выдают вам ошибки при их обнаружении.

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

Разве встраивание не решает эту проблему?
KjMag

@KjMag: Да, в определенной степени. Но есть много ошибок в зависимости от компилятора. Функции обычно будут встроены только до определенного уровня (если вы вызываете встроенную функцию, которая вызывает встроенную функцию, которая вызывает встроенную функцию ....). Если функция велика и вызывается из разных мест, повсеместное встраивание делает двоичный код больше, что может означать больше пропусков в I-кэше. Так что вставка может помочь, но это не серебряная пуля. (Не говоря уже о том, что есть довольно много старых встроенных компиляторов, которые не поддерживают inline.)
Leo

7

Когда список параметров увеличится до пяти, рассмотрите возможность определения "контекстной" структуры или объекта.

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

В процедурном мире C подойдет простая структура. В Java C ++ достаточно простого объекта. Не связывайтесь с геттерами или сеттерами, потому что единственная цель объекта - хранить «общедоступные» устанавливаемые значения.


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

5

Нет, стандартного руководства нет

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

Вы можете использовать параметр list-if-args (args *) или параметр dictionary-of-args (kwargs **)

Например, в Python:

// Example definition
def example_function(normalParam, args*, kwargs**):
  for i in args:
    print 'args' + i + ': ' + args[i] 
  for key in kwargs:
    print 'keyword: %s: %s' % (key, kwargs[key])
  somevar = kwargs.get('somevar','found')
  missingvar = kwargs.get('somevar','missing')
  print somevar
  print missingvar

// Example usage

    example_function('normal parameter', 'args1', args2, 
                      somevar='value', missingvar='novalue')

Выходы:

args1
args2
somevar:value
someothervar:novalue
value
missing

Или вы можете использовать синтаксис определения литерала объекта

Например, вот JavaScript-вызов jQuery для запуска AJAX GET-запроса:

$.ajax({
  type: 'GET',
  url: 'http://someurl.com/feed',
  data: data,
  success: success(),
  error: error(),
  complete: complete(),
  dataType: 'jsonp'
});

Если вы посмотрите на класс jQuery ajax, вы можете установить гораздо больше (примерно 30) свойств; главным образом потому, что связь ajax очень сложна. К счастью, буквальный синтаксис объекта облегчает жизнь.


C # intellisense предоставляет активную документацию параметров, поэтому весьма часто можно увидеть очень сложные схемы перегруженных методов.

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

Я предпочитаю определения литералов объектов ( даже в C # ) для управления сложными методами, потому что вы можете явно видеть, какие свойства устанавливаются при создании экземпляра объекта. Вам придется проделать немного больше работы для обработки аргументов по умолчанию, но в долгосрочной перспективе ваш код будет намного более читабельным. Используя определения литералов объектов, вы можете избавиться от зависимости от документации, чтобы понять, что делает ваш код на первый взгляд.

ИМХО, перегруженные методы сильно переоценены.

Примечание. Если я правильно помню, управление доступом только для чтения должно работать для литеральных конструкторов объектов в C #. По сути, они работают так же, как установка свойств в конструкторе.


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

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

Обновить:

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

То , что я действительно знаю, объект синтаксиса буквальное определение вполне возможно в статически типизированных языках (по крайней мере , в C # и Java) , потому что я использовал их раньше. В статически типизированных языках они называются «инициализаторами объектов». Вот несколько ссылок, чтобы показать их использование в Java и C # .


3
Я не уверен, что мне нравится этот метод, главным образом потому, что вы теряете самодокументируемое значение отдельных параметров. Для списков похожих элементов это имеет смысл (например, метод, который берет список строк и объединяет их), но для произвольного набора параметров это хуже, чем длинный вызов метода.
Майкл К

@MichaelK Еще раз взглянем на инициализаторы объектов. Они позволяют вам явно определять свойства в отличие от того, как они неявно определяются в традиционных параметрах метода / функции. Прочтите это, msdn.microsoft.com/en-us/library/bb397680.aspx , чтобы понять, что я имею в виду.
Эван Плейс

3
Создание нового типа только для обработки списка параметров звучит точно так же, как и определение ненужной сложности ... Конечно, динамические языки позволяют этого избежать, но затем вы получаете один шарик параметра goo. Несмотря на это, это не отвечает на заданный вопрос.
Теластин

@Telastyn О чем ты говоришь? Новый тип не создан, вы объявляете свойства напрямую, используя синтаксис литерала объекта. Это похоже на определение анонимного объекта, но метод интерпретирует его как группировку параметров ключ = значение. То, на что вы смотрите, - это создание экземпляра метода (а не объекта, инкапсулирующего параметр). Если вы говорите с упаковкой параметров, взгляните на шаблон объекта параметров, упомянутый в одном из других вопросов, потому что это именно то, что есть.
Эван Плейс

@EvanPlaice - за исключением того, что для статических языков программирования в общем случае требуется (часто новый) объявленный тип, чтобы разрешить шаблон Parameter Object.
Теластин

3

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

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


2

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

Таким образом, вместо (например) этого:

<?php

createBlogPost('the title', 'the summary', 'the author', 'the date of publication, 'the keywords', 'the category', 'etc');

?>

Перейти на:

<?php

// create a hash of post data
$post_data = array(
  'title'    => 'the title',
  'summary'  => 'the summary',
  'author'   => 'the author',
  'pubdate'  => 'the publication date',
  'keywords' => 'the keywords',
  'category' => 'the category',
  'etc'      => 'etc',
);

// and pass it to the appropriate function
createBlogPost($post_data);

?>

Wordpress делает много вещей таким образом, и я думаю, что это работает хорошо. (Хотя мой пример кода выше является мнимым, а не сам пример из Wordpress.)

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

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

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


Просмотр этого поста подчеркивает еще одно преимущество подхода с передачей хеша: обратите внимание, что мой первый пример кода настолько длинный, что генерирует полосу прокрутки, а второй точно помещается на странице. То же самое, вероятно, будет верно в вашем редакторе кода.
Крис Аллен Лейн

0

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

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

  • Вторая причина, чтобы у функции было как можно меньше параметров, это тестирование: например, если у вас есть функция с 10 параметрами, подумайте, сколько комбинаций параметров вам нужно, чтобы покрыть все тестовые случаи, например, для единицы контрольная работа. Меньше параметров = меньше тестов.


0

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

Аргументы жесткие. Они берут много концептуальной силы. Вот почему я избавился почти от всех из этого примера. Рассмотрим, например, StringBufferпример. Мы могли бы передать это как аргумент, а не сделать его переменной экземпляра, но тогда нашим читателям приходилось бы интерпретировать это каждый раз, когда они это видели. Когда вы читаете историю, рассказанную модулем, includeSetupPage() это легче понять, чем includeSetupPageInto(newPageContent). Аргумент находится на другом уровне абстракции, чем имя функции, и заставляет вас знать детали (другими словами, StringBuffer), которые не особенно важны в этот момент.

Для includeSetupPage()приведенного выше примера вот небольшой фрагмент его переработанного «чистого кода» в конце главы:

// *** NOTE: Commments are mine, not the author's ***
//
// Java example
public class SetupTeardownIncluder {
    private StringBuffer newPageContent;

    // [...] (skipped over 4 other instance variables and many very small functions)

    // this is the zero-argument function in the example,
    // which calls a method that eventually uses the StringBuffer instance variable
    private void includeSetupPage() throws Exception {
        include("SetUp", "-setup");
    }

    private void include(String pageName, String arg) throws Exception {
        WikiPage inheritedPage = findInheritedPage(pageName);
        if (inheritedPage != null) {
            String pagePathName = getPathNameForPage(inheritedPage);
            buildIncludeDirective(pagePathName, arg);
        }
    }

    private void buildIncludeDirective(String pagePathName, String arg) {
        newPageContent
            .append("\n!include ")
            .append(arg)
            .append(" .")
            .append(pagePathName)
            .append("\n");
    }
}

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

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


-2

В идеале ноль. Один или два в порядке, три в некоторых случаях.
Четыре или более обычно плохая практика.

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

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

Рассмотрим программу «Что надеть в такую ​​погоду». Посмотрите, что он может сделать с одним входом - температура. Как вы можете себе представить, результаты того, что надеть, довольно просты в зависимости от этого фактора. Теперь подумайте, что может / может / должна сделать программа, если она действительно прошла температуру, влажность, точку росы, осадки и т. Д. Теперь представьте, как трудно будет отладить, если она даст «неправильный» ответ на что-то.


12
Если функция имеет нулевые параметры, она либо возвращает постоянное значение (полезно в некоторых обстоятельствах, но скорее ограничивает), либо использует некое скрытое состояние, которое было бы лучше сделать явным. (В вызовах метода OO объект контекста является достаточно явным, чтобы не вызывать проблем.)
Donal Fellows

4
-1 за не цитирование источника
Джошуа Дрейк

Вы серьезно говорите, что в идеале все функции не будут иметь параметров? Или это гипербола?
GreenAsJade

1
См. Аргумент дяди Боба по адресу: informit.com/articles/article.aspx?p=1375308 и обратите внимание, что внизу он говорит: «У функций должно быть небольшое количество аргументов. Ни один аргумент не является лучшим, за ним следуют один, два и три. Более трех очень сомнительно, и его следует избегать с предрассудками ".
Майкл Даррант

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