This is a more advanced alternative:
When you hear "context" and "steps", there's a functional pattern that directly comes to mind: Monads. Rolling up your own monad instance can simplify the user-side of putting valid steps together, while providing warranties that the context will be cleaned up after them.
Here, we are going to develop a "CleanableContext" construction that follows that pattern.
We base our construct on the most simple monad, one whose only function is to hold a value. We're going to call that Context
trait Context[A] { self =>
def flatMap[B](f:A => Context[B]): Context[B] = f(value)
def map[B](f:A => B): Context[B] = flatMap(f andThen ((b:B) => Context(b)))
def value: A
}
object Context {
def apply[T](x:T): Context[T] = new Context[T] { val value = x }
}
Then we have a CleanableContext
, which is capable of "cleaning up after itself" provided some 'cleanup' function:
trait CleanableContext[A] extends Context[A] {
override def flatMap[B](f:A => Context[B]): Context[B] = {
val res = super.flatMap(f)
cleanup
res
}
def cleanup: Unit
}
And now, we have an object that's able to produce a cleanable UserContext
that will take care of managing the creation and destruction of users.
object UserContext {
def apply(x:UserManager): CleanableContext[User] = new CleanableContext[User] {
val value = x.createUser
def cleanup = x.deleteUser(value)
}
}
Let's say that we have also our model and business functions already defined:
trait Model
trait TestModel extends Model
trait ValidatedModel extends Model
trait OpResult
object Ops {
def prepareModel(user: User, model: TestModel): Model = new Model {}
def validateModel(model: Model): ValidatedModel = new ValidatedModel {}
def commitModel(user: User, vmodel: ValidatedModel): OpResult = new OpResult {}
}
Usage
With that reusable machinery in place, our users can express our process in a succinct way:
import Ops._
val ctxResult = for {
user <- UserContext(new UserManager{})
validatedModel <- Context(Ops.prepareModel(user, testModel)).map(Ops.validateModel)
commitResult <- Context(commitModel(user, validatedModel))
} yield commitResult
The result of the process is still encapsulated, and can be taken "out" from the Context
with the value
method:
val result = ctxResult.value
Notice that we need to encapsulate the business operations into a Context
to be used in this monadic composition. Note as well that we don't need to manually create nor cleanup the user used for the operations. That's taken care of for us.
Furthermore, if we needed more than one kind of managed resource, this method could be used to take care of managing additional resources by composing different contexts together.
With this, I just want to provide another angle to the problem. The plumbing is more complex, but it creates a solid ground for users to create safe processes through composition.
help.cleanUp
at the last line? – Alvaalvandef prepareModel(model: TestModel) : _____?
def commitModel(): _____?
– Crake