Bulletproof Code: Abstract Data Types

Flax's Domain Model

Currently it's (almost) this:

final case class OpenEnrollment(
  id: UUID,
  startDate: Instant,
  endDate: Instant,
  status: OpenEnrollmentState,
  productMappings: NonEmptyList[ProductMapping],
  eligibilityGroups: NonEmptyList[EligibilityGroup],
  employerId: String,
  createdByTitanUser: String)

def createOpenEnrollment(...): ValidatedNel[OpenEnrollmentError, OpenEnrollment]
def editOpenEnrollment(...): ValidatedNel[OpenEnrollmentError, OpenEnrollment]
def deleteOpenEnrollment(...): ValidatedNel[OpenEnrollmentError, OpenEnrollment]
def runOpenEnrollment(...): ValidatedNel[OpenEnrollmentError, OpenEnrollment]
def finishRunningOpenEnrollment(...): ValidatedNel[OpenEnrollmentError, OpenEnrollment]
def errorRunningOpenEnrollment(...): ValidatedNel[OpenEnrollmentError, OpenEnrollment]

To Implement This State Machine

oe-state-transitions.jpg

The Problem

We Can't Actually Prevent Invalid Data

  • Clients can instantiate arbitrary and invalid OEs

The Solution

Yaron Minsky says:

"Make illegal states unrepresentable"
yaronminsky.jpg https://blogs.janestreet.com/effective-ml-revisited/

But How Do We Do This In Scala?

Traits!

A New Model

trait OpenEnrollment {
  val id: UUID
  val startDate: Instant
  val endDate: Instant
  val status: OpenEnrollmentState
  val productMappings: NonEmptyList[ProductMapping]
  val eligibilityGroups: NonEmptyList[EligibilityGroup]
  val employerId: String
  val createdByTitanUser: String
}

object OpenEnrollment {
  private case class OpenEnrollmentImpl(
    id: UUID,
    startDate: Instant,
    endDate: Instant,
    status: OpenEnrollmentState,
    productMappings: NonEmptyList[ProductMapping],
    eligibilityGroups: NonEmptyList[EligibilityGroup],
    employerId: String,
    createdByTitanUser: String
  ) extends OpenEnrollment {}
}

Demo

trait X {
  val a: Int
  val b: String
}

object X {
  private case class XImpl(a: Int, b: String) extends X {}

  def mkX(a: Int, b: String): X = XImpl(a, b)
  def editA(x: X, newA: Int): X = XImpl(newA, x.b)

  def unapply(x: XImpl): Option[(Int, String)] = Some((x.a, x.b))
}

Disadvantages

  • Lose the ability to pattern match unless you define your own unapply method.

Questions?