Objetos de comportamiento cero en OOP: mi dilema de diseño

94

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 Cardclase necesita determinar la visibilidad para los jugadores.

Una forma es tener boolean isVisible(Player p)en Cardclase.

Otro es tener boolean isVisible(Card c)en Playerclase.

No me gusta el primer enfoque en particular, ya que otorga conocimiento sobre la Playerclase de nivel superior a una Cardclase de nivel inferior .

En su lugar, opté por la tercera opción donde tenemos una Viewportclase que, dada una Playery una lista de cartas, determina qué cartas son visibles.

Sin embargo, este enfoque le roba a ambos Cardy a las Playerclases una posible función miembro. Una vez hecho esto para otras cosas que la visibilidad de las tarjetas, lo que queda Cardy Playerclases 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 Viewportanterior.

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 DocumentIdque es inmutable, solo tiene un solo BigDecimal idmiembro y un captador para este miembro. Ahora necesita tener un método en alguna parte, que proporcione DocumentIdretornos Documentpara esta identificación de una base de datos.

Vos si:

  • Agregue un Document getDocument(SqlSession)método a la DocumentIdclase, 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 la DocumentIdclase como muerta, sin comportamiento, clase de estructura.
RokL
fuente
21
Algunas de sus premisas aquí están completamente equivocadas, lo que hará que responder la pregunta subyacente sea muy difícil. Mantenga sus preguntas lo más concisas y libres de opiniones que pueda y obtendrá mejores respuestas.
pdr
31
"Esto está claramente en contra de la idea principal de OOP" - no, no lo es, pero es una falacia común.
Doc Brown
55
Supongo que el problema radica en el hecho de que en el pasado ha habido diferentes escuelas para la "Orientación a objetos", tal como lo entendieron originalmente personas como Alan Kay (ver geekswithblogs.net/theArchitectsNapkin/archive/2013/09/08/ ... ), y la forma en que se enseñó en el contexto de OOA / OOD por esas personas de Rational ( en.wikipedia.org/wiki/Object-oriented_analysis_and_design ).
Doc Brown
21
Esta es una muy buena pregunta y está bien publicada, al contrario de algunos de los otros comentarios, diría. Muestra claramente cuán ingenuos o incompletos son la mayoría de los consejos sobre cómo estructurar el programa, y ​​lo difícil que es hacerlo, y cuán inalcanzable es un diseño adecuado en muchas situaciones, sin importar cuánto se esfuerce por hacerlo bien. Y aunque una respuesta obvia a la pregunta específica son los métodos múltiples, el problema subyacente del diseño persiste.
Thiago Silva
55
¿Quién dice que las clases sin comportamiento son un antipatrón?
James Anderson el

Respuestas:

42

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 Cardsaber para quién Playeres visible?

¿Y por qué implementar Card.isVisibleTo(Player p), pero no Player.isVisibleTo(Card c)? ¿O viceversa?

Sí, puedes intentar crear una especie de regla para eso como lo hiciste, como Playertener un nivel más alto que un Card(?), Pero no es tan fácil de adivinar y tendré que buscar en más de un lugar para encontrar el método

Con el tiempo puede conducir a un compromiso de diseño podrida de implementar isVisibleToen tanto Card y Playerde clase, que creo que es un no-no. ¿Porque? Debido a que ya imagino el día vergonzoso cuando player1.isVisibleTo(card1)devolverá un valor diferente al card1.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, Dealo Game.

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 Cardinstancias para cada as de espada?

Todavía podría implementar isVisibleToen Card, pero pasar un objeto de contexto a ella y hacer Carddelegado 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 structs en C #.

Ver

para referencia. Es un lenguaje altamente orientado a objetos, pero nadie le da mucha importancia.

Konrad Morawski
fuente
1
Solo una nota sobre las estructuras en C #: no son estructuras típicas, como las conoce en C. De hecho, también admiten OOP con herencia, encapsulación y polimorfismo. Además de algunas peculiaridades, la principal diferencia es cómo el tiempo de ejecución maneja las instancias cuando se pasan a otros objetos: ¡las estructuras son tipos de valor y las clases son tipos de referencia!
Aschratt
3
@Aschratt: Las estructuras no admiten herencia. Las estructuras pueden implementar interfaces, pero las estructuras que implementan interfaces se comportan de manera diferente a los objetos de clase que hacen lo mismo. Si bien es posible hacer que las estructuras se comporten de manera similar a los objetos, el mejor caso de uso para las estructuras es cuando uno quiere algo que se comporte como una estructura C, y las cosas que uno está encapsulando son primitivas o tipos de clase inmutables.
supercat
1
+1 por qué "no es igual a tener funciones globales". Esto no ha sido muy abordado por otros. (Aunque si tiene varias barajas, una función global aún devolvería valores diferentes para instancias distintas de la misma tarjeta).
alexis
@supercat Esto es digno de una pregunta separada o una sesión de chat, pero actualmente no estoy interesado en :-( Usted dice (en C #) "las estructuras que implementan interfaces se comportan de manera diferente a los objetos de clase que hacen lo mismo". Estoy de acuerdo en que Hay otras diferencias de comportamiento a tener en cuenta, pero AFAIK en el código siguiente Interface iObj = (Interface)obj;, el comportamiento de iObjno se ve afectado por el estado structo (excepto que será una copia en caja en esa asignación si es un ).classobjstruct
Mark Hurd
150

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.

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.

Supongamos que tiene una API general de juegos de cartas. Tienes una tarjeta de clase. Ahora esta clase de cartas necesita determinar la visibilidad para los jugadores.

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.

Una forma es tener Boolean isVisible (Player p) en la clase Card. Otra es tener boolean isVisible (Card c) en la clase Player.

Ambos son horribles; no hagas ninguno de esos. ¡Ni el jugador ni la carta son responsables de implementar las reglas de Bridge!

En cambio, opté por la tercera opción donde tenemos una clase de Viewport que, dado un Jugador y una lista de cartas, determina qué cartas son visibles.

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?

Sin embargo, este enfoque roba a las clases Card y Player de una posible función miembro.

¡Bueno!

Una vez que hace esto para otras cosas que no sean la visibilidad de las tarjetas, le quedan clases de Tarjeta y Jugador que contienen puramente datos, ya que toda la funcionalidad se implementa en otras clases, que en su mayoría son clases sin datos, solo métodos, como el Viewport anterior. Esto está claramente en contra de la idea principal de OOP.

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:

  • Jugador uno, el jugador tres te ha entregado los diez corazones.
  • Jugador dos, una carta ha sido entregada al jugador uno por el jugador tres.

Y luego le pide al jugador una acción.

  • Jugador uno, ¿qué quieres hacer?
  • El jugador uno dice: triplica el fromp.
  • Jugador uno, esa es una acción ilegal porque un triplete de p produce un gambito indefendible.
  • Jugador uno, ¿qué quieres hacer?
  • El jugador uno dice: descarta la reina de espadas.
  • Jugador dos, jugador uno ha descartado la reina de espadas.

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.

Eric Lippert
fuente
41
@gnat: Una opinión contrastante de Alan Kay es "En realidad, inventé el término" orientado a objetos ", y puedo decirle que no tenía en mente C ++". Hay lenguajes OO sin clases; JavaScript me viene a la mente.
Eric Lippert
19
@gnat: Estoy de acuerdo en que JS tal como está hoy no es un gran ejemplo de un lenguaje OOP, pero muestra que uno podría construir fácilmente un lenguaje OO sin clases. Estoy de acuerdo en que la unidad fundamental de OO-ness en Eiffel y C ++ es la clase; donde no estoy de acuerdo es la noción de que las clases son la condición sine qua non de OO. La condición sine qua non de OO son los objetos que encapsulan el comportamiento y se comunican entre sí a través de una interfaz pública bien definida.
Eric Lippert
16
Estoy de acuerdo con @EricLippert, las clases no son fundamentales para OO, ni la herencia, lo que sea que la corriente principal pueda decir. Sin embargo, se logra la encapsulación de datos, comportamiento y responsabilidad. Hay lenguajes basados ​​en prototipos más allá de Javascript que son OO pero sin clase. Es particularmente un error centrarse en la herencia sobre estos conceptos. Dicho esto, las clases son medios muy útiles para organizar la encapsulación del comportamiento. El hecho de que pueda tratar las clases como objetos (y en lenguajes prototipo, viceversa) hace que la línea sea borrosa.
Schwern
66
Considérelo de esta manera: ¿qué comportamiento exhibe una tarjeta real en el mundo real por sí sola? Creo que la respuesta es "ninguna". Otras cosas actúan en la tarjeta. La tarjeta en sí, en el mundo real, literalmente es solo información (4 de clubes), sin comportamiento intrínseco de ningún tipo. La forma en que se usa esa información (también conocida como "una tarjeta") depende 100% de algo / alguien más, también conocido como "reglas" y "jugadores". Las mismas cartas se pueden usar para una variedad infinita (bueno, tal vez no del todo) de diferentes juegos, por cualquier número de jugadores diferentes. Una carta es solo una carta, y todo lo que tiene son propiedades.
Craig
55
@Montagist: Déjame aclarar un poco las cosas entonces. Considere C. Creo que estaría de acuerdo en que C no tiene clases. Sin embargo, podría decir que las estructuras son "clases", podría crear campos de tipo puntero de función, podría construir vtables, podría crear métodos llamados "constructores" que configuran las vtables para que algunas estructuras "se hereden" unas de otras, y así. Puede emular la herencia basada en clases en C. Y puede emularla en JS. Pero hacerlo significa construir algo sobre el lenguaje que aún no está allí.
Eric Lippert
29

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 isVisiblerelació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 con

    static boolean isVisible(Card c, Player p);
    

    y no hay nada de malo en Cardno tener métodos más allá ranky suitaccesores.

amon
fuente
11
@UMad sí, ese es exactamente mi punto, y no hay nada de malo en eso. Utilice el paradigma <del> idioma </del> correcto para el trabajo en cuestión. (Por cierto, la mayoría de los lenguajes además de Smalltalk no están orientados a objetos puros. Por ejemplo, Java, C # y C ++ admiten programación imperativa, estructurada, procesal, modular, funcional y orientada a objetos. Todos esos paradigmas no OO están disponibles por una razón : para que pueda usarlos)
amon
1
Hay una forma sensata de hacer OO Fibonacci, llame al fibonaccimétodo en una instancia de integer. 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 propio fibonaccimétodo ajustado de rendimiento .
Schwern
2
@Schwern si algo Fibonaccies una subclase de la clase abstracta Sequence, 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.
George Reith
2
No esperaba que "OOP Fibonacci puro" fuera tan efectivo para disparar a los nerds . Detenga cualquier discusión circular sobre eso en estos comentarios, aunque tenía un cierto valor de entretenimiento. ¡Ahora hagamos algo constructivo para un cambio!
amon
3
sería una tontería hacer de Fibonacci un método de enteros, solo para que puedas decir que es POO. Es una función y debe tratarse como una función.
immibis
19

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. (...) Esto está claramente en contra de la idea principal de OOP.

Esta es una pregunta difícil porque se basa en bastantes premisas defectuosas:

  1. La idea de que OOP es la única forma válida de escribir código.
  2. La idea de que OOP es un concepto bien definido. Se ha convertido en una palabra de moda que hace que sea difícil encontrar dos personas que puedan ponerse de acuerdo sobre lo que es POO.
  3. La idea de que OOP se trata de agrupar datos y comportamiento.
  4. La idea de que todo es / debería ser una abstracción.

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 ielemento 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 isVisibleun método de este Cardtipo 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 del Playertipo? Bueno, probablemente esa tampoco sea una cualidad definitoria de los jugadores. ¿Debería ser parte de algún Viewporttipo? 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 isVisibleque solo sea una función gratuita.

Doval
fuente
1
+1 para sentido común en lugar de zumbidos sin sentido.
derecha
Por las líneas que he leído, el ensayo que vinculó parece una lectura sólida que no he tenido en mucho tiempo.
Arthur Havlicek
@ArthurHavlicek Es más difícil de seguir si no comprende los idiomas utilizados en el ejemplo de código, pero lo encontré bastante esclarecedor.
Doval
9

Obviamente, según los principios de OOP, los objetos que son solo datos (como las estructuras C) se consideran un antipatrón.

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 Playerclase completa cuando se lee de la Playerstabla, 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 tu Playerclase 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.

DougM
fuente
55
No esté en desacuerdo con nada de lo que ha dicho, pero esto no responde a la pregunta en absoluto. Dicho esto, culpo más a la pregunta que a la respuesta.
pdr
2
Exactamente, las tarjetas y los documentos son solo contenedores de información, incluso en el mundo real, y cualquier "patrón" que no pueda manejarlo debe ignorarse.
JeffO
1
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.
RokL
8

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.

RibaldEddie
fuente
La respuesta anterior de Lippert es un mejor ejemplo de este concepto.
RibaldEddie
5

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: Playerproblema: la creación de una ViewPortabstracción tiene sentido si se piensa en Cardy Playercomo dos bibliotecas independientes (lo que implicaría Playerque a veces se usa sin Card). Sin embargo, me inclino a pensar que Playertiene una retención Cardsy debería proporcionar un Collection<Card> getVisibleCards ()accesorio para ellos. Ambas soluciones ( ViewPorty la mía) son mejores que proporcionar isVisiblecomo método Cardo Player, 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.

Arthur Havlicek
fuente
Me gusta tu blog.
RokL
3

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

  1. ¿Está bien tener construcciones de soporte de datos simples (clases / estructuras; no me importa lo que se modelen para esta pregunta) que realmente no ofrecen mucha funcionalidad?
  2. En caso afirmativo, ¿cuál es la mejor o preferida forma de modelarlos?
  3. En caso negativo, ¿cómo incorporamos estas partes de datos en clases de API más altas (incluido el comportamiento)

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.

Harsha
fuente
3
El problema es que la pregunta (y su respuesta) son sobre cómo hacer las cosas "en general". El hecho es que nunca hacemos cosas "en general". Siempre hacemos cosas específicas. Es necesario examinar nuestras cosas específicas y compararlas con nuestros requisitos para determinar si nuestras cosas específicas son las correctas para la situación.
John Saunders
@ JohnSaunders Percibo su sabiduría aquí y estoy de acuerdo en cierta medida, pero también se requiere un enfoque conceptual simple antes de abordar un problema. Después de todo, la pregunta aquí no es tan abierta como parece ser. Creo que es una pregunta válida de OOD que cualquier diseñador de OO enfrenta en los usos iniciales de OOP. ¿Cuál es tu opinión? Si una concreción ayuda, podríamos discutir la construcción de un ejemplo de su elección.
Harsha
He estado fuera de la escuela por más de 35 años. En el mundo real, encuentro muy poco valor en los "enfoques conceptuales". Encuentro experiencia para ser un mejor maestro que Meyers en este caso.
John Saunders
Realmente no entiendo la clase de datos vs clase para la distinción de comportamiento. Si abstraes tus objetos correctamente, no hay distinción. Imagina un Pointcon getX()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 datos
Arthur Havlicek
@ArthurHavlicek: Saber lo que una clase no hará a menudo es tan útil como saber lo que hará. Tener algo que especifique en su contrato que se comportará como nada más que un titular de datos inmutable compartible, o como nada más que un titular de datos mutable no compartible, es útil.
supercat
2

Una 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 CardEntityobjeto 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 como CardSpaceobjetos; para mover una carta a un espacio, uno le daría una referencia alCardSpaceobjeto; 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).

Super gato
fuente
0

Sin embargo, este enfoque roba a las clases Card y Player de una posible función miembro.

¿Y cómo es eso malo / mal aconsejado?

Para usar una analogía similar al ejemplo de sus tarjetas, considere a Car, a Drivery necesita determinar si Driverpuede manejar el Car.

OK, entonces decidiste que no querías Carsaber si Drivertenía la llave del auto correcta o no, y por alguna razón desconocida también decidiste que no querías Driverque supieras sobre la Carclase (no estabas completamente de acuerdo esto también en tu pregunta original). Por lo tanto, tiene una clase intermediaria, algo similar a una Utilsclase, que contiene el método con reglas comerciales para devolver un booleanvalor 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.

hjk
fuente