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 FlagObject
s 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 FlagObject
s 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 aContainerType
on one hand (which can already indicates whether something is a container or not) , and an enumObjectType
with a valueContainer
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 withIsContainer
(why should every container not be edible?). Technically you can also use anenum 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
FlagObject
s.
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 case 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 lables from different categories:
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 anything 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.