¿Deberíamos evitar los objetos personalizados como parámetros?

49

Supongamos que tengo un objeto personalizado, Estudiante :

public class Student{
    public int _id;
    public String name;
    public int age;
    public float score;
}

Y una clase, Window , que se usa para mostrar información de un Estudiante :

public class Window{
    public void showInfo(Student student);
}

Parece bastante normal, pero descubrí que Window no es fácil de probar individualmente, porque necesita un objeto real de Student para llamar a la función. Así que trato de modificar showInfo para que no acepte un objeto Student directamente:

public void showInfo(int _id, String name, int age, float score);

para que sea más fácil probar Windows individualmente:

showInfo(123, "abc", 45, 6.7);

Pero descubrí que la versión modificada tiene otros problemas:

  1. Modificar estudiante (por ejemplo: agregar nuevas propiedades) requiere modificar la firma del método de showInfo

  2. Si Student tuviera muchas propiedades, la firma del método de Student sería muy larga.

Entonces, usando objetos personalizados como parámetro o acepte cada propiedad en los objetos como parámetro, ¿cuál es más fácil de mantener?

ggrr
fuente
40
Y su 'mejorado' showInforequiere una cadena real, un flotador real y dos entradas reales. ¿Cómo es Stringmejor proporcionar un Studentobjeto real que proporcionar un objeto real ?
Bart van Ingen Schenau
28
Un problema importante al pasar los parámetros directamente: ahora tiene dos intparámetros. Desde el sitio de la llamada, no hay verificación de que realmente los esté pasando en el orden correcto. ¿Qué pasa si intercambias idy age, o firstNamey lastName? Está introduciendo un posible punto de falla que puede ser muy difícil de detectar hasta que explote en su cara, y lo está agregando en cada sitio de llamada .
Chris Hayes
38
@ChrisHayes ah, el viejo showForm(bool, bool, bool, bool, int)método - Me encantan esos ...
Boris the Spider
3
@ChrisHayes al menos no es JS ...
Jens Schauder
2
una propiedad subestimada de las pruebas: si es difícil crear / usar sus propios objetos en las pruebas, su API podría usar algo de trabajo :)
Eevee

Respuestas:

131

El uso de un objeto personalizado para agrupar parámetros relacionados es en realidad un patrón recomendado. Como refactorización, se llama Introducir objeto de parámetro .

Tu problema yace en otra parte. Primero, genérico no Windowdebe saber nada sobre el estudiante. En cambio, debe tener algún tipo de StudentWindowconocimiento que solo se muestre Students. En segundo lugar, no hay absolutamente ningún problema en crear una Studentinstancia para probar StudentWindowsiempre y Studentcuando no contenga ninguna lógica compleja que pueda complicar drásticamente la prueba StudentWindow. Si tiene esa lógica, entonces Studentdebería preferirse hacer una interfaz y burlarse de ella.

Eufórico
fuente
14
Vale la pena advertir que puede meterse en problemas si el nuevo objeto no es realmente una agrupación lógica. No intente calzar todos los parámetros en un solo objeto; decidir caso por caso. El ejemplo en la pregunta parece claramente ser un buen candidato para ello. La Studentagrupación tiene sentido y es probable que surja en otras áreas de la aplicación.
jpmc26
Hablando de forma pendiente si ya tiene el objeto, por ejemplo Student, sería un Objeto completo de conservación
abuzittin gillifirca
44
También recuerda la Ley de Deméter . Hay que lograr un equilibrio, pero el tldr es no hacer a.b.csi su método toma a. Si su método llega al punto en el que necesita tener aproximadamente más de 4 parámetros o 2 niveles de profundidad de acceso a la propiedad, probablemente sea necesario tenerlo en cuenta. También tenga en cuenta que esta es una directriz, como todas las demás, requiere discreción del usuario. No lo sigas a ciegas.
Dan Pantry
77
Encontré la primera oración de esta respuesta extremadamente difícil de analizar.
Helrich
55
@Qwerky Estoy muy en desacuerdo. El estudiante REALMENTE suena como una hoja en el gráfico de objetos (salvo otros objetos triviales como quizás Name, DateOfBirth, etc.), simplemente un contenedor para el estado de un estudiante. No hay razón alguna para que un estudiante sea difícil de construir, ya que debe ser un tipo de registro. Crear dobles de prueba para un estudiante suena como una receta para pruebas difíciles de mantener y / o una fuerte dependencia de algún marco de aislamiento elegante.
sara
26

Tu dices que es

no es fácil de probar individualmente, ya que necesita un objeto real de estudiante para llamar a la función

Pero solo puede crear un objeto de estudiante para pasar a su ventana:

showInfo(new Student(123,"abc",45,6.7));

No parece mucho más complejo llamar.

Tom.Bowen89
fuente
77
El problema surge cuando se Studentrefiere a a University, que se refiere a muchos Facultysy Campuss, con Professorsy Buildings, ninguno de los cuales showInforealmente usa, pero no ha definido ninguna interfaz que permita que las pruebas "sepan" eso y suministren solo al estudiante relevante datos, sin construir toda la organización. El ejemplo Studentes un objeto de datos simple y, como usted dice, las pruebas deberían estar felices de trabajar con él.
Steve Jessop
44
El problema surge cuando el Estudiante se refiere a una Universidad, que se refiere a muchas Facultades y Campuss, con profesores y edificios, sin descanso para los malvados.
abuzittin gillifirca
1
@abuzittingillifirca, "Object Mother" es una solución, también su objeto de estudiante puede ser demasiado complejo. Puede ser mejor tener UniversityId y un servicio (que usa inyección de dependencia) que proporcionará un objeto University de UniversityId.
Ian
12
Si el estudiante es muy complejo o es difícil de inicializar, simplemente búrlate. Las pruebas son mucho más potentes con marcos como Mockito u otros equivalentes de idiomas.
Borjab
44
Si showInfo no se preocupa por University, entonces simplemente configúrelo como nulo. Los nulos son horribles en la producción y un envío de Dios en las pruebas. Ser capaz de especificar un parámetro como nulo en una prueba comunica intención y dice que "esto no es necesario aquí". Aunque también consideraría crear algún tipo de modelo de vista para estudiantes que contengan solo los datos relevantes, teniendo en cuenta que showInfo suena como un método en una clase de IU.
Sara
22

En términos simples:

  • Lo que llamas un "objeto personalizado" generalmente se llama simplemente un objeto.
  • No puede evitar pasar objetos como parámetros al diseñar cualquier programa o API no trivial, o al utilizar cualquier API o biblioteca no trivial.
  • Está perfectamente bien pasar objetos como parámetros. Eche un vistazo a la API de Java y verá muchas interfaces que reciben objetos como parámetros.
  • Las clases en las bibliotecas que usa fueron escritas por simples mortales como usted y yo, por lo que las que escribimos no son "personalizadas" , simplemente lo son.

Editar:

Como @ Tom.Bowen89 afirma que no es mucho más complejo probar el método showInfo:

showInfo(new Student(8812372,"Peter Parker",16,8.9));
Tulains Córdova
fuente
3
  1. En su ejemplo de estudiante, supongo que es trivial llamar al constructor de Estudiantes para crear un estudiante que pase a showInfo. Entonces no hay problema.
  2. Suponiendo el ejemplo, el estudiante es deliberadamente trivializado para esta pregunta y es más difícil de construir, entonces podría usar una prueba doble . Hay una serie de opciones para dobles de prueba, simulacros, talones, etc., que se mencionan en el artículo de Martin Fowler para elegir.
  3. Si desea hacer que la función showInfo sea más genérica, puede hacer que itere sobre las variables públicas, o tal vez los accesores públicos del objeto pasen y realicen la lógica de show para todos ellos. Entonces podría pasar cualquier objeto que se ajustara a ese contrato y funcionaría como se esperaba. Este sería un buen lugar para usar una interfaz. Por ejemplo, pase un objeto Showable o ShowInfoable a la función showInfo que puede mostrar no solo la información de los estudiantes, sino también la información de cualquier objeto que implemente la interfaz (obviamente, esas interfaces necesitan mejores nombres dependiendo de qué tan específico o genérico desea que pase el objeto al que puede pasarle). ser y de lo que un estudiante es una subclase)
  4. A menudo es más fácil pasar primitivas, y a veces es necesario para el rendimiento, pero cuanto más pueda agrupar conceptos similares, más comprensible será generalmente su código. Lo único que hay que tener en cuenta es tratar de no hacerlo en exceso y terminar con fizzbuzz empresarial .
Encaitar
fuente
3

Steve McConnell en Code Complete abordó este mismo problema, discutiendo los beneficios y los inconvenientes de pasar objetos a métodos en lugar de usar propiedades.

Perdóname si me equivoco con algunos detalles, estoy trabajando de memoria ya que ha pasado más de un año desde que tuve acceso al libro:

Llega a la conclusión de que es mejor no usar un objeto, sino que solo envía aquellas propiedades absolutamente necesarias para el método. El método no debería tener que saber nada sobre el objeto fuera de las propiedades que usará como parte de sus operaciones. Además, con el tiempo, si alguna vez se cambia el objeto, esto podría tener consecuencias no deseadas en el método que usa el objeto.

También abordó que si terminas con un método que acepta muchos argumentos diferentes, entonces eso es probablemente una señal de que el método está haciendo demasiado y debería dividirse en más métodos más pequeños.

Sin embargo, a veces, a veces, realmente necesitas muchos parámetros. El ejemplo que da sería un método que construye una dirección completa, utilizando muchas propiedades de dirección diferentes (aunque esto podría obtenerse utilizando una matriz de cadenas cuando lo piensa).

usuario1666620
fuente
77
Tengo el código completo 2. Hay una página completa dedicada a este problema. La conclusión es que los parámetros deben estar en el nivel de abstracción correcto. A veces eso requiere pasar un objeto completo, a veces solo atributos individuales.
VENIDO DEL
UV Hacer referencia al código completo es doble más bueno. Una buena consideración del diseño sobre la conveniencia de la prueba. Prefiero el diseño, creo que McConnell diría en nuestro contexto. Por lo tanto, una conclusión excelente sería "integrar el objeto de parámetro en el diseño" ( Studenten este caso). Y así es como las pruebas informan el diseño , abrazando completamente la respuesta con más votos y manteniendo la integridad del diseño.
radarbob
2

Es mucho más fácil escribir y leer pruebas si pasa todo el objeto:

public class AStudentView {
    @Test 
    public void displays_failing_grade_warning_when_a_student_with_a_failing_grade_is_shown() {
        StudentView view = aStudentView();
        view.show(aStudent().withAFailingGrade().build());
        Assert.that(view, displaysFailingGradeWarning());
    }

    private Matcher<StudentView> displaysFailingGradeWarning() {
        ...
    }
}

Para comparacion,

view.show(aStudent().withAFailingGrade().build());

la línea podría escribirse, si pasa valores por separado, como:

showAStudentWithAFailingGrade(view);

donde la llamada al método real está enterrada en algún lugar como

private showAStudentWithAFailingGrade(StudentView view) {
    int someId = .....
    String someName = .....
    int someAge = .....
    // why have been I peeking and poking values I don't care about
    decimal aFailingGrade = .....
    view.show(someId, someName, someAge, aFailingGrade);
}

Para ir al grano, que no puede poner la llamada al método real en la prueba es una señal de que su API es mala.

abuzittin gillifirca
fuente
1

Debes pasar lo que tiene sentido, algunas ideas:

Más fácil de probar. Si los objetos necesitan ser editados, ¿qué requiere la menor refactorización? ¿Es útil reutilizar esta función para otros fines? ¿Cuál es la menor cantidad de información que necesito dar a esta función para cumplir su propósito? (Al dividirlo, puede permitirle volver a usar este código, tenga cuidado de no caer en el agujero de diseño para hacer que esta función funcione y luego cuele todo para usar este objeto exclusivamente).

Todas estas reglas de programación son solo guías para que pienses en la dirección correcta. Simplemente no construya una bestia de código: si no está seguro y solo necesita continuar, elija una dirección / suya o una sugerencia aquí, y si llega a un punto en el que piensa 'oh, debería haberlo hecho así way '- probablemente puedas volver y refactorizarlo con bastante facilidad. (Por ejemplo, si tiene la clase Profesor, solo necesita la misma propiedad establecida que Estudiante, y cambia su función para aceptar cualquier objeto del formulario Persona)

Me inclinaría más a mantener el objeto principal que se pasa, porque la forma en que codifique va a explicar más fácilmente lo que está haciendo esta función.

Lilly
fuente
1

Una ruta común alrededor de esto es insertar una interfaz entre los dos procesos.

public class Student {

    public int id;
    public String name;
    public int age;
    public float score;
}

interface HasInfo {
    public String getInfo();
}

public class StudentInfo implements HasInfo {
    final Student student;

    public StudentInfo(Student student) {
        this.student = student;
    }

    @Override
    public String getInfo() {
        return student.name;
    }

}

public class Window {

    public void showInfo(HasInfo info) {

    }
}

Esto se vuelve un poco complicado a veces, pero las cosas se ponen un poco más ordenadas en Java si usa una clase interna.

interface HasInfo {
    public String getInfo();
}

public class Student {

    public int id;
    public String name;
    public int age;
    public float score;

    public HasInfo getInfo() {
        return new HasInfo () {
            @Override
            public String getInfo() {
                return name;
            }

        };
    }
}

Luego puede probar la Windowclase simplemente dándole un HasInfoobjeto falso .

Sospecho que este es un ejemplo del Patrón Decorador .

Adicional

Parece haber cierta confusión causada por la simplicidad del código. Aquí hay otro ejemplo que puede demostrar mejor la técnica.

interface Drawable {

    public void Draw(Pane pane);
}

/**
 * Student knows nothing about Window or Drawable.
 */
public class Student {

    public int id;
    public String name;
    public int age;
    public float score;
}

/**
 * DrawsStudents knows about both Students and Drawable (but not Window)
 */
public class DrawsStudents implements Drawable {

    private final Student subject;

    public DrawsStudents(Student subject) {
        this.subject = subject;
    }

    @Override
    public void Draw(Pane pane) {
        // Draw a Student on a Pane
    }

}

/**
 * Window only knows about Drawables.
 */
public class Window {

    public void showInfo(Drawable info) {

    }
}
OldCurmudgeon
fuente
Si showInfo solo quería mostrar el nombre del alumno, ¿por qué no simplemente pasar el nombre? envolver un campo con nombre semánticamente significativo en una interfaz abstracta que contiene una cadena sin pistas sobre lo que representa la cadena se siente como una GRAN rebaja, tanto en términos de mantenibilidad como de comprensibilidad.
sara
@kai: el uso de Studenty Stringaquí para el tipo de retorno es solo para demostración. Con toda probabilidad habría parámetros adicionales a getInfocomo el Paneseñalar a si el dibujo. El concepto aquí es pasar componentes funcionales como decoradores del objeto original .
OldCurmudgeon
en ese caso, estarías acoplando la entidad estudiantil estrechamente a tu marco de UI, eso suena aún peor ...
Sara
1
@kai: todo lo contrario. Mi interfaz de usuario solo sabe sobre HasInfoobjetos. StudentSabe ser uno.
OldCurmudgeon
Si le das getInfovuelta al vacío, pásala Panepara dibujar, entonces la implementación (en la Studentclase) se acopla repentinamente al swing o lo que sea que estés usando. Si hace que devuelva alguna cadena y tome 0 parámetros, su interfaz de usuario no sabrá qué hacer con la cadena sin suposiciones mágicas y acoplamiento implícito. Si hace que en getInforealidad devuelva algún modelo de vista con propiedades relevantes, entonces su Studentclase está nuevamente acoplada a la lógica de presentación. No creo que ninguna de estas alternativas sea deseable
Sara
1

Ya tiene muchas buenas respuestas, pero aquí hay algunas sugerencias más que pueden permitirle ver una solución alternativa:

  • Su ejemplo muestra un Estudiante (claramente un objeto modelo) que se pasa a una Ventana (aparentemente un objeto a nivel de vista). Un objeto de controlador o presentador intermediario puede ser beneficioso si aún no tiene uno, lo que le permite aislar su interfaz de usuario de su modelo. El controlador / presentador debe proporcionar una interfaz que se pueda usar para reemplazarlo para las pruebas de IU, y debe usar interfaces para referirse a los objetos del modelo y ver los objetos para poder aislarlo de ambos para las pruebas. Es posible que deba proporcionar alguna forma abstracta de crearlos o cargarlos (por ejemplo, objetos Factory, objetos de repositorio o similares).

  • Transferir partes relevantes de los objetos de su modelo a un Objeto de transferencia de datos es un enfoque útil para interactuar cuando su modelo se vuelve demasiado complejo.

  • Puede ser que su estudiante viole el Principio de segregación de interfaz. Si es así, podría ser beneficioso dividirlo en múltiples interfaces con las que sea más fácil trabajar.

  • Lazy Loading puede facilitar la construcción de gráficos de objetos grandes.

Jules
fuente
0

Esta es realmente una pregunta decente. El problema real aquí es el uso del término genérico "objeto", que puede ser un poco ambiguo.

Generalmente, en un lenguaje OOP clásico, el término "objeto" ha llegado a significar "instancia de clase". Las instancias de clase pueden ser bastante pesadas: propiedades públicas y privadas (y las intermedias), métodos, herencia, dependencias, etc. Realmente no querría usar algo así para simplemente pasar algunas propiedades.

En este caso, está utilizando un objeto como contenedor que simplemente contiene algunas primitivas. En C ++, los objetos como estos se conocían como structs(y todavía existen en lenguajes como C #). De hecho, las estructuras se diseñaron exactamente para el uso del que hablas: agruparon objetos relacionados y primitivas cuando tenían una relación lógica.

Sin embargo, en los lenguajes modernos, realmente no hay diferencia entre una estructura y una clase cuando se escribe el código , por lo que está bien usar un objeto. (Detrás de escena, sin embargo, hay algunas diferencias que debe tener en cuenta; por ejemplo, una estructura es un tipo de valor, no un tipo de referencia). Básicamente, siempre que mantenga su objeto simple, será fácil Para probar manualmente. Sin embargo, los lenguajes y herramientas modernos le permiten mitigar esto un poco (a través de interfaces, marcos de imitación, inyección de dependencias, etc.)

almuerzo317
fuente
1
Pasar una referencia no es costoso, incluso si el objeto tiene un tamaño de mil millones de terabytes, porque la referencia sigue siendo solo del tamaño de un int en la mayoría de los idiomas. Debería preocuparse más sobre si el método de recepción está expuesto a una API demasiado grande y si acopla las cosas de una manera indeseable. Consideraría hacer una capa de mapeo que traduzca los objetos de negocios ( Student) en modelos de vista ( StudentInfoo StudentInfoViewModeletc.), pero podría no ser necesario.
Sara
Las clases y las estructuras son muy diferentes. Uno se pasa por valor (lo que significa que el método que lo recibe obtiene una copia ) y el otro se pasa por referencia (el receptor solo recibe un puntero al original). No entender esta diferencia es peligroso.
RubberDuck
@kai Entiendo que pasar una referencia no es costoso. Lo que digo es que crear una función que requiera una instancia de clase completa puede ser más difícil de probar dependiendo de las dependencias de esa clase, sus métodos, etc., ya que de alguna manera tendrías que burlarte de esa clase.
lunchmeat317
Personalmente, estoy en contra de burlarme de casi cualquier cosa, excepto las clases límite que acceden a sistemas externos (archivo / red IO) o que no son deterministas (por ejemplo, aleatorio, basado en el tiempo del sistema, etc.). Si una clase que estoy probando tiene una dependencia que no es relevante para la característica actual que se está probando, prefiero pasar nula siempre que sea posible. Pero si está probando un método que toma un objeto de parámetro, si ese objeto tiene muchas dependencias, me preocuparía el diseño general. Tales objetos deben ser livianos.
Sara