I am working on a C# project and I have a somewhat large number of labels (~100) that have some sort of relationships between one another. Here is a minimal dummy example that illustrates this:
Suppose you have to handle metal cans, paper cups, paper bags and apples in code. You have human labeled data that tells you that cans are metal objects and containers that hold liquid. Paper cups are made of paper, are also containers and hold liquid. Apples are edible fruit etc. We also know that liquid containers are obviously containers and for example edible objects can't be made of metal.
I have considered several solutions to this problem:
1. The current solution (multiple separate enums)
enum MaterialType { None = 0, Metal = 1, Paper = 2, Plastic = 3 }
enum ContainerType { None = 0, Liquid = 1, Solid = 2 }
enum ObjectType { None = 0, Edible = 1, Container = 2 }
enum FoodType { None = 0, Fruit = 1 }
class ObjectWithFlags
{
public MaterialType MaterialType;
public ContainerType ContainerType;
public ObjectType ObjectType;
public FoodType FoodType;
}
And then create concrete objects like this:
ObjectWithFlags apple = new()
{
ObjectType = ObjectType.Edible,
FoodType = FoodType.Fruit
};
ObjectWithFlags can = new()
{
ObjectType = ObjectType.Container,
MaterialType = MaterialType.Metal,
ContainerType = ContainerType.Liquid,
};
Check for flags like this:
if (apple.ObjectType is ObjectType.Edible)
Console.WriteLine("Apples are edible");
Pros: Easier to read when enums have only a few values and provides some kind of logical grouping of these labels. Ensures a kind of mutual exclusion - for example an object can't be both metal and paper at the same time.
Cons: Not all rules are enforced - for example you can make an edible metal object. A lot of fields to store each type and subtype. This propagates throughout the code - when calling a function you must either pass the entire object containing all of the fields, group the enums into another meaningless object or pass a bunch of variables. When a new type or subtype appears in the data you must add new fields, modify the Equals override, copy constructor etc.
2. Using a flags enum in C#
[Flags]
private enum Flags
{
None = 0,
MetalObject = 1 << 0,
PaperObject = 1 << 1,
Container = 1 << 2,
LiquidContainer = 1 << 3 | Container,
SolidContainer = 1 << 4 | Container,
Edible = 1 << 5,
Fruit = 1 << 6 | Edible,
}
Create objects like so:
Flags apple = Flags.Edible | Flags.Fruit;
Flags can = Flags.MetalObject | Flags.LiquidContainer;
Check for flags like this:
if (apple.HasFlag(Flags.Edible))
Console.WriteLine("Apples are edible");
Pros: A single variable to store all of the flags of an object - easy to pass to functions, copy etc. No need to add new fields when a new flag appears, just add it to the enum. Supports "inheritance".
Cons: No mutual exclusion (you can set both the metal and paper flag). Ugly binary values - easy to make a horrible bug if someone specifies a non power of two in the enum or something similar.
This feels like a somewhat common problem so I would like to know if there is a design pattern that would handle this or some kind of builtin language feature? Is the current solution unacceptable? To me it seems quite bad to have 7 fields that you have to handle and I also think the structure / relationships in the data should be as separate from the code structure as possible but I guess this is subjective, so are there any guidelines or best practices regarding this?
A couple of notes: the labels / flags in my case arise from data. They are human labels that only classify things so using class inheritance, I think, makes no sense. For example metal containers do not have any functionality - nothing special they can do, it is just a label. The flags are used in code to perform different operations on the data (for example a certain function handles metal objects, another one uses liquid containers etc). New labels might be added to the data, the relationships changed etc.