2010-04-12 37 views
8

Manypeople han discutido sobre el tamaño de la función. Dicen que las funciones en general deberían ser bastante cortas. Las opiniones varían desde algo así como 15 líneas hasta "aproximadamente una pantalla", que hoy probablemente sean alrededor de 40-80 líneas.
Además, las funciones siempre deben cumplir una sola tarea.¿Es una mala práctica tener un método de inicialización largo?

Sin embargo, hay un tipo de función que falla con frecuencia en ambos criterios en mi código: funciones de inicialización.

Por ejemplo, en una aplicación de audio, el hardware/API de audio tiene que ser establecido, los datos de audio tiene que ser convertido a un formato adecuado y el estado del objeto tiene que inicializado correctamente. Estas son claramente tres tareas diferentes y dependiendo de la API esto puede abarcar fácilmente más de 50 líneas.

La cosa con init-funciones es que generalmente sólo se llama una vez, así que no hay necesidad de volver a utilizar cualquiera de los componentes. ¿Todavía los dividiría en varias funciones más pequeñas consideraría que las funciones de inicialización son buenas?

Respuesta

12

Me gustaría volver a romper la función por tarea, y luego llamar a cada una de las funciones de nivel más bajo desde el interior de mi cara al público inicializar función:

void _init_hardware() { } 
void _convert_format() { } 
void _setup_state() { } 

void initialize_audio() { 
    _init_hardware(); 
    _convert_format(); 
    _setup_state(); 
} 

Escritura de funciones sucintas se trata tanto de aislamiento de fallos y el cambio como mantener las cosas legibles Si sabe que la falla se encuentra en _convert_format(), puede rastrear las ~ 40 líneas responsables de un error bastante más rápido. Lo mismo se aplica si realiza cambios que solo tocan una función.

Un último punto, utilizo assert() con bastante frecuencia, así que puedo "fallar a menudo y fallar temprano", y el comienzo de una función es el mejor lugar para un par de aseveraciones de comprobación de la cordura.Mantener la función corta le permite probar la función de forma más exhaustiva en función de su conjunto de tareas más estrecho. Es muy difícil probar la unidad de una función de 400 líneas que hace 10 cosas diferentes.

+1

1 para 'assert()' solo. – ndim

+0

+1: "no hay necesidad de volver a utilizar ninguno de los componentes". La reutilización no es el problema. Escribir algo que pueda ser entendido y mantenido por otras personas es muchísimo más importante. –

+0

Recuerdo un consejo sobre dejar los identificadores que comienzan con guiones bajos al uso interno del compilador C y evitarlos en los programas. Además, debe marcar esas tres funciones init de una vez como 'estática'. Por una vez, no se usarán fuera del archivo fuente actual. Y como un beneficio adicional, un compilador inteligente verá que solo se los llama una vez, y simplemente insertará el código (en caso de que le preocupe la sobrecarga de llamadas). – ndim

5

Si rompiendo en partes más pequeñas hace que el código mejor estructurado y/o más legible - lo hacen, no importa lo que hace la función. No se trata de la cantidad de líneas sobre la calidad del código.

2

En una situación como esta creo que todo se reduce a una cuestión de preferencia personal. Prefiero que las funciones hagan solo una cosa, así que dividiría la inicialización en funciones separadas, incluso si solo se llaman una vez. Sin embargo, si alguien quisiera hacerlo todo en una sola función, no me preocuparía demasiado (siempre que el código sea claro). Hay cosas más importantes para discutir (como si las llaves pertenecen a su propia línea).

1

Si tiene muchos componentes que necesitan ser enchufados, ciertamente puede ser razonable tener un método grande, incluso si la creación de cada componente se refactoriza en un método separado cuando sea posible.

Una alternativa a esto es utilizar un marco de inyección de dependencias (por ejemplo Spring, castillo de Windsor, Guice etc). Eso tiene ventajas y desventajas definidas ... mientras trabajas en un método grande puede ser bastante doloroso, al menos tienes una buena idea de dónde se inicializa todo, y no hay necesidad de preocuparte sobre qué "magia" podría estar pasando. . Por otra parte, la inicialización no se puede cambiar después de la implementación (como puede hacerlo con un archivo XML para Spring, por ejemplo).

creo que tiene sentido para diseñar el cuerpo principal de su código para que puede inyectar - pero si que la inyección se realiza a través de un marco o simplemente una lista no modificable (y potencialmente de largo) de las llamadas de inicialización es una elección que bien puede cambiar para diferentes proyectos. En ambos casos, los resultados son difíciles de probar aparte de simplemente ejecutar la aplicación.

3

Todavía trataría de dividir las funciones en unidades lógicas. Deben ser tan largos o cortos como tenga sentido. Por ejemplo:

SetupAudioHardware(); 
ConvertAudioData(); 
SetupState(); 

Asignarles nombres claros hace que todo sea más intuitivo y legible. Además, separarlos hace que sea más fácil para los cambios futuros y/u otros programas reutilizarlos.

1

En primer lugar, se debe utilizar una fábrica en lugar de una función de inicialización. Es decir, en lugar de tener initialize_audio(), tiene un new AudioObjectFactory (puede pensar en un nombre mejor aquí). Esto mantiene la separación de las preocupaciones.

Sin embargo, tenga cuidado también de no abstraer demasiado pronto. Claramente ya tienes dos preocupaciones: 1) inicialización de audio y 2) uso de ese audio. Hasta que, por ejemplo, abstraiga el dispositivo de audio que va a inicializar, o la forma en que un dispositivo determinado puede configurarse durante la inicialización, su método de fábrica (audioObjectFactory.Create() o lo que sea), debería mantenerse en un solo método. La abstracción temprana sirve solo para ofuscar el diseño.

Tenga en cuenta que audioObjectFactory.Create() no es algo que pueda ser probado por la unidad. Probarlo es una prueba de integración, y hasta que haya partes de ella que puedan abstraerse, seguirá siendo una prueba de integración. Más adelante, puede encontrar que tiene múltiples fábricas diferentes para diferentes configuraciones; en ese punto, podría ser beneficioso resumir las llamadas de hardware en una interfaz, para que pueda crear pruebas unitarias para garantizar que las distintas fábricas configuren el hardware de manera adecuada.

+0

En realidad, eso es más o menos lo que estoy haciendo. La clase en sí se supone que es un objeto de reproductor de audio que abstrae el manejo del desorden que es CoreAudio. En realidad, la preparación de datos de audio y la inicialización del hardware de audio están estrechamente conectadas, ya que diferentes datos de audio requieren una configuración diferente y viceversa. – bastibe

1

Creo que es un enfoque equivocado intentar contar el número de líneas y determinar las funciones en función de eso. Para algo como el código de inicialización, a menudo tengo una función separada para él, pero principalmente para que las funciones Load o Init o New no sean desordenadas y confusas. Si puede separarlo en algunas tareas como otros han sugerido, puede nombrar algo útil y ayudar a organizar. Incluso si lo llamas solo una vez, no es un mal hábito, y a menudo te das cuenta de que hay otros momentos en los que puedes reiniciar las cosas y puedes volver a usar esa función.

1

Sólo pensé en tirar esto por ahí, ya que aún no se ha mencionado - el Facade Pattern a veces se cita como una interfaz para un subsistema complejo. No he hecho mucho con él, pero las metáforas suelen ser algo así como encender una computadora (requiere varios pasos) o encender un sistema de cine en casa (encienda la TV, encienda el receptor, baje las luces, etc. .)

Dependiendo de la estructura del código, podría ser algo que valga la pena considerar para abstraer sus grandes funciones de inicialización. Todavía estoy de acuerdo con el punto de Meagar, aunque desglosar funciones en _init_X(), _init_Y(), etc. es una buena forma de hacerlo. Incluso si no va a reutilizar los comentarios en este código, en su próximo proyecto, cuando se dice a sí mismo: "¿Cómo inicié ese componente X?", Será mucho más fácil volver y seleccionarlo de la función _init_X() más pequeña de lo que sería seleccionarla de una función más grande, especialmente si la inicialización en X está dispersa por ella.

1

La longitud de la función es, como la etiquetaron, una cuestión muy subjetiva. Sin embargo, una mejor práctica estándar es aislar código que a menudo se repite y/o puede funcionar como su propia entidad. Por ejemplo, si su función de inicialización está cargando archivos de biblioteca u objetos que serán utilizados por una biblioteca específica, ese bloque de código debe modularizarse.

Dicho esto, no está mal tener un método de inicialización que sea largo, siempre que no sea largo debido a la gran cantidad de código repetido u otros fragmentos que pueden ser eliminados.

Espero que ayude,
Carlos Núñez

Cuestiones relacionadas