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 A
tiene que tener un acceso a B
, no es raro encontrar una B *pB
en 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é A
está hecho , no de qué depende . Por ejemplo, en un juego de video, si tenemos un tipo Character
, es legítimo hablar de Trait
, Skill
o ese tipo de cosas, pero es que si hablamos de Weapon
o 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 Character
tipo, 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?
fuente
Character
debería ser capaz de sostener armas. Cuando se tiene un mapa a partirCharacters
deWeapons
y 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 siCharacter
puede sostener un arma o no.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 quegiveCharacterWeapon :: WeaponGraph -> Character -> Weapon -> WeaponGraph
actúe comoid
si esa bandera fueraFalse
para el personaje provisto, o useMaybe
para representar una falla.-Wall
el GHC:data Foo = Foo { foo :: Int } | Bar { bar :: String }
. Luego escribe cheques para llamarfoo (Bar "")
obar (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.Respuestas:
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.
fuente