Relación entre objetos

8

Durante algunas semanas he estado pensando en la relación entre los objetos, no especialmente los objetos de OOP. Por ejemplo, en C ++ , estamos acostumbrados a representar eso al agrupar punteros o contenedores de punteros en la estructura que necesita un acceso al otro objeto. Si un objeto Atiene que tener un acceso a B, no es raro encontrar una B *pBen A.

Pero ya no soy un programador de C ++ , escribo programas usando lenguajes funcionales, y más especialmente en Haskell , que es un lenguaje funcional puro. Es posible usar punteros, referencias o ese tipo de cosas, pero me siento extraño con eso, como "hacerlo de una manera que no sea de Haskell".

Luego pensé un poco más sobre todas esas cosas de relación y llegué al punto:

“¿Por qué representamos tal relación por capas?

Leí algunas personas que ya pensaron en eso ( aquí ). En mi punto de vista, representar las relaciones a través de gráficos explícitos es mucho mejor, ya que nos permite centrarnos en el núcleo de nuestro tipo y expresar relaciones más tarde a través de combinadores (un poco como lo hace SQL ).

Por núcleo quiero decir que cuando definimos A, esperamos definir de qué Aestá hecho , no de qué depende . Por ejemplo, en un juego de video, si tenemos un tipo Character, es legítimo hablar de Trait, Skillo ese tipo de cosas, pero es que si hablamos de Weapono Items? Ya no estoy tan seguro. Entonces:

data Character = {
    chSkills :: [Skill]
  , chTraits :: [Traits]
  , chName   :: String
  , chWeapon :: IORef Weapon -- or STRef, or whatever
  , chItems  :: IORef [Item] -- ditto
  }

me suena muy mal en términos de diseño. Prefiero algo como:

data Character = {
    chSkills :: [Skill]
  , chTraits :: [Traits]
  , chName   :: String
  }

-- link our character to a Weapon using a Graph Character Weapon
-- link our character to Items using a Graph Character [Item] or that kind of stuff

Además, cuando llega el día de agregar nuevas características, simplemente podemos crear nuevos tipos, nuevos gráficos y enlaces. En el primer diseño, tendríamos que romper el Charactertipo, o usar algún tipo de trabajo para extenderlo.

¿Qué opinas de esa idea? ¿Qué crees que es mejor tratar con ese tipo de problemas en Haskell , un lenguaje funcional puro?

Phaazon
fuente
2
La votación de opiniones no está permitida aquí. Debería reformular la pregunta a algo como "¿hay algún inconveniente en este enfoque?" en lugar de "¿qué opinas de esto?" o "¿cuál es la mejor manera de ...?" Para lo que sea que valga, no tiene sentido preguntar por qué las cosas se hacen de cierta manera en C ++ en lugar de hacerlo en lenguajes funcionales, porque C ++ carece de recolección de basura, cualquier cosa análoga a las clases de tipos (a menos que use plantillas, que abre una gran lata de gusanos), y muchas otras características que facilitan la programación funcional.
Doval
Una cosa que no vale nada es que, en su enfoque, no es posible decir usando el sistema de tipos si un Character debería ser capaz de sostener armas. Cuando se tiene un mapa a partir Charactersde Weaponsy un carácter está ausente del mapa, ¿significa que el carácter actualmente no contener un arma, o que el personaje no puede tener armas en absoluto? Además, estaría haciendo búsquedas innecesarias porque no sabe a priori si Characterpuede sostener un arma o no.
Doval
@Doval Si los personajes que no pueden tener armas eran una característica de tu juego, ¿no tendría sentido darle una marca al personaje canHoldWeapons :: Bool, que te permite saber de inmediato si puede sostener armas y luego si tu personaje no lo es? en el gráfico puedes decir que el personaje no tiene armas actualmente. Haga que giveCharacterWeapon :: WeaponGraph -> Character -> Weapon -> WeaponGraphactúe como idsi esa bandera fuera Falsepara el personaje provisto, o use Maybepara representar una falla.
bheklilr
Me gusta esto y, francamente, creo que lo que estás diseñando se adapta muy bien a un estilo aplicativo, que tiendo a encontrar común cuando veo problemas de modelado OO en Haskell. Los solicitantes le permiten componer comportamientos de estilo CRUD de manera agradable y fluida, por lo que la idea de modelar bits de forma independiente, luego tener algunas implementaciones aplicativas que le permiten componer esos tipos junto con operaciones que se deslizan entre las composiciones para cosas como validación, persistencia, evento reactivo disparando etc. Esto encaja mucho con lo que sugieres allí. Los gráficos funcionan muy bien con los solicitantes.
Jimmy Hoffa
3
Específicamente @Doval con tipos de datos de grabación de estilos, si usted tiene varios constructores el siguiente código es perfectamente legal y no emite advertencias con -Wallel GHC: data Foo = Foo { foo :: Int } | Bar { bar :: String }. Luego escribe cheques para llamar foo (Bar "")o bar (Foo 0), pero ambos generarán excepciones. Si está utilizando constructores de datos de estilo posicional, entonces no hay problema porque el compilador no genera los accesos por usted, pero no obtiene la conveniencia de los tipos de registro para estructuras grandes y se necesita más repetitivo para usar bibliotecas como lentes.
bheklilr

Respuestas:

2

En realidad has respondido tu propia pregunta, simplemente no lo sabes todavía. La pregunta que hace no es sobre Haskell sino sobre la programación en general. En realidad te estás haciendo muy buenas preguntas (felicitaciones).

Mi enfoque del problema que tiene a mano se divide básicamente en dos aspectos principales: el diseño del modelo de dominio y las compensaciones.

Dejame explicar.

Diseño del modelo de dominio : así es como decide organizar el modelo central de su aplicación. Puedo ver que ya lo estás haciendo al pensar en Personajes, Armas, etc. Además, debes definir las relaciones entre ellos. Su enfoque de definir objetos por lo que son en lugar de lo que dependen es totalmente válido. Sin embargo, tenga cuidado porque no es una bala de plata para cada decisión con respecto a su diseño.

Definitivamente estás haciendo lo correcto al pensar en esto de antemano, pero en algún momento debes dejar de pensar y comenzar a escribir algo de código. Verá entonces si sus primeras decisiones fueron las correctas o no. Lo más probable es que no, ya que aún no tiene un conocimiento completo de su aplicación. Entonces no tenga miedo de refactorizar cuando se dé cuenta de que ciertas decisiones se están convirtiendo en un problema, no en una solución. Es importante escribir código al pensar en esas decisiones para poder validarlas y evitar tener que volver a escribir todo.

Hay varios buenos principios que puede seguir, solo busque en Google "principios de software" y encontrará muchos de ellos.

Compromisos : todo es un compromiso. Por un lado, tener demasiadas dependencias es malo, sin embargo, tendrá que lidiar con la complejidad adicional de administrar dependencias en otro lugar en lugar de en sus objetos de dominio. No hay una decisión correcta. Si tiene un presentimiento, hágalo. Aprenderá mucho tomando ese camino y viendo el resultado.

Como dije, estás haciendo las preguntas correctas y eso es lo más importante. Personalmente, estoy de acuerdo con su razonamiento, pero parece que ya ha pasado demasiado tiempo pensando en ello. Simplemente deja de tener miedo de cometer un error y cometer errores . Son realmente valiosos y siempre puedes refactorizar tu código cuando lo desees.

Alex
fuente
Gracias por su respuesta. Desde que publiqué eso, tomé muchos caminos. Intenté AST, AST encuadernado, mónadas libres, ahora estoy tratando de representar todo a través de clases de tipos . De hecho, no hay una solución absoluta. Simplemente siento la necesidad de desafiar la forma común de hacer una relación, que para mí está lejos de ser robusta y flexible.
phaazon
@phaazon Eso es genial y desearía que más desarrolladores tuvieran un enfoque similar. Experimentar es la mejor herramienta de aprendizaje. Buena suerte con tu proyecto.
Alex