En el código heredado ocasionalmente veo clases que no son más que envoltorios para datos. algo como:
class Bottle {
int height;
int diameter;
Cap capType;
getters/setters, maybe a constructor
}
Entiendo que OO es que las clases son estructuras para datos y los métodos para operar con esos datos. Esto parece excluir objetos de este tipo. Para mí no son más que una structs
especie de derrota del propósito de OO. No creo que sea necesariamente malo, aunque puede ser un olor a código.
¿Hay algún caso en el que tales objetos serían necesarios? Si esto se usa con frecuencia, ¿hace que el diseño sea sospechoso?
java
code-quality
object-oriented
code-smell
Michael K
fuente
fuente
Respuestas:
Definitivamente no es malvado ni un código de olor en mi mente. Los contenedores de datos son ciudadanos de OO válidos. A veces desea encapsular información relacionada juntos. Es mucho mejor tener un método como
que
El uso de una clase también le permite agregar un parámetro adicional a Bottle sin modificar cada llamada de DoStuffWithBottle. Y puede subclasificar Bottle y aumentar aún más la legibilidad y la organización de su código, si es necesario.
También hay objetos de datos simples que pueden devolverse como resultado de una consulta de base de datos, por ejemplo. Creo que el término para ellos en ese caso es "Objeto de transferencia de datos".
En algunos idiomas también hay otras consideraciones. Por ejemplo, en C # las clases y las estructuras se comportan de manera diferente, ya que las estructuras son un tipo de valor y las clases son tipos de referencia.
fuente
DoStuffWith
debería ser un método de laBottle
clase en OOP (y probablemente también debería ser inmutable). Lo que ha escrito anteriormente no es un buen patrón en un lenguaje OO (a menos que esté interactuando con una API heredada). Que es , sin embargo, un diseño válido en un entorno no-OO.Las clases de datos son válidas en algunos casos. Los DTO son un buen ejemplo mencionado por Anna Lear. Sin embargo, en general, debe considerarlos como la semilla de una clase cuyos métodos aún no han brotado. Y si se encuentra con muchos de ellos en código antiguo, trátelos como un fuerte olor a código. A menudo los usan viejos programadores de C / C ++ que nunca han hecho la transición a la programación OO y son un signo de programación procesal. Confiar en getters y setters todo el tiempo (o peor aún, en el acceso directo de miembros no privados) puede meterte en problemas antes de que te des cuenta. Considere un ejemplo de un método externo que necesita información
Bottle
.Aquí
Bottle
hay una clase de datos):Aquí hemos dado
Bottle
algún comportamiento):El primer método viola el principio Tell-Don't-Ask, y al mantenernos
Bottle
tontos, hemos permitido que el conocimiento implícito sobre las botellas, como lo que hace que uno fragle (theCap
), se deslice a la lógica que está fuera de laBottle
clase. Tienes que estar alerta para evitar este tipo de "fuga" cuando habitualmente confías en los captadores.El segundo método
Bottle
solo pregunta qué necesita para hacer su trabajo, y dejaBottle
decidir si es frágil o más grande que un tamaño determinado. El resultado es un acoplamiento mucho más flexible entre el método y la implementación de Bottle. Un efecto secundario agradable es que el método es más limpio y más expresivo.Raramente utilizará tantos campos de un objeto sin escribir alguna lógica que debería residir en la clase con esos campos. Averigua cuál es esa lógica y luego muévela a donde pertenece.
fuente
Si este es el tipo de cosas que necesitas, está bien, pero por favor, por favor, hazlo como
en lugar de algo como
Por favor.
fuente
Como dijo @Anna, definitivamente no es malo. Claro que puede poner operaciones (métodos) en clases, pero solo si lo desea . Usted no tiene a.
Permítame una pequeña queja sobre la idea de que tiene que poner operaciones en las clases, junto con la idea de que las clases son abstracciones. En la práctica, esto alienta a los programadores a
Haga más clases de las que necesitan (estructura de datos redundante). Cuando una estructura de datos contiene más componentes de los mínimos necesarios, no está normalizada y, por lo tanto, contiene estados inconsistentes. En otras palabras, cuando se altera, debe alterarse en más de un lugar para mantenerse consistente. No realizar todos los cambios coordinados lo hace inconsistente y es un error.
Resuelva el problema 1 colocando métodos de notificación , de modo que si se modifica la parte A, intente propagar los cambios necesarios a las partes B y C. Esta es la razón principal por la que se recomienda tener métodos de acceso para obtener y establecer. Dado que esta es una práctica recomendada, parece disculpar el problema 1, causando más del problema 1 y más de la solución 2. Esto da como resultado no solo errores debidos a la implementación incompleta de las notificaciones, sino a un problema de disminución de rendimiento de las notificaciones fuera de control. Estos no son cálculos infinitos, sino muy largos.
Estos conceptos se enseñan como cosas buenas, generalmente por maestros que no han tenido que trabajar dentro de aplicaciones monstruosas de millones de líneas plagadas de estos problemas.
Esto es lo que trato de hacer:
Mantenga los datos lo más normalizados posible, de modo que cuando se realice un cambio en los datos, se haga en el menor número de puntos de código posible, para minimizar la probabilidad de entrar en un estado inconsistente.
Cuando los datos no se deben normalizar y la redundancia es inevitable, no use notificaciones para mantener la coherencia. Más bien, tolere la inconsistencia temporal. Resuelva la inconsistencia con barridos periódicos a través de los datos mediante un proceso que solo hace eso. Esto centraliza la responsabilidad de mantener la coherencia, al tiempo que evita los problemas de rendimiento y corrección a los que son susceptibles las notificaciones. Esto da como resultado un código mucho más pequeño, sin errores y eficiente.
fuente
Este tipo de clases son bastante útiles cuando se trata de aplicaciones medianas / grandes, por algunas razones:
En resumen, en mi experiencia, generalmente son más útiles que molestos.
fuente
Con el diseño del juego, la sobrecarga de miles de llamadas a funciones y los oyentes de eventos a veces pueden hacer que valga la pena tener clases que solo almacenen datos, y tener otras clases que recorran todas las clases de solo datos para realizar la lógica.
fuente
De acuerdo con Anna Lear,
A veces las personas se olvidan de leer las Convenciones de codificación de Java de 1999 que dejan muy claro que este tipo de programación está perfectamente bien. De hecho, si lo evita, ¡su código huele mal! (demasiados captadores / setters)
De Java Code Conventions 1999: Un ejemplo de variables de instancia pública apropiadas es el caso donde la clase es esencialmente una estructura de datos, sin comportamiento. En otras palabras, si hubiera utilizado una estructura en lugar de una clase (si Java admite estructura), entonces es apropiado hacer públicas las variables de instancia de la clase. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177
Cuando se usan correctamente, los POD (estructuras de datos antiguas) son mejores que los POJO, al igual que los POJO a menudo son mejores que los EJB.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures
fuente
Las estructuras tienen su lugar, incluso en Java. Solo debe usarlos si las siguientes dos cosas son ciertas:
Si este es el caso, debe hacer públicos los campos y omitir los captadores / establecedores. Getters y setters son torpes de todos modos, y Java es tonto por no tener propiedades como un lenguaje útil. Dado que su objeto tipo estructura no debería tener ningún método de todos modos, los campos públicos tienen más sentido.
Sin embargo, si alguno de esos no se aplica, se trata de una clase real. Eso significa que todos los campos deben ser privados. (Si realmente necesita un campo en un alcance más accesible, use un captador / definidor).
Para verificar si su supuesta estructura tiene comportamiento, observe cuándo se usan los campos. Si parece violar la información , no preguntes , entonces debes mover ese comportamiento a tu clase.
Si algunos de sus datos no cambian, entonces debe hacer que todos esos campos sean finales. Puede considerar hacer que su clase sea inmutable . Si necesita validar sus datos, proporcione la validación en los instaladores y constructores. (Un truco útil es definir un setter privado y modificar su campo dentro de su clase usando solo ese setter).
Su ejemplo de botella probablemente fallaría en ambas pruebas. Podría tener un código (artificial) que se vea así:
En cambio debería ser
Si cambiara la altura y el diámetro, ¿sería la misma botella? Probablemente no. Esos deberían ser finales. ¿Está bien un valor negativo para el diámetro? ¿Debe su botella ser más alta que ancha? ¿Puede la gorra ser nula? ¿No? ¿Cómo estás validando esto? Suponga que el cliente es estúpido o malvado. ( Es imposible notar la diferencia ) . Debe verificar estos valores.
Así es como podría verse su nueva clase de botella:
fuente
En mi humilde opinión, a menudo no hay suficientes clases como esta en sistemas muy orientados a objetos. Necesito calificar eso cuidadosamente.
Por supuesto, si los campos de datos tienen un amplio alcance y visibilidad, eso puede ser extremadamente indeseable si hay cientos o miles de lugares en su base de código que alteran dichos datos. Eso es pedir problemas y dificultades para mantener invariantes. Sin embargo, al mismo tiempo, eso no significa que todas las clases de la base de código completa se beneficien de la ocultación de información.
Pero hay muchos casos en los que dichos campos de datos tendrán un alcance muy limitado. Un ejemplo muy sencillo es una
Node
clase privada de una estructura de datos. A menudo puede simplificar mucho el código al reducir la cantidad de interacciones de objetos que suceden si se trataNode
simplemente de datos sin procesar. Que sirve como un mecanismo de desacoplamiento ya que la versión alternativa podría requerir acoplamiento bidireccional de, por ejemplo,Tree->Node
yNode->Tree
en lugar de simplementeTree->Node Data
.Un ejemplo más complejo serían los sistemas de componentes de entidad, como se usan a menudo en los motores de juego. En esos casos, los componentes a menudo son solo datos sin procesar y clases como la que ha mostrado. Sin embargo, su alcance / visibilidad tiende a ser limitado ya que generalmente solo hay uno o dos sistemas que pueden acceder a ese tipo particular de componente. Como resultado, todavía tiende a encontrar bastante fácil mantener invariantes en esos sistemas y, además, estos sistemas tienen muy pocas
object->object
interacciones, lo que hace que sea muy fácil comprender lo que está sucediendo a nivel del ojo del pájaro.En tales casos, puede terminar con algo más como esto en lo que respecta a las interacciones (este diagrama indica interacciones, no acoplamiento, ya que un diagrama de acoplamiento puede incluir interfaces abstractas para la segunda imagen a continuación):
... a diferencia de esto:
... y el primer tipo de sistema tiende a ser mucho más fácil de mantener y razonar en términos de corrección a pesar del hecho de que las dependencias realmente fluyen hacia los datos. Obtiene mucho menos acoplamiento principalmente porque muchas cosas pueden convertirse en datos sin procesar en lugar de objetos que interactúan entre sí formando un gráfico muy complejo de interacciones.
fuente
Incluso existen criaturas mucho más extrañas en el mundo OOP. Una vez he creado una clase, que es abstracta y no contiene nada, solo para ser un padre común de otras dos clases ... es la clase Port:
Por lo tanto, SceneMember es la clase base, hace todo el trabajo, que debe aparecer en la escena: agregar, eliminar, obtener ID de configuración, etc. El mensaje es la conexión entre puertos, tiene su propia vida. InputPort y OutputPort contienen las funciones específicas de E / S propias.
El puerto está vacío Solo se usa para agrupar InputPort y OutputPort juntos para, por ejemplo, una lista de puertos. Está vacío porque SceneMember contiene todas las cosas que son para actuar como miembro, y InputPort y OutputPort contienen las tareas especificadas por el puerto. InputPort y OutputPort son tan diferentes que no tienen funciones comunes (excepto SceneMember). Entonces, el puerto está vacío. Pero es legal.
Quizás sea un antipatrón , pero me gusta.
(Es como la palabra "compañero", que se usa tanto para "esposa" como para "esposo". Nunca se usa la palabra "compañero" para una persona concreta, porque conoce el sexo de él / ella, y no importa si él / ella está casado o no, entonces usa "alguien" en su lugar, pero todavía existe y se usa en situaciones raras y abstractas, por ejemplo, un texto de ley).
fuente