Surfacing Hidden Design: Seeking A Better Alternative To Interrelated Mutable Fields

What is better, a bunch of mutable boolean fields and methods operating on them, or an explicit expression of the individual states and transitions between them? Lets study an example from a simulation of the progression of a multi-stage infection.

1. Design hidden in primitive mutable fields and methods

The following class, with a number of interrelated mutable (public) fields, which I should have completed with methods for transitions between their states, made me really uneasy (a var is a mutable field, val is immutable):


class Person (val id: Int) {
var infected = false
var sick = false
var immune = false
var dead = false
// to complete with simulation logic
// [infection progression]
}

The fields keep the state of the progress of an infection. They depend on each other – f.ex. when sick, the person must also be infected, while when dead, she must not be immune.

The progression of the infection is: healthy -> infected and infectious with the chance of 40% (but not visibly sick yet) -> on day 6 visibly sick -> on day 14 dies with the chance of 25% -> if not dead, becomes immune on day 16 – not visibly sick but still infectious -> healthy on day 18.

The problem I have with keeping the state in a bunch of fields is that there is no explicit expression of these rules and that it opens for defects such as setting sick to true while forgetting to set also infected. It is also hard to understand. You could learn the rules by studying the methods that alter the fields but it requires a lot of effort, it isn’t easy to distinguish between an incidental implementation detail and intentional design and this code does not prevent mistakes like the one described.

Pros:

  • Familiar
  • Easy to design

Cons:

  • The rules of the infection progression are not clear from the code (well, it wouldn’t be even if the methods were actually shown :)), i.e. it communicates poorly the domain concepts and rules
  • The values of multiple fields must be synchronized, failing to ensure that leads to defects
  • Leads to spaghetti code

2. Explicit and defect-preventing expression of the states and transitions

What I would prefer is to make the rules the central piece of the design rather then basing it on the implementation details of how we keep the state (i.e. a set of booleans to manage). In other words, I want to surface the design hidden in this. I want to have an explicit notion of the states – Healthy, Incubating, Sick,  Dead, Immune – with clear transitions between them and I want these states to explicitely carry the secondary information about whether the person is infectious or not and whether this is visible or not.

One way to express this design explicitely is this:


def randomBelow = …
/** When should the health state change and what to */
class HealthChange(val time: Int, val newHealth: Health) {
def immediate = (time == 0)
}
/** The root of the health hierarchy; the health (i.e. infection) evolves in stages */
sealed abstract class Health(val infectious: Boolean, val visiblyInfectious: Boolean) {
def evolve(): Option[HealthChange] // returns None if no further change possibly/expected
}
/** In some stages the person is infected but it isn't visible*/
sealed abstract class CovertlyInfected extends Health(infectious = true, visiblyInfectious = false) {}
/** In other stages the person is infected and it is clearly visible*/
sealed abstract class VisiblyInfected extends Health(infectious = true, visiblyInfectious = true) {}
case object HEALTHY extends Health(infectious = false, visiblyInfectious = false) {
def evolve() = // 40% chance of getting infected, triggered on meeting an infected person
if (randomBelow(101) <= 40/*%*/)
Some(new HealthChange(0, INCUBATIOUS))
else
None // no change, stays healthy
}
case object INCUBATIOUS extends CovertlyInfected {
def evolve() = // After 6 days if infection without visible effects becomes sick
Some(new HealthChange(6, SICK))
}
case object SICK extends VisiblyInfected {
def evolve() = // die with 25% on day 14 or stays sick for 2 more days, then immune
if (randomBelow(101) <= 25/*%*/)
Some(new HealthChange(14 – 6, DEAD))
else
Some(new HealthChange(16 – 6, IMMUNE))
}
case object DEAD extends VisiblyInfected {
def evolve() = None // Dead people stay dead
}
/** The symptoms have disappeared but the person can still infect others for 2 more days */
case object IMMUNE extends CovertlyInfected {
def evolve() =
Some(new HealthChange(18 – 16, HEALTHY))
}
class Person (val id: Int, var health: Health = HEALTHY) {
def tryInfect() { // upon meeting an infected person
if (health != HEALTHY) throw new IllegalStateException("Only healthy can be infected")
health.evolve().foreach(
healthChng => setHealth(healthChng.newHealth) // infection happens immediately
)
}
/** Set the new health stage and schedule the next health change, if any */
def setHealth(h: Health) {
this.health = h
h.evolve().foreach(hNext => {
if (hNext.immediate)
setHealth(hNext.newHealth)
else
afterDelay(hNext.time) {setHealth(hNext.newHealth)}
})
}
}

Pros

  • The rules and stages of the infection are now explicit and first-class members of the code; Domain-Driven Design in practice
  • The transitions between the states are clear, explicit, and we cannot get the person into an invalid state (provided we defined the transitions correctly)
  • We don’t need anymore to synchronize the state of multiple variables

Cons

  • The code is likely longer than a bunch of bool fields and methods for transitioning between their states
  • It may seem complicated because, instead of one class and few methods, we have suddenly a class hierarchy; but it actually prevents the complexity of the original spaghetti code so, though not “easy” to comprehend, it is “simple” as per R. Hickey

Conclusion

I often encounter code like this, especially in older legacy applications that have been evolved according to changing business needs with primary focus on “features” without updating/refactoring the underlaying design accordingly. Frankly, it is hell. The low-level code working on a number of interdependant fields (in a class that has likely also a number of unrelated fields or fields that are depending on these only in certain use cases) is a  heaven for defects to hide in and multiply in. And it is hard to understand since the design – the rules, concepts, and intentions – have not been made explicit (yet). And hard to understand means hard to change.

Therefore it is important to survey your code regularly and surface the design hidden in it, hiding away low-level implementation details and making the key concepts, states, transitions, rules etc. first-class members of the code base so that reading the code feels as communicating and learning rather than as an archeology.

Updates

Update 1

Jerrinot’s Enum and Java based code, included here for easier readability:


public enum Health {
HEALTHY(false, false, false, false),
INCUBATIOUS(true, false, false, false),
SICK(true…),
DEAD(….);
private boolean infected;
private boolean sick;
private boolean immune;
private boolean dead;
private Health(boolean infected, boolean sick, boolean immune, boolean dead) {
this.infected = infected;
this.sick = sick;
this.immune = immune;
this.dead = dead;
}
public boolean isInfected() {
return infected;
}
public boolean isSick() {
return sick;
}
public Health evolve() {
switch (this) {
case HEALTHY:
return computeProbabability..() ? INCUBATIOUS : HEALTHY;
[….]
}
}
}

view raw

Health.java

hosted with ❤ by GitHub

Update 2: Short Enum-like version and the full “primitive” version

Jerrinot’s version in Scala:


sealed class Health(val infectious: Boolean, val visiblyInfectious: Boolean) {
def evolve(implicit randomGenerator: RandomIntGenerator, config: MySimConfig): Option[HealthChange] =
this match {
case Healthy =>
if (randomGenerator.randomBelow(101) <= config.transmissibilityPct)
Some(new HealthChange(0, Incubatious))
else None
case Incubatious =>
Some(new HealthChange(6, Sick))
case Sick =>
if (randomGenerator.randomBelow(101) <= config.mortalityPct)
Some(new HealthChange(14 – 6, Dead))
else None
case Dead =>
None
case Immune =>
Some(new HealthChange(18 – 16, Healthy))
case Vaccinated =>
None
}
}
// infected? visibly?
object Healthy extends Health(false, false)
object Incubatious extends Health(true, false)
object Sick extends Health(true, true)
object Dead extends Health(true, true)
object Immune extends Health(true, false)
object Vaccinated extends Health(false, false)

The “primitive” version with nearly full code, excluding the information what happens when:


// P.S.: Excuse the underscores …
class Person (val id: Int) {
// Simplified primitive version, assuming the information when should the evolution
// happen is handled somewhere else
def evolveHealth(): Unit = {
if (!_infected)
if (randomGenerator.randomBelow(101) <= config.transmissibilityPct)
this._infected = true
if (_infected && !_sick && !_immune)
this._sick = true
if (_sick)
if (randomGenerator.randomBelow(101) <= config.mortalityPct)
this._dead = true
else
this._unDead = true // did not die but still sick
if (_unDead)
this._immune = true
this._sick = true
if (_immune)
this._immune = false
this._infected = false
}
private var _unDead = false
private var _infected = false
private var _sick = false
private var _immune = false
private var _dead = false
}

Update 3: Other criteria to consider when evaluating the code

My mentor has reminded me than there are other criteria worth considering than a personal sense of beauty and that, when I have a painful experience with something (such as interrelated mutable primitive fields), I tend to go too far in an attempt to avoid it, stepping into another problem without realising it (code verbosity and lack of understandability).

So some of the criteria to consider are

  • beauty? <=> maintainability and likelihood of defects
  • understandability
  • testability and how quickly/early we can start with testing

The code with explicit states is much, much longer than the one with four booleans. Which to write when, is the important question.

Another factor to consider is succession: which code can you start testing and using soonest? Is one style more conducive to being used while it is incomplete. Let’s say I use the four booleans model and make them public fields so I don’t have to write or invoke getters and setters. Can I start writing my simulation, then notice that there are patterns to how the booleans are getting set and move smoothly to the explicit finite state machine?

I also have to acknowledge that the full primitive code is actually much shorter and simpler than I feared. Still uglier and more open for defects but perhaps not enough to justify the effort put into inventing a model with explicit states.

6 thoughts on “Surfacing Hidden Design: Seeking A Better Alternative To Interrelated Mutable Fields

  1. jerrinot

    Hi Jakub,
    well, the main problem of the 1st example is mixing a state and properties of the state. The 2nd solution fixed that, but I’m not sure if the solution is very readable. What about something like this:


    public enum Health {
    HEALTHY(false, false, false, false),
    INCUBATIOUS(true, false, false, false),
    SICK(true…),
    DEAD(….);
    private boolean infected;
    private boolean sick;
    private boolean immune;
    private boolean dead;
    private Health(boolean infected, boolean sick, boolean immune, boolean dead) {
    this.infected = infected;
    this.sick = sick;
    this.immune = immune;
    this.dead = dead;
    }
    public boolean isInfected() {
    return infected;
    }
    public boolean isSick() {
    return sick;
    }
    public Health evolve() {
    switch (this) {
    case HEALTHY:
    return computeProbabability..() ? INCUBATIOUS : HEALTHY;
    [….]
    }
    }
    }

    view raw

    Health.java

    hosted with ❤ by GitHub

    Obviously the Health would be a property of Person.
    I consider it to be very similar to your 2nd approach, but without the (unnecessary?) class hierarchy.

    Reply
    1. Jakub Holý Post author

      Hi, thank you!

      I too don’t like the verbosity of my solution but I do actually like the hierarchy as it helps me to keep it DRY and makes clear what is what without the risk that I forget to switch one of the 2 booleans that matter (infectious, visiblyInfectious) – though the advantage of that vs. the cost of verbosity is arguable.
      I, of course, am not found of switch statements 🙂 even though, once again, it is more conscise than implementing evolve in each subclass/object.
      I would love to see a solution that would make me really happy. Neither mine or yours is that solution but both are good enough. Perhaps some Scala wiz can find something nice and cure between the two? 🙂

      Once again, thank you for participating in the discussion!

      Reply
    1. Jakub Holý Post author

      That is a good point to consider, thank you. But is it worth the additional complexity of one more class? And what if I renamed Health to DiseaseStage , couldn’t a stage know what is the next legal stage?

      Reply
  2. Achim

    I have two suggestions:

    1. You could represent this as some little expert system consisting of if-then rules. These in turn could be expressed as partial functions with pattern matching and chained with orElse like this:

    sealed trait HealthCondition
    object HEALTHY extends HealthCondition
    object INFECTED extends HealthCondition
    object SICK extends HealthCondition
    // more HealthConditions here
    case class Person(daysSinceInfection: Int, healthCondition: HealthCondition)
    type DiseaseRule = PartialFunction[Person, Person]
    import scala.util.Random
    val healthyToInfected: DiseaseRule = {case Person(0, HEALTHY) if (Random.nextDouble Person(1, INFECTED)}
    val infectedToSick: DiseaseRule = {case Person(6, INFECTED) => Person(7, SICK)}
    // more rules here
    val justPassingTime: DiseaseRule = {case p@Person(days, health) if (health != HEALTHY) => p.copy(daysSinceInfection = days + 1)}
    val nothingHappens: DiseaseRule = {case p => p}
    val evolve = healthyToInfected orElse infectedToSick orElse justPassingTime orElse nothingHappens
    val person1 = Person(6, INFECTED)
    evolve(evolve(person1)) // Person(8, SICK)

    2. Another (somewhat strange) idea could be to use traits and the type system more directly:

    import scala.util.Random
    trait HealthCondition { self: Person =>
    def evolve(): Person with HealthCondition
    }
    trait Healthy extends HealthCondition { self: Person =>
    override def evolve() = if (Random.nextDouble
    override def evolve() = if (daysSinceInfection == 6) new Person(7) with Sick else new Person(daysSinceInfection + 1) with Infected
    }
    trait Sick extends HealthCondition { self: Person =>
    override def evolve() = new Person(daysSinceInfection + 1) with Sick
    }
    case class Person(daysSinceInfection: Int)
    val person1 = new Person(6) with Infected
    person1.evolve.evolve // Person(8) with Sick

    Reply

Leave a reply to Gordon Tyler Cancel reply