Question

I'm looking for a way to create a generic base class that has a typesafe taxonomy using internal properties. Just to be clear, the class doesn't have to use the generics language feature as long as it is generic itself and I'm looking for something that has compile-time type safety.

As an example here is a simple taxonomy I want to represent using multiple instances of the same class

Wood
  Crate
  Box
Metal
  Crate
  Bar

The permutations of which are

Wood Crate
Wood Box
Metal Crate
Metal Bar

initially I though I could use enums to represent the different levels of taxonomy like so

public enum EFirstLevel
{
    Wood,
    Metal
}

public enum ESecondLevel
{
    Crate,
    Box,
    Bar
}


public class BaseItem
{
    EFirstLevel FirstLevel;
    ESecondLevel SecondLevel;

    public BaseItem(EFirstLevel aFirst, ESecondLevel aSecond)
    {
        FirstLevel = aFirst;
        SecondLevel = aSecond;
    }
}

I could create the items above using:

var item1 = new BaseItem(EFirstLevel.Wood,ESecondLevel.Crate)
var item2 = new BaseItem(EFirstLevel.Wood,ESecondLevel.Box)
var item3 = new BaseItem(EFirstLevel.Metal,ESecondLevel.Crate)
var item4 = new BaseItem(EFirstLevel.Metal,ESecondLevel.Bar)

but I could also create

var item5 = new BaseItem(EFirstLevel.Wood,ESecondLevel.Bar)

which for my purposes is incorrect.

Do any of you know of a pattern that would let me create a single class to represent the example taxonomy in a type-safe way that prohibits the creation of incorrect combinations.

It also needs to be applicable to N levels of taxonomy, the 2 levels above are just an example.

Thank you


Update: I do require compile-time type safety. I could do this with multiple classes quite easily using inheritance and such, I'm trying to find a solution using instances of just a single base class.

let me know if you need any more info


Update: @Maarten Yes, i'm trying to sure that the hierarchy is maintained so if EFirstLevel is 1 then ESecondLevel must be either Crate or Box.

Just to clairify i'm happy to have other supporting classes, what i'm trying to avoid is having to explicitly create a class for each taxanomic value.

What I'm trying to accomplish is providing an example layout of class that that maintains this taxanomic type safety so I can reflect over it and permute combinations. While maintaining the type safety should I need to generically instantiate said permutations.

The class upon which I might reflect could come form a third party and as such I might not know beforehand the values for each level. I could generate all the possible combinations into a set of classes with type safe internal enums but this would require regeneration of said classes any time you changed the items in any level. I was just wondering if there was a was to achieve my goals without having to generate any classes.


EDIT: Moved this section to an answer

Was it helpful?

Solution 4

Think I've found what I'm looking for @Iridium had an answer close to what I think is going to be my solution, however rather than having to define each item as a class I think I've found a way to maintain the type safety and still be able to create the items as properties of a single base class.

As in @Iridium's answer it does require the creation of linked classes defining the taxonomic relationships.

Instead of using interfaces I remembered an SO answer I found a long time ago about pseudo enum inheritance with a protected constructor Question is Here see the answer by "Seven"

If I define 2 base classes on which I can base the taxonomic chaining classes

public class ChainEnum
{
    public int IntValue { get; protected set; }

    public static readonly ChainEnum None = new ChainEnum(1);

    protected ChainEnum(int internalValue)
    {
        this.IntValue = internalValue;
    }
}

public class ChainLinkEnum<TParent> : ChainEnum where TParent : ChainEnum
{
    public TParent Parent { get; protected set; }

    protected ChainLinkEnum(int internalValue, TParent aParent)
        : base(internalValue)
    {
        Parent = aParent;
    }
}

I can then use these to chain as many levels deep as needed (for very deep trees this may not be ideal)

The first level inherits from the chain enum with no parent

public class HEBaseMaterial : ChainEnum
{
    public static readonly HEBaseMaterial Wood = new HEBaseMaterial(1);
    public static readonly HEBaseMaterial Metal = new HEBaseMaterial(1);

    protected HEBaseMaterial(int internalValue) : base(internalValue) { }
}

Subsequent levels inherit from the chain link enum which defines a parent

public class HEWoodItemTypes : ChainLinkEnum<HEBaseMaterial>
{
    private static readonly HEBaseMaterial InternalParent = HEBaseMaterial.Wood;

    public static readonly HEWoodItemTypes Box = new HEWoodItemTypes(1);
    public static readonly HEWoodItemTypes Crate = new HEWoodItemTypes(1);

    protected HEWoodItemTypes(int internalValue) : base(internalValue, InternalParent)
    { }
}

public class HEMetalItemTypes : ChainLinkEnum<HEBaseMaterial>
{
    private static readonly HEBaseMaterial InternalParent = HEBaseMaterial.Metal;

    public static readonly HEMetalItemTypes Box = new HEMetalItemTypes(1);
    public static readonly HEMetalItemTypes Bar = new HEMetalItemTypes(1);

    protected HEMetalItemTypes(int internalValue) : base(internalValue, InternalParent) { }
}

A third level would use a signature like

public class HEThirdLevelType : ChainLinkEnum<HEWoodItemTypes>

After that set-up I can then define my single base item class like:

public class TwoLevelItem<T1,T2>
    where T1 : ChainEnum
    where T2 : ChainLinkEnum<T1>
{
    public T1 LevelOne { get; set; }
    public T2 LevelTwo { get; set; }
}

or if I wanted an item with 5 levels of taxonomy where each is linked to the one before

  public class FiveLevelItem<T1,T2>
        where T1 : ChainEnum
        where T2 : ChainLinkEnum<T1>
        where T3 : ChainLinkEnum<T2>
        where T4 : ChainLinkEnum<T3>
        where T5 : ChainLinkEnum<T4>

    {
        public T1 LevelOne { get; set; }
        public T2 LevelTwo { get; set; }
        public T3 LevelThree { get; set; }
        public T4 LevelFour { get; set; }
        public T5 LevelFive { get; set; }
    }

or 3 properties with one first level and 2 second levels both linked to the first

public class LinkedItem<T1,T2_1,T2_2>
    where T1 : ChainEnum
    where T2_1 : ChainLinkEnum<T1>
    where T2_2 : ChainLinkEnum<T1>
{
    public T1 LevelOne { get; set; }
    public T2_1 LevelTwoOne { get; set; }
    public T2_2 LevelTwoTwo { get; set; }
}

Once the single base class is defined, i can reflect over it and the chain enums to get the permutations.

each item is created as a property

var metalBox = new TwoLevelItem<HEBaseMaterial,HEMetalItemTypes>()
{
    LevelOne = HEBaseMaterial.Metal,
    LevelTwo = HEMetalItemTypes.Box
}

This maintains the type safety and means that I can new properties to a taxonomy level and not have to create classes for items(although I do have to generate the extra items as properties)

This seems to do all i wanted but i've yet to try it extensively.

@Iridium's answer was close but not quite what I was looking for, although it did help.

OTHER TIPS

I don't think you're going to be able to get away without creating classes/interfaces and having compile-time checks that that objects conform to your taxonomy.

I'd suggest a solution as follows:

// Define the taxonomic levels here. Each level (except the first) references its next-higher taxonomic level in a type constraint
interface Material { }
interface Object<TMaterial> where TMaterial : Material { }

// Define the items in the highest taxonomic level (materials)
interface Wood : Material { }
interface Metal : Material { }

// Define the items in the 2nd taxonomic level (objects), implementing the appropriate interfaces to specify what the valid top-level taxonomies it can fall under.
interface Crate : Object<Wood>, Object<Metal> { }
interface Bar : Object<Metal> { }
interface Box : Object<Wood> { }

// Define an item class with type constraints to ensure the taxonomy is correct
abstract class Item<TMaterial, TObject>
    where TMaterial : Material
    where TObject : Object<TMaterial>
{
}

With the above defined, we can now define valid items:

class MetalBar : Item<Metal, Bar> { }
class MetalCrate : Item<Metal, Crate> { }
class WoodCrate : Item<Wood, Crate> { }
class WoodBox : Item<Wood, Box> { }

However attempting to create an invalid item (e.g. a wooden bar) results in a compile time error:

class WoodBar : Item<Wood, Bar> { }

The type 'Taxonomy.Bar' cannot be used as type parameter 'TObject' in the generic type or method 'Taxonomy.Item'. There is no implicit reference conversion from 'Taxonomy.Bar' to 'Taxonomy.Object'

Two ways -

  1. Create an enum which will contain the leaves of your tree, in your case Wood_Crate, Wood_box and so on. Easy to do and less easy to maintain or read.

  2. Define a class called Category and a static class for each of the elements in your tree. For example

    public class Category
      {
        internal Category() {};
        public string Id; // This would be used to understand what you got, can be a list of enums or something
      }
    
      public static Category CrateCategory = new Category {Id = "Wood.Crate"};
    
      public static class Wood
      {
        // either way will work, one will let you directly access CrateCategory (if that is what you wish), the second will remove the need for different Crate categories
        public static Category Crate { get { return CrateCategory; } }
        public static Category Box { get { return new Category { Id = "Wood.Box" }; } }
      }
    

Your BaseItem constructor will only receive a Category. And could write something like

new BaseItem(Box.Crate);

If you place the Category class in another assembly, you will sure no one would be able to create their own Categories.

That's a bit more work, but seems more elegant and readable to me. If N is extremely large, you could write some code to generate the classes and Category identifiers for you.

My suggestion would be to create a class for each type and then let the valid subtype inherit from this type.

Finally edit the BaseItem type to be generic and only accept valid types.

Like this (Sorry bad at explaining)

class Wood {}
class Metal {}
class Crate : Wood, Metal {}
class Box : Wood {}
class Bar : Metal {}

class BaseItem<T1, T2> where T2 : T1
{
}

This will give you compile time type safety (but I don't think it's the best way)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top