15 December 2020
A Bit of State
There’s a developer aphorism that claims “shared mutable state is the root of all evil”. It’s a fun, pithy statement that’s intentionally exaggerating to make a point, but there are also some good reasons for the sentiment. As we create applications, being mindful of how and where we introduce stateful elements into our design can help us write code that’s clearer and easier to understand as well as limit opportunities for error.
A colleague of mine was working on a side project, modeling the rules for a role-playing adventure game. Part of the problem was keeping track of a character’s basic statistics, which could be modified during the course of the game by the player adding equipment to the character.
A simplified version of the class representing the character would be something like this (C#):
public class Adventurer
{
public Attributes BaseAttributes { get; set; }
public List<Equipment> EquippedItems { get; set; }
public Attributes EffectiveAttributes { get; set; }
public void AddEquipment(Equipment equipment)
{
EffectiveAttributes.Add(equipment.AttributeModifiers);
EquippedItems.Add(equipment);
}
public void RemoveEquipment(Equipment equipment)
{
if (EquippedItems.Remove(equipment))
EffectiveAttributes.Subtract(equipment.AttributeModifiers);
}
}
While writing tests, my colleague wanted to ensure that the character’s “Effective Attributes” were equal to their “Base Attributes” plus the sum of their equipment’s modifiers. However, they were concerned that they couldn’t effectively cover all the possible scenarios and corner cases by simply writing a series of tests that added and removed some combination of equipment and checked the result. They wanted to know if there was a good way to enforce that the invariant held at all times, rather than hope their tests were comprehensive enough.
I think there’s an enlightening observation we can see in this example. We have a stateful element of our design- the “EffectiveAttributes” property- whose value is wholly determined by other adjacent elements. We could entirely eliminate the ability for this value to get out-of-sync by reducing the state our class uses to a minimum. One way to do this in C# would be to change the EffectiveAttributes to a getter only property, implemented with exactly the invariant calculation we want:
public class Adventurer
{
public Attributes BaseAttributes { get; set; }
public List<Equipment> EquippedItems { get; set; }
public Attributes EffectiveAttributes =>
Attributes.Add(
BaseAttributes,
EquippedItems
.Select(x => x.AttributeModifiers)
.Aggregate(Attributes.Add)
);
}
Not only can we now be confident that our invariant holds, but the invariant itself is expressed in code. It’s a nice side benefit that makes our intent for the property more immediately clear.
This practice of limiting state to prevent desynchronization can be seen as similar to the idea of having a “Single Source of Truth” in the context of database design. We have a single source of “truth” in the base elements, and the derived value is calculated from those.
While this is effective at ensuring the consistency of our data, this design choice does come with a clear tradeoff- we’re re-calculating the value each time we read the EffectiveAttributes property, instead of whenever we modify the underlying values. Is this change worth the performance penalty of doing so?
Well, it’s not really knowable from this example. It’s very possible that this code would be called quite frequently. If this code was being used for a game, the program could be re-calculating these basic values quite a large number of times each fraction of a second. On the other hand, the performance difference might be entirely unnoticeable. For a toy project, I’d lean toward not worrying about it in initial design and waiting to see if it indeed becomes a problem. After all, according to Donald Knuth, “Premature optimization is the root of all evil.”