I was answering a question that led down a rabbit hole of creating an EnumValueObject
with [Flags]
capabilities.
I started with a simple EnumValuObject
implementation:
public abstract class EnumValueObject {
public abstract string Name { get; }
public abstract int Value { get; }
public override bool Equals(object obj) {
var otherValue = obj as EnumValueObject;
if (otherValue == null) {
return false;
}
var typeMatches = GetType().Equals(obj.GetType());
var valueMatches = Value.Equals(otherValue.Value);
return typeMatches && valueMatches;
}
public override int GetHashCode() {
return string.Join(",", Value, Name).GetHashCode();
}
public override string ToString() {
return Name;
}
}
Simple enough...
Then moved on to a generic FlagsValueObject
to satisfy the desired functionality:
public abstract class FlagsValueObject<T> : EnumValueObject where T : FlagsValueObject<T> {
protected IDictionary<int, string> Types = new Dictionary<int, string>();
protected FlagsValueObject(int value, string name) {
Types[value] = name;
}
protected FlagsValueObject() {
}
public override string Name {
get {
return string.Join(", ", Types.OrderBy(_ => _.Value).Select(_ => _.Value));
}
}
public override int Value {
get { return Types.Keys.Aggregate((a, b) => a + b); }
}
public abstract T And(T other);
public virtual bool HasFlag(T flag) {
var typeMatches = GetType().Equals(flag.GetType());
return typeMatches && (Value & flag.Value) == flag.Value;
}
public virtual bool HasFlagValue(int value) {
return (Value & value) == value;
}
public override int GetHashCode() {
return Types.GetHashCode();
}
}
With that foundation in place the desired implementation was designed as follows:
public class ScheduleType : FlagsValueObject<ScheduleType> {
public static readonly ScheduleType Fixed = new FixedType();
public static readonly ScheduleType Flexible = new FlexibleType();
public static readonly ScheduleType FullTime = new FullTimeType();
public static readonly ScheduleType PartTime = new PartTimeType();
public static readonly ScheduleType Rotated = new RotatedType();
protected ScheduleType(int value, string name)
: base(value, name) {
}
private ScheduleType(ScheduleType a, ScheduleType b) {
foreach (var kvp in a.Types.Union(b.Types)) {
Types[kvp.Key] = kvp.Value;
}
}
public override string Name {
get {
return string.Join(", ", Types.OrderBy(_ => _.Value).Select(_ => _.Value)) + " Work Schedule";
}
}
public override ScheduleType And(ScheduleType other) {
return new ScheduleType(this, other);
}
#region Values
private class FixedType : ScheduleType {
public FixedType()
: base(0x01, "Fixed") {
}
}
private class FlexibleType : ScheduleType {
public FlexibleType()
: base(0x02, "Flexible") {
}
}
private class FullTimeType : ScheduleType {
public FullTimeType()
: base(0x04, "Full Time") {
}
}
private class PartTimeType : ScheduleType {
public PartTimeType()
: base(0x08, "Part Time") {
}
}
private class RotatedType : ScheduleType {
public RotatedType()
: base(0x10, "Rotated") {
}
}
#endregion
}
Note the hex values used for the concrete implementations.
With a few simple unit tests to show usability:
[TestClass]
public class ScheduleTypeValueObjectTests {
[TestMethod]
public void Should_Merge_DisplayNames() {
//Arrange
var fixedSchedult = ScheduleType.Fixed; //Fixed Work Schedule
var fullTime = ScheduleType.FullTime; // Full Time Work Schedule
var type = fixedSchedult.And(fullTime);
//Act
var actual = type.Name;
//Assert
actual.Should().Be("Fixed, Full Time Work Schedule");
}
[TestMethod]
public void Should_Check_HasFlag() {
//Arrange
var fixedSchedult = ScheduleType.Fixed; //Fixed Work Schedule
var fullTime = ScheduleType.FullTime; // Full Time Work Schedule
var partTime = ScheduleType.PartTime;
var value = fixedSchedult.And(fullTime).And(partTime);
//Act
var actual = value.HasFlag(fullTime.And(partTime));
//Assert
actual.Should().Be(true);
}
}
The question I was answering was implying that there are some business rules associated with the possible combinations of values that can be used together in one schedule type.
I am torn about placing such concerns within the concrete implementation and believe that they should be applied within the business layer.
The HasFlag
in my opinion allows for that kind of validation to be applied as needed, which was demonstrated in the second example unit test above. For example if a ScheduleType
is not allowed to be both FullTime and PartTime it should be a simple matter of checking that within the value object.
Thoughts on current design choices and suggestions on changes if any are welcome.
bool IsValidCombination(ScheduleTypes)
orScheduleTypes[] GetValidCombinations()
won't suffice? What if the business rules change, and you have to migrate old data? \$\endgroup\$enum
is that this allows you to prevent invalid combinations (such thatMyEnum.A | MyEnum.NotA
would throw an exception). The benefit is obvious: you don't need to check validity anywhere else. But that approach does not work well if validity depends on context (not all companies allowing the same working schedules, or an older software version allowing different combinations). And if you decide to do validation elsewhere, in some business-layer code, then what's the point of using this custom enum type instead of a simpleenum
? \$\endgroup\$