3

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.

5
  • 1
    Whatever abstraction you end up with, the most important thing here would be ensuring you have a set of unit tests (which should be triggered in your build pipeline) to enforce and protect the rules that you've set out. Ultimately what you're describing here falls into the category of business rules, and indeed this looks to be more of a domain data modelling problem so it doesn't need to be enforced by the compiler nor even necessarily represented using programming language features; for example, perhaps these associations could be represented using a relational database or YAML. Commented Jun 13 at 6:49
  • Since you're using .NET you could also consider representing these relationships as conditional rules in FluentValidation - docs.fluentvalidation.net/en/latest/conditions.html Commented Jun 13 at 7:12
  • One problem with toy examples in questions is that the responses cannot take the real requirements into account. Depending on the actual use case, anything from elaborate enum sets to a data-driven label system and business rules evaluation engine might be possible. Commented Jun 13 at 11:51
  • Do everyone a favour and use parentheses for things like a << b | c. A good compiler will give you a warning.
    – gnasher729
    Commented Jun 13 at 16:58
  • Do these attributes have any behaviors associated with them?
    – John Wu
    Commented Jun 15 at 8:31

3 Answers 3

3

Modeling flags/labels in code

It is not really clear from your contrived example whether your approach #1 means "an object with these flags, plus some extra, non-flag properties not mentioned", or if your ObjectWithFlags should be just a FlagObject - a type holding these flags exclusively, and the objects which are labelled get a member variable of type FlagObject indicating their "domain types". So let me assume it is the latter, and I will speak of FlagObjects to make this clear.

Let's look at what you wrote about your second solution:

A single variable to store all of the flags of an object - easy to pass to functions, copy etc.

Sorry, but that is true for a FlagObject as well. You can create single variables of type FlagObject and can do almost everything what you can do with the type Flags, just in a more readable way. Of course, to make FlagObject really as easy to handle as Flags, you should change the design a little bit (see below).

No need to add new fields when a new flag appears, just add it to the enum.

When you add new flags, you will have the need to change something in the type Flags, and an equivalent change to FlagObject will be required as well. With the right kind of design, you can prevent changes of existing code with FlagObjects just the way as with Flags.

So as you noted, I strongly prefer your approach #1, but as I said, I would change the design a little bit. I would

  • make FlagObject an immutable type (all fields "readonly). That lets you pass objects of it around like strings or 'Flags' with no need to copy them explicitly, without the risk of introducing unwanted side effects.

  • choose the enums in FlagObject as orthogonal as possible. There should not be a ContainerType on one hand (which can already indicates whether something is a container or not) , and an enum ObjectType with a value Container as well, that's clearly redundant.

    Instead, you can implement derived properties like a boolean property

    IsContainer => ContainerType != ContainerType.None

    Moreover IsEdible should probably be just a boolean property, not mutual-exclusive with IsContainer (why should every container not be edible?). Technically you can also use an enum Editbility { None = 0, Edible = 1} if you prefer this for reasons of unitformity.

  • provide explicit constructors which check any extra constraints of mutual-exclusive enum values (and throw exceptions in case those constraints are violated, for example, when someone tries to construct an edible metal object).

  • provide certain default values in the constructor parameters, especially when new flags are introduced at a later point in time. That saves you from changing existing code already relying on FlagObjects.

That's IMHO a readable and clean solution, hence that is the way I would start with. I leave it as an exercise to you write down a implementation of FlagObject with these recommendations in mind.

There are cases where #2 your Flags enum has some advantages. These are cases where you need to optimize heavily for memory, performance, or where you are abound by certain requirements related to external APIs or persistence. But if this is not the case, I would recommend to go with a FlagObject first. And if you really come to the point wher you need some optimization, you have always the option of keeping the public API of a FlagObject unchanged and implement it as a bitset like Flags internally at a later point in time.

Modeling flags/labels by a user of the system

In a comment, you mentioned that you see some requirement to create the flags/labels and their relationships as dynamic data in your system, so they can be modified without changing the actual program. This makes most sense when the person who maintains the flags/labels isn't the developer, but a user, or maybe some power user or administrator.I assume there is something like a list of items which are tagged accordingly.

For this, you first need to model the possible label categories themselves in code, each one with a list of labels (within the category):

class LabelCategory
{
     public readonly string Name;
     public readonly string[] Labels;
}

Then you need a UI to maintain a List<LabelCategory> availableCategories and persist it somewhere.

Next you will have to categorize your items. Each item can have a list of labels of different categories. I call this a Tag, to make the difference clear:

 class Tag
 {
     public LabelCategory Category;

     private int labelIndex;
     public string Label => Category.Labels[labelIndex];
     
     public Tag(LabelCategory category, int labelIndex)
     {
         if(labelIndex<0 || labelIndex >= category.Length)
             throw ArgumentException("...");
         this.Category=category;
         this.labelIndex=labelIndex;
     }
 }

(This is just a rough scetch, please read it as pseudocode, just as the following code, a real implementation should be more sophisticated and may implement these classes in an immutable fashion).

Next, I would introduce a class Tagsets holding a list of tags from different categories:

 class Tagset
 {
      public List<Tag> Tags;
      // have some code making sure all tags belong to different categories
 }

Then, each item gets a tagset:

 class Item
 {
     public Tagset Tagset;

     // ... specific item attributes
 }

Let me finally come to the question of how to describe the relationships / constraints between the different tags / labels of an item, and how to validate these constraints.

In your example, you mention just two types of rules:

  • exclusion rules: certain combinations of tag values are not allowed (like "Metal" and "Edible")

  • inclusion rules: a certain tag value in one category implies the availability of another tag value in another category (like "Fruit" implies "Edible")

Both kind of rules can be described by lists of tagsets or list of tag pairs:

 // each entry here describes a forbidden combination
 List<Tagset> exclusionRules;   
 
 // eache entry says "Item1 implies Item2 to be in the same tagset"
 List<(Tag,Tag)> inclusionRules;

Now it should be easily possible to create a UI for letting a user manage these exclusionRules and inclusionRules. In case you need other types of rules, please feel free to extend this model.

The validation of these rules then has to happen when the tagsets for items are created and called in that context. This is left as an exercise to the reader;-) Hope this helps.

4
  • The biggest problem I have with the "FlagObject" approach is that to me these relationships between the tags are a part of the data. The flags are data and the relationships are data. By adding multiple enums to code I am forcing the relationships from the data into code which seems strange to me. For example if you are writing software that handles graphs you would not hard code the edges of the graph into the source code or use any other language feature to implement the graph edges. Commented Jun 13 at 10:14
  • 1
    @evolved_antenna: when I got you right, this issue is the same in your bitset approach with the Flags approach and a different question than the one you asked. If that is your concern, you may ask a specific follow-up question about it (though I can tell you the answer will be to use a rule engine, and I am sure there are some older questions on this site which already deal with that).
    – Doc Brown
    Commented Jun 13 at 11:29
  • @evolved_antenna, I am curious about a follow-up question with a more concrete use case. The trouble with how this question is asked is it assumes enums and flags are suitable solutions to the problem. By focusing on the problem, we might come up with a dramatically different solution that works better with fewer drawbacks. Commented Jun 13 at 13:50
  • @evolved_antenna: ok, I took the time to write a second part in case you want labels/tags and their constraints modeled as user-manageable data. I guess that is roughly what you had in mind.
    – Doc Brown
    Commented Jun 13 at 15:29
3

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.

Going to put in my standard answer for this problem.

It looks like you are trying to make a dynamically programmable system. Stop! just program the rules.

The problem is that unless you have some rigid constraints on your relationship rules, you end up writing your own generic programming language to allow the user to edit them. This soon becomes harder for the user to do than it would be for you to edit and release a new version of the codebase with the new rules in.

eg.

  • Metal objects can be Edible if they are iron vitamin tablets.
  • objects with Poison can never be Edible unless they are also Small and Medicine
  • Pizza can only be Edible on Thursday
  • Sieves are Containers but can't hold Liquid

You can easily code up these rules and apply the checks before you process whatever methods you have made to do stuff. I could make up 50 more crazy conditions and you would have decades worth of libraries and components to help you create program the rules.

If you try to make a dynamic system with a user interface someone will invent a new rule tommorrow that it wont be able to support. Soon the language will become too complex for users to edit themselves and you will hire programmers to do it for them. The programmers will want tests, source control and change control for their rules. Before you know it you will have invented BizTalk.

2

One solution at least worth considering is to use the type system:

interface IObject{}
interface IContainer : IObject{
    MaterialType Material {get;}
}
interface ILiquidContainer : IContainer {}
interface ISolidContainer : IContainer {}
interface IEdible : IObject{}
interface IFruit : IEdible {get;}
class Apple : IFruit{}

This has some advantages. It lets you express relationships like "an Apple is a fruit" or "a Container has a material". It uses the type system to ensure the type model is followed, so an apple cannot have a Material.

There are also disadvantages. It is probably not appropriate if you want to define your objects, relationships and rules dynamically.

Whatever model or rules you create you will always risk problems and conflicts. You say "cans are metal objects and containers that hold liquid", but a watering can may be made of plastic. Or "Cans" may refer to headphones. Human language can be messy, inconsistent and ambiguous, so if you are working with human labeled data you likely want to take this into account.

5
  • 1
    Not all fruit is edible. Something to remember next time you go camping. Commented Jun 13 at 11:59
  • 1
    Not all metal is inedible. Commented Jun 13 at 12:04
  • @candied_orange I'm not sure if you disagree with my example of a model, or just wanted emphasize that language is messy. But yes, no matter what model or rules you create, it will be wrong in at least some ways. I would expect wide disagreement about the "edibility" of various objects.
    – JonasH
    Commented Jun 13 at 13:03
  • @candied_orange: some people surely disagree to Wikipedia and say all fruit is edible, but some fruit only once ;-)
    – Doc Brown
    Commented Jun 13 at 15:31
  • @JonasH Simply pointing out how pointy the corner cases are. As a toy project I don't disagree with your design. But I fail to see how it could accommodate those realities. I do believe others could. But others will have their own issues. Was curious how gracefully you could accommodate them. I've been burned by baking what seemed like reasonable assumptions exactly like these into a design. Now I code like I can't assume water is wet. Sometimes it's hard and sticky. Commented Jun 13 at 15:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.