Je poursuis sur cette question , mais je passe mon attention du code à un principe.
D'après ma compréhension du principe de substitution de Liskov (LSP), quelles que soient les méthodes de ma classe de base, elles doivent être implémentées dans ma sous-classe, et selon cette page, si vous remplacez une méthode de la classe de base et qu'elle ne fait rien ou lance un exception, vous êtes en violation du principe.
Maintenant, mon problème peut être résumé comme ceci: j'ai un résumé Weapon
class
, et deux classes, Sword
et Reloadable
. Si Reloadable
contient un spécifique method
, appelé Reload()
, je devrais abattre pour y accéder method
, et, idéalement, vous voudriez éviter cela.
J'ai alors pensé à utiliser le Strategy Pattern
. De cette façon, chaque arme n'était consciente que des actions qu'elle est capable d'accomplir, donc par exemple, une Reloadable
arme peut évidemment se recharger, mais un Sword
ne peut pas, et n'est même pas au courant d'un Reload class/method
. Comme je l'ai indiqué dans mon article Stack Overflow, je n'ai pas à abattre et je peux maintenir une List<Weapon>
collection.
Sur un autre forum , la première réponse a suggéré de permettre Sword
d'être au courant Reload
, ne faites rien. Cette même réponse a été donnée sur la page Stack Overflow que j'ai liée à ci-dessus.
Je ne comprends pas vraiment pourquoi. Pourquoi violer le principe et permettre à Sword d'en être conscient Reload
et de le laisser vide? Comme je l'ai dit dans mon article sur Stack Overflow, le SP a à peu près résolu mes problèmes.
Pourquoi n'est-ce pas une solution 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) ;
}
}
Interface d'attaque et implémentation:
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(); }
. Les clients testeraient si pris en charge avant le rechargement.reload
est défini contractuellement pour lancer ssi!supportsReload()
. Cela adhère au LSP si les classes conduites ne respectent pas le protocole que je viens de décrire.reload()
vide oustandardActions
ne contienne pas d'action de rechargement est juste un mécanisme différent. Il n'y a pas de différence fondamentale. Vous pouvez faire les deux. => Votre solution est viable (ce qui était votre question) .; Sword n'a pas besoin de connaître le rechargement si Weapon contient une implémentation par défaut vide.Réponses:
Le LSP se préoccupe du sous-typage et du polymorphisme. Tout le code n'utilise pas réellement ces fonctionnalités, auquel cas le LSP n'est pas pertinent. Deux cas d'utilisation courants des constructions de langage d'héritage qui ne sont pas un cas de sous-typage sont:
Héritage utilisé pour hériter de l'implémentation d'une classe de base, mais pas de son interface. Dans presque tous les cas, la composition doit être préférée. Des langages comme Java ne peuvent pas séparer l'héritage de l'implémentation et de l'interface, mais par exemple C ++ a l'
private
héritage.Héritage utilisé pour modéliser un type / union de somme, par exemple: a
Base
est soitCaseA
ouCaseB
. Le type de base ne déclare aucune interface pertinente. Pour utiliser ses instances, vous devez les couler dans le type de béton approprié. Le casting peut être fait en toute sécurité et n'est pas le problème. Malheureusement, de nombreux langages POO ne sont pas en mesure de restreindre les sous-types de classe de base aux seuls sous-types prévus. Si le code externe peut créer unCaseC
, alors le code en supposant que aBase
ne peut être qu'unCaseA
ouCaseB
est incorrect. Scala peut le faire en toute sécurité avec soncase class
concept. En Java, cela peut être modélisé lorsque laBase
est une classe abstraite avec un constructeur privé, et les classes statiques imbriquées héritent ensuite de la base.Certains concepts comme les hiérarchies conceptuelles d'objets du monde réel sont très mal mappés dans des modèles orientés objet. Pensées comme « arme A est une arme, et une épée est une arme, donc je vais avoir une
Weapon
classe de base à partir de laquelleGun
etSword
inherit » induisent en erreur: mot réel est-une relation ne signifie pas une telle relation dans notre modèle. Un problème connexe est que les objets peuvent appartenir à plusieurs hiérarchies conceptuelles ou peuvent changer leur affiliation de hiérarchie pendant l'exécution, que la plupart des langages ne peuvent pas modéliser car l'héritage est généralement par classe et non par objet, et défini au moment de la conception et non au moment de l'exécution.Lors de la conception de modèles POO, nous ne devons pas penser à la hiérarchie ni à la manière dont une classe «étend» une autre. Une classe de base n'est pas un endroit pour prendre en compte les parties communes de plusieurs classes. Pensez plutôt à la façon dont vos objets seront utilisés, c'est-à-dire au type de comportement dont les utilisateurs de ces objets ont besoin.
Ici, les utilisateurs peuvent avoir besoin d'
attack()
armes et peut-être d'reload()
eux. Si nous voulons créer une hiérarchie de types, alors ces deux méthodes doivent être dans le type de base, bien que les armes non rechargeables puissent ignorer cette méthode et ne rien faire lorsqu'elles sont appelées. La classe de base ne contient donc pas les parties communes, mais l'interface combinée de toutes les sous-classes. Les sous-classes ne diffèrent pas dans leur interface, mais uniquement dans leur implémentation de cette interface.Il n'est pas nécessaire de créer une hiérarchie. Les deux types
Gun
etSword
peuvent être totalement indépendants. Alors qu'unGun
bidonfire()
etreload()
unSword
seul peutstrike()
. Si vous devez gérer ces objets de manière polymorphe, vous pouvez utiliser le modèle d'adaptateur pour capturer les aspects pertinents. Dans Java 8, cela est possible de manière assez pratique avec des interfaces fonctionnelles et des références lambdas / méthode. Par exemple, vous pourriez avoir uneAttack
stratégie pour laquelle vous fournissezmyGun::fire
ou() -> mySword.strike()
.Enfin, il est parfois judicieux d'éviter toute sous-classe, mais modélisez tous les objets via un seul type. Ceci est particulièrement pertinent dans les jeux car de nombreux objets de jeu ne s'intègrent pas bien dans une hiérarchie et peuvent avoir de nombreuses capacités différentes. Par exemple, un jeu de rôle peut avoir un objet qui est à la fois un objet de quête, améliore vos statistiques avec une force de +2 lorsqu'il est équipé, a 20% de chances d'ignorer les dégâts reçus et fournit une attaque de mêlée. Ou peut-être une épée rechargeable car c'est * magique *. Qui sait ce que l'histoire requiert.
Au lieu d'essayer de comprendre une hiérarchie de classes pour ce gâchis, il est préférable d'avoir une classe qui fournit des emplacements pour diverses capacités. Ces emplacements peuvent être modifiés au moment de l'exécution. Chaque emplacement serait une stratégie / rappel comme
OnDamageReceived
ouAttack
. Avec vos armes, nous pouvons avoirMeleeAttack
,RangedAttack
etReload
machines à sous. Ces emplacements peuvent être vides, auquel cas l'objet ne fournit pas cette capacité. Les créneaux horaires sont alors appelés sous conditions:if (item.attack != null) item.attack.perform()
.la source
Parce qu'avoir une stratégie pour
attack
ne suffit pas à vos besoins. Bien sûr, cela vous permet d'abstraire les actions que l'objet peut faire, mais que se passe-t-il lorsque vous avez besoin de connaître la portée de l'arme? Ou la capacité en munitions? Ou quelle sorte de munitions cela prend? Vous êtes de retour au downcasting pour y arriver. Et avoir ce niveau de flexibilité rendra l'interface utilisateur un peu plus difficile à mettre en œuvre, car elle devra avoir un modèle de stratégie similaire pour gérer toutes les capacités.Cela dit, je ne suis pas particulièrement d'accord avec les réponses à vos autres questions. Ayant
sword
Hériterweapon
est horrible, naïve OO qui invariablement conduit à des méthodes non-op ou contrôles de type jonché sur le code.Mais à l'origine du problème, aucune des deux solutions n'est fausse . Vous pouvez utiliser les deux solutions pour créer un jeu fonctionnel et amusant à jouer. Chacun vient avec son propre ensemble de compromis, comme toute solution que vous choisissez.
la source
Weapon
classe avec une instance d'épée et de pistolet.WeaponBuilder
qui pourrait construire des épées et des fusils en composant une arme de stratégies.Bien sûr, c'est une solution viable; c'est juste une très mauvaise idée.
Le problème n'est pas si vous avez cette seule instance où vous mettez reload sur votre classe de base. Le problème est que vous devez également mettre le "swing", "shoot" "parry", "knock", "polish", "démonter", "sharpen", and "replace the nailes of the pointy end of the club" " sur votre classe de base.
Le point de LSP est que vos algorithmes de haut niveau doivent fonctionner et avoir du sens. Donc, si j'ai un code comme celui-ci:
Maintenant, si cela lève une exception non implémentée et fait planter votre programme, c'est une très mauvaise idée.
Si votre code ressemble à ceci,
alors votre code peut devenir encombré de propriétés très spécifiques qui n'ont rien à voir avec l'idée abstraite d '«arme».
Cependant, si vous implémentez un jeu de tir à la première personne et que toutes vos armes peuvent tirer / recharger sauf qu'un couteau, alors (dans votre contexte spécifique), il est très logique que le rechargement de votre couteau ne fasse rien, car c'est l'exception et les chances d'avoir votre classe de base encombrée de propriétés spécifiques est faible.
Mise à jour: Essayez de penser au cas / termes abstraits. Par exemple, peut-être que chaque arme a une action de "préparation" qui est une recharge pour les fusils et une gaine pour les épées.
la source
Évidemment, c'est OK si vous ne créez pas de sous-classe avec l'intention de remplacer une instance de la classe de base, mais si vous créez une sous-classe en utilisant la classe de base comme référentiel pratique de fonctionnalités.
Maintenant, que ce soit une bonne idée ou non est très discutable, mais si vous ne substituez jamais la sous-classe à la classe de base, le fait que cela ne fonctionne pas ne pose aucun problème. Vous pouvez avoir des problèmes, mais LSP n'est pas le problème dans ce cas.
la source
Le LSP est bon car il permet au code appelant de ne pas se soucier du fonctionnement de la classe.
par exemple. Je peux appeler Weapon.Attack () sur toutes les armes montées sur mon BattleMech et ne pas craindre que certaines d'entre elles puissent lever une exception et planter mon jeu.
Maintenant, dans votre cas, vous souhaitez étendre votre type de base avec de nouvelles fonctionnalités. Attack () n'est pas un problème, car la classe Gun peut garder une trace de ses munitions et arrêter de tirer quand elle est épuisée. Mais Reload () est quelque chose de nouveau et ne fait pas partie d'une arme.
La solution simple consiste à abattre, je ne pense pas que vous ayez à vous soucier excessivement des performances, vous n'allez pas le faire à chaque image.
Alternativement, vous pouvez réévaluer votre architecture et considérer que dans l'abstrait toutes les armes sont rechargeables, et certaines armes n'ont tout simplement jamais besoin d'être rechargées.
Ensuite, vous n'étendez plus la classe pour les armes à feu ou ne violez pas le LSP.
Mais c'est problématique à long terme parce que vous êtes obligé de penser à des cas plus spéciaux, Gun.SafteyOn (), Sword.WipeOffBlood () etc. et si vous les mettez tous dans Weapon, alors vous avez une classe de base généralisée super compliquée que vous gardez avoir à changer.
edit: pourquoi le modèle de stratégie est mauvais (tm)
Ce n'est pas le cas, mais considérez la configuration, les performances et le code global.
Je dois avoir une configuration quelque part qui me dit qu'un pistolet peut recharger. Lorsque j'instancie une arme, je dois lire cette configuration et ajouter dynamiquement toutes les méthodes, vérifier qu'il n'y a pas de noms en double, etc.
Lorsque j'appelle une méthode, je dois parcourir cette liste d'actions et faire une correspondance de chaîne pour voir laquelle appeler.
Lorsque je compile le code et appelle Weapon.Do ("atack") au lieu de "attack", je n'obtiendrai pas d'erreur lors de la compilation.
Cela peut être une solution appropriée pour certains problèmes, disons que vous avez des centaines d'armes, toutes avec différentes combinaisons de méthodes aléatoires, mais vous perdez beaucoup des avantages de l'OO et du typage fort. Cela ne vous sauve vraiment rien par rapport au downcasting
la source
SafteyOn()
etSword
auraitwipeOffBlood()
. Chaque arme n'est pas au courant des autres méthodes (et elles ne devraient pas l'être)weapon.do("attack")
et le type sûrweapon.attack.perform()
peuvent être des exemples du modèle de stratégie. La recherche de stratégies par nom n'est nécessaire que lors de la configuration de l'objet à partir d'un fichier de configuration, bien que l'utilisation de la réflexion soit tout aussi sûre pour le type.