Estoy siguiendo esta pregunta , pero estoy cambiando mi enfoque del código a un principio.
Desde mi entendimiento del principio de sustitución de Liskov (LSP), cualquier método que esté en mi clase base, debe implementarse en mi subclase, y de acuerdo con esta página, si anula un método en la clase base y no hace nada o arroja un excepción, estás violando el principio.
Ahora, mi problema puede resumirse así: tengo un resumen Weapon
class
, y dos clases, Sword
y Reloadable
. Si Reloadable
contiene un específico method
, llamado Reload()
, tendría que bajar para acceder a eso method
, e idealmente, querrás evitarlo.
Entonces pensé en usar el Strategy Pattern
. De esta manera, cada arma solo era consciente de las acciones que es capaz de realizar, por lo que, por ejemplo, un Reloadable
arma, obviamente, puede recargarse, pero Sword
no puede, y ni siquiera es consciente de a Reload class/method
. Como dije en mi publicación de Stack Overflow, no tengo que abatir y puedo mantener una List<Weapon>
colección.
En otro foro , la primera respuesta sugerida Sword
para ser consciente Reload
, simplemente no haga nada. Esta misma respuesta se dio en la página de desbordamiento de pila a la que he vinculado anteriormente.
No entiendo completamente por qué. ¿Por qué violar el principio y permitir que Sword sea consciente Reload
y dejarlo en blanco? Como dije en mi publicación Stack Overflow, el SP resolvió mis problemas.
¿Por qué no es una solución viable?
public final Weapon{
private final String name;
private final int damage;
private final List<AttackStrategy> validactions;
private final List<Actions> standardActions;
private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
{
this.name = name;
this.damage = damage;
standardActions = new ArrayList<Actions>(standardActions);
validAttacks = new ArrayList<AttackStrategy>(validActions);
}
public void standardAction(String action){} // -- Can call reload or aim here.
public int attack(String action){} // - Call any actions that are attacks.
public static Weapon Sword(String name, damage, List<AttackStrategy> standardActions, List<Actions> attacks){
return new Weapon(name, damage,standardActions, attacks) ;
}
}
Interfaz de ataque e implementación:
public interface AttackStrategy{
void attack(Enemy enemy);
}
public class Shoot implements AttackStrategy {
public void attack(Enemy enemy){
//code to shoot
}
}
public class Strike implements AttackStrategy {
public void attack(Enemy enemy){
//code to strike
}
}
class Weapon { bool supportsReload(); void reload(); }
. Los clientes probarían si son compatibles antes de volver a cargar.reload
se define contractualmente para lanzar iff!supportsReload()
. Eso se adhiere al LSP si las clases derivadas se adhieren al protocolo que acabo de describir.reload()
blanco o sistandardActions
no contiene una acción de recarga es solo un mecanismo diferente. No hay diferencia fundamental. Puedes hacer las dos cosas. => Su solución es viable (que era su pregunta) .; Sword no necesita saber sobre la recarga si Arma contiene una implementación predeterminada en blanco.Respuestas:
El LSP está preocupado por el subtipo y el polimorfismo. No todo el código realmente usa estas características, en cuyo caso el LSP es irrelevante. Dos casos de uso común de construcciones de lenguaje de herencia que no son un caso de subtipo son:
La herencia solía heredar la implementación de una clase base, pero no su interfaz. En casi todos los casos se debe preferir la composición. Lenguajes como Java no pueden separar la herencia de la implementación y la interfaz, pero, por ejemplo, C ++ tiene
private
herencia.Herencia utilizada para modelar un tipo / unión de suma, por ejemplo: a
Base
es unoCaseA
oCaseB
. El tipo base no declara ninguna interfaz relevante. Para usar sus instancias, debe convertirlas al tipo concreto correcto. El casting se puede hacer de forma segura y no es el problema. Desafortunadamente, muchos lenguajes OOP no pueden restringir los subtipos de clase base solo a los subtipos previstos. Si el código externo puede crear unCaseC
, entonces el código supone que aBase
solo puede ser aCaseA
oCaseB
es incorrecto. Scala puede hacer esto de forma segura con sucase class
concepto. En Java, esto se puede modelar cuandoBase
es una clase abstracta con un constructor privado, y las clases estáticas anidadas se heredan de la base.Algunos conceptos como las jerarquías conceptuales de objetos del mundo real se mapean muy mal en modelos orientados a objetos. Pensamientos como “Una pistola es un arma y una espada es un arma, por lo tanto, voy a tener una
Weapon
clase base desde la cualGun
ySword
hereda” puedan inducir a error: Real-palabra es-a relaciones no implican una relación tal en nuestro modelo. Un problema relacionado es que los objetos pueden pertenecer a múltiples jerarquías conceptuales o pueden cambiar su afiliación jerárquica durante el tiempo de ejecución, que la mayoría de los lenguajes no pueden modelar ya que la herencia generalmente es por clase, no por objeto, y se define en tiempo de diseño, no en tiempo de ejecución.Al diseñar modelos OOP, no debemos pensar en la jerarquía o en cómo una clase "extiende" a otra. Una clase base no es un lugar para factorizar las partes comunes de múltiples clases. En cambio, piense en cómo se usarán sus objetos, es decir, qué tipo de comportamiento necesitan los usuarios de estos objetos.
Aquí, los usuarios pueden necesitar
attack()
armas y tal vezreload()
ellas. Si queremos crear una jerarquía de tipos, ambos métodos deben estar en el tipo base, aunque las armas no recargables pueden ignorar ese método y no hacer nada cuando se les llama. Entonces, la clase base no contiene las partes comunes, sino la interfaz combinada de todas las subclases. Las subclases no difieren en su interfaz, sino solo en su implementación de esta interfaz.No es necesario crear una jerarquía. Los dos tipos
Gun
ySword
pueden ser completamente ajenos. Mientras que unaGun
latafire()
yreload()
unSword
solo puedestrike()
. Si necesita administrar estos objetos polimórficamente, puede usar el patrón adaptador para capturar los aspectos relevantes. En Java 8 esto es posible de manera bastante conveniente con interfaces funcionales y referencias lambdas / método. Por ejemplo, puede tener unaAttack
estrategia para la que suministramyGun::fire
o() -> mySword.strike()
.Finalmente, a veces es sensato evitar las subclases, pero modelar todos los objetos a través de un solo tipo. Esto es particularmente relevante en los juegos porque muchos objetos del juego no encajan bien en ninguna jerarquía y pueden tener muchas capacidades diferentes. Por ejemplo, un juego de rol puede tener un elemento que es a la vez un elemento de búsqueda, mejora tus estadísticas con +2 de fuerza cuando está equipado, tiene un 20% de posibilidades de ignorar cualquier daño recibido y proporciona un ataque cuerpo a cuerpo. O tal vez una espada recargable porque es * mágica *. Quién sabe lo que requiere la historia.
En lugar de tratar de descubrir una jerarquía de clases para ese desastre, es mejor tener una clase que proporcione ranuras para diversas capacidades. Estas ranuras se pueden cambiar en tiempo de ejecución. Cada ranura sería una estrategia / devolución de llamada como
OnDamageReceived
oAttack
. Con sus armas, podemos tenerMeleeAttack
,RangedAttack
yReload
las franjas horarias. Estas ranuras pueden estar vacías, en cuyo caso el objeto no proporciona esta capacidad. Las ranuras son entonces llamados condicionalmente:if (item.attack != null) item.attack.perform()
.fuente
Porque tener una estrategia
attack
no es suficiente para tus necesidades. Claro, te permite abstraer qué acciones puede hacer el objeto, pero ¿qué sucede cuando necesitas saber el alcance del arma? ¿O la capacidad de munición? ¿O qué tipo de munición se necesita? Has vuelto a abatir para llegar a eso. Y tener ese nivel de flexibilidad hará que la interfaz de usuario sea un poco más difícil de implementar, ya que necesitará tener un patrón de estrategia similar para manejar todas las capacidades.Dicho todo esto, no estoy particularmente de acuerdo con las respuestas a sus otras preguntas. Haber
sword
heredadoweapon
es horrible, ingenua OO que invariablemente conduce a métodos no operativos o verificaciones de tipo esparcidas sobre el código.Pero en la raíz del asunto, ninguna de las soluciones está mal . Puedes usar ambas soluciones para crear un juego funcional que sea divertido de jugar. Cada uno viene con su propio conjunto de compensaciones, al igual que cualquier solución que elija.
fuente
Weapon
clase con una instancia de espada y arma.WeaponBuilder
que podría construir espadas y armas componiendo un arma de estrategias.Por supuesto, es una solución viable; Es solo una muy mala idea.
El problema no es si tiene esta única instancia donde coloca la recarga en su clase base. El problema es que también necesitas poner el "swing", "shoot", "parry", "knock", "polaco", "desmontar", "afilar" y "reemplazar los clavos del extremo puntiagudo del palo". método en su clase base.
El punto de LSP es que sus algoritmos de nivel superior deben funcionar y tener sentido. Entonces, si tengo un código como este:
Ahora, si eso arroja una excepción no implementada y hace que su programa se bloquee, entonces es una muy mala idea.
Si su código se ve así,
entonces su código puede estar repleto de propiedades muy específicas que no tienen nada que ver con la idea abstracta de 'arma'.
Sin embargo, si está implementando un tirador en primera persona y todas sus armas pueden disparar / recargar, excepto ese cuchillo, entonces (en su contexto específico) tiene mucho sentido que la recarga de su cuchillo no haga nada, ya que esa es la excepción y las probabilidades de tener su clase base desordenada con propiedades específicas es baja.
Actualización: intente pensar en el caso / términos abstractos. Por ejemplo, tal vez cada arma tiene una acción de "preparación" que es una recarga para armas y una funda para espadas.
fuente
Obviamente, está bien si no crea una subclase con la intención de sustituir una instancia de la clase base, pero si crea una subclase utilizando la clase base como un repositorio conveniente de funcionalidad.
Ahora, si esa es una buena idea o no, es muy discutible, pero si nunca sustituyes la subclase por la clase base, entonces el hecho de que no funcione no es un problema. Puede tener problemas, pero LSP no es el problema en este caso.
fuente
El LSP es bueno porque permite que el código de llamada no se preocupe por cómo funciona la clase.
p.ej. Puedo llamar a Weapon.Attack () en todas las armas montadas en mi BattleMech y no preocuparme de que algunas de ellas puedan arrojar una excepción y bloquear mi juego.
Ahora, en su caso, desea ampliar su tipo base con una nueva funcionalidad. Attack () no es un problema, porque la clase Gun puede hacer un seguimiento de su munición y dejar de disparar cuando se agota. Pero Reload () es algo nuevo y no forma parte de ser un arma.
La solución fácil es bajar, no creo que deba preocuparse demasiado por el rendimiento, no lo hará en cada fotograma.
Alternativamente, puede reevaluar su arquitectura y considerar que, en resumen, todas las armas son recargables, y algunas armas simplemente no necesitan recargarse.
Entonces ya no estás extendiendo la clase para armas, o estás violando el LSP.
Pero es problemático a largo plazo porque está obligado a pensar en casos más especiales, Gun.SafteyOn (), Sword.WipeOffBlood (), etc. y si los pone todos en Arma, entonces tiene una clase base generalizada muy complicada que mantiene tener que cambiar
editar: por qué el patrón de estrategia es malo (tm)
No lo es, pero tenga en cuenta la configuración, el rendimiento y el código general.
Tengo que tener alguna configuración en algún lugar que me diga que una pistola puede recargarse. Cuando crea una instancia de un arma, tengo que leer esa configuración y agregar dinámicamente todos los métodos, verificar que no haya nombres duplicados, etc.
Cuando llamo a un método, tengo que recorrer esa lista de acciones y hacer una coincidencia de cadena para ver a qué llamar.
Cuando compilo el código y llamo Weapon.Do ("atack") en lugar de "ataque", no obtendré un error en la compilación.
Puede ser una solución adecuada para algunos problemas, digamos que tiene cientos de armas, todas con diferentes combinaciones de métodos aleatorios, pero pierde muchos de los beneficios de OO y un tipeo fuerte. Realmente no te ahorra nada por abatir
fuente
SafteyOn()
ySword
tendríawipeOffBlood()
. Cada arma no es consciente de los otros métodos (y no deberían serlo)weapon.do("attack")
y el tipo seguroweapon.attack.perform()
pueden ser ejemplos del patrón de estrategia. Buscar estrategias por nombre solo es necesario al configurar el objeto desde un archivo de configuración, aunque el uso de la reflexión sería igualmente seguro para los tipos.