¿Generalmente envía objetos o sus variables miembro a funciones?


31

Lo cual es una práctica generalmente aceptada entre estos dos casos:

function insertIntoDatabase(Account account, Otherthing thing) {
    database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue());
}

o

function insertIntoDatabase(long accountId, long thingId, double someValue) {
    database.insertMethod(accountId, thingId, someValue);
}

En otras palabras, ¿es generalmente mejor pasar objetos enteros o solo los campos que necesita?


55
Dependería completamente de para qué sirve la función y cómo se relaciona (o no se relaciona) con el objeto en cuestión.
MetaFight

Ese es el problema. No puedo decir cuándo usaría uno u otro. Siento que siempre podría cambiar el código para acomodar cualquier enfoque.
AJJ

1
En términos de API (y sin mirar las implementaciones en absoluto), el primero es abstracto y orientado al dominio (lo cual es bueno), mientras que el segundo no lo es (lo cual es malo).
Erik Eidt

1
El primer enfoque sería más OO de 3 niveles. Pero debería ser aún más así eliminando la palabra base de datos del método. Debe ser "Almacenar" o "Persistir" y hacer Cuenta o Cosa (no ambas). Como cliente de esta capa, no debe conocer el medio de almacenamiento. Sin embargo, al recuperar una cuenta, deberá pasar la identificación o una combinación de valores de propiedad (no valores de campo) para identificar el objeto deseado. O / e implementa un método de enumeración que pasa todas las cuentas.
Martin Maat

1
Típicamente, ambos estarían equivocados (o, más bien, menos que óptimos). La forma en que un objeto debe ser serializado en la base de datos debe ser una propiedad (una función miembro) del objeto, porque generalmente depende directamente de las variables miembro del objeto. En caso de que cambie miembros del objeto, también necesitará cambiar el método de serialización. Eso funciona mejor si es parte del objeto
tofro

Respuestas:


24

Ninguno de los dos es generalmente mejor que el otro. Es una decisión judicial que debe hacer caso por caso.

Pero en la práctica, cuando está en una posición en la que realmente puede tomar esta decisión, es porque puede decidir qué capa en la arquitectura general del programa debe dividir el objeto en primitivos, por lo que debe pensar en toda la llamada apilar , no solo este método en el que estás actualmente. Presumiblemente, la ruptura debe hacerse en algún lugar, y no tendría sentido (o sería innecesariamente propenso a errores) hacerlo más de una vez. La pregunta es dónde debería estar ese lugar.

La forma más fácil de tomar esta decisión es pensar qué código debería o no debería modificarse si se cambia el objeto . Expandamos un poco su ejemplo:

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account, data);
}
function insertIntoDatabase(Account account, Otherthing data) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(account.getId(), data.getId(), data.getSomeValue());
}

vs

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account.getId(), data.getId(), data.getSomeValue());
}
function insertIntoDatabase(long accountId, long dataId, double someValue) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(accountId, dataId, someValue);
}

En la primera versión, el código de la interfaz de usuario pasa ciegamente el dataobjeto y depende del código de la base de datos extraer los campos útiles. En la segunda versión, el código de la interfaz de usuario está dividiendo el dataobjeto en sus campos útiles, y el código de la base de datos los recibe directamente sin saber de dónde provienen. La implicación clave es que, si la estructura del dataobjeto cambiara de alguna manera, la primera versión requeriría solo el código de la base de datos para cambiar, mientras que la segunda versión requeriría solo el código de la IU para cambiar . Cuál de esos dos es correcto depende en gran medida de qué tipo de datos datacontiene el objeto, pero generalmente es muy obvio. Por ejemplo, sidataes una cadena proporcionada por el usuario como "20/05/1999", debe corresponder al código de la interfaz de usuario para convertirla en un Datetipo adecuado antes de pasarla.


8

Esta no es una lista exhaustiva, pero considere algunos de los siguientes factores cuando decida si un objeto debe pasarse a un método como argumento:

¿El objeto es inmutable? ¿Es la función 'pura'?

Los efectos secundarios son una consideración importante para la mantenibilidad de su código. Cuando ve código con una gran cantidad de objetos con estado mutable que se pasan por todas partes, ese código a menudo es menos intuitivo (de la misma manera que las variables de estado globales a menudo pueden ser menos intuitivas), y la depuración a menudo se vuelve más difícil y el tiempo consumidor.

Como regla general, procure garantizar, en la medida de lo razonablemente posible, que cualquier objeto que pase a un método sea claramente inmutable.

Evite (nuevamente, en la medida de lo razonablemente posible) cualquier diseño por el cual se espera que el estado de un argumento cambie como resultado de una llamada a la función; uno de los argumentos más fuertes para este enfoque es el Principio de Menos Asombro ; es decir, alguien que lee su código y ve pasar un argumento a una función es 'menos probable' que espere que su estado cambie después de que la función haya regresado.

¿Cuántos argumentos tiene el método?

Los métodos con listas de argumentos excesivamente largas (incluso si la mayoría de esos argumentos tienen valores 'predeterminados') comienzan a parecer un olor a código. Sin embargo, a veces tales funciones son necesarias, y puede considerar crear una clase cuyo único propósito sea actuar como un objeto de parámetro .

Este enfoque puede involucrar una pequeña cantidad de mapeo adicional de código repetitivo de su objeto 'fuente' a su objeto de parámetro, pero eso es un costo bastante bajo tanto en términos de rendimiento como de complejidad, y hay una serie de beneficios en términos de desacoplamiento e inmutabilidad de objetos.

¿El objeto pasado pertenece exclusivamente a una "capa" dentro de su aplicación (por ejemplo, un modelo de vista o una entidad ORM?)

Piense en la separación de preocupaciones (SoC) . A veces, preguntándose si el objeto "pertenece" a la misma capa o módulo en el que existe su método (por ejemplo, una biblioteca de envoltura de API enrollada a mano, o su capa lógica empresarial principal, etc.) puede informar si ese objeto realmente debe pasarse a ese método.

SoC es una buena base para escribir código limpio, débilmente acoplado y modular. por ejemplo, un objeto de entidad ORM (mapeo entre su código y su esquema de base de datos) idealmente no debería pasarse en su capa empresarial, o peor aún en su capa de presentación / UI.

En el caso de pasar datos entre 'capas', pasar parámetros de datos simples a un método generalmente es preferible a pasar un objeto desde la capa 'incorrecta'. Aunque probablemente sea una buena idea tener modelos separados que existan en la capa 'correcta' en la que pueda asignar en su lugar.

¿Es la función en sí misma demasiado grande y / o compleja?

Cuando una función necesita muchos elementos de datos, puede valer la pena considerar si esa función está asumiendo demasiadas responsabilidades; Busque oportunidades potenciales para refactorizar utilizando objetos más pequeños y funciones más cortas y simples.

¿Debería la función ser un objeto de comando / consulta?

En algunos casos, la relación entre los datos y la función puede ser cercana; en esos casos, considere si un objeto de comando o un objeto de consulta sería apropiado.

¿Agregar un parámetro de objeto a un método obliga a la clase contenedor a adoptar nuevas dependencias?

A veces, el argumento más sólido para los argumentos de "Datos antiguos simples" es simplemente que la clase receptora ya está perfectamente autocontenida, y agregar un parámetro de objeto a uno de sus métodos contaminaría la clase (o si la clase ya está contaminada, entonces lo hará empeorar la entropía existente)

¿Realmente necesita pasar un objeto completo o solo necesita una pequeña parte de la interfaz de ese objeto?

Considere el Principio de segregación de interfaz con respecto a sus funciones, es decir, al pasar un objeto, solo debe depender de las partes de la interfaz de ese argumento que realmente necesita (la función).


5

Entonces, cuando crea una función, está declarando implícitamente algún contrato con el código que lo está llamando. "Esta función toma esta información y la convierte en otra cosa (posiblemente con efectos secundarios)".

Por lo tanto, debe ser lógicamente su contrato con los objetos (no obstante que se implementen), o con los campos que tan pasan a ser parte de estos otros objetos. Está agregando acoplamiento de cualquier manera, pero como programador, depende de usted decidir a dónde pertenece.

En general , si no está claro, favorezca los datos más pequeños necesarios para que la función funcione. Eso a menudo significa pasar solo los campos, ya que la función no necesita las otras cosas que se encuentran en los objetos. Pero a veces tomar todo el objeto es más correcto, ya que resulta en un menor impacto cuando las cosas cambian inevitablemente en el futuro.


¿Por qué no cambia el nombre del método a insertAccountIntoDatabase o va a pasar algún otro tipo? En cierto número de argumentos para usar obj es fácil leer el código. En su caso, preferiría pensar si el nombre del método aclara lo que voy a insertar en lugar de cómo lo voy a hacer.
Laiv

3

Depende.

Para elaborar, los parámetros que acepta su método deben coincidir semánticamente con lo que está tratando de hacer. Considere una EmailInvitery estas tres posibles implementaciones de un invitemétodo:

void invite(String emailAddressString) {
  invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
  ...
}
void invite(User user) {
  invite(user.getEmailAddress());
}

Pasar en un lugar Stringdonde debe pasar EmailAddresses defectuoso porque no todas las cadenas son direcciones de correo electrónico. La EmailAddressclase coincide semánticamente mejor con el comportamiento del método. Sin embargo, pasar a Usertambién es defectuoso porque ¿por qué debería EmailInviterlimitarse a invitar a los usuarios? ¿Qué pasa con las empresas? ¿Qué sucede si está leyendo direcciones de correo electrónico de un archivo o una línea de comandos y no están asociadas con los usuarios? ¿Listas de correo? La lista continua.

Aquí hay algunas señales de advertencia que puede usar como guía. Si usa un tipo de valor simple como Stringo intpero no todas las cadenas o entradas son válidas o hay algo "especial" en ellas, debería usar un tipo más significativo. Si está utilizando un objeto y lo único que hace es llamar a un captador, entonces debería pasar el objeto directamente al captador. Estas pautas no son duras ni rápidas, pero pocas pautas lo son.


0

Clean Code recomienda tener la menor cantidad de argumentos posible, lo que significa que Object generalmente sería el mejor enfoque y creo que tiene sentido. porque

insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));

es una llamada más legible que

insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );

No puedo estar de acuerdo allí. Los dos no son sinónimos. Crear 2 nuevas instancias de objeto solo para pasarlas a un método no es bueno. Usaría insertIntoDatabase (myAccount, myOtherthing) en lugar de cualquiera de sus opciones.
Jwent

0

Pase alrededor del objeto, no su estado constituyente. Esto es compatible con los principios orientados a objetos de encapsulación y ocultación de datos. Exponer las entrañas de un objeto en varias interfaces de métodos donde no es necesario viola los principios básicos de OOP.

¿Qué sucede si cambias los campos Otherthing? Tal vez cambie un tipo, agregue un campo o elimine un campo. Ahora todos los métodos como el que mencionas en tu pregunta deben actualizarse. Si pasa el objeto, no hay cambios en la interfaz.

La única vez que debe escribir un método aceptando campos en un objeto es cuando escribe un método para recuperar el objeto:

public User getUser(String primaryKey) {
  return ...;
}

En el momento de hacer esa llamada, el código de llamada aún no tiene una referencia al objeto porque el punto de llamar a ese método es obtener el objeto.


1
"¿Qué sucede si cambias los campos Otherthing?" (1) Eso sería una violación del principio abierto / cerrado. (2) incluso si pasa todo el objeto, el código dentro de él luego accede a los miembros de ese objeto (y si no lo hace, ¿por qué pasar el objeto?) Aún se rompería ...
David Arno

@DavidArno, el punto de mi respuesta no es que nada se rompería, pero menos se rompería. Tampoco olvide el primer párrafo: independientemente de lo que se rompa, el estado interno de un objeto debe abstraerse utilizando la interfaz del objeto. Transmitir su estado interno es una violación de los principios de OOP.

0

Desde una perspectiva de mantenibilidad, los argumentos deben ser claramente distinguibles entre sí, preferiblemente a nivel del compilador.

// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)

// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)

El primer diseño conduce a la detección temprana de errores. El segundo diseño puede conducir a problemas sutiles de tiempo de ejecución que no aparecen en las pruebas. Por lo tanto, se debe preferir el primer diseño.


0

De los dos, mi preferencia es el primer método:

function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }

La razón es que los cambios realizados en cualquiera de los objetos en el futuro, siempre que los cambios conserven a esos captadores para que el cambio sea transparente fuera del objeto, entonces tendrá menos código para cambiar y probar y menos posibilidades de interrumpir la aplicación.

Este es solo mi proceso de pensamiento, basado principalmente en cómo me gusta trabajar y estructurar cosas de esta naturaleza y que resultan ser bastante manejables y mantenibles a largo plazo.

No voy a entrar en convenciones de nomenclatura, pero señalaría que aunque este método tiene la palabra "base de datos", ese mecanismo de almacenamiento puede cambiar en el futuro. Según el código que se muestra, no hay nada que vincule la función con la plataforma de almacenamiento de la base de datos que se está utilizando, ni siquiera si se trata de una base de datos. Simplemente asumimos porque está en el nombre. Nuevamente, suponiendo que esos captadores siempre se conserven, será fácil cambiar cómo / dónde se almacenan estos objetos.

Sin embargo, volvería a pensar la función y los dos objetos porque usted tiene una función que depende de dos estructuras de objetos, y específicamente de los captadores que están siendo empleados. También parece que esta función está vinculando esos dos objetos en una cosa acumulativa que persiste. Mi instinto me dice que un tercer objeto podría tener sentido. Necesitaría saber más sobre estos objetos y cómo se relacionan en la actualidad y la hoja de ruta anticipada. Pero mi instinto se inclina en esa dirección.

Tal como está ahora el código, la pregunta plantea "¿Dónde estaría o debería esta función? ¿Es parte de la cuenta o de otra cosa? ¿A dónde va?

Supongo que ya hay una "base de datos" de un tercer objeto, y me estoy inclinando a poner esta función en ese objeto, y luego se convierte en el trabajo de ese objeto para poder manejar una Cuenta y un OtroThing, transformar y luego persistir el resultado .

Si tuviera que ir tan lejos como para hacer que el tercer objeto se ajuste a un patrón de mapeo relacional de objetos (ORM), mucho mejor. Eso haría que sea muy obvio para cualquiera que trabaje con el código entender "Ah, aquí es donde Account y OtherThing se unieron y persistieron".

Pero también podría tener sentido introducir un cuarto objeto, que maneja el trabajo de combinar y transformar una Cuenta y un OtroThing, pero no maneja la mecánica de persistencia. Lo haría si anticipa muchas más interacciones con o entre estos dos objetos, porque a medida que crezca, me gustaría que los bits de persistencia se factoreen en un objeto que solo maneje la persistencia.

Discutiría por mantener el diseño de modo que cualquiera de la Cuenta, OtherThing o el tercer objeto ORM pueda cambiarse sin tener que cambiar también los otros tres. A menos que haya una buena razón para no hacerlo, me gustaría que Account y OtherThing sean independientes y no tengan que conocer el funcionamiento interno y las estructuras de cada uno.

Por supuesto, si supiera el contexto completo que será, podría cambiar mis ideas por completo. De nuevo, así es como pienso cuando veo cosas como esta, y cómo es una inclinación.


0

Ambos enfoques tienen sus propios pros y contras. Lo que es mejor en un escenario depende mucho del caso de uso en cuestión.


Parámetros múltiples pro, referencia de objeto Con:

  • La persona que llama no está vinculada a una clase específica , puede pasar valores de diferentes fuentes por completo
  • El estado del objeto está a salvo de modificaciones inesperadas dentro de la ejecución del método.

Referencia de objeto profesional:

  • Interfaz clara de que el método está vinculado al tipo de referencia de objeto, lo que dificulta pasar accidentalmente valores no relacionados / no válidos
  • Cambiar el nombre de un campo / captador requiere cambios en todas las invocaciones del método y no solo en su implementación
  • Si se agrega una nueva propiedad y se necesita pasar, no se requieren cambios en la firma del método
  • El método puede mutar el estado del objeto
  • Pasar demasiadas variables de tipos primitivos similares hace que sea confuso para la persona que llama con respecto al orden (problema del patrón del generador)

Entonces, qué debe usarse y cuándo depende mucho de los casos de uso

  1. Pasar parámetros individuales: en general, si el método no tiene nada que ver con el tipo de objeto, es mejor pasar la lista de parámetros individuales para que sea aplicable a un público más amplio.
  2. Introducir un nuevo objeto modelo: si la lista de parámetros crece para ser grande (más de 3), es mejor introducir un nuevo objeto modelo que pertenezca a la API llamada (se prefiere el patrón de construcción)
  3. Pasar referencia de objeto: si el método está relacionado con los objetos de dominio, es mejor desde el punto de vista de facilidad de mantenimiento y legibilidad para pasar las referencias de objeto.

0

Por un lado tiene una cuenta y un objeto Otherthing. Por otro lado, tiene la capacidad de insertar un valor en una base de datos, dada la identificación de una cuenta y la identificación de un Otherthing. Esas son las dos cosas dadas.

Puede escribir un método tomando Account y Otherthing como argumentos. En el lado profesional, la persona que llama no necesita conocer ningún detalle sobre Cuenta y Otherthing. En el lado negativo, la persona que llama necesita saber sobre los métodos de Cuenta y Otherthing. Y también, no hay forma de insertar nada más en una base de datos que el valor de un objeto Otherthing y no hay forma de usar este método si tiene la identificación de un objeto de cuenta, pero no el objeto en sí.

O puede escribir un método tomando dos identificadores y un valor como argumentos. En el lado negativo, la persona que llama necesita conocer los detalles de Cuenta y Otherthing. Y puede haber una situación en la que realmente necesite más detalles de una Cuenta u Otherthing que solo la identificación para insertar en la base de datos, en cuyo caso esta solución es totalmente inútil. Por otro lado, es de esperar que no se necesite conocimiento de Cuenta y Otherthing en la persona que llama, y ​​hay más flexibilidad.

Su juicio: ¿se necesita más flexibilidad? Esto a menudo no se trata de una sola llamada, pero sería coherente a través de todo su software: o usa identificadores de la cuenta la mayor parte del tiempo, o usa los objetos. Mezclarlo te lleva a lo peor de ambos mundos.

En C ++, puede tener un método que tome dos identificadores más el valor, y un método en línea que tome Cuenta y Otherthing, por lo que tiene ambas formas con cero sobrecarga.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.