3

Before I explain my problem, keep in mind that I went for an architecture like this because this is going to be used in an inventory system under Unity, so I had to separate the Item which is a MonoBehavior which is just something in the world from its Data which are just values used for Inventory purposes... If that makes sense.

I have an architecture that goes like this:

public class ItemData
{
    // some fields...
    public string _name; //should be private with its properties but it doesn't matter in the current example
}

public class EquipmentData : ItemData
{
    // some additional fields...
    public float _weight;
}

public class Item
{
    private ItemData _data;

    //Properties
    public virtual ItemData Data { get; set; }
}

public class Equipment : Item
{
    //Properties
    public override ItemData Data 
    {
        get { return _data as EquipmentData; }
        set { _data = value as EquipmentData; }
    }
}

So, basically I have an item hierarchy that goes even deeper but 1 level is enough to explain myself. (It keeps going like Weapon : Equipment)...

The thing is, if I leave private ItemData _data; in Item class, and add a private EquipmentData _eData; in Equipment class, I will have the ItemData fields twice since EquipmentData inherits ItemData and so on for the other derived classes... getting it a third time if I have a class that derives from Equipment etc...

Something like:

public class Item
{
    private ItemData _data;
}

public class Equipment : item
{
    private EquipmentData _eData;
}

The fields of ItemData such as _name will appear twice in Equipment and I don't want that...

So I am guessing there is something wrong with my architecture, and there might be a way around this method that looks kind of "dirty", but I couldn't find anything specific for this problem online and I have reached my limits.

What I have tried:

  • I have tried using the keyword new in Equipment thinking I could hide the initial protected ItemData _data; that is in my Item class, and then have protected new EquipmentData _data; in Equipment but it obviously does not let me since to Shadow _data it needs to be the same type, doesn't seem to work with derived types.
  • Also, as shown in my code sample, I have tried overriding the property to return the proper type depending on the class it is called in, the casts always return null...

I still find it strange that I ended up trying to implement something like that, so I am open to new ideas to restructure things in a better way, or if someone has a solution I haven't thought of to keep things that way but make them work, it'd be very nice.

I hope I was detailed enough in my problem, if not I can clear things up if needed.

user3071284
  • 6,955
  • 6
  • 43
  • 57
Daemencer
  • 33
  • 5
  • what is the problem with your current design? why `private` itemdata instead of `protected`? – Nikhil Vartak Nov 19 '15 at 09:27
  • Because I access it through Properties anyways. I was trying to Override the properties to have the same field for every derived class, but return a casted version of that field from the properties by overriding properties in every derived classes. c.f. my code snippet The problem is, the cast always returns null because it does not succeed in casting an `EquipmentData` into an `ItemData`... – Daemencer Nov 19 '15 at 09:30
  • I guess you are doing opposite in overridden prop which will not work. See my response below. – Nikhil Vartak Nov 19 '15 at 10:02

3 Answers3

0

What You need is Generic Classes. By this, You can assign each Item its proper type of ItemData. So Equipment will have its EquipmentData assigned.

//TItemData is a name of this generic type. 
//It could be T for example, just like variable name. 
//These type names start from T by convention. 
//This is not a new class or something like that.
//where TItemData : ItemData is a constraint, 
//that assumes that this type should be a subtype of ItemData
public abstract class Item<TItemData> where TItemData : ItemData
{
    protected TItemData Data;
}

//EquipmentData is a subtype of ItemData, so it fits here. 
//No chances to write, for example, IntEquipment : Item<int> , 
//because int does not derive from ItemData.
public class Equipment : Item<EquipmentData>
{
    //here Data will be of EquipmentData type, without any casting.
}

By implementing the above You will achieve Type Safety.

EDIT

To make a class that properly extends Equipment (let's call it a Weapon), and has its proper ItemData (let's call it WeaponData), You need to write something like this:

Edit Equipment, and make it abstract:

public abstract class Equipment<TEquipmentData> 
: Item<TEquipmentData> 
//this constraint is VERY important, as EquipmentData derives from ItemData, thus fulfill Item<TItemData> constraint.
where TEquipmentData : EquipmentData
{
    //Data will have EquipmentData type here.
}

Create WeaponData

public WeaponData : EquipmentData
{
}

Create Weapon

public class Weapon : Equipment<WeaponData>
{
   //Data will have WeaponData type here.
}
Community
  • 1
  • 1
DreamOnJava
  • 635
  • 6
  • 12
  • Will try that. I just have a problem getting the difference between `TItemData` and `ItemData`, mind clearing that up for me please ? Thank you ! – Daemencer Nov 19 '15 at 09:57
  • Edited, did it clarify things a bit? – DreamOnJava Nov 19 '15 at 10:01
  • Oh yeah, just realized how stupid my question was... Thanks, will try Oh and for the sake of details, I guess we could point out that `ITemData` should be `ItemData`, no capital letter on the first T, but it doesn't change anything to your explaination, just a typo I guess. – Daemencer Nov 19 '15 at 10:02
  • Ok now, that's a bit of a syntaxe question, but I have a `Weapon` class that is derived from `Equipment`. I tried `public class Weapon : Equipment` which obviously does not work, but I can't find the proper syntax. It needs to derive from `Equipment` and not directly `Item`... Any advice ? – Daemencer Nov 19 '15 at 10:09
  • Nevermind, found the answer [here](http://stackoverflow.com/questions/29316762/c-sharp-how-to-specify-generic-type-constraints-for-multiple-level-of-inherita) – Daemencer Nov 19 '15 at 10:13
  • Done refactoring this, but now new problem... I had a method for my inventory that was: `public void AddItem(Item item, int amount = 1)` but now, when I try to call it from the `Item` script: `entity.GetComponent().AddItem(this);` it tells me that there are invalid arguments. I am guessing it's just a syntax problem, but I can find it... – Daemencer Nov 19 '15 at 10:21
  • Edited, added how to extend Equipment. – DreamOnJava Nov 19 '15 at 10:21
  • You can use an interface `IItemAccess`, implement it in `Item` class, and use it as an argument whenever You need `Item` object. So the method would look like this: `public void AddItem(IItemAccess item, int amount = 1)`. Of course interfaces can be generified in same way as classes. This solution is because Java permits `Item item;` while `Item` is generic `Item`, and C# **does not** permit it. – DreamOnJava Nov 19 '15 at 10:25
  • The edit you made is what I had tried, but I got stuck on the function call problem. And it's in C#, not sure it works exactly the same under the hood as Java :x I am trying out another solution someone proposed that would be much simpler for my current problem, and I will try generic classes if the problem remains. Thank you again for your time, will keep you up to date – Daemencer Nov 19 '15 at 10:33
  • Your function parameter won't work because there is no polymorphism between `Weapon : Equipment` and `Item`. You cannot cast `this` (now being `Weapon`) to `Item`. To solve it, use non-generic interfaces. And [here You have Your why](http://stackoverflow.com/questions/6557/in-c-why-cant-a-liststring-object-be-stored-in-a-listobject-variable?lq=1) :) . – DreamOnJava Nov 19 '15 at 10:37
  • So what you're suggesting is creating an interface such as `IItemAccess`, and make `Item : IItemAccess where TItemData : ItemData` that way ? But what exactly does IItemAccess do ? because if it implements a function returning an Item, the same problem will occurr in derived classes, I won't be able to cast a derived class into the base class :x Or maybe I missunderstood something ? or does it do absolutely nothing except making it possible to get anything inheriting Item simply because it's implementing IItemAccess ? – Daemencer Nov 19 '15 at 10:54
  • Yes, exactly that. You shouldn't be needing concrete types during `IItemAccess` usage (it depends, however). This interface should have normal methods as all other interfaces do... and maybe something like `ItemData GetItemData();`. Remember You can simply do more interfaces, extend them and implement in classes, that derive from `Item`. So `IWeaponAccess : IEquipmentAccess`, that derives from `IItemAccess`, for example. – DreamOnJava Nov 19 '15 at 10:58
  • Ok so I started doing what you suggested. And it seems to work so far for the Data, but I am stuck with the same problem now for the items.... Ugh I feel stupid but my brain refuses to think straight after all this haha So `public void AddItem(IItemAccess _item, int amount = 1)` is the method, and inside: `Item item = _item as Item;` because I want to get the real type Item to access its members and stuff, which makes sense, but this cast returns null, as always, casting the base class into derived :x – Daemencer Nov 19 '15 at 11:11
  • Your derived class here is `Weapon`. If `AddItem` adds something to list, then make it a list of interfaces. If You need a concrete type action outside of this type, it is mostly a code smell and this action should be done inside type. – DreamOnJava Nov 19 '15 at 11:19
  • I really need the `Item` as an `Item` and not as anything else, because I am accessing some of its members in the `AddItem` function :x – Daemencer Nov 19 '15 at 11:31
  • Properties and functions inside interface should do the trick. Yes, Properties can be abstract and be inside interfaces. Thus, these grant even field access. All that is missing is concrete `Item` type, which shouldn't be an issue considering You have proper interface. – DreamOnJava Nov 19 '15 at 11:34
  • Ok I haven't actually fixed my problem all together, but the problem I explained in the post was actually solved using your solution, and it's only another problem in my code that appeared afterward that stopped me from making it work, so I'll mark your answer as 'the' answer for the sake of the argument, and I'll work on the rest of my problem on my own. Thank you very much for your time and help !! – Daemencer Nov 20 '15 at 15:32
  • No problem. Just rely on interfaces while doing concrete job in concrete classes and You will be fine. If come to Your mind that above code is a little too complex for such trivial issue, then remember that You just have to write it once. Usage should be easy, if done properly. – DreamOnJava Nov 20 '15 at 15:47
0

This works:

public class OverridePropertiesWithSameField
{
    public void Test()
    {
        ChildItem ci = new ChildItem();
        ChildItemData cid = new ChildItemData();
        cid.ItemDataProp = "ItemDataProperty"; // Inherited
        cid.ChildItemDataProp = "ChildItemDataProp"; // Specific
        ci.ItemData = cid;

        // You know you need ChildItemData type here.
        var childItemData = ci.ItemData as ChildItemData;
        string itemDataProp = childItemData.ItemDataProp;
        string childItemDataProp = childItemData.ChildItemDataProp;
    }
}

public class Item
{
    protected ItemData data;
    public virtual ItemData ItemData { get; set; }
}

public class ChildItem : Item
{
    public override ItemData ItemData
    {
        get { return base.data; }
        set { base.data = value; }
    }
}

public class ItemData
{
    public string ItemDataProp { get; set; }
}

public class ChildItemData : ItemData
{
    public string ChildItemDataProp { get; set; }
}
Nikhil Vartak
  • 5,002
  • 3
  • 26
  • 32
  • Ok so I actually tried that, and the cast returns null again... Looks like this: `EquipmentData data = _data as EquipmentData; data.Type = ItemType.EType.Equipment;` (where `_data` is the inherited protected field that is in the `Item` class) but when I get to the second line where I try to use what's inside `data`, it's `null`... which kind of makes sense since it's not from a proper instance of `Item` but rather from the parent of the current `Equipment` (this code is in the Equipment script), but still I see no way out of this based on where I call these instructions... – Daemencer Nov 19 '15 at 10:35
  • To me, casting a Base type into a Derived type variable isn't allowed in C#, so your example should compile but not actually work. What do you think ? – Daemencer Nov 19 '15 at 10:49
0

You can use a generic type parameter, with a generic type constraint (where).

public class Item<DATA> where DATA : ItemData
{
    public virtual DATA Data { get; set; }
}

Now your class can use a specific ItemData:

Item<ItemData> has property 
public virtual ItemData Data { get; set; }

Item<EquipmentData> has property
public virtual EquipmentData Data { get; set; }

Item<ANOTHER> has property
public virtual ANOTHER Data { get; set; }
  • This is somewhat close to what has been suggested by @DreamOnJava but here you don't take into account the inheritance of my classes, and it's what I'm getting stuck on when trying to implement this solution. – Daemencer Nov 19 '15 at 10:19
  • It seemed to me you don't really need to inherit the property, just alter it's type. Type parameter and constraint are the missing ingredients. –  Nov 19 '15 at 10:22
  • If by Alter the type you mean changing the return type of the Property, I tried, it does not let me do it. I also tried keeping the same return type and casting the returned variable in the get {} but sadly, that's where I get my `null` from... :x – Daemencer Nov 19 '15 at 10:37
  • No, I mean use a Type parameter so the property has the type you need. –  Nov 19 '15 at 10:39
  • Oh yeah, so yeah it does look like the answer from dreamonjava, I will try that and let you guys know, thanks for your time man – Daemencer Nov 19 '15 at 10:43