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

90

La idea básica detrás de la POO es que los datos y el comportamiento (sobre esos datos) son inseparables y están acoplados 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 la POO, los objetos que son solo datos (como estructuras C) se consideran un antipatrón.

Hasta ahora 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 anti-patrón últimamente. Me parece que cuanto más trato de lograr que la información se oculte entre las clases y los diseños poco acoplados, más mis clases se convierten en una mezcla de datos puros, clases de comportamiento y todo tipo de comportamiento, no clases de datos.

Generalmente, diseño las 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 aplico esto de manera descendente, las clases de nivel inferior no saben acerca de las clases de nivel superior. Por ejemplo:

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

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

Otro es tener boolean isVisible(Card c) en la clase Player .

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

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

Sin embargo, este enfoque roba las clases Card y Player de una posible función miembro. Una vez que haga esto para otras cosas que no sean la visibilidad de las tarjetas, se le dejarán las clases Card y Player 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 arriba.

Esto está claramente en contra de la idea principal de la POO.

¿Cuál es la forma correcta? ¿Cómo debo realizar la tarea de minimizar las interdependencias de clase y minimizar el conocimiento y el acoplamiento asumidos, pero sin terminar con un diseño extraño donde todas las clases de bajo nivel contienen solo datos y las 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?

P.S. Aquí hay otro ejemplo:

Supongamos que tiene la clase DocumentId que es inmutable, solo tiene un único miembro BigDecimal id y un captador para este miembro. Ahora necesitas tener un método en algún lugar, que dado un DocumentId devuelve Document para este ID desde una base de datos.

¿Usted:

  • Agregue el método Document getDocument(SqlSession) a la clase DocumentId , 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 la base de datos y similares. Además, esta clase ahora requiere un archivo JAR de persistencia solo para compilar.
  • Agregue alguna otra clase con el método Document getDocument(DocumentId id) , dejando la clase DocumentId como muerta, sin comportamiento, clase de estructura.
pregunta U Mad 02.04.2014 - 14:23

10 respuestas

42

Lo que describe se conoce como modelo de dominio anémico . Al igual que con muchos principios de diseño OOP (como la Ley de Demeter, etc.), no vale la pena inclinarse hacia atrás solo para satisfacer una regla.

No hay nada malo en tener bolsas de valores, siempre que no abarroten todo el paisaje y no confíen en otros objetos para hacer el mantenimiento que podrían hacer por sí mismos .

Sin duda, sería un olor a código si tuviera una clase separada solo por modificar las propiedades de Card - si se pudiera esperar razonablemente que cuidara de ellos por su cuenta.

Pero, ¿es realmente un trabajo de Card saber en qué Player es visible?

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

Sí, puedes intentar establecer algún tipo de regla para eso como lo hiciste, como Player es un nivel más alto que un Card (?), pero no es tan sencillo de adivinar y yo Tendré que buscar en más de un lugar para encontrar el método.

Con el tiempo, puede llevar a un compromiso de diseño podrido al implementar isVisibleTo en ambas Card y Player , lo que creo que es un no-no. ¿Porque? Porque ya me imagino el vergonzoso día en que player1.isVisibleTo(card1) devolverá un valor diferente al de card1.isVisibleTo(player1). , creo que es subjetivo, esto debería hacerse imposible por diseño .

La visibilidad mutua de las cartas y los jugadores debería estar mejor gobernada por algún tipo de objeto de contexto, ya sea Viewport , Deal o 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 utilizar simultáneamente en muchas tablas. ¿Crearemos muchas instancias Card para cada as de pala?

Aún podría implementar isVisibleTo en Card , pero pasarle un objeto de contexto y hacer que Card delegue la consulta. Programa de interfaz para evitar un alto acoplamiento.

En cuanto al segundo ejemplo, si la ID del documento consiste solo en un BigDecimal , ¿por qué crear una clase de contenedor para eso?

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

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

    
respondido por el Konrad Morawski 02.04.2014 - 15:52
145
  

La idea básica detrás de la POO es que los datos y el comportamiento (sobre esos datos) son inseparables y están acoplados por la idea de un objeto de una clase.

Está 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 se deslice.

  

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

BUENOS CIELOS NO. Cuando estás jugando a Bridge, ¿ pides a los siete de corazones cuándo es el momento de cambiar la mano del muñeco de un secreto conocido solo por el muñeco para que todos lo conozcan? Por supuesto no. Eso no es una preocupación de la tarjeta en absoluto.

  

Una forma es tener booleano isVisible (Jugador p) en la clase de Cartas.   Otra es tener booleano isVisible (Tarjeta c) en la clase Jugador.

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

  

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

Nunca he jugado cartas con un "viewport" antes, así que no tengo idea de lo que se supone que esta clase debe resumir. He jugado a cartas con un par de mazos de cartas, algunos jugadores, una mesa y una copia de Hoyle. ¿Cuál de esas cosas representa Viewport?

  

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

¡Bien!

  

Una vez que haga esto para otras cosas que no sean la visibilidad de las tarjetas, se quedarán con las 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 la ventana gráfica. encima. Esto está claramente en contra de la idea principal de la POO.

No; la idea básica de la POO es que los objetos encapsulan sus preocupaciones . En su sistema una tarjeta no le preocupa mucho. Tampoco es un jugador. Esto se debe a que estás modelando el mundo con precisión . En el mundo real, las propiedades son cartas que son relevantes para un juego y 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 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, entonces su estado interno podría ser extremadamente complicado. Pero esa IA no determina si puede ver una carta. Las reglas lo determinan .

Así es como diseñaría tu sistema.

En primer lugar, las cartas son sorprendentemente complicadas si hay juegos con más de un mazo. Debes 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, ¿el jugador tres puede determinar que era el mismo siete corazones? Considera esto cuidadosamente. Pero aparte de esa preocupación, las tarjetas deben ser muy simples; son sólo 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 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 tarjeta 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, eso es una acción ilegal porque un fromp triplicado produce un gambito indefendible.
  • Jugador uno, ¿qué quieres hacer?
  • El jugador uno dice: descartar a la reina de espadas.
  • Jugador dos, jugador uno ha descartado a la reina de espadas.

Y así sucesivamente.

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 tarjetas son solo un mecanismo.

    
respondido por el Eric Lippert 02.04.2014 - 19:40
28

Usted tiene razón en que el acoplamiento de datos y comportamiento es la idea central de la POO, pero hay más en ello. Por ejemplo, encapsulation : la programación OOP / modular nos permite separar una interfaz pública de los detalles de la implementación. En OOP, esto significa que los datos nunca deben ser de acceso público, y solo deben usarse a través de los 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 detalles internos, lo que no hace una estructura. Por ejemplo, en lugar de almacenar un valor en un campo miembro, se podría volver a calcular cada vez. O bien, se modifica un algoritmo de respaldo y, con él, el estado del que se debe realizar un seguimiento.

Si bien la POO tiene algunas ventajas claras (sobre todo en la programación de procedimiento simple), es ingenuo esforzarse por la POO "pura". Algunos problemas no se asignan bien a un enfoque orientado a objetos, y se resuelven más fácilmente con otros paradigmas. Cuando se encuentre con un problema de este tipo, no insista en un enfoque inferior.

  • Considere calcular la secuencia de Fibonacci de una manera orientada a objetos . No puedo pensar en una forma sana de hacer eso; La programación estructurada simple ofrece la mejor solución a este problema.

  • Su relación isVisible 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 mejor para su problema. No hay nada malo con

    static boolean isVisible(Card c, Player p);
    

    y no hay nada de malo en que Card no tenga métodos más allá de los accesores rank y suit .

respondido por el amon 02.04.2014 - 14:59
19
  

La idea básica detrás de la POO es que los datos y el comportamiento (sobre esos datos) son inseparables y están acoplados 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 la POO, los objetos que son solo datos (como estructuras C) se consideran un antipatrón. (...) Esto está claramente en contra de la idea principal de la POO.

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

  1. La idea de que OOP es la única forma válida de escribir código.
  2. La idea de que la POO es un concepto bien definido. Se ha convertido en una palabra de moda que es difícil encontrar dos personas que puedan ponerse de acuerdo sobre de qué se trata la 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 # 1-3, porque cada uno podría generar su propia respuesta, e invita a una gran cantidad de discusiones basadas en opiniones. Pero me parece 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. Poder recuperar el elemento i th es esencial para el concepto de una matriz, pero la clasificación 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 es sobre el uso de objetos. Los objetos son solo una forma de lograr la abstracción . La abstracción es un medio para evitar el 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 simple tupla o registro. No hay detalles no esenciales que cualquier otra parte del código pueda formar una dependencia. A veces simplemente no tienes nada que esconder.

No haría de isVisible un método del tipo Card porque ser visible probablemente no sea esencial para su noción de una tarjeta (a menos que tenga tarjetas muy especiales que puedan volverse translúcidas u opacas ...). ¿Debería ser un método del tipo Player ? Bueno, eso probablemente tampoco sea una cualidad definitoria de los jugadores. ¿Debería ser parte de algún tipo Viewport ? Una vez más, eso depende de lo que usted defina como una ventana gráfica y si la noción de verificar la visibilidad de las tarjetas es parte integral de la definición de una ventana gráfica.

Es muy posible que isVisible solo sea una función gratuita.

    
respondido por el Doval 02.04.2014 - 15:56
9
  

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

No, no lo son. Los objetos de datos antiguos son un patrón perfectamente válido, y los esperaría en cualquier programa que se ocupe de los datos que deben persistir o comunicarse entre distintas áreas de su programa.

Aunque su capa de datos podría enrollar una clase Player completa cuando se lee en la tabla Players , podría ser una biblioteca de datos general que devuelve un POD con el campos de la tabla, que pasa a otra área de su programa que convierte un POD de jugador a su clase concreta Player .

El uso de objetos de datos, ya sean escritos o sin tipo, puede que no tenga sentido en su programa, pero eso no los convierte en un antipatrón. Si tienen sentido, utilícelos, y si no lo tienen, no lo hagan.

    
respondido por el DougM 02.04.2014 - 14:37
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 coincida con su comportamiento, crearía el objeto viewport y haría que haga lo que lógicamente debería hacer.

Sin embargo, si no tengo el concepto de la 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 en agregarlo a mi modelo de dominio.

El modelo de palabra significa que estás construyendo una representación de algo. Le advierto que no incluya una clase que represente algo abstracto más allá de lo que usted representa.

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.

    
respondido por el RibaldEddie 02.04.2014 - 18:46
5

No suelo promocionarme a mí mismo, pero el hecho es que escribí mucho sobre los 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 desde allí tiene mayores posibilidades de proporcionar abstracciones significativas, ajustar las especificaciones y evitar la hinchazón de clases concretas con código no reutilizable.

Cómo se aplica esto a Card - Player problema: Crear una abstracción ViewPort tiene sentido si piensa que Card y Player son dos bibliotecas independientes (lo que implicaría que Player a veces se usa sin Card ). Sin embargo, me inclino a pensar que Player tiene Cards y debería proporcionarles un acceso Collection<Card> getVisibleCards () . Ambas soluciones ( ViewPort y las mías) son mejores que proporcionar isVisible como método de Card o 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 (básicamente, un entero) depender de una biblioteca de base de datos compleja.

    
respondido por el Arthur Havlicek 02.04.2014 - 21:07
3

No estoy seguro de que la pregunta sea respondida en el nivel correcto. Yo había instado 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 la POO generalmente daría lugar a que muchos nodos de hoja sean portadores de datos, mientras que su API de nivel superior forma parte de 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 presionado al experimentado para que viera el problema en cuestión. Creo que hay una buena pregunta que U Mad ha impulsado. Entiendo que usted empujaría las reglas y la lógica en cuestión a un objeto propio; pero como entiendo la pregunta es

  1. ¿Está bien tener construcciones simples de datos (clases / estructuras; no me importa cómo están modeladas para esta pregunta) que realmente no ofrecen mucha funcionalidad?
  2. En caso afirmativo, ¿cuál es la mejor o la mejor forma de modelarlos?
  3. Si la respuesta es negativa, ¿cómo incorporamos estas partes del contador de datos a las clases API más altas (incluido el comportamiento)?

Mi vista:

Creo que estás haciendo una pregunta de granularidad que es difícil de entender en programación orientada a objetos. En mi pequeña experiencia, no incluiría una entidad en mi modelo que no incluya ningún comportamiento por sí mismo. Si tuviera que hacerlo, probablemente hubiera utilizado una estructura que está diseñada para mantener tal abstracción a diferencia de una clase que tiene la idea de encapsular datos y comportamiento.

    
respondido por el Harsha 03.04.2014 - 06:35
2

Una fuente común de confusión en la OOP se deriva del hecho de que muchos objetos encapsulan dos aspectos del estado: las cosas que saben y las que saben sobre ellos. Las discusiones sobre el estado de los objetos frecuentemente ignoran este último aspecto, ya que en los marcos donde las referencias de los objetos son promiscuas, no hay una manera general de determinar qué cosas podrían saber acerca de cualquier objeto cuya referencia haya estado expuesta al mundo exterior.

Sugeriría que probablemente sería útil tener un objeto CardEntity que encapsule esos aspectos de la tarjeta en componentes separados. Uno de los componentes estaría relacionado con las marcas en la tarjeta (por ejemplo, "Diamond King" o "Lava Blast; los jugadores tienen la posibilidad de esquivar AC-3, o bien recibir daño 2D6"). Uno podría relacionarse con un aspecto único del estado, como la posición (por ejemplo, está en la cubierta, o en la mano de Joe, o en la mesa frente a Larry). Un tercero podría relacionarse con lo que puede verlo (tal vez nadie, quizás un jugador o quizás muchos jugadores). Para asegurarse de que todo se mantenga sincronizado, los lugares donde podría haber una tarjeta no se encapsularían como campos simples, sino más bien objetos CardSpace ; para mover una tarjeta a un espacio, uno le daría una referencia al objeto CardSpace apropiado; luego se eliminaría del espacio antiguo y se colocaría en el nuevo espacio).

Encapsular explícitamente "quien sabe sobre X" por separado de "lo que X sabe" debería ayudar a evitar mucha confusión. A veces se necesita cuidado para evitar pérdidas de memoria, especialmente con muchas asociaciones (por ejemplo, si pueden surgir nuevas tarjetas y desaparecen las antiguas, hay que asegurarse de que las tarjetas que deben abandonarse no se queden pegadas a objetos de larga duración). ) pero si la existencia de referencias a un objeto formará una parte relevante de su estado, es totalmente correcto que el objeto en sí encapsule explícitamente dicha información (incluso si delega a alguna otra clase el trabajo de administrarlo). p>     

respondido por el supercat 02.04.2014 - 19:16
0
  

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

¿Y qué tan mal / mal aconsejado?

Para usar una analogía similar al ejemplo de tus tarjetas, considera un Car , un Driver y debes determinar si el Driver puede impulsar el Car .

Bien, entonces decidió que no quiere que su Car sepa si el Driver tiene la llave correcta para el auto o no, y por alguna razón desconocida también decidió que no quiere su Driver para saber sobre la clase Car (usted no completó esto completamente en su pregunta original). Por lo tanto, tiene una clase intermedia, algo similar a una clase Utils , que contiene el método con reglas de negocio para devolver un valor boolean para la pregunta anterior.

Creo que esto está bien. Es posible que la clase intermedia solo necesite verificar las llaves del auto ahora, pero se puede refactorizar para considerar si el conductor tiene un permiso de conducir válido, bajo la influencia del alcohol o en un futuro distópico, verifique la biometría del ADN. Por encapsulación, realmente no hay grandes problemas si estas tres clases coexisten juntas.

    
respondido por el h.j.k. 03.04.2014 - 05:31

Lea otras preguntas en las etiquetas