¿Existen pautas sobre cuántos parámetros debe aceptar una función?

94

He notado que algunas de las funciones con las que trabajo tienen 6 o más parámetros, mientras que en la mayoría de las bibliotecas que uso es raro encontrar una función que tome más de 3.

A menudo, muchos de estos parámetros adicionales son opciones binarias para alterar el comportamiento de la función. Creo que algunas de estas funciones con múltiples parámetros probablemente deberían ser refactorizadas. ¿Existe una directriz sobre qué número es demasiado?

    
pregunta Darth Egregious 18.04.2012 - 19:31

10 respuestas

92

Nunca he visto una guía, pero en mi experiencia, una función que toma más de tres o cuatro parámetros indica uno de dos problemas:

  1. La función está haciendo demasiado. Debe dividirse en varias funciones más pequeñas, cada una con un conjunto de parámetros más pequeño.
  2. Hay otro objeto escondido allí. Es posible que deba crear otro objeto o estructura de datos que incluya estos parámetros. Consulte este artículo en el patrón de Parameter Object para obtener más información.

Es difícil decir lo que estás viendo sin más información. Lo más probable es que la refactorización que necesita hacer sea dividir la función en funciones más pequeñas, a las que se llama desde el padre, dependiendo de los indicadores que se están pasando a la función actualmente.

Hay algunas buenas ganancias que se pueden obtener al hacer esto:

  • Hace que tu código sea más fácil de leer. Personalmente, me parece mucho más fácil leer una "lista de reglas" compuesta por una estructura if que llama a muchos métodos con nombres descriptivos que a una estructura que lo hace todo en un solo método.
  • Es más unidad comprobable. Ha dividido su problema en varias tareas más pequeñas que son individualmente muy simples. La recopilación de pruebas unitarias se compondría de un conjunto de pruebas de comportamiento que comprueba las rutas a través del método maestro y una recopilación de pruebas más pequeñas para cada procedimiento individual.
respondido por el Michael K 18.04.2012 - 20:09
37

Según "Clean Code: A Handbook of Agile Software Craftsmanship", cero es lo ideal, uno o dos son aceptables, tres en casos especiales y cuatro o más, ¡nunca!

Las palabras del autor:

  

El número ideal de argumentos para una función es cero (niladic). Luego viene uno (monádico), seguido de cerca por dos (diádico). Deben evitarse tres argumentos (triádicos) cuando sea posible. Más de tres (políada) requiere una justificación muy especial y, por lo tanto, no debe utilizarse de todos modos.

En este libro hay un capítulo que habla solo de las funciones donde se discuten los parámetros, por lo que creo que este libro puede ser una buena guía de cuántos parámetros necesita.

En mi opinión personal, un parámetro es mejor que nadie porque creo que está más claro lo que está pasando.

Como ejemplo, en mi opinión, la segunda opción es mejor porque está más claro qué procesa el método:

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

vs.

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

Sobre muchos parámetros, esto puede ser una señal de que algunas variables se pueden agrupar en un solo objeto o, en este caso, muchos booleanos pueden representar que la función / método está haciendo más de una cosa, y en En este caso, es mejor refactorizar cada uno de estos comportamientos en una función diferente.

    
respondido por el Renato Dinhani 18.04.2012 - 22:01
22

Si las clases de dominio en la aplicación están diseñadas correctamente, la cantidad de parámetros que pasamos a una función se reducirá automáticamente, porque las clases saben cómo hacer su trabajo y tienen suficientes datos para hacer su trabajo.

Por ejemplo, supongamos que tiene una clase de gerente que le pide a una clase de 3er grado que complete las tareas.

Si modelas correctamente,

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

Esto es simple.

Si no tienes el modelo correcto, el método será así

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

El modelo correcto siempre reduce los parámetros de función entre las llamadas de método, ya que las funciones correctas se delegan a sus propias clases (responsabilidad única) y tienen datos suficientes para realizar su trabajo.

Cada vez que veo que aumenta el número de parámetros, verifico mi modelo para ver si diseñé mi modelo de aplicación correctamente.

Sin embargo, hay algunas excepciones: cuando necesito crear un objeto de transferencia o un objeto de configuración, usaré un patrón de construcción para producir pequeños objetos construidos antes de construir un objeto de gran configuración.

    
respondido por el java_mouse 18.04.2012 - 20:59
15

Un aspecto que las otras respuestas no abordan es el rendimiento.

Si está programando en un lenguaje de nivel suficientemente bajo (C, C ++, ensamblaje), un gran número de parámetros puede ser muy perjudicial para el rendimiento en algunas arquitecturas, especialmente si la función se llama una gran cantidad de veces.

Cuando se realiza una llamada a la función en ARM, por ejemplo, los primeros cuatro argumentos se colocan en los registros r0 a r3 y los argumentos restantes se deben insertar en la pila. Mantener el número de argumentos por debajo de cinco puede hacer una gran diferencia para las funciones críticas.

Para las funciones que se llaman con mucha frecuencia, incluso el hecho de que el programa tiene que configurar los argumentos antes de que cada llamada pueda afectar el rendimiento ( r0 to r3 puede ser sobrescrito por la función llamada y tendrá que ser reemplazado antes de la siguiente llamada), por lo que, en este sentido, lo mejor es cero argumentos.

Actualizar:

KjMag trae a colación el interesante tema de la alineación. De alguna manera, la incorporación mitigará esto, ya que permitirá que el compilador realice las mismas optimizaciones que podría hacer si escribiera en ensamblador puro. En otras palabras, el compilador puede ver qué parámetros y variables utiliza la función llamada y puede optimizar el uso del registro para minimizar la lectura / escritura de la pila.

Sin embargo, hay algunos problemas con la alineación.

  1. Inlining hace que el binario compilado crezca, ya que el mismo código se duplica en forma binaria si se llama desde varios lugares. Esto es perjudicial cuando se trata del uso de I-cache.
  2. Los compiladores generalmente solo permiten la alineación hasta un cierto nivel (3 pasos IIRC?). Imagine llamar a una función en línea desde una función en línea desde una función en línea. El crecimiento binario explotaría si inline se tratara como obligatorio en todos los casos.
  3. Hay muchos compiladores que ignorarán completamente inline o en realidad te darán errores cuando lo encuentren.
respondido por el Leo 19.04.2012 - 16:38
6

Cuando la lista de parámetros crezca a más de cinco, considere la posibilidad de definir una estructura u objeto de "contexto".

Esto es básicamente una estructura que contiene todos los parámetros opcionales con algunos valores predeterminados establecidos.

En el mundo procesal C una estructura simple haría. En Java, C ++ bastará un simple objeto. No te metas con los buscadores o los que establecen, porque el único propósito del objeto es mantener los valores configurables "públicamente".

    
respondido por el James Anderson 19.04.2012 - 03:39
5

No, no hay una guía estándar

Pero hay algunas técnicas que pueden hacer que una función con muchos parámetros sea más soportable.

Podría usar un parámetro list-if-args (args *) o un parámetro de diccionario de args (kwargs ** )

Por ejemplo, en 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')

Salidas:

args1
args2
somevar:value
someothervar:novalue
value
missing

O puede utilizar la sintaxis de definición literal del objeto

Por ejemplo, aquí hay una llamada jQuery de JavaScript para iniciar una solicitud GET de AJAX:

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

Si echa un vistazo a la clase ajax de jQuery, hay un lote (aproximadamente 30) más propiedades que se pueden configurar; Sobre todo porque las comunicaciones ajax son muy complejas. Afortunadamente, la sintaxis literal del objeto facilita la vida.

C # intellisense proporciona documentación activa de parámetros, por lo que no es raro ver arreglos muy complejos de métodos sobrecargados.

Los lenguajes de escritura dinámica como python / javascript no tienen esa capacidad, por lo que es mucho más común ver los argumentos de palabras clave y las definiciones literales de los objetos.

Prefiero definiciones literales de objeto ( incluso en C # ) para administrar métodos complejos porque puedes ver explícitamente qué propiedades se establecen cuando se crea una instancia de un objeto. Tendrá que trabajar un poco más para manejar los argumentos predeterminados, pero a la larga, su código será mucho más legible. Con las definiciones literales de los objetos, puede romper su dependencia de la documentación para comprender qué hace su código a primera vista.

En mi humilde opinión, los métodos sobrecargados están muy sobrevalorados.

Nota: Si recuerdo correctamente, el control de acceso de solo lectura debería funcionar para los constructores de literal de objeto en C #. Básicamente, funcionan igual que establecer propiedades en el constructor.

Si nunca ha escrito ningún código no trivial en un lenguaje basado en javaScript tipificado dinámicamente (python) y / funcional / prototipo, le sugiero que lo intente. Puede ser una experiencia esclarecedora.

Puede ser aterrador primero romper tu confianza en los parámetros para el enfoque de finalización total de la función / método, pero aprenderás a hacer mucho más con tu código sin tener que agregar una complejidad innecesaria.

Actualizar:

Probablemente debería haber proporcionado ejemplos para demostrar el uso en un lenguaje de tipo estático, pero actualmente no estoy pensando en un contexto de tipo estático. Básicamente, he estado haciendo demasiado trabajo en un contexto de tipo dinámico para volver a cambiar repentinamente.

Lo que hago sé es que la sintaxis de la definición literal del objeto es completamente posible en lenguajes tipificados estáticamente (al menos en C # y Java) porque los he usado antes. En los lenguajes tipificados estáticamente se les llama 'Inicializadores de objetos'. Aquí hay algunos enlaces para mostrar su uso en Java y C # .

    
respondido por el Evan Plaice 18.04.2012 - 20:25
3

Personalmente, más de 2 es donde se activa la alarma de olor de código. Cuando considera que las funciones son operaciones (es decir, una traducción de entrada a salida), es poco común que se usen más de 2 parámetros en una operación. Procedimientos (es decir, una serie de pasos para alcanzar una meta) tomará más insumos y, a veces, es el mejor enfoque, pero en la mayoría de los idiomas, estos días no deberían ser la norma.

Pero, de nuevo, esa es la pauta en lugar de una regla. A menudo tengo funciones que toman más de dos parámetros debido a circunstancias inusuales o facilidad de uso.

    
respondido por el Telastyn 18.04.2012 - 20:09
2

Al igual que dice Evan Plaice, soy un gran fanático de simplemente pasar matrices asociativas (o la estructura de datos comparable de su idioma) a las funciones siempre que sea posible.

Por lo tanto, en lugar de (por ejemplo) esto:

<?php

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

?>

Ir para:

<?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 hace muchas cosas de esta manera, y creo que funciona bien. (Aunque mi código de ejemplo anterior es imaginario, y no es en sí mismo un ejemplo de Wordpress).

Esta técnica le permite pasar una gran cantidad de datos a sus funciones fácilmente, pero le libera de tener que recordar el orden en el que cada uno debe pasar.

También apreciará esta técnica cuando llegue el momento de refactorizar, en lugar de tener que cambiar potencialmente el orden de los argumentos de una función (como cuando se da cuenta de que necesita pasar otro argumento), no necesita cambiar la lista de parámetros de sus funciones en absoluto.

Esto no solo le evita tener que volver a escribir la definición de la función, sino que le evita tener que cambiar el orden de los argumentos cada vez que se invoca la función. Esa es una gran victoria.

    
respondido por el Chris Allen Lane 24.04.2012 - 21:26
0

Un answer mencionó a un autor confiable que afirmó que cuantos menos parámetros tengan sus funciones, mejor estará haciendo. La respuesta no explicó por qué, pero los libros lo explican, y aquí hay dos de las razones más convincentes de por qué necesita adoptar esta filosofía y con las que personalmente estoy de acuerdo:

  • Los parámetros pertenecen a un nivel de abstracción que es diferente del de la función. Esto significa que el lector de su código tendrá que pensar sobre la naturaleza y el propósito de los parámetros de sus funciones: este pensamiento es "de nivel inferior" que el del nombre y el propósito de sus funciones correspondientes.

  • La segunda razón para tener la menor cantidad de parámetros posibles para una función es probar: por ejemplo, si tiene una función con 10 parámetros, piense en cuántas combinaciones de parámetros tiene que cubrir todos los casos de prueba para , por ejemplo, una prueba unitaria. Menos parámetros = menos pruebas.

respondido por el Billal Begueradj 27.09.2017 - 19:23
-2

Idealmente cero. Uno o dos están bien, tres en ciertos casos.
Cuatro o más es generalmente una mala práctica.

Además de los principios de responsabilidad única que otros han observado, también puedes pensar en ello desde las perspectivas de prueba y depuración.

Si hay un parámetro, conocer sus valores, probarlos y encontrar errores es relativamente fácil, ya que solo hay un factor. A medida que aumentan los factores, la complejidad total aumenta rápidamente. Para un ejemplo abstracto:

Considere un programa de 'qué ponerse en este clima'. Considere lo que podría hacer con una entrada: la temperatura. Como puedes imaginar, los resultados de lo que debes vestir son bastante simples según ese factor. Ahora considere lo que el programa podría / podría / debería hacer si realmente se pasa la temperatura, la humedad, el punto de rocío, la precipitación, etc. Ahora imagine lo difícil que sería depurar si dio la respuesta "incorrecta" a algo.

    
respondido por el Michael Durrant 18.04.2012 - 22:42

Lea otras preguntas en las etiquetas