En esta serie de publicaciones de blog , Eric Lippert describe un problema en el diseño orientado a objetos utilizando magos y guerreros como ejemplos, donde:
abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }
abstract class Player
{
public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }
y luego agrega un par de reglas:
- Un guerrero solo puede usar una espada.
- Un asistente solo puede usar un bastón.
Luego continúa demostrando los problemas con los que se encuentra si intenta hacer cumplir estas reglas utilizando el sistema de tipo C # (por ejemplo, hacer que la Wizard
clase sea responsable de asegurarse de que un asistente solo pueda usar un bastón). Viola el Principio de sustitución de Liskov, arriesga excepciones de tiempo de ejecución o termina con un código que es difícil de extender.
La solución que se le ocurre es que la clase Player no realiza ninguna validación. Solo se usa para rastrear el estado. Luego, en lugar de darle un arma a un jugador:
player.Weapon = new Sword();
el estado se modifica por Command
sy según Rule
s:
... hacemos un
Command
objeto llamadoWield
que toma dos objetos de estado del juego, aPlayer
y aWeapon
. Cuando el usuario emite un comando para el sistema "este asistente debería empuñar esa espada", entonces ese comando se evalúa en el contexto de un conjunto deRule
s, que produce una secuencia deEffect
s. Tenemos unoRule
que dice que cuando un jugador intenta empuñar un arma, el efecto es que el arma existente, si la hay, se cae y la nueva arma se convierte en el arma del jugador. Tenemos otra regla que fortalece la primera regla, que dice que los efectos de la primera regla no se aplican cuando un mago intenta empuñar una espada.
En principio, me gusta esta idea, pero me preocupa cómo podría usarse en la práctica.
Nada parece impedir que un desarrollador se sustraigan a la Commands
y Rule
es simplemente fijando el Weapon
sobre una Player
. El comando Weapon
debe poder acceder a la propiedad Wield
, por lo que no se puede hacer private set
.
Por lo tanto, lo que hace evitar que un desarrollador de hacer esto? ¿Solo tienen que recordar no hacerlo?
Respuestas:
Todo el argumento al que conduce una serie de publicaciones de blog está en la Parte Cinco :
Las armas, los personajes, los monstruos y otros objetos del juego no son responsables de verificar lo que pueden o no pueden hacer. El sistema de reglas es responsable de eso. El
Command
objeto tampoco está haciendo nada con los objetos del juego. Simplemente representa el intento de hacer algo con ellos. El sistema de reglas luego verifica si el comando es posible, y cuando lo es, ejecuta el comando llamando a los métodos apropiados en los objetos del juego.Si un desarrollador quiere crear un segundo sistema de reglas que haga cosas con personajes y armas que el primer sistema de reglas no permitiría, puede hacerlo porque en C # no puede (sin los trucos de reflexión desagradables) averiguar de dónde proviene una llamada al método desde.
Una solución alternativa que podría funcionar en algunas situaciones es colocar los objetos del juego (o sus interfaces) en un ensamblaje con el motor de reglas y marcar los métodos mutadores como
internal
. Cualquier sistema que necesite acceso de solo lectura a los objetos del juego estaría en un ensamblaje diferente, lo que significa que solo podrían acceder a lospublic
métodos. Esto todavía deja la laguna de los objetos del juego que se llaman métodos internos de cada uno. Pero hacer eso sería un olor de código obvio, porque acordó que se supone que las clases de objetos del juego son titulares tontos.fuente
El problema obvio del código original es que está haciendo modelado de datos en lugar de modelado de objetos . ¡Tenga en cuenta que no hay absolutamente ninguna mención de los requisitos comerciales reales en el artículo vinculado!
Comenzaría tratando de obtener los requisitos funcionales reales. Por ejemplo: "Cualquier jugador puede atacar a cualquier otro jugador, ...". Aquí:
"Los jugadores pueden empuñar un arma que se usa en el ataque, los magos pueden empuñar un bastón, los guerreros una espada":
"Cada arma hace daño al enemigo atacado". Ok, ahora tenemos que tener una interfaz común para Arma:
Y así sucesivamente ... ¿Por qué no hay
Wield()
en elPlayer
? Porque no había ningún requisito de que ningún jugador pudiera empuñar ningún arma.Puedo imaginar que habría un requisito que diga: "Cualquiera
Player
puede tratar de manejar cualquierWeapon
". Sin embargo, esto sería algo completamente diferente. Tal vez lo modelaría de esta manera:Resumen: Modele los requisitos y solo los requisitos. No haga modelado de datos, eso no es demasiado modelado.
fuente
Una forma sería pasar el
Wield
comando aPlayer
. Luego, el jugador ejecuta elWield
comando, que verifica las reglas apropiadas y devuelve elWeapon
, quePlayer
luego establece su propio campo de Arma. De esta manera, el campo Arma puede tener un setter privado y solo se puede configurar pasando unWield
comando al jugador.fuente
Nada impide que el desarrollador haga eso. En realidad, Eric Lippert probó muchas técnicas diferentes, pero todas tenían debilidades. Ese fue el objetivo de esa serie de que impedir que el desarrollador lo haga no es fácil y todo lo que intentó tenía desventajas. Finalmente decidió que usar un
Command
objeto con reglas es el camino a seguir.Con las reglas, puedes establecer que la
Weapon
propiedad deWizard
a sea unSword
pero cuando le pidesWizard
que empuñe el arma (Espada) y ataque, no tendrá ningún efecto y, por lo tanto, no cambiará ningún estado. Como él dice a continuación:En otras palabras, no podemos hacer cumplir esa regla a través de
type
relaciones que él intentó de muchas maneras diferentes, pero que no le gustó o no funcionó. Por lo tanto, lo único que dijo que podemos hacer es hacer algo al respecto en tiempo de ejecución. Lanzar una excepción no fue bueno porque no lo considera una excepción.Finalmente eligió ir con la solución anterior. Esta solución básicamente dice que puede configurar cualquier arma, pero cuando la entrega, si no es el arma correcta, sería esencialmente inútil. Pero no se lanzaría ninguna excepción.
Creo que es una buena solución. Aunque en algunos casos también elegiría el patrón try-set.
fuente
This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.
No pude encontrarlo en esa serie, ¿podría indicarme dónde se propone esta solución?that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon
. Mientras que la segunda regla es,that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.
entonces, creo que hay una regla que comprueba si el arma es espada, por lo que no puede ser manejada por un hechicero, por lo que no está establecida. En cambio suena un triste trombón.Wield
aquí. Creo que es un nombre ligeramente engañoso para el comando. Algo asíChangeWeapon
sería más exacto. Supongo que podría tener un modelo diferente en el que puede configurar cualquier arma, pero cuando la entrega, si no es el arma correcta, sería esencialmente inútil . Eso suena interesante, pero no creo que sea lo que describe Eric Lippert.La primera solución descartada del autor fue representar las reglas por el sistema de tipos. El sistema de tipos se evalúa en tiempo de compilación. Si separa las reglas del sistema de tipos, el compilador ya no las verifica, por lo que no hay nada que impida que un desarrollador cometa un error per se.
Pero este problema se enfrenta a cada pieza de lógica / modelado que no es verificada por el compilador y la respuesta general a esto es la prueba (unidad). Por lo tanto, la solución propuesta por el autor necesita un arnés de prueba fuerte para evitar los errores de los desarrolladores. Para subrayar este punto de necesitar un arnés de prueba fuerte para errores que solo se detectan en tiempo de ejecución, mire este artículo de Bruce Eckel, que argumenta que necesita intercambiar el tipo fuerte para pruebas más fuertes en lenguajes dinámicos.
En conclusión, lo único que puede evitar que los desarrolladores cometan errores es tener un conjunto de pruebas (unitarias) que verifiquen que se cumplan todas las reglas.
fuente
Puede que me haya perdido una sutileza aquí, pero no estoy seguro de que el problema sea con el sistema de tipos. Tal vez sea con convención en C #.
Por ejemplo, puede hacer que este tipo sea completamente seguro haciendo que el
Weapon
setter esté protegidoPlayer
. Luego agreguesetSword(Sword)
ysetStaff(Staff)
aWarrior
yWizard
respectivamente que llaman al setter protegido.De esa manera, la relación
Player
/Weapon
se verifica estáticamente y el código que no le importa puede usar aPlayer
para obtener unWeapon
.fuente
Weapon
aPlayer
. Pero no existe un sistema de tipos en el que no conozca los tipos concretos en tiempo de compilación que pueda actuar sobre esos tipos concretos en tiempo de compilación. Por definición. Este esquema significa que es solo ese caso el que debe tratarse en tiempo de ejecución, como tal, en realidad es mejor que cualquiera de los esquemas de Eric.Esta pregunta es efectivamente la misma con el tema de la guerra santa llamado " dónde poner la validación " (muy probablemente señalando ddd también).
Entonces, antes de responder esta pregunta, uno debe preguntarse: ¿cuál es la naturaleza de las reglas que desea seguir? ¿Están tallados en piedra y definen la entidad? ¿El incumplimiento de esas reglas hace que una entidad deje de ser lo que es? En caso afirmativo, junto con mantener estas reglas en la validación de comandos , colóquelas también en una entidad. Entonces, si un desarrollador olvida validar el comando, sus entidades no estarán en un estado no válido.
Si no, bueno, implica inherentemente que estas reglas son específicas del comando y no deberían residir en entidades de dominio. Por lo tanto, violar estas reglas da como resultado acciones que no deberían haberse permitido, pero no en un estado de modelo no válido.
fuente