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?
Respuestas:
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:
vs
En la primera versión, el código de la interfaz de usuario pasa ciegamente el
data
objeto 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 eldata
objeto 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 deldata
objeto 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 datosdata
contiene el objeto, pero generalmente es muy obvio. Por ejemplo, sidata
es una cadena proporcionada por el usuario como "20/05/1999", debe corresponder al código de la interfaz de usuario para convertirla en unDate
tipo adecuado antes de pasarla.fuente
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).
fuente
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.
fuente
Depende.
Para elaborar, los parámetros que acepta su método deben coincidir semánticamente con lo que está tratando de hacer. Considere una
EmailInviter
y estas tres posibles implementaciones de uninvite
método:Pasar en un lugar
String
donde debe pasarEmailAddress
es defectuoso porque no todas las cadenas son direcciones de correo electrónico. LaEmailAddress
clase coincide semánticamente mejor con el comportamiento del método. Sin embargo, pasar aUser
también es defectuoso porque ¿por qué deberíaEmailInviter
limitarse 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
String
oint
pero 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.fuente
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
es una llamada más legible que
fuente
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:
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.
fuente
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 ...Desde una perspectiva de mantenibilidad, los argumentos deben ser claramente distinguibles entre sí, preferiblemente a nivel del compilador.
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.
fuente
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.
fuente
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:
Referencia de objeto profesional:
Entonces, qué debe usarse y cuándo depende mucho de los casos de uso
fuente
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.
fuente