Securing inherited item types in inherited classes

I try to adhere to good design principles and design design, etc. Therefore, developing this application in C #, I often can find several solutions for design and architecture, I always want to find and implement a more "canonical" one in the hope of creating highly supported and flexible software, as well as become the best OO programmer.

So, suppose I have two abstract classes: Character and Weapon .

 abstract class Weapon { public string Name { get; set; } } abstract class Character { public Weapon weapon { get; set; } } 

Derived classes can be Sword and Staff from Weapon and Warrior and Mage from Character , etc. (This is all hypothetically not related to my real software!).

Each Character has a Weapon , but for each Character implementation I know which Weapon implementation it will have. For example, I know (and I want to enforce!) That at run time each instance of Warrior will have a Weapon type Sword . Of course I could do this:

 class Sword : Weapon { public void Draw() { } } class Warrior : Character { public Warrior() { weapon = new Sword(); } } 

But so, every time I want to use my Weapon object correctly inside Warrior , I need to perform a throw, which, in my opinion, is not so good in practice. In addition, I have no means to stop me from getting spoiled, that is, there is no type safety!

An ideal solution would be to override the Weapon weapon property in the Warrior class with the Sword weapon property, so I have type security, and if the user uses my Warrior as Character , he can still use my Sword as Weapon . Unfortunately, C # seems to support this design.

So here are my questions: is this some kind of classic OO problem with a name and a well-documented solution? In this case, I would very much like to know the name of the problem and solution. Some links to good reading material would be very helpful! If not, what design class would you offer to maintain functionality and ensure type safety in an elegant and idiomatic way?

Thank you for reading!

+6
source share
5 answers

UPDATE: This question was the inspiration for a series of articles on my blog . Thanks for the interesting question!

Is this some kind of classic OO problem with a name and a well-documented solution?

The main problem you are facing is that it is difficult to add a subclass restriction in an OO system. Saying “a character can use a weapon” and then “a warrior is a kind of character that can only use a sword” is difficult to model in the OO system.

If you are interested in the theory behind this, you need to look for the "Liskov Substitution Principle." (As well as any discussion of why “Square” is not a subtype of “Rectangle” and vice versa).

However, there are ways to do this. Here is one.

 interface ICharacter { public IWeapon Weapon { get; } } interface IWeapon { } class Sword : IWeapon { } class Warrior : ICharacter { IWeapon ICharacter.Weapon { get { return this.Weapon; } } public Sword Weapon { get; private set; } } 

Now in the area of ​​the public surface of the Warrior there is a Weapon, which is always a Sword, but anyone who uses the ICharacter interface sees a Weapon property of the IWeapon type.

The key point here is the transition from the relationship "this is a special kind" and the transition to "can fulfill the contract." Instead of saying that "a warrior is a special character," say, "a warrior is a thing that can be used when a character is required."

All this suggests that you are embarking on a whole world of fascinating problems, where you will find how poor are the representations of these types of OO languages. You’ll quickly plunge into the dual-send problem and learn how to solve it with the visitor’s template as soon as you start asking more complex questions such as “what happens when a warrior uses a sharp sword to attack an orc in leather armor?” and find that you have four class hierarchies - character, weapon, monster, armor - all of which have subclasses that affect the outcome. Virtual methods allow you to send only one type of runtime, but you will find that you need two, three, four, or more levels of dispatching runtime.

This is becoming a mess. Consider not trying to capture too much in the type system.

+12
source

Just use a generics type constraint ! Here is an example

 public abstract class Weapon { public string Name { get; set; } } public interface ICharacter { Weapon GetWeapon(); } public interface ICharacter<out TWeapon> : ICharacter where TWeapon : Weapon { TWeapon Weapon { get; } // no set allowed here since TWeapon must be covariant } public abstract class Character<TWeapon> : ICharacter<TWeapon> where TWeapon : Weapon { public TWeapon Weapon { get; set; } public abstract void Fight(); public Weapon GetWeapon() { return this.Weapon; } } public class Sword : Weapon { public void DoSomethingWithSword() { } } public class Warrior : Character<Sword> { public override void Fight() { this.Weapon.DoSomethingWithSword(); } } 
+9
source

For documentation and future use, Im posting a solution that is likely to ultimately use:

 abstract class Weapon {} abstract class Character { public abstract Weapon GetWeapon(); } class Sword : Weapon {} class Warrior : Character { public Sword Sword { get; private set; } public override Weapon GetWeapon() { return Sword; } } 

As I can see, the only drawback is that objects of type Warrior have the Sword Sword property and the Weapon GetWeapon() method. In my case, this happens as a cosmetic problem only because both the property and the method return a reference to the same object when called.

Any comments are welcome!

+1
source

The problem is that “warriors can only use swords” the restriction is not something that needs to be modeled in a type system.

You have "characters are things that have weapons"

 abstract class Character { public Weapon weapon { get; set; } } 

but you really want to say something like "characters are things that have weapons, but with some limitation." I would suggest not having specific types, such as “warrior” and “sword” in general, but rather specific types should be instances:

 sealed class Weapon { public string Name { get; set; } } sealed class Character { const int sSkillToUseWeapon = 50; readonly Dictionary<string, int> mWeaponSkills = new Dictionary<string, int>(); Weapon mCurrentWeapon; public Weapon CurrentWeapon { get { return mCurrentWeapon; } set { if (!mWeaponSkills.ContainsKey(value.Name)) return; if (mWeaponSkills[value.Name] < mSkillToUseWeapon) return; mCurrentWeapon = value; } } public void SetWeaponSkill(string pWeaponName, int pSkill) { if (mWeaponSkills.ContainsKey(pWeaponName)) mWeaponSkills[pWeaponName] = pSkill; else mWeaponSkill.Add(pWeaponName, pSkill); } } 

Then you can declare instances of them for use and reuse:

 var warrior = new Character(); warrior.SetWeaponSkill("Sword", 100); warrior.SetWeaponSkill("Bow", 25); var archer = new Character(); archer.SetWeaponSkill("Bow", 125); archer.SetWeaponSkill("Sword", 25); archer.SetWeaponSkill("Dagger", 55); //etc... 

A Warrior is a case of a character with a specific set of weapon skills.

When you approach this path, another problem that you must solve is that there is a difference between, for example, "Rusty Sword" and "Rusty Sword". In other words, even if Rusty Sword is an instance of Weapons, there is still a difference between the general concept of Rusty Sword and a real rusty sword that lies on the ground. The same goes for Goblin or Warrior.

The terms I use for myself are "Stock____" and "Placed", so there is a difference between StockWeapon, which defines the "class" of weapons, and "Placed world", which is the actual "instance" of StockWeapon in the game world.

Placed objects then have the “presence” of instances of stock objects that determine their common attributes (base damage, base strength, etc.) and have other attributes that determine their current state regardless of other similar items (location, etc.)

( Here is a good site about using OOP templates for game programming. (Unfortunately, I forgot halfway through writing that game programming part was just an analogy. Nevertheless, I really assumed that, hopefully, the explanation remains the same.))

+1
source

I think you lack support in C # for the covariant return type . Note that you can see get properties as a special kind of method.

0
source

Source: https://habr.com/ru/post/982505/


All Articles