La idea básica detrás de OOP es que los datos y el comportamiento (sobre esos datos) son inseparables y están unidos por la idea de un objeto de una clase. Los objetos tienen datos y métodos que funcionan con eso (y otros datos). Obviamente, según los principios de OOP, los objetos que son solo datos (como las estructuras C) se consideran un antipatrón.
Hasta aquí todo bien.
El problema es que he notado que mi código parece ir más y más en la dirección de este antipatrón últimamente. Me parece que cuanto más trato de lograr que la información se oculte entre las clases y los diseños vagamente acoplados, más mis clases llegan a ser una mezcla de datos puros sin clases de comportamiento y todos los comportamientos sin clases de datos.
Generalmente diseño clases de una manera que minimiza su conocimiento de la existencia de otras clases y minimiza su conocimiento de las interfaces de otras clases. Especialmente hago cumplir esto de arriba hacia abajo, las clases de nivel inferior no conocen las clases de nivel superior. P.ej:
Supongamos que tiene una API general de juegos de cartas. Tiene una clase Card
. Ahora esta Card
clase necesita determinar la visibilidad para los jugadores.
Una forma es tener boolean isVisible(Player p)
en Card
clase.
Otro es tener boolean isVisible(Card c)
en Player
clase.
No me gusta el primer enfoque en particular, ya que otorga conocimiento sobre la Player
clase de nivel superior a una Card
clase de nivel inferior .
En su lugar, opté por la tercera opción donde tenemos una Viewport
clase que, dada una Player
y una lista de cartas, determina qué cartas son visibles.
Sin embargo, este enfoque le roba a ambos Card
y a las Player
clases una posible función miembro. Una vez hecho esto para otras cosas que la visibilidad de las tarjetas, lo que queda Card
y Player
clases que contienen los datos puramente como toda la funcionalidad se implementa en otras clases, que son sobre todo las clases que no tienen datos, sólo métodos, como el Viewport
anterior.
Esto está claramente en contra de la idea principal de OOP.
¿Cuál es la forma correcta? ¿Cómo debo realizar la tarea de minimizar las interdependencias de clase y minimizar el conocimiento asumido y el acoplamiento, pero sin terminar con un diseño extraño donde todas las clases de bajo nivel contienen solo datos y las clases de alto nivel contienen todos los métodos? ¿Alguien tiene alguna tercera solución o perspectiva sobre el diseño de clase que evite todo el problema?
PD Aquí hay otro ejemplo:
Supongamos que tiene una clase DocumentId
que es inmutable, solo tiene un solo BigDecimal id
miembro y un captador para este miembro. Ahora necesita tener un método en alguna parte, que proporcione DocumentId
retornos Document
para esta identificación de una base de datos.
Vos si:
- Agregue un
Document getDocument(SqlSession)
método a laDocumentId
clase, introduciendo de repente el conocimiento sobre su persistencia ("we're using a database and this query is used to retrieve document by id"
), la API utilizada para acceder a DB y similares. Además, esta clase ahora requiere un archivo JAR de persistencia solo para compilar. - Agregue alguna otra clase con método
Document getDocument(DocumentId id)
, dejando laDocumentId
clase como muerta, sin comportamiento, clase de estructura.
fuente
Respuestas:
Lo que usted describe se conoce como un modelo de dominio anémico . Al igual que con muchos principios de diseño de OOP (como la Ley de Demeter, etc.), no vale la pena inclinarse hacia atrás solo para satisfacer una regla.
No tiene nada de malo tener bolsas de valores, siempre que no abarroten todo el paisaje y no confíen en otros objetos para hacer la limpieza que podrían estar haciendo por sí mismos .
Ciertamente sería un olor a código si tuviera una clase separada solo para modificar propiedades de
Card
, si se pudiera esperar razonablemente que las cuide por sí sola.¿Pero es realmente un trabajo
Card
saber para quiénPlayer
es visible?¿Y por qué implementar
Card.isVisibleTo(Player p)
, pero noPlayer.isVisibleTo(Card c)
? ¿O viceversa?Sí, puedes intentar crear una especie de regla para eso como lo hiciste, como
Player
tener un nivel más alto que unCard
(?), Pero no es tan fácil de adivinar y tendré que buscar en más de un lugar para encontrar el métodoCon el tiempo puede conducir a un compromiso de diseño podrida de implementar
isVisibleTo
en tantoCard
yPlayer
de clase, que creo que es un no-no. ¿Porque? Debido a que ya imagino el día vergonzoso cuandoplayer1.isVisibleTo(card1)
devolverá un valor diferente alcard1.isVisibleTo(player1).
que creo, es subjetivo, esto debería hacerse imposible por diseño .La visibilidad mutua de las cartas y los jugadores debería regirse mejor por algún tipo de objeto de contexto, ya sea
Viewport
,Deal
oGame
.No es igual a tener funciones globales. Después de todo, puede haber muchos juegos concurrentes. Tenga en cuenta que la misma tarjeta se puede usar simultáneamente en muchas tablas. ¿Creamos muchas
Card
instancias para cada as de espada?Todavía podría implementar
isVisibleTo
enCard
, pero pasar un objeto de contexto a ella y hacerCard
delegado de la consulta. Programa para interactuar para evitar un alto acoplamiento.En cuanto a su segundo ejemplo: si la ID del documento consta solo de a
BigDecimal
, ¿por qué crear una clase de contenedor para ella?Yo diría que todo lo que necesitas es un
DocumentRepository.getDocument(BigDecimal documentID);
Por cierto, mientras está ausente de Java, hay
struct
s en C #.Ver
http://msdn.microsoft.com/en-us/library/ah19swz4.aspx
http://msdn.microsoft.com/en-us/library/0taef578.aspx
para referencia. Es un lenguaje altamente orientado a objetos, pero nadie le da mucha importancia.
fuente
Interface iObj = (Interface)obj;
, el comportamiento deiObj
no se ve afectado por el estadostruct
o (excepto que será una copia en caja en esa asignación si es un ).class
obj
struct
Estás cometiendo el error común de suponer que las clases son un concepto fundamental en OOP. Las clases son solo una forma particularmente popular de lograr la encapsulación. Pero podemos permitir que eso se deslice.
BUENOS CIELOS NO. Cuando juegas al Bridge, ¿le preguntas al siete de corazones cuándo es el momento de cambiar la mano del muñeco de un secreto conocido solo por el muñeco a ser conocido por todos? Por supuesto no. Eso no es una preocupación de la tarjeta en absoluto.
Ambos son horribles; no hagas ninguno de esos. ¡Ni el jugador ni la carta son responsables de implementar las reglas de Bridge!
Nunca he jugado cartas con un "viewport" antes, así que no tengo idea de lo que se supone que encapsula esta clase. Yo he jugado a las cartas con un par de barajas de cartas, algunos jugadores, una mesa, y una copia de Hoyle. ¿Cuál de esas cosas representa Viewport?
¡Bueno!
No; La idea básica de OOP es que los objetos encapsulan sus preocupaciones . En su sistema, a una tarjeta no le preocupa mucho. Tampoco es un jugador. Esto se debe a que estás modelando con precisión el mundo . En el mundo real, las propiedades de las tarjetas que son relevantes para un juego son extremadamente simples. Podríamos reemplazar las imágenes en las tarjetas con los números del 1 al 52 sin cambiar mucho el juego. Podríamos reemplazar a las cuatro personas con maniquíes etiquetados como Norte, Sur, Este y Oeste sin cambiar mucho el juego. Los jugadores y las cartas son las cosas más simples en el mundo de los juegos de cartas. Las reglas son lo que es complicado, por lo que la clase que representa las reglas es donde debería estar la complicación.
Ahora, si uno de tus jugadores es una IA, su estado interno podría ser extremadamente complicado. Pero esa IA no determina si puede ver una carta. Las reglas determinan eso .
Así es como diseñaría su sistema.
En primer lugar, las cartas son sorprendentemente complicadas si hay juegos con más de una baraja. Debe considerar la pregunta: ¿pueden los jugadores distinguir entre dos cartas del mismo rango? Si el jugador uno juega uno de los siete corazones y luego suceden algunas cosas, y luego el jugador dos juega uno de los siete corazones, ¿puede el jugador tres determinar que eran los mismos siete corazones? Considera esto cuidadosamente. Pero aparte de esa preocupación, las tarjetas deberían ser muy simples; son solo datos.
A continuación, ¿cuál es la naturaleza de un jugador? Un jugador consume una secuencia de acciones visibles y produce una acción .
El objeto de las reglas es lo que coordina todo esto. Las reglas producen una secuencia de acciones visibles e informan a los jugadores:
Y luego le pide al jugador una acción.
Y así.
Separe sus mecanismos de sus políticas . Las políticas del juego deben estar encapsuladas en un objeto de política , no en las cartas . Las cartas son solo un mecanismo.
fuente
Tiene razón en que el acoplamiento de datos y comportamiento es la idea central de OOP, pero hay más en ello. Por ejemplo, la encapsulación : la programación modular / OOP nos permite separar una interfaz pública de los detalles de implementación. En OOP, esto significa que los datos nunca deben ser accesibles públicamente, y solo deben usarse a través de accesores. Según esta definición, un objeto sin ningún método es realmente inútil.
Una clase que no ofrece métodos más allá de los accesores es esencialmente una estructura demasiado complicada. Pero esto no es malo, porque OOP le brinda la flexibilidad de cambiar los detalles internos, lo que no hace una estructura. Por ejemplo, en lugar de almacenar un valor en un campo miembro, podría volver a calcularse cada vez. O se cambia un algoritmo de respaldo, y con él el estado que se debe seguir.
Si bien OOP tiene algunas ventajas claras (especialmente sobre la programación procesal simple), es ingenuo luchar por una OOP "pura". Algunos problemas no se asignan bien a un enfoque orientado a objetos, y otros paradigmas los resuelven más fácilmente. Cuando se encuentre con tal problema, no insista en un enfoque inferior.
Considere calcular la secuencia de Fibonacci de una manera orientada a objetos . No puedo pensar en una forma sensata de hacer eso; La programación estructurada simple ofrece la mejor solución a este problema.
Su
isVisible
relación pertenece a ambas clases, o a ninguna, o en realidad: al contexto . Los registros sin comportamiento son típicos de un enfoque de programación funcional o de procedimiento, que parece ser el más adecuado para su problema. No hay nada malo cony no hay nada de malo en
Card
no tener métodos más allárank
ysuit
accesores.fuente
fibonacci
método en una instancia deinteger
. Deseo enfatizar su punto de que OO se trata de encapsulación, incluso en lugares aparentemente pequeños. Deje que el número entero descubra cómo hacer el trabajo. Más tarde, puede mejorar la implementación, agregar almacenamiento en caché para mejorar el rendimiento. A diferencia de las funciones, los métodos siguen los datos para que todas las personas que llaman obtengan el beneficio de una implementación mejorada. Tal vez se agreguen enteros de precisión arbitraria más adelante, se puedan tratar de manera transparente como enteros normales y pueden tener su propiofibonacci
método ajustado de rendimiento .Fibonacci
es una subclase de la clase abstractaSequence
, la secuencia es utilizada por cualquier conjunto de números y es responsable de almacenar las semillas, el estado, el almacenamiento en caché y un iterador.Esta es una pregunta difícil porque se basa en bastantes premisas defectuosas:
No tocaré mucho el número 1-3, porque cada uno podría generar su propia respuesta, e invita a mucha discusión basada en la opinión. Pero encuentro que la idea de "OOP es sobre el acoplamiento de datos y comportamiento" es especialmente preocupante. No solo conduce al # 4, sino que también lleva a la idea de que todo debería ser un método.
Hay una diferencia entre las operaciones que definen un tipo y las formas en que puede usar ese tipo. Ser capaz de recuperar el
i
elemento th es esencial para el concepto de una matriz, pero ordenar es solo una de las muchas cosas que puedo elegir hacer con una. La clasificación no necesita ser un método más de lo que debe ser "crear una nueva matriz que contenga solo los elementos pares".OOP se trata de usar objetos. Los objetos son solo una forma de lograr la abstracción . La abstracción es un medio para evitar un acoplamiento innecesario en su código, no un fin en sí mismo. Si su noción de una tarjeta se define únicamente por el valor de su conjunto y rango, está bien implementarla como una simple tupla o registro. No hay detalles no esenciales de los que cualquier otra parte del código pueda formar una dependencia. A veces simplemente no tienes nada que ocultar.
No haría
isVisible
un método de esteCard
tipo porque ser visible probablemente no sea esencial para su noción de una tarjeta (a menos que tenga tarjetas muy especiales que puedan volverse transparentes u opacas ...). ¿Debería ser un método delPlayer
tipo? Bueno, probablemente esa tampoco sea una cualidad definitoria de los jugadores. ¿Debería ser parte de algúnViewport
tipo? Una vez más, eso depende de lo que defina una ventana gráfica y de si la noción de verificar la visibilidad de las tarjetas es esencial para definir una ventana gráfica.Es muy posible
isVisible
que solo sea una función gratuita.fuente
No, no lo son. Los objetos Plain-Old-Data son un patrón perfectamente válido, y los esperaría en cualquier programa que trate con datos que necesitan ser persistentes o comunicados entre distintas áreas de su programa.
Si bien su capa de datos puede poner en cola una
Player
clase completa cuando se lee de laPlayers
tabla, podría ser una biblioteca de datos general que devuelve un POD con los campos de la tabla, que pasa a otra área de su programa que convierte un jugador POD a tuPlayer
clase concreta .El uso de objetos de datos, con o sin tipo, puede no tener sentido en su programa, pero eso no los convierte en un antipatrón. Si tienen sentido, úselos, y si no lo hacen, no lo haga.
fuente
Plain-Old-Data objects are a perfectly valid pattern
No dije que no, digo que está mal cuando pueblan la mitad inferior de la aplicación.Personalmente, creo que Domain Driven Design ayuda a aclarar este problema. La pregunta que hago es, ¿cómo describo el juego de cartas a los seres humanos? En otras palabras, ¿qué estoy modelando? Si lo que estoy modelando realmente incluye la palabra "viewport" y un concepto que coincide con su comportamiento, entonces crearía el objeto viewport y haría que hiciera lo que lógicamente debería.
Sin embargo, si no tengo el concepto de ventana gráfica en mi juego, y es algo que creo que necesito porque de lo contrario el código "se siente mal". Pienso dos veces antes de agregarlo a mi modelo de dominio.
La palabra modelo significa que estás construyendo una representación de algo. Advierto contra poner en una clase que representa algo abstracto más allá de lo que estás representando.
Editaré para agregar que es posible que necesite el concepto de Viewport en otra parte de su código, si necesita interactuar con una pantalla. Pero en términos de DDD, esto sería un problema de infraestructura y existiría fuera del modelo de dominio.
fuente
No suelo hacer autopromociones, pero el hecho es que escribí mucho sobre problemas de diseño de OOP en mi blog . Para resumir varias páginas: no debe comenzar el diseño con clases. Comenzar con interfaces o API y el código de forma a partir de ahí tienen mayores posibilidades de proporcionar abstracciones significativas, ajustar especificaciones y evitar inflar clases concretas con código no reutilizable.
Cómo se aplica esto
Card
:Player
problema: la creación de unaViewPort
abstracción tiene sentido si se piensa enCard
yPlayer
como dos bibliotecas independientes (lo que implicaríaPlayer
que a veces se usa sinCard
). Sin embargo, me inclino a pensar quePlayer
tiene una retenciónCards
y debería proporcionar unCollection<Card> getVisibleCards ()
accesorio para ellos. Ambas soluciones (ViewPort
y la mía) son mejores que proporcionarisVisible
como métodoCard
oPlayer
, en términos de crear relaciones de código comprensibles.Una solución fuera de la clase es mucho, mucho mejor para el
DocumentId
. Hay poca motivación para hacer que (básicamente, un número entero) dependa de una biblioteca de base de datos compleja.fuente
No estoy seguro de que la pregunta en cuestión se esté respondiendo en el nivel correcto. Exhorté a los sabios en el foro a pensar activamente en el núcleo de la pregunta aquí.
U Mad está planteando una situación en la que cree que la programación según su comprensión de OOP generalmente daría lugar a que muchos nodos hoja sean titulares de datos, mientras que su API de nivel superior es la mayor parte del comportamiento.
Creo que el tema fue ligeramente tangencial para determinar si isVisible se definiría en Card vs Player; fue un mero ejemplo ilustrado, aunque ingenuo.
Sin embargo, había empujado a los experimentados aquí para ver el problema en cuestión. Creo que hay una buena pregunta que U Mad ha impulsado. Entiendo que empujarías las reglas y la lógica en cuestión a un objeto propio; pero como entiendo la pregunta es
Mi vista:
Creo que está haciendo una pregunta de granularidad que es difícil de acertar en la programación orientada a objetos. En mi poca experiencia, no incluiría una entidad en mi modelo que no incluya ningún comportamiento en sí mismo. Si tengo que hacerlo, probablemente haya usado una construcción de una estructura diseñada para mantener tal abstracción a diferencia de una clase que tiene la idea de encapsular datos y comportamiento.
fuente
Point
congetX()
función. Podrías imaginar que está obteniendo uno de sus atributos, pero también podría leerlo desde el disco o Internet. Conseguir y establecer es un comportamiento, y tener clases que hagan eso está completamente bien. Las bases de datos solo obtienen y configuran datosUna fuente común de confusión en OOP se deriva del hecho de que muchos objetos encapsulan dos aspectos del estado: las cosas que saben y las cosas que saben sobre ellos. Las discusiones sobre el estado de los objetos con frecuencia ignoran este último aspecto, ya que en los marcos donde las referencias de objetos son promiscuas, no hay una forma general de determinar qué cosas podrían saber sobre cualquier objeto cuya referencia haya estado expuesta al mundo exterior.
Sugeriría que probablemente sería útil tener un
CardEntity
objeto que encapsule esos aspectos de la tarjeta en componentes separados. Un componente se relacionaría con las marcas en la tarjeta (por ejemplo, "Rey Diamante" o "Explosión de lava; los jugadores tienen la posibilidad de esquivar AC-3, o de lo contrario recibir daño 2D6"). Uno podría relacionarse con un aspecto único del estado, como la posición (por ejemplo, en la baraja, en la mano de Joe o en la mesa frente a Larry). Un tercero con el que se puede relacionar puede verlo (quizás nadie, quizás un jugador o quizás muchos jugadores). Para garantizar que todo se mantenga sincronizado, los lugares donde podría estar una tarjeta no se encapsularían como campos simples, sino comoCardSpace
objetos; para mover una carta a un espacio, uno le daría una referencia alCardSpace
objeto; luego se eliminaría del espacio antiguo y se colocaría en el nuevo espacio).Encapsular explícitamente "quién sabe sobre X" por separado de "lo que X sabe" debería ayudar a evitar mucha confusión. A veces es necesario tener cuidado para evitar pérdidas de memoria, especialmente con muchas asociaciones (por ejemplo, si pueden surgir nuevas tarjetas y desaparecen las viejas, hay que asegurarse de que las tarjetas que deben abandonarse no se queden pegadas a ningún objeto de larga vida) ) pero si la existencia de referencias a un objeto formará una parte relevante de su estado, es completamente apropiado que el objeto mismo encapsule explícitamente dicha información (incluso si delega en otra clase el trabajo de administrarlo realmente).
fuente
¿Y cómo es eso malo / mal aconsejado?
Para usar una analogía similar al ejemplo de sus tarjetas, considere a
Car
, aDriver
y necesita determinar siDriver
puede manejar elCar
.OK, entonces decidiste que no querías
Car
saber siDriver
tenía la llave del auto correcta o no, y por alguna razón desconocida también decidiste que no queríasDriver
que supieras sobre laCar
clase (no estabas completamente de acuerdo esto también en tu pregunta original). Por lo tanto, tiene una clase intermediaria, algo similar a unaUtils
clase, que contiene el método con reglas comerciales para devolver unboolean
valor para la pregunta anterior.creo que esto esta bien. Es posible que la clase intermediaria solo necesite verificar las llaves del auto ahora, pero se puede refactorizar para considerar si el conductor tiene una licencia de conducir válida, bajo la influencia del alcohol o en un futuro distópico, verificar la biometría de ADN. Por encapsulación, realmente no hay grandes problemas para que estas tres clases coexistan juntas.
fuente